diff --git a/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx b/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx index 70f6493..dff3f43 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx @@ -9,7 +9,6 @@ import { type WorkflowJSON, } from "@flowgram.ai/free-layout-editor" import { createFreeLinesPlugin } from "@flowgram.ai/free-lines-plugin" -import { createFreeNodePanelPlugin } from "@flowgram.ai/free-node-panel-plugin" import { createFreeSnapPlugin } from "@flowgram.ai/free-snap-plugin" import { createMinimapPlugin } from "@flowgram.ai/minimap-plugin" @@ -18,7 +17,6 @@ import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" import { FlowgramNodeRenderer } from "./flowgram-node-renderer" import { buildFlowgramNodeRegistries } from "./flowgram-node-registries" import { WorkflowLineAddButton } from "./workflow-line-add-button" -import { WorkflowLineNodePanel } from "./workflow-line-node-panel" import { normalizeConditionPortsForFlowgram, syncConditionBranchTargetsFromEdges, @@ -91,14 +89,6 @@ export function useFlowgramEditorProps({ createFreeLinesPlugin({ renderInsideLine: WorkflowLineAddButton, }), - createFreeNodePanelPlugin({ - renderer: (props) => ( - - ), - }), createMinimapPlugin({ disableLayer: true, }), diff --git a/web/app/dashboard/ai-workflows/_components/flowgram-node-renderer.tsx b/web/app/dashboard/ai-workflows/_components/flowgram-node-renderer.tsx index cb40de4..df1d4b2 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-node-renderer.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-node-renderer.tsx @@ -40,7 +40,7 @@ export function FlowgramNodeRenderer(props: WorkflowNodeProps) { return } event.stopPropagation() - requestPortAdd?.({ port, event }) + requestPortAdd?.({ sourcePort: port, event }) }} > {form?.render()} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx index 6f7c5f9..3ba8acd 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx @@ -6,6 +6,7 @@ import { EditorRenderer, FreeLayoutEditorProvider, WorkflowDocument, + type WorkflowLineEntity, WorkflowLinesManager, WorkflowSelectService, type WorkflowJSON, @@ -203,7 +204,13 @@ function WorkflowEditorInner({ const selectService = useService(WorkflowSelectService) const editorRootRef = useRef(null) const [autoLayouting, setAutoLayouting] = useState(false) - const [nodeMenu, setNodeMenu] = useState<(WorkflowPortNodeMenuState & { port: WorkflowPortEntity }) | null>(null) + const [nodeMenu, setNodeMenu] = useState<( + WorkflowPortNodeMenuState & { + sourcePort: WorkflowPortEntity + targetPort?: WorkflowPortEntity + line?: WorkflowLineEntity + } + ) | null>(null) const zoomPercent = `${Math.round(playgroundTools.zoom * 100)}%` useEffect(() => { @@ -223,7 +230,9 @@ function WorkflowEditorInner({ const openNodeMenuFromPort = useCallback((request: WorkflowPortAddRequest) => { const rootRect = editorRootRef.current?.getBoundingClientRect() setNodeMenu({ - port: request.port, + sourcePort: request.sourcePort, + targetPort: request.targetPort, + line: request.line, x: rootRect ? request.event.clientX - rootRect.left + 10 : request.event.clientX, y: rootRect ? request.event.clientY - rootRect.top - 10 : request.event.clientY, }) @@ -233,11 +242,11 @@ function WorkflowEditorInner({ if (!nodeMenu) { return } - const sourcePort = nodeMenu.port + const sourcePort = nodeMenu.sourcePort const nextNode = createWorkflowNodeFromSpec( spec, context.document.toJSON().nodes ?? definition.nodes, - nextNodePositionFromPort(sourcePort) + nextNodePositionFromAddMenu(nodeMenu) ) const created = workflowDocument.createWorkflowNodeByType( spec.type, @@ -250,6 +259,17 @@ function WorkflowEditorInner({ to: created.id, toPort: "", }) + if (nodeMenu.targetPort) { + linesManager.createLine({ + from: created.id, + fromPort: "", + to: nodeMenu.targetPort.node.id, + toPort: nodeMenu.targetPort.portID, + }) + if (nodeMenu.line && !nodeMenu.line.disposed) { + nodeMenu.line.dispose() + } + } setNodeMenu(null) await selectService.selectNodeAndScrollToView(created) onSelectNode(created.id) @@ -365,9 +385,21 @@ function WorkflowEditorInner({ ) } -function nextNodePositionFromPort(port: WorkflowPortEntity) { +function nextNodePositionFromAddMenu( + menu: WorkflowPortNodeMenuState & { + sourcePort: WorkflowPortEntity + targetPort?: WorkflowPortEntity + line?: WorkflowLineEntity + } +) { + if (menu.line && !menu.line.disposed) { + return { + x: menu.line.center.labelX, + y: menu.line.center.labelY - 40, + } + } return { - x: port.point.x + 120, - y: port.point.y - 40, + x: menu.sourcePort.point.x + 120, + y: menu.sourcePort.point.y - 40, } } diff --git a/web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx b/web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx index ac67f7c..3c48686 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx @@ -1,11 +1,10 @@ "use client" -import { useCallback } from "react" - import type { LineRenderProps } from "@flowgram.ai/free-lines-plugin" -import { WorkflowNodePanelService } from "@flowgram.ai/free-node-panel-plugin" import { PlusIcon } from "lucide-react" -import { usePlayground, useService } from "@flowgram.ai/free-layout-editor" +import { usePlayground } from "@flowgram.ai/free-layout-editor" + +import { useWorkflowPortAdd } from "./workflow-port-add-context" export function WorkflowLineAddButton({ line, @@ -14,34 +13,10 @@ export function WorkflowLineAddButton({ color, }: LineRenderProps) { const playground = usePlayground() - const nodePanelService = useService(WorkflowNodePanelService) + const requestPortAdd = useWorkflowPortAdd() const { fromPort, toPort } = line const visible = !line.disposed && !playground.config.readonly && (selected || hovered) - const openNodePanel = useCallback(() => { - if (!fromPort || !toPort) { - return - } - void nodePanelService.call({ - panelPosition: { - x: line.center.labelX, - y: line.center.labelY, - }, - fromPort, - toPort, - panelProps: { - fromPort, - }, - enableBuildLine: true, - enableAutoOffset: true, - afterAddNode: (node) => { - if (node && !line.disposed) { - line.dispose() - } - }, - }) - }, [fromPort, line, nodePanelService, toPort]) - if (!visible) { return null } @@ -59,7 +34,15 @@ export function WorkflowLineAddButton({ data-line-id={line.id} onClick={(event) => { event.stopPropagation() - openNodePanel() + if (!fromPort || !toPort) { + return + } + requestPortAdd?.({ + sourcePort: fromPort, + targetPort: toPort, + line, + event, + }) }} > diff --git a/web/app/dashboard/ai-workflows/_components/workflow-line-node-panel.tsx b/web/app/dashboard/ai-workflows/_components/workflow-line-node-panel.tsx deleted file mode 100644 index b0b3dbc..0000000 --- a/web/app/dashboard/ai-workflows/_components/workflow-line-node-panel.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client" - -import { useEffect, useMemo, useRef } from "react" - -import type { NodePanelRenderProps } from "@flowgram.ai/free-node-panel-plugin" -import { useClientContext } from "@flowgram.ai/free-layout-editor" - -import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" -import { WorkflowNodeIcon } from "./workflow-node-icon" -import { createWorkflowNodeFromSpec } from "./workflow-utils" - -export function WorkflowLineNodePanel({ - position, - onSelect, - onClose, - nodeSpecs, -}: NodePanelRenderProps & { - nodeSpecs: AIWorkflowNodeSpec[] -}) { - const context = useClientContext() - const panelRef = useRef(null) - const insertableNodeSpecs = useMemo( - () => nodeSpecs.filter((spec) => spec.type !== "start" && spec.type !== "end"), - [nodeSpecs] - ) - - useEffect(() => { - const closeOnPointerDown = (event: PointerEvent) => { - const target = event.target - if (target instanceof Node && panelRef.current?.contains(target)) { - return - } - onClose() - } - const closeOnEscape = (event: KeyboardEvent) => { - if (event.key === "Escape") { - onClose() - } - } - window.addEventListener("pointerdown", closeOnPointerDown) - window.addEventListener("keydown", closeOnEscape) - return () => { - window.removeEventListener("pointerdown", closeOnPointerDown) - window.removeEventListener("keydown", closeOnEscape) - } - }, [onClose]) - - return ( -
event.stopPropagation()} - > -
- 插入节点 -
-
- {insertableNodeSpecs.map((spec) => ( - - ))} -
-
- ) -} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-port-add-context.tsx b/web/app/dashboard/ai-workflows/_components/workflow-port-add-context.tsx index 96e9ec6..6024d8c 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-port-add-context.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-port-add-context.tsx @@ -2,11 +2,13 @@ import { createContext, useContext } from "react" -import type { WorkflowPortEntity } from "@flowgram.ai/free-layout-editor" +import type { WorkflowLineEntity, WorkflowPortEntity } from "@flowgram.ai/free-layout-editor" export type WorkflowPortAddRequest = { - port: WorkflowPortEntity - event: React.MouseEvent + sourcePort: WorkflowPortEntity + targetPort?: WorkflowPortEntity + line?: WorkflowLineEntity + event: React.MouseEvent } const WorkflowPortAddContext = createContext<((request: WorkflowPortAddRequest) => void) | null>(null) diff --git a/web/package.json b/web/package.json index 29e068a..20d32bc 100644 --- a/web/package.json +++ b/web/package.json @@ -18,7 +18,6 @@ "@dnd-kit/utilities": "^3.2.2", "@flowgram.ai/free-layout-editor": "1.0.11", "@flowgram.ai/free-lines-plugin": "1.0.11", - "@flowgram.ai/free-node-panel-plugin": "1.0.11", "@flowgram.ai/free-snap-plugin": "1.0.11", "@flowgram.ai/minimap-plugin": "1.0.11", "@hookform/resolvers": "^5.2.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4b9c416..ea4cb3c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: '@flowgram.ai/free-lines-plugin': specifier: 1.0.11 version: 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(styled-components@6.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@flowgram.ai/free-node-panel-plugin': - specifier: 1.0.11 - version: 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@flowgram.ai/free-snap-plugin': specifier: 1.0.11 version: 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(styled-components@6.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) @@ -625,12 +622,6 @@ packages: react-dom: '>=16.8' styled-components: '>=5' - '@flowgram.ai/free-node-panel-plugin@1.0.11': - resolution: {integrity: sha512-1XqzHZhimJLK6G3zUhHUKBHgLlnWNRmWYmpCci0e9yKzOBduS6xZTfMSgc7bl5TeaG9u++nooTqirx6xP5kwXQ==} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - '@flowgram.ai/free-snap-plugin@1.0.11': resolution: {integrity: sha512-6YPtxwIYkUgj2BBBQgJvIPeSgDZQJpHSDl9r6FyJyalAdQKHeKniCBAR8b3tgaQf/oKR1v/8zt01h+IzMFhodQ==} peerDependencies: @@ -5272,18 +5263,6 @@ snapshots: reflect-metadata: 0.2.2 styled-components: 6.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@flowgram.ai/free-node-panel-plugin@1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@flowgram.ai/core': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@flowgram.ai/document': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@flowgram.ai/free-history-plugin': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@flowgram.ai/free-layout-core': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@flowgram.ai/utils': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - inversify: 6.2.2(reflect-metadata@0.2.2) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - reflect-metadata: 0.2.2 - '@flowgram.ai/free-snap-plugin@1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(styled-components@6.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: '@flowgram.ai/core': 1.0.11(react-dom@19.2.3(react@19.2.3))(react@19.2.3)