diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 9fa788b54..03f8a9ee8 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -120,9 +120,6 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; @@ -274,6 +271,8 @@ interface VideoCanvasProjectExportPayload { interface VideoCanvasInsertMenuState { screenPosition: Position; canvasPosition: Position; + sourceNodeId?: string | null; + openLeft?: boolean; } interface VideoCanvasInsertMenuChild { @@ -1430,6 +1429,7 @@ export default function CanvasEditorPage({ params }: PageProps) { }); const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] = useState(null); + const videoCanvasInsertMenuRef = useRef(null); const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] = useState(false); const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] = @@ -2708,6 +2708,31 @@ export default function CanvasEditorPage({ params }: PageProps) { return () => window.removeEventListener("resize", updateViewportWidth); }, []); + useEffect(() => { + if (!videoCanvasInsertMenu) return; + + const handlePointerDown = (event: PointerEvent) => { + const menu = videoCanvasInsertMenuRef.current; + if (menu && event.target instanceof Node && menu.contains(event.target)) { + return; + } + setVideoCanvasInsertMenu(null); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setVideoCanvasInsertMenu(null); + } + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [videoCanvasInsertMenu]); + useEffect(() => { if (isAgentMode) { setIsPanelOpen(true); @@ -5714,15 +5739,18 @@ export default function CanvasEditorPage({ params }: PageProps) { nodeConfig: Parameters[0], options?: { connectFromSelection?: boolean; + connectFromNodeId?: string | null; selectNewNode?: boolean; } ) => { const connectFromSelection = options?.connectFromSelection ?? true; const selectNewNode = options?.selectNewNode ?? true; + const explicitSourceNodeId = options?.connectFromNodeId || null; const nodeId = addNode(nodeConfig); const sourceNodeId = - connectFromSelection && isVideoMode && selectedNodeIds.length === 1 - ? selectedNodeIds[0] || null + connectFromSelection && isVideoMode + ? explicitSourceNodeId || + (selectedNodeIds.length === 1 ? selectedNodeIds[0] || null : null) : null; if (sourceNodeId && sourceNodeId !== nodeId) { addEdge({ @@ -5745,6 +5773,7 @@ export default function CanvasEditorPage({ params }: PageProps) { options?: { position?: Position; connectFromSelection?: boolean; + connectFromNodeId?: string | null; selectNewNode?: boolean; } ) => { @@ -5754,6 +5783,7 @@ export default function CanvasEditorPage({ params }: PageProps) { options?.position || getVideoWorkflowPlacement(definition.size); createCanvasNode(buildVideoWorkflowCanvasNode(templateType, placement), { connectFromSelection: options?.connectFromSelection ?? true, + connectFromNodeId: options?.connectFromNodeId ?? null, selectNewNode: options?.selectNewNode ?? true, }); }, @@ -5773,7 +5803,21 @@ export default function CanvasEditorPage({ params }: PageProps) { if (!videoCanvasInsertMenu) return; const definition = getVideoWorkflowNodeDefinition(templateType); - const anchoredPosition = definition + const sourceNode = videoCanvasInsertMenu.sourceNodeId + ? nodes.find((node) => node.id === videoCanvasInsertMenu.sourceNodeId) + : null; + const anchoredPosition = + sourceNode && definition + ? { + x: videoCanvasInsertMenu.openLeft + ? sourceNode.position.x - definition.size.width - 180 + : sourceNode.position.x + sourceNode.size.width + 180, + y: + sourceNode.position.y + + sourceNode.size.height / 2 - + definition.size.height / 2, + } + : definition ? { x: videoCanvasInsertMenu.canvasPosition.x - @@ -5786,11 +5830,12 @@ export default function CanvasEditorPage({ params }: PageProps) { handleAddWorkflowNode(templateType, { position: anchoredPosition, - connectFromSelection: false, + connectFromSelection: Boolean(sourceNode), + connectFromNodeId: sourceNode?.id || null, }); setVideoCanvasInsertMenu(null); }, - [handleAddWorkflowNode, videoCanvasInsertMenu] + [handleAddWorkflowNode, nodes, videoCanvasInsertMenu] ); const handleCreateSelectedAgentResultBranches = useCallback(async () => { @@ -8574,125 +8619,120 @@ export default function CanvasEditorPage({ params }: PageProps) { /> - {isVideoMode && videoCanvasInsertMenu ? ( - { - if (!open) { - setVideoCanvasInsertMenu(null); - } - }} - open={Boolean(videoCanvasInsertMenu)} - > - - + ); + const renderInsertItem = ( + item: VideoCanvasInsertMenuItem, + highlighted = false + ) => { const Icon = item.icon; + const textClassName = highlighted + ? "text-orange-600 dark:text-orange-300" + : "text-slate-700 dark:text-slate-100"; + const iconClassName = highlighted + ? "text-orange-500" + : "text-slate-400 dark:text-zinc-500"; + if (item.children?.length) { return ( - - - - {item.label} - - - {item.children.map((child) => ( - - handleInsertWorkflowNodeFromMenu( - child.templateType - ) - } - > - {child.label} - - ))} - - +
+
+ + {item.label} + +
+
+ {item.children.map(renderChildButton)} +
+
); } return ( - + onClick={() => item.templateType && handleInsertWorkflowNodeFromMenu(item.templateType) } + type="button" > - + {item.label} - + ); - })} + }; + const highlightedItem = VIDEO_CANVAS_INSERT_MENU_ITEMS.at(-1); - + return ( +
event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + ref={videoCanvasInsertMenuRef} + style={{ + left: `${menuLeft}px`, + top: `${menuTop}px`, + width: `${menuWidth}px`, + }} + > +
+ @{siteSettingsData?.siteName || "视频画布"} +
- {(() => { - const highlightedItem = VIDEO_CANVAS_INSERT_MENU_ITEMS.at(-1); - if (!highlightedItem) return null; - const HighlightIcon = highlightedItem.icon; - return highlightedItem.children?.length ? ( - - - - {highlightedItem.label} - - - {highlightedItem.children.map((child) => ( - - handleInsertWorkflowNodeFromMenu(child.templateType) - } - > - {child.label} - - ))} - - - ) : ( - - highlightedItem.templateType && - handleInsertWorkflowNodeFromMenu( - highlightedItem.templateType - ) - } - > - - {highlightedItem.label} - - ); - })()} - - - ) : null} +
+ {VIDEO_CANVAS_INSERT_MENU_ITEMS.slice(0, -1).map((item) => + renderInsertItem(item) + )} +
+ + {highlightedItem ? ( + <> +
+ {renderInsertItem(highlightedItem, true)} + + ) : null} +
+ ); + })() + : null} {isVideoMode && isVideoModuleSidebarEnabled ? (
+ onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); setVideoCanvasInsertMenu({ screenPosition: { x: selectedWorkflowQuickConnectAnchor.x, @@ -8944,8 +8986,10 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedWorkflowQuickConnectSourceNode.position.y + selectedWorkflowQuickConnectSourceNode.size.height / 2, }, - }) - } + sourceNodeId: selectedWorkflowQuickConnectSourceNode.id, + openLeft: selectedWorkflowQuickConnectAnchor.openLeft, + }); + }} type="button" > @@ -8959,12 +9003,37 @@ export default function CanvasEditorPage({ params }: PageProps) { ? -1 : 1; const Icon = getVideoWorkflowQuickConnectIcon(item.type); + const definition = getVideoWorkflowNodeDefinition(item.type); return ( +
+
+ - + - + - + - + +
-
+
- + {formatSavedAt(lastSavedAt)}