From f97b7a1bfa220a0947f2cd63c23f4faa9fcd42e7 Mon Sep 17 00:00:00 2001 From: blackhao <13851610112@163.com> Date: Mon, 8 Dec 2025 15:07:12 -0600 Subject: merge logic --- frontend/src/App.tsx | 183 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 158 insertions(+), 25 deletions(-) (limited to 'frontend/src/App.tsx') 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(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 ( -
+
setIsLeftOpen(!isLeftOpen)} />
{ 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} > - - - + + + - +
+ + + +
@@ -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) } ]; - })() + })() : [] } /> )}
- setIsRightOpen(!isRightOpen)} /> + setIsRightOpen(!isRightOpen)} onInteract={closeMenu} />
); } -- cgit v1.2.3