From 21defe6c9e1863a4dba61effaf99cd7160bf075b Mon Sep 17 00:00:00 2001 From: shrimbly Date: Mon, 29 Dec 2025 23:45:20 +1300 Subject: [PATCH] Introduce expand and run functionalities in BaseNode, and integrate modal for prompt editing in PromptNode. --- src/components/WorkflowCanvas.tsx | 15 +- src/components/modals/PromptEditorModal.tsx | 147 ++++++++++++++++++++ src/components/nodes/BaseNode.tsx | 55 +++++++- src/components/nodes/NanoBananaNode.tsx | 13 +- src/components/nodes/PromptNode.tsx | 83 +++++++---- src/store/workflowStore.ts | 9 ++ 6 files changed, 279 insertions(+), 43 deletions(-) create mode 100644 src/components/modals/PromptEditorModal.tsx diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 7336a50e..450f4c6b 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -187,7 +187,7 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num }; export function WorkflowCanvas() { - const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow } = + const { nodes, edges, groups, onNodesChange, onEdgesChange, onConnect, addNode, updateNodeData, loadWorkflow, getNodeById, addToGlobalHistory, setNodeGroupId, executeWorkflow, isModalOpen } = useWorkflowStore(); const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport } = useReactFlow(); const [isDragOver, setIsDragOver] = useState(false); @@ -1091,17 +1091,20 @@ export function WorkflowCanvas() { fitView deleteKeyCode={["Backspace", "Delete"]} multiSelectionKeyCode="Shift" - selectionOnDrag={isMacOS} - panOnDrag={!isMacOS} + selectionOnDrag={isMacOS && !isModalOpen} + panOnDrag={!isMacOS && !isModalOpen} selectNodesOnDrag={false} nodeDragThreshold={5} zoomOnScroll={false} - zoomOnPinch={true} + zoomOnPinch={!isModalOpen} minZoom={0.1} maxZoom={4} defaultViewport={{ x: 0, y: 0, zoom: 1 }} - panActivationKeyCode="Space" - onWheel={handleWheel} + panActivationKeyCode={isModalOpen ? null : "Space"} + onWheel={isModalOpen ? undefined : handleWheel} + nodesDraggable={!isModalOpen} + nodesConnectable={!isModalOpen} + elementsSelectable={!isModalOpen} className="bg-neutral-900" defaultEdgeOptions={{ type: "editable", diff --git a/src/components/modals/PromptEditorModal.tsx b/src/components/modals/PromptEditorModal.tsx new file mode 100644 index 00000000..631c4c84 --- /dev/null +++ b/src/components/modals/PromptEditorModal.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect, useCallback } from 'react'; + +const FONT_SIZE_STORAGE_KEY = 'prompt-editor-font-size'; +const DEFAULT_FONT_SIZE = 14; +const MIN_FONT_SIZE = 10; +const MAX_FONT_SIZE = 24; +const FONT_SIZE_OPTIONS = [10, 12, 14, 16, 18, 20, 24]; + +interface PromptEditorModalProps { + isOpen: boolean; + initialPrompt: string; + onSubmit: (prompt: string) => void; + onClose: () => void; +} + +export const PromptEditorModal: React.FC = ({ + isOpen, + initialPrompt, + onSubmit, + onClose, +}) => { + const [prompt, setPrompt] = useState(initialPrompt); + const [fontSize, setFontSize] = useState(() => { + // Load font size from localStorage on mount + if (typeof window !== 'undefined') { + const saved = localStorage.getItem(FONT_SIZE_STORAGE_KEY); + if (saved) { + const parsed = parseInt(saved, 10); + if (!isNaN(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) { + return parsed; + } + } + } + return DEFAULT_FONT_SIZE; + }); + + // Update local state when initial prompt changes + useEffect(() => { + setPrompt(initialPrompt); + }, [initialPrompt]); + + // Save font size to localStorage when it changes + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem(FONT_SIZE_STORAGE_KEY, fontSize.toString()); + } + }, [fontSize]); + + // Handle Escape key to close + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + if (isOpen) { + window.addEventListener('keydown', handleKeyDown); + } + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onClose]); + + const handleSubmit = useCallback(() => { + onSubmit(prompt); + onClose(); + }, [prompt, onSubmit, onClose]); + + const handleFontSizeChange = useCallback((e: React.ChangeEvent) => { + setFontSize(parseInt(e.target.value, 10)); + }, []); + + const handleBackdropClick = useCallback( + (e: React.MouseEvent) => { + // Only close if clicking the backdrop itself, not the dialog content + if (e.target === e.currentTarget) { + onClose(); + } + }, + [onClose] + ); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ Edit Prompt +

+
+ + {/* Box containing toolbar and textarea */} +
+ {/* Toolbar - header of the box */} +
+ {/* Font Size Control */} + +
+ + {/* Textarea */} +