From eaffca5bad5bd965acd78ebd227c323ebf518990 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sun, 28 Jun 2026 20:22:18 +0800 Subject: [PATCH] feat: add WorkflowLineAddButton and WorkflowLineNodePanel components for enhanced workflow editing --- .../_components/flowgram-editor-provider.tsx | 15 +++ .../_components/workflow-line-add-button.tsx | 68 ++++++++++++++ .../_components/workflow-line-node-panel.tsx | 94 +++++++++++++++++++ web/package.json | 2 + web/pnpm-lock.yaml | 24 +++++ 5 files changed, 203 insertions(+) create mode 100644 web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/workflow-line-node-panel.tsx 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 eb467d9..a1d0a50 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx @@ -8,6 +8,8 @@ import { type WorkflowNodeEntity, 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" @@ -15,6 +17,8 @@ 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, @@ -84,6 +88,17 @@ export function useFlowgramEditorProps({ } }, plugins: () => [ + createFreeLinesPlugin({ + renderInsideLine: WorkflowLineAddButton, + }), + createFreeNodePanelPlugin({ + renderer: (props) => ( + + ), + }), createMinimapPlugin({ disableLayer: true, }), 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 new file mode 100644 index 0000000..59e9269 --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx @@ -0,0 +1,68 @@ +"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" + +export function WorkflowLineAddButton({ + line, + selected, + hovered, + color, +}: LineRenderProps) { + const playground = usePlayground() + const nodePanelService = useService(WorkflowNodePanelService) + 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 + } + + return ( + + ) +} 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 new file mode 100644 index 0000000..b0b3dbc --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/workflow-line-node-panel.tsx @@ -0,0 +1,94 @@ +"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/package.json b/web/package.json index 5dde145..29e068a 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,8 @@ "@dnd-kit/sortable": "^10.0.0", "@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 b92670d..4b9c416 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@flowgram.ai/free-layout-editor': 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-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)) @@ -619,6 +625,12 @@ 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: @@ -5260,6 +5272,18 @@ 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)