From bd8011dab889e351a9014e71d2fe28c56d9f1acf Mon Sep 17 00:00:00 2001 From: mlogclub Date: Mon, 27 Jul 2026 13:47:53 +0800 Subject: [PATCH] refactor workflow --- internal/ai/workflow/dsl/types.go | 20 + internal/ai/workflow/dsl/types_test.go | 30 + .../_components/workflow-run-audit-graph.tsx | 16 +- .../_components/editor/base-node.tsx | 76 ++ .../_components/editor/comment-node.tsx | 89 ++ .../editor/editor-canvas-events.tsx | 53 ++ .../_components/editor/editor-context.tsx | 49 ++ .../_components/editor/editor-provider.tsx | 151 ++++ .../_components/editor/editor-tools.tsx | 483 +++++++++++ .../_components/editor/line-add-button.tsx | 96 +++ .../_components/editor/node-form-panel.tsx | 438 ++++++++++ .../_components/editor/node-icon.tsx | 46 + .../_components/editor/node-panel.tsx | 66 ++ .../_components/editor/node-registry.tsx | 114 +++ .../_components/editor/on-drag-line-end.ts | 57 ++ .../ai-workflows/_components/editor/types.ts | 34 + .../_components/editor/use-port-click.ts | 69 ++ .../_components/editor/workflow-editor.tsx | 155 ++++ .../_components/editor/workflow-model.ts | 273 ++++++ .../_components/flowgram-editor-provider.tsx | 134 --- .../_components/flowgram-node-registries.tsx | 109 --- .../_components/flowgram-node-renderer.tsx | 49 -- .../_components/node-config-panel.tsx | 793 ------------------ .../_components/variable-selector.tsx | 45 - .../_components/workflow-branch-selection.tsx | 41 - .../_components/workflow-canvas-controls.tsx | 94 --- .../workflow-condition-node-content.tsx | 165 ---- .../_components/workflow-config-sidebar.tsx | 179 ---- .../_components/workflow-editor-status.tsx | 39 - .../_components/workflow-editor-toolbar.tsx | 78 -- .../_components/workflow-editor.tsx | 425 ---------- .../_components/workflow-line-add-button.tsx | 51 -- .../_components/workflow-node-card.tsx | 41 - .../_components/workflow-node-icon.tsx | 29 - .../_components/workflow-port-add-context.tsx | 32 - .../_components/workflow-port-node-menu.tsx | 104 --- .../_components/workflow-utils.test.mjs | 488 ----------- .../_components/workflow-utils.ts | 557 ------------ .../_components/workflow-workbench.tsx | 378 ++++++++- web/app/globals.css | 12 - web/app/layout.tsx | 1 + web/lib/api/admin.ts | 19 +- web/package.json | 5 + web/pnpm-lock.yaml | 106 +++ 44 files changed, 2785 insertions(+), 3504 deletions(-) create mode 100644 web/app/dashboard/ai-workflows/_components/editor/base-node.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/comment-node.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/editor-canvas-events.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/editor-context.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/editor-provider.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/editor-tools.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/line-add-button.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/node-form-panel.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/node-icon.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/node-panel.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/node-registry.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/on-drag-line-end.ts create mode 100644 web/app/dashboard/ai-workflows/_components/editor/types.ts create mode 100644 web/app/dashboard/ai-workflows/_components/editor/use-port-click.ts create mode 100644 web/app/dashboard/ai-workflows/_components/editor/workflow-editor.tsx create mode 100644 web/app/dashboard/ai-workflows/_components/editor/workflow-model.ts delete mode 100644 web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/flowgram-node-renderer.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/node-config-panel.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/variable-selector.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-branch-selection.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-canvas-controls.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-condition-node-content.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-editor-status.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-editor-toolbar.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-editor.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-line-add-button.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-node-card.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-node-icon.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-port-add-context.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-port-node-menu.tsx delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs delete mode 100644 web/app/dashboard/ai-workflows/_components/workflow-utils.ts diff --git a/internal/ai/workflow/dsl/types.go b/internal/ai/workflow/dsl/types.go index d193648..5f0cc57 100644 --- a/internal/ai/workflow/dsl/types.go +++ b/internal/ai/workflow/dsl/types.go @@ -7,6 +7,7 @@ const SchemaVersion = 2 type Definition struct { SchemaVersion int `json:"schemaVersion"` Nodes []Node `json:"nodes"` + Annotations []Node `json:"annotations,omitempty"` Edges []Edge `json:"edges"` } @@ -187,3 +188,22 @@ func (d *NodeData) UnmarshalJSON(data []byte) error { } return nil } + +func (d NodeData) MarshalJSON() ([]byte, error) { + type alias NodeData + base, err := json.Marshal(alias(d)) + if err != nil { + return nil, err + } + values := make(map[string]json.RawMessage) + if err := json.Unmarshal(base, &values); err != nil { + return nil, err + } + for key, value := range d.Extra { + if _, exists := values[key]; exists { + continue + } + values[key] = value + } + return json.Marshal(values) +} diff --git a/internal/ai/workflow/dsl/types_test.go b/internal/ai/workflow/dsl/types_test.go index 2b6f30e..e1e4b7c 100644 --- a/internal/ai/workflow/dsl/types_test.go +++ b/internal/ai/workflow/dsl/types_test.go @@ -2,6 +2,7 @@ package dsl_test import ( "encoding/json" + "strings" "testing" "agent-desk/internal/ai/workflow/dsl" @@ -81,3 +82,32 @@ func TestDefinitionUnmarshalsFlowGramStyleSchema(t *testing.T) { t.Fatalf("unexpected edge: %#v", edge) } } + +func TestDefinitionPreservesCanvasAnnotations(t *testing.T) { + var def dsl.Definition + err := json.Unmarshal([]byte(`{ + "schemaVersion": 2, + "nodes": [], + "annotations": [{ + "id": "comment_1", + "type": "comment", + "meta": {"position": {"x": 12, "y": 34}}, + "data": {"note": "check this branch", "size": {"width": 240, "height": 150}} + }], + "edges": [] + }`), &def) + if err != nil { + t.Fatalf("unmarshal definition: %v", err) + } + if len(def.Annotations) != 1 || def.Annotations[0].ID != "comment_1" { + t.Fatalf("expected annotation to be preserved, got %#v", def.Annotations) + } + encoded, err := json.Marshal(def) + if err != nil { + t.Fatalf("marshal definition: %v", err) + } + if !strings.Contains(string(encoded), `"annotations"`) || + !strings.Contains(string(encoded), `"check this branch"`) { + t.Fatalf("expected annotation JSON to round trip, got %s", encoded) + } +} diff --git a/web/app/dashboard/ai-workflow-runs/_components/workflow-run-audit-graph.tsx b/web/app/dashboard/ai-workflow-runs/_components/workflow-run-audit-graph.tsx index 1d457f3..183e4af 100644 --- a/web/app/dashboard/ai-workflow-runs/_components/workflow-run-audit-graph.tsx +++ b/web/app/dashboard/ai-workflow-runs/_components/workflow-run-audit-graph.tsx @@ -2,7 +2,6 @@ import { useMemo, useState } from "react" -import { EditorRenderer, FreeLayoutEditorProvider } from "@flowgram.ai/free-layout-editor" import { AlertTriangleIcon, CheckCircle2Icon, TimerIcon } from "lucide-react" import { JsonTreeViewer } from "@/components/json-tree-viewer" @@ -11,26 +10,21 @@ import { ScrollArea } from "@/components/ui/scroll-area" import type { AIWorkflowNodeRun, AIWorkflowRun } from "@/lib/api/admin" import { cn } from "@/lib/utils" -import { useFlowgramEditorProps } from "../../ai-workflows/_components/flowgram-editor-provider" +import { WorkflowReadonlyCanvas } from "../../ai-workflows/_components/editor/workflow-editor" export function WorkflowRunAuditGraph({ run }: { run: AIWorkflowRun }) { - const nodeRuns = run.nodes ?? [] + const nodeRuns = useMemo(() => run.nodes ?? [], [run.nodes]) const firstNodeId = nodeRuns[0]?.nodeId ?? run.definition?.nodes?.[0]?.id ?? "" const [selectedNodeId, setSelectedNodeId] = useState(firstNodeId) const selectedNodeRun = nodeRuns.find((item) => item.nodeId === selectedNodeId) ?? nodeRuns[0] const executedNodeIds = useMemo(() => new Set(nodeRuns.map((item) => item.nodeId)), [nodeRuns]) - const editorProps = useFlowgramEditorProps({ - definition: run.definition ?? { schemaVersion: 2, nodes: [], edges: [] }, - nodeSpecs: [], - readonly: true, - }) return (
- - - +
diff --git a/web/app/dashboard/ai-workflows/_components/editor/base-node.tsx b/web/app/dashboard/ai-workflows/_components/editor/base-node.tsx new file mode 100644 index 0000000..f681a3e --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/editor/base-node.tsx @@ -0,0 +1,76 @@ +"use client" + +/* eslint-disable react-hooks/refs -- FlowGram useNodeRender exposes reactive render state through a ref-backed adapter. */ + +import { useLayoutEffect, useState } from "react" + +import { + WorkflowPortRender, + type WorkflowNodeProps, + useNodeRender, +} from "@flowgram.ai/free-layout-editor" +import { usePanelManager } from "@flowgram.ai/panel-manager-plugin" + +import { cn } from "@/lib/utils" + +import { WorkflowEditorSurfaceProvider } from "./editor-context" +import { usePortClick } from "./use-port-click" + +export const NODE_FORM_PANEL = "workflow-node-form" + +export function BaseNode(props: WorkflowNodeProps) { + const render = useNodeRender(props.node) + const panelManager = usePanelManager() + const onPortClick = usePortClick() + const [dragging, setDragging] = useState(false) + + useLayoutEffect(() => { + if (String(render.node.flowNodeType) !== "condition") return + const frame = window.requestAnimationFrame(() => { + render.node.ports.updateDynamicPorts() + }) + return () => window.cancelAnimationFrame(frame) + }, [render.data, render.node]) + + return ( + +
{ + render.startDrag(event) + setDragging(true) + }} + onTouchStart={(event) => { + render.startDrag(event as unknown as React.MouseEvent) + setDragging(true) + }} + onMouseUp={() => setDragging(false)} + onClick={(event) => { + render.selectNode(event) + if (!render.readonly && !dragging) { + panelManager.open(NODE_FORM_PANEL, "docked-right", { + props: { nodeId: render.node.id }, + }) + } + }} + onFocus={render.onFocus} + onBlur={render.onBlur} + > + {render.form?.render()} +
+ {render.ports.map((port) => ( + + ))} +
+ ) +} diff --git a/web/app/dashboard/ai-workflows/_components/editor/comment-node.tsx b/web/app/dashboard/ai-workflows/_components/editor/comment-node.tsx new file mode 100644 index 0000000..9e31e03 --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/editor/comment-node.tsx @@ -0,0 +1,89 @@ +"use client" + +/* eslint-disable react-hooks/refs -- FlowGram useNodeRender exposes reactive render state through a ref-backed adapter. */ + +import { useLayoutEffect } from "react" + +import { + Field, + FlowNodeFormData, + Form, + type FormModelV2, + type WorkflowNodeProps, + useNodeRender, +} from "@flowgram.ai/free-layout-editor" +import { Trash2Icon } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +type CommentSize = { + width?: number + height?: number +} + +export function CommentNode(props: WorkflowNodeProps) { + const render = useNodeRender(props.node) + const formModel = render.node + .getData(FlowNodeFormData) + .getFormModel() + const size = (formModel?.getValueIn("size") ?? {}) as CommentSize + const width = Math.max(120, Number(size.width) || 240) + const height = Math.max(80, Number(size.height) || 150) + + useLayoutEffect(() => { + render.node.transform.update({ + size: { width, height }, + }) + }, [height, render.node, width]) + + return ( +
+
+ {!render.readonly ? ( + + ) : null} +
+ name="note"> + {({ field }) => ( +