From 172ef4c099421938e650613fcb3dbf6cf314f9ba Mon Sep 17 00:00:00 2001 From: v-zhangjc9 Date: Fri, 20 Jun 2025 18:15:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=A2=9E=E5=8A=A0=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=AE=9A=E4=B9=89=E5=9F=BA=E6=9C=AC=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service-web/client/package.json | 4 +- service-web/client/pnpm-lock.yaml | 84 ++++++ service-web/client/src/pages/Test.tsx | 383 +++++++++++++++++++++++++- 3 files changed, 458 insertions(+), 13 deletions(-) diff --git a/service-web/client/package.json b/service-web/client/package.json index e391062..23fadc3 100644 --- a/service-web/client/package.json +++ b/service-web/client/package.json @@ -16,6 +16,7 @@ "@fortawesome/fontawesome-free": "^6.7.2", "@lightenna/react-mermaid-diagram": "^1.0.20", "@tinyflow-ai/react": "^0.2.1", + "@xyflow/react": "^12.7.0", "ahooks": "^3.8.5", "amis": "^6.12.0", "antd": "^5.26.1", @@ -30,7 +31,8 @@ "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-router": "^7.6.2", - "styled-components": "^6.1.18" + "styled-components": "^6.1.18", + "zustand": "^5.0.5" }, "devDependencies": { "@types/markdown-it": "^14.1.2", diff --git a/service-web/client/pnpm-lock.yaml b/service-web/client/pnpm-lock.yaml index 35c4c86..d973123 100644 --- a/service-web/client/pnpm-lock.yaml +++ b/service-web/client/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@tinyflow-ai/react': specifier: ^0.2.1 version: 0.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.28.2) + '@xyflow/react': + specifier: ^12.7.0 + version: 12.7.0(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ahooks: specifier: ^3.8.5 version: 3.8.5(react@18.3.1) @@ -74,6 +77,9 @@ importers: styled-components: specifier: ^6.1.18 version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^5.0.5 + version: 5.0.5(@types/react@18.3.23)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)) devDependencies: '@types/markdown-it': specifier: ^14.1.2 @@ -1092,6 +1098,12 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + '@xyflow/react@12.7.0': + resolution: {integrity: sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + '@xyflow/svelte@0.1.39': resolution: {integrity: sha512-QZ5mzNysvJeJW7DxmqI4Urhhef9tclqtPr7WAS5zQF5Gk6k9INwzey4CYNtEZo8XMj9H8lzgoJRmgMPnJEc1kw==} peerDependencies: @@ -1100,6 +1112,9 @@ packages: '@xyflow/system@0.0.59': resolution: {integrity: sha512-+xgqYhoBv5F10TQx0SiKZR/DcWtuxFYR+e/LluHb7DMtX4SsMDutZWEJ4da4fDco25jZxw5G9fOlmk7MWvYd5Q==} + '@xyflow/system@0.0.62': + resolution: {integrity: sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA==} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -3599,6 +3614,39 @@ packages: zrender@5.6.0: resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.5: + resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4613,6 +4661,17 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@xyflow/react@12.7.0(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.62 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.23)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + '@xyflow/svelte@0.1.39(svelte@5.28.2)': dependencies: '@svelte-put/shortcut': 3.1.1(svelte@5.28.2) @@ -4630,6 +4689,18 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + '@xyflow/system@0.0.62': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + abbrev@1.1.1: optional: true @@ -7708,4 +7779,17 @@ snapshots: dependencies: tslib: 2.3.0 + zustand@4.5.7(@types/react@18.3.23)(react@18.3.1): + dependencies: + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + react: 18.3.1 + + zustand@5.0.5(@types/react@18.3.23)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.23 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + zwitch@2.0.4: {} diff --git a/service-web/client/src/pages/Test.tsx b/service-web/client/src/pages/Test.tsx index 893575c..de09ca4 100644 --- a/service-web/client/src/pages/Test.tsx +++ b/service-web/client/src/pages/Test.tsx @@ -1,19 +1,378 @@ -import {Tinyflow} from '@tinyflow-ai/react' -import '@tinyflow-ai/react/dist/index.css' +import {PlusCircleFilled, SaveFilled} from '@ant-design/icons' +import { + addEdge, + applyEdgeChanges, + applyNodeChanges, + Background, + BackgroundVariant, + Controls, + type Edge, + Handle, + MiniMap, + type Node, + type NodeProps, + type OnConnect, + type OnEdgesChange, + type OnNodesChange, + Position, + ReactFlow, +} from '@xyflow/react' +import {useMount} from 'ahooks' +import type {Schema} from 'amis' +import {Button, Dropdown, message} from 'antd' +import {arrToMap, contain, find, isEqual, isNil, randomId} from 'licia' +import {type JSX, useState} from 'react' +import styled from 'styled-components' +import '@xyflow/react/dist/style.css' +import {create} from 'zustand/react' +import {amisRender, commonInfo} from '../util/amis.tsx' -function Test() { +const FlowableDiv = styled.div` + height: 93vh; + + .toolbar { + z-index: 999; + position: absolute; + } +` + +type AmisNodeType = 'normal' | 'start' | 'end' + +const AmisNode = ( + props: NodeProps, + type: AmisNodeType, + name: String, + description?: String, + columnSchema?: Schema[], +) => { + const {id, data} = props + const {setDataById} = data return ( -
- { - console.log(value) - console.log(JSON.stringify(value)) - }} - /> +
+ {amisRender( + { + type: 'card', + header: { + title: name, + subTitle: description, + }, + body: isNil(columnSchema) + ? undefined + : { + debug: commonInfo.debug, + type: 'form', + className: 'nodrag nopan', + wrapWithPanel: false, + onEvent: { + change: { + actions: [ + { + actionType: 'custom', + // @ts-ignore + script: (context, action, event) => { + // @ts-ignore + setDataById(id, context.props.data) + }, + }, + ], + }, + }, + body: [ + ...(columnSchema ?? []), + { + type: 'hidden', + name: 'nodeId', + value: props.id, + }, + ], + }, + }, + )} + {isEqual(type, 'start') || isEqual(type, 'normal') + ? : undefined} + {isEqual(type, 'end') || isEqual(type, 'normal') + ? : undefined}
) } +const StartAmisNode = (props: NodeProps) => AmisNode( + props, + 'start', + '开始节点', + '定义输入变量', + [ + { + type: 'input-kvs', + name: 'fields', + addButtonText: '新增入参', + draggable: false, + keyItem: { + label: '参数名称', + }, + valueItems: [ + { + type: 'input-text', + name: 'description', + label: '参数描述', + }, + { + type: 'select', + name: 'type', + label: '参数类型', + required: true, + selectFirst: true, + options: [ + { + label: '文本', + value: 'text', + }, + { + label: '数字', + value: 'number', + }, + { + label: '文件', + value: 'files', + }, + ], + }, + ], + }, + ], +) +const EndAmisNode = (props: NodeProps) => AmisNode( + props, + 'end', + '结束节点', + '定义输出变量', + [ + { + type: 'input-kvs', + name: 'fields', + addButtonText: '新增输出', + draggable: false, + keyItem: { + label: '参数名称', + }, + valueItems: [ + { + type: 'select', + name: 'type', + label: '参数', + required: true, + selectFirst: true, + options: [], + }, + ], + }, + ], +) +const LlmAmisNode = (props: NodeProps) => AmisNode( + props, + 'normal', + '大模型节点', + '使用大模型对话', + [ + { + type: 'select', + name: 'model', + label: '大模型', + required: true, + selectFirst: true, + options: [ + { + label: 'Qwen3', + value: 'qwen3', + }, + { + label: 'Deepseek', + value: 'deepseek', + }, + ], + }, + { + type: 'textarea', + name: 'systemPrompt', + label: '系统提示词', + required: true, + }, + ], +) + +const initialNodes: Node[] = [ + { + id: 'BMFP3Eov94', + type: 'start-amis-node', + position: {x: 10, y: 50}, + data: {}, + }, + { + id: 'PYK8LjduQ1', + type: 'end-amis-node', + position: {x: 500, y: 50}, + data: {}, + }, +] +const initialEdges: Edge[] = [] + +const useStore = create<{ + data: Record, + getData: () => Record, + setData: (data: Record) => void, + getDataById: (id: string) => any, + setDataById: (id: string, data: any) => void, +}>((set, get) => ({ + data: {}, + getData: () => get().data, + setData: (data) => set(data), + getDataById: id => get().data[id], + setDataById: (id, data) => { + let updateData = get().data + updateData[id] = data + set({ + data: updateData, + }) + }, +})) + +const useFlowStore = create<{ + nodes: Node[], + onNodesChange: OnNodesChange, + addNode: (node: Node) => void, + setNodes: (nodes: Node[]) => void, + + edges: Edge[], + onEdgesChange: OnEdgesChange, + setEdges: (edges: Edge[]) => void, + + onConnect: OnConnect, +}>((set, get) => ({ + nodes: [], + onNodesChange: changes => { + set({ + nodes: applyNodeChanges(changes, get().nodes), + }) + }, + addNode: node => set({nodes: get().nodes.concat(node)}), + setNodes: nodes => set({nodes}), + + edges: [], + onEdgesChange: changes => { + set({ + edges: applyEdgeChanges(changes, get().edges), + }) + }, + setEdges: edges => set({edges}), + + onConnect: connection => { + set({ + edges: addEdge(connection, get().edges), + }) + }, +})) + +function Test() { + const [messageApi, contextHolder] = message.useMessage() + const [nodeDef] = useState<{ + key: string, + name: string, + component: (props: NodeProps) => JSX.Element + }[]>([ + { + key: 'start-amis-node', + name: '开始', + component: StartAmisNode, + }, + { + key: 'end-amis-node', + name: '结束', + component: EndAmisNode, + }, + { + key: 'llm-amis-node', + name: '大模型', + component: LlmAmisNode, + }, + ]) + + const {getData, getDataById, setDataById} = useStore() + const { + nodes, + addNode, + setNodes, + onNodesChange, + edges, + setEdges, + onEdgesChange, + onConnect, + } = useFlowStore() + + useMount(() => { + for (let node of initialNodes) { + node.data = { + getDataById, + setDataById, + } + } + setNodes(initialNodes) + setEdges(initialEdges) + }) + + return ( + + {contextHolder} +
+ + ({key: def.key, label: def.name})), + onClick: ({key}) => { + if (isEqual(key, 'start-amis-node') || isEqual(key, 'end-amis-node')) { + contain(nodes, (node: Node) => isEqual(key, node.type)) + messageApi.error('只能存在1个开始/结束节点') + return + } + + addNode({ + id: randomId(10), + type: key, + position: {x: 100, y: 100}, + data: { + getDataById, + setDataById, + }, + }) + }, + }} + > + + +
+ def.key), + key => find(nodeDef, def => isEqual(key, def.key))!.component) + } + > + + + + +
+ ) +} + export default Test \ No newline at end of file