From 1772c41f1899472f50810658f6664752eeb6f29a Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 9 Jun 2026 10:50:15 +0800 Subject: [PATCH] Enhance AssistantWorkflowPage with workflow node management and styling Updated the AssistantWorkflowPage to include a comprehensive node catalog for workflow management, allowing users to add and connect various node types. Introduced new styles for the workflow editor in globals.css to improve UI consistency and visual appeal. Enhanced state management for node connections and saving workflows, providing a more intuitive user experience. --- src/app/globals.css | 53 +++ .../pages/AssistantWorkflowPage.tsx | 427 +++++++++++++++--- 2 files changed, 410 insertions(+), 70 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 8e6077f..ac88ff1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -232,4 +232,57 @@ letter-spacing: 0.08em; text-transform: uppercase; } + + .workflow-editor { + --xy-controls-button-background-color: var(--card); + --xy-controls-button-background-color-hover: var(--surface-strong); + --xy-controls-button-color: var(--foreground); + --xy-controls-button-color-hover: var(--foreground); + --xy-controls-button-border-color: var(--hairline); + --xy-controls-box-shadow: none; + } + + .workflow-editor .react-flow__controls { + overflow: hidden; + border: 1px solid var(--hairline); + border-radius: 14px; + background-color: var(--card); + box-shadow: 0 8px 24px color-mix(in srgb, var(--foreground) 8%, transparent); + backdrop-filter: blur(12px); + } + + .workflow-editor .react-flow__controls-button { + border-bottom-color: var(--hairline); + background-color: var(--card); + color: var(--foreground); + } + + .workflow-editor .react-flow__controls-button:hover { + background-color: var(--surface-strong); + } + + .workflow-editor .react-flow__controls-button svg { + fill: currentColor; + color: currentColor; + } + + .workflow-editor .react-flow__edge-path { + stroke: var(--muted-soft); + } + + .workflow-editor .react-flow__edge.selected .react-flow__edge-path { + stroke: var(--foreground); + } + + .workflow-editor .workflow-handle { + width: 11px; + height: 11px; + border: 2px solid var(--card); + background: var(--muted-soft); + box-shadow: 0 0 0 1px var(--hairline-strong); + } + + .workflow-editor .workflow-handle:hover { + background: var(--foreground); + } } diff --git a/src/components/pages/AssistantWorkflowPage.tsx b/src/components/pages/AssistantWorkflowPage.tsx index dc8e9e5..00c3b1b 100644 --- a/src/components/pages/AssistantWorkflowPage.tsx +++ b/src/components/pages/AssistantWorkflowPage.tsx @@ -1,55 +1,160 @@ "use client"; -import { useCallback } from "react"; +import { useCallback, useMemo, useState, type ComponentType } from "react"; import { addEdge, Background, + BackgroundVariant, Connection, Controls, Edge, - MiniMap, + Handle, Node, + NodeProps, + Position, ReactFlow, useEdgesState, useNodesState, } from "@xyflow/react"; -import { ChevronLeft, Plus, Save } from "lucide-react"; +import { + Bot, + Brain, + ChevronLeft, + CircleStop, + Database, + GitBranch, + MessageSquareText, + Save, + Sparkles, + Wrench, +} from "lucide-react"; import { Button } from "@/components/ui/button"; -const initialNodes: Node[] = [ +type WorkflowNodeKind = "start" | "intent" | "knowledge" | "answer" | "tool" | "end"; + +type WorkflowNodeData = { + label: string; + description: string; + kind: WorkflowNodeKind; +}; + +type WorkflowNode = Node; + +type NodeCatalogItem = { + kind: WorkflowNodeKind; + label: string; + description: string; + icon: ComponentType<{ size?: number; className?: string }>; +}; + +const nodeCatalog: NodeCatalogItem[] = [ + { + kind: "intent", + label: "意图识别", + description: "识别用户目标并进入对应分支", + icon: GitBranch, + }, + { + kind: "knowledge", + label: "知识库检索", + description: "从已配置的知识库获取上下文", + icon: Database, + }, + { + kind: "answer", + label: "模型回答", + description: "调用大模型生成回复内容", + icon: Brain, + }, + { + kind: "tool", + label: "工具调用", + description: "调用外部工具完成具体任务", + icon: Wrench, + }, +]; + +const nodeMeta: Record< + WorkflowNodeKind, + { + icon: ComponentType<{ size?: number; className?: string }>; + caption: string; + accent: string; + } +> = { + start: { + icon: Sparkles, + caption: "入口节点", + accent: "var(--gradient-mint)", + }, + intent: { + icon: GitBranch, + caption: "判断节点", + accent: "var(--gradient-lavender)", + }, + knowledge: { + icon: Database, + caption: "资源节点", + accent: "var(--gradient-sky)", + }, + answer: { + icon: Brain, + caption: "生成节点", + accent: "var(--gradient-peach)", + }, + tool: { + icon: Wrench, + caption: "执行节点", + accent: "var(--gradient-rose)", + }, + end: { + icon: CircleStop, + caption: "结束节点", + accent: "var(--muted-soft)", + }, +}; + +const initialNodes: WorkflowNode[] = [ { id: "start", - type: "input", - position: { x: 80, y: 180 }, - data: { label: "开始" }, + type: "workflow", + position: { x: 50, y: 190 }, + data: { + label: "开始", + description: "接收用户消息并启动工作流", + kind: "start", + }, }, { id: "answer", - position: { x: 340, y: 180 }, - data: { label: "模型回答" }, + type: "workflow", + position: { x: 390, y: 190 }, + data: { + label: "模型回答", + description: "根据上下文生成自然语言回复", + kind: "answer", + }, }, { id: "end", - type: "output", - position: { x: 600, y: 180 }, - data: { label: "结束" }, + type: "workflow", + position: { x: 730, y: 190 }, + data: { + label: "结束", + description: "完成当前对话流程", + kind: "end", + }, }, ]; const initialEdges: Edge[] = [ - { - id: "start-answer", - source: "start", - target: "answer", - }, - { - id: "answer-end", - source: "answer", - target: "end", - }, + { id: "start-answer", source: "start", target: "answer" }, + { id: "answer-end", source: "answer", target: "end" }, ]; +const nodeTypes = { workflow: WorkflowNodeCard }; + type AssistantWorkflowPageProps = { workflowName?: string; onBack?: () => void; @@ -61,63 +166,101 @@ export function AssistantWorkflowPage({ }: AssistantWorkflowPageProps = {}) { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const [saved, setSaved] = useState(false); const handleConnect = useCallback( (connection: Connection) => { - setEdges((currentEdges) => addEdge(connection, currentEdges)); + setEdges((currentEdges) => + addEdge( + { + ...connection, + type: "smoothstep", + animated: true, + }, + currentEdges, + ), + ); + setSaved(false); }, [setEdges], ); - const handleAddNode = () => { - const id = crypto.randomUUID(); + const handleAddNode = useCallback( + (item: NodeCatalogItem = nodeCatalog[2]) => { + const id = crypto.randomUUID(); - setNodes((currentNodes) => [ - ...currentNodes, - { - id, - position: { - x: 200 + currentNodes.length * 40, - y: 100 + currentNodes.length * 40, + setNodes((currentNodes) => [ + ...currentNodes, + { + id, + type: "workflow", + position: { + x: 250 + (currentNodes.length % 3) * 300, + y: 90 + Math.floor(currentNodes.length / 3) * 210, + }, + data: { + label: item.label, + description: item.description, + kind: item.kind, + }, }, - data: { - label: `新节点 ${currentNodes.length + 1}`, - }, - }, - ]); - }; + ]); + setSaved(false); + }, + [setNodes], + ); const handleSave = () => { - const workflow = { nodes, edges }; - - localStorage.setItem("assistant-workflow", JSON.stringify(workflow)); - - console.log("Saved workflow:", workflow); + localStorage.setItem( + "assistant-workflow", + JSON.stringify({ nodes, edges }), + ); + setSaved(true); }; + const defaultEdgeOptions = useMemo( + () => ({ + type: "smoothstep", + animated: true, + style: { stroke: "var(--muted-soft)", strokeWidth: 1.5 }, + }), + [], + ); + return ( -
-
-
-

{workflowName}

-

- 拖动节点,并从节点连接点拖出连线。 -

+
+
+
+
+ + 工作流助手 + / + {workflowName} +
+

+ {workflowName} +

-
+
+
+ + {saved ? "已保存到本地" : "有未保存更改"} +
+ {onBack ? ( - ) : null} - -
-
- - - - - +
+ + +
+
+
+ + { + onNodesChange(changes); + setSaved(false); + }} + onEdgesChange={(changes) => { + onEdgesChange(changes); + setSaved(false); + }} + onConnect={handleConnect} + defaultEdgeOptions={defaultEdgeOptions} + fitView + minZoom={0.45} + maxZoom={1.6} + proOptions={{ hideAttribution: true }} + > + + + + +
+ 选中节点后按 Backspace 删除 · 滚轮缩放 · 拖动画布移动 +
+
); } + +function WorkflowNodeCard({ data, selected }: NodeProps) { + const meta = nodeMeta[data.kind]; + const Icon = meta.icon; + const hasTarget = data.kind !== "start"; + const hasSource = data.kind !== "end"; + + return ( +
+ {hasTarget ? ( + + ) : null} + +
+ +
+
+ +
+ +
+
{meta.caption}
+
+ {data.label} +
+
+ + +
+ +

+ {data.description} +

+ + {hasSource ? ( + + ) : null} +
+ ); +}