diff options
Diffstat (limited to 'frontend/src/App.tsx')
| -rw-r--r-- | frontend/src/App.tsx | 183 |
1 files changed, 158 insertions, 25 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9ec1340..5776091 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,21 +6,27 @@ import ReactFlow, { ReactFlowProvider, Panel, useReactFlow, + SelectionMode, type Node, type Edge } from 'reactflow'; import 'reactflow/dist/style.css'; import useFlowStore from './store/flowStore'; import LLMNode from './components/nodes/LLMNode'; +import MergedEdge from './components/edges/MergedEdge'; import Sidebar from './components/Sidebar'; import LeftSidebar from './components/LeftSidebar'; import { ContextMenu } from './components/ContextMenu'; -import { Plus } from 'lucide-react'; +import { Plus, Sun, Moon, LayoutGrid } from 'lucide-react'; const nodeTypes = { llmNode: LLMNode, }; +const edgeTypes = { + merged: MergedEdge, +}; + function Flow() { const { nodes, @@ -32,24 +38,45 @@ function Flow() { deleteEdge, deleteNode, deleteBranch, + deleteTrace, setSelectedNode, toggleNodeDisabled, archiveNode, createNodeFromArchive, - toggleTraceDisabled + toggleTraceDisabled, + theme, + toggleTheme, + autoLayout, + findNonOverlappingPosition } = useFlowStore(); const reactFlowWrapper = useRef<HTMLDivElement>(null); - const { project } = useReactFlow(); - const [menu, setMenu] = useState<{ x: number; y: number; type: 'pane' | 'node' | 'edge'; id?: string } | null>(null); + const { project, getViewport } = useReactFlow(); + const [menu, setMenu] = useState<{ x: number; y: number; type: 'pane' | 'node' | 'edge' | 'multiselect'; id?: string; selectedIds?: string[] } | null>(null); const [isLeftOpen, setIsLeftOpen] = useState(true); const [isRightOpen, setIsRightOpen] = useState(true); - const onPaneClick = () => { + // Get selected nodes + const selectedNodes = nodes.filter(n => n.selected); + + const onPaneClick = useCallback(() => { setSelectedNode(null); setMenu(null); - }; + }, [setSelectedNode]); + + // Close menu on various interactions + const closeMenu = useCallback(() => { + setMenu(null); + }, []); + + const handleNodeDragStart = useCallback(() => { + setMenu(null); + }, []); + + const handleMoveStart = useCallback(() => { + setMenu(null); + }, []); const handlePaneContextMenu = (event: React.MouseEvent) => { event.preventDefault(); @@ -58,7 +85,33 @@ function Flow() { const handleNodeContextMenu = (event: React.MouseEvent, node: Node) => { event.preventDefault(); - setMenu({ x: event.clientX, y: event.clientY, type: 'node', id: node.id }); + // Check if multiple nodes are selected and the right-clicked node is one of them + if (selectedNodes.length > 1 && selectedNodes.some(n => n.id === node.id)) { + setMenu({ + x: event.clientX, + y: event.clientY, + type: 'multiselect', + selectedIds: selectedNodes.map(n => n.id) + }); + } else { + setMenu({ x: event.clientX, y: event.clientY, type: 'node', id: node.id }); + } + }; + + // Batch operations for multi-select + const handleBatchDelete = (nodeIds: string[]) => { + nodeIds.forEach(id => deleteNode(id)); + setMenu(null); + }; + + const handleBatchDisable = (nodeIds: string[]) => { + nodeIds.forEach(id => toggleNodeDisabled(id)); + setMenu(null); + }; + + const handleBatchArchive = (nodeIds: string[]) => { + nodeIds.forEach(id => archiveNode(id)); + setMenu(null); }; const handleEdgeContextMenu = (event: React.MouseEvent, edge: Edge) => { @@ -68,7 +121,20 @@ function Flow() { const handleAddNode = (position?: { x: number, y: number }) => { const id = `node_${Date.now()}`; - const pos = position || { x: Math.random() * 400, y: Math.random() * 400 }; + + // If no position provided, use viewport center + let basePos = position; + if (!basePos && reactFlowWrapper.current) { + const { x, y, zoom } = getViewport(); + const rect = reactFlowWrapper.current.getBoundingClientRect(); + // Calculate center of viewport in flow coordinates + basePos = { + x: (-x + rect.width / 2) / zoom - 100, // offset by half node width + y: (-y + rect.height / 2) / zoom - 40 // offset by half node height + }; + } + basePos = basePos || { x: 200, y: 200 }; + const pos = findNonOverlappingPosition(basePos.x, basePos.y); addNode({ id, @@ -76,7 +142,7 @@ function Flow() { position: pos, data: { label: 'New Question', - model: 'gpt-4o', + model: 'gpt-5.1', temperature: 0.7, systemPrompt: '', userPrompt: '', @@ -86,6 +152,7 @@ function Flow() { traces: [], outgoingTraces: [], forkedTraces: [], + mergedTraces: [], response: '', status: 'idle', inputs: 1 @@ -124,11 +191,11 @@ function Flow() { }; return ( - <div style={{ width: '100vw', height: '100vh', display: 'flex' }}> + <div className={`w-screen h-screen flex ${theme === 'dark' ? 'dark bg-gray-900' : 'bg-white'}`}> <LeftSidebar isOpen={isLeftOpen} onToggle={() => setIsLeftOpen(!isLeftOpen)} /> <div - style={{ flex: 1, height: '100%', position: 'relative' }} + className={`flex-1 h-full relative ${theme === 'dark' ? 'bg-slate-900' : 'bg-slate-50'}`} ref={reactFlowWrapper} onDragOver={onDragOver} onDrop={onDrop} @@ -140,23 +207,64 @@ function Flow() { onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes} - onNodeClick={onNodeClick} + edgeTypes={edgeTypes} + defaultEdgeOptions={{ type: 'merged' }} + onNodeClick={(e, node) => { closeMenu(); onNodeClick(e, node); }} onPaneClick={onPaneClick} onPaneContextMenu={handlePaneContextMenu} onNodeContextMenu={handleNodeContextMenu} onEdgeContextMenu={handleEdgeContextMenu} + onNodeDragStart={handleNodeDragStart} + onMoveStart={handleMoveStart} + onSelectionStart={closeMenu} fitView + panOnDrag + selectionOnDrag + selectionKeyCode="Shift" + multiSelectionKeyCode="Shift" + selectionMode={SelectionMode.Partial} > - <Background color="#aaa" gap={16} /> - <Controls /> - <MiniMap /> + <Background color={theme === 'dark' ? '#374151' : '#aaa'} gap={16} /> + <Controls className={theme === 'dark' ? 'dark-controls' : ''} /> + <MiniMap + nodeColor={theme === 'dark' ? '#4b5563' : '#e5e7eb'} + maskColor={theme === 'dark' ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.6)'} + /> <Panel position="top-left"> - <button - onClick={() => handleAddNode()} - className="bg-white px-4 py-2 rounded-md shadow-md font-medium text-gray-700 hover:bg-gray-50 flex items-center gap-2" - > - <Plus size={16} /> Add Block - </button> + <div className="flex gap-2"> + <button + onClick={() => handleAddNode()} + className={`px-4 py-2 rounded-md shadow-md font-medium flex items-center gap-2 transition-colors ${ + theme === 'dark' + ? 'bg-gray-800 text-gray-200 hover:bg-gray-700 border border-gray-600' + : 'bg-white text-gray-700 hover:bg-gray-50' + }`} + > + <Plus size={16} /> Add Node + </button> + <button + onClick={autoLayout} + className={`px-3 py-2 rounded-md shadow-md font-medium flex items-center gap-2 transition-colors ${ + theme === 'dark' + ? 'bg-gray-800 text-gray-200 hover:bg-gray-700 border border-gray-600' + : 'bg-white text-gray-700 hover:bg-gray-50' + }`} + title="Auto Layout" + > + <LayoutGrid size={16} /> + </button> + <button + onClick={toggleTheme} + className={`px-3 py-2 rounded-md shadow-md font-medium flex items-center gap-2 transition-colors ${ + theme === 'dark' + ? 'bg-gray-800 text-gray-200 hover:bg-gray-700 border border-gray-600' + : 'bg-white text-gray-700 hover:bg-gray-50' + }`} + title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'} + > + {theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />} + </button> + </div> </Panel> </ReactFlow> @@ -180,7 +288,27 @@ function Flow() { } } } - ] : menu.type === 'node' ? (() => { + ] : menu.type === 'multiselect' ? (() => { + // Multi-select menu - batch operations + const ids = menu.selectedIds || []; + const allDisabled = ids.every(id => nodes.find(n => n.id === id)?.data?.disabled); + + return [ + { + label: allDisabled ? `Enable ${ids.length} Nodes` : `Disable ${ids.length} Nodes`, + onClick: () => handleBatchDisable(ids) + }, + { + label: `Archive ${ids.length} Nodes`, + onClick: () => handleBatchArchive(ids) + }, + { + label: `Delete ${ids.length} Nodes`, + danger: true, + onClick: () => handleBatchDelete(ids) + } + ]; + })() : menu.type === 'node' ? (() => { const targetNode = nodes.find(n => n.id === menu.id); const isDisabled = targetNode?.data?.disabled; @@ -210,7 +338,7 @@ function Flow() { onClick: () => menu.id && deleteBranch(menu.id) } ]; - })() : (() => { + })() : menu.type === 'edge' ? (() => { // Check if any node connected to this edge is disabled const targetEdge = edges.find(e => e.id === menu.id); const sourceNode = nodes.find(n => n.id === targetEdge?.source); @@ -230,14 +358,19 @@ function Flow() { label: 'Delete Branch', danger: true, onClick: () => menu.id && deleteBranch(undefined, menu.id) + }, + { + label: 'Delete Trace', + danger: true, + onClick: () => menu.id && deleteTrace(menu.id) } ]; - })() + })() : [] } /> )} </div> - <Sidebar isOpen={isRightOpen} onToggle={() => setIsRightOpen(!isRightOpen)} /> + <Sidebar isOpen={isRightOpen} onToggle={() => setIsRightOpen(!isRightOpen)} onInteract={closeMenu} /> </div> ); } |
