diff --git a/service-web/client/src/pages/Test.tsx b/service-web/client/src/pages/Test.tsx index 6eee028..d90d4e6 100644 --- a/service-web/client/src/pages/Test.tsx +++ b/service-web/client/src/pages/Test.tsx @@ -1,486 +1,6 @@ -import {DeleteFilled, EditFilled, 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, Card, Drawer, Dropdown, message, Space} from 'antd' -import {arrToMap, filter, find, findIdx, 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' - -const FlowableDiv = styled.div` - height: 93vh; - - .toolbar { - z-index: 999; - position: absolute; - } - - .node-card { - cursor: grab; - - .card-container { - cursor: default; - } - } -` - -type AmisNodeType = 'normal' | 'start' | 'end' - -const AmisNode = ( - props: NodeProps, - type: AmisNodeType, - name: String, - description?: String, - columnSchema?: Schema[], -) => { - const {id, data} = props - const {getDataById, removeNode, editNode} = data - return ( -
- - , - }, - { - key: 'remove', - label: '删除', - icon: , - }, - ], - onClick: menu => { - switch (menu.key) { - case 'edit': - // @ts-ignore - editNode(id, name, description, columnSchema) - break - case 'remove': - // @ts-ignore - removeNode(id) - break - } - }, - }} - > -
- {description} -
-
-
- {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: 100}, - data: {}, - }, - { - id: 'PYK8LjduQ1', - type: 'end-amis-node', - position: {x: 500, y: 100}, - 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, - removeNode: (id: string) => 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)}), - removeNode: id => { - set({ - nodes: filter(get().nodes, node => !isEqual(node.id, id)), - }) - }, - 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 [open, setOpen] = useState(false) - - const {getData, getDataById, setDataById} = useStore() - const { - nodes, - addNode, - removeNode, - setNodes, - onNodesChange, - edges, - setEdges, - onEdgesChange, - onConnect, - } = useFlowStore() - - const [currentNodeForm, setCurrentNodeForm] = useState() - const editNode = (id: string, name: string, description: string, columnSchema?: Schema[]) => { - if (!isNil(columnSchema)) { - setCurrentNodeForm( - amisRender( - { - type: 'wrapper', - size: 'none', - body: [ - { - type: 'tpl', - className: 'text-secondary', - tpl: description, - }, - { - debug: commonInfo.debug, - title: name, - type: 'form', - wrapWithPanel: false, - onEvent: { - submitSucc: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - setDataById(id, context.props.data) - setOpen(false) - }, - }, - ], - }, - }, - body: [ - ...(columnSchema ?? []), - { - type: 'wrapper', - size: 'none', - className: 'space-x-1 float-right', - body: [ - { - type: 'action', - label: '取消', - onEvent: { - click: { - actions: [ - { - actionType: 'custom', - // @ts-ignore - script: (context, action, event) => { - setOpen(false) - }, - }, - ], - }, - }, - }, - { - type: 'submit', - label: '保存', - level: 'primary', - }, - ], - }, - ], - }, - ] - }, - getDataById(id), - ), - ) - setOpen(true) - } - } - - useMount(() => { - for (let node of initialNodes) { - node.data = { - getDataById, - setDataById, - removeNode, - editNode, - } - } - setNodes(initialNodes) - setEdges(initialEdges) - }) - return ( - - {contextHolder} - - - ({key: def.key, label: def.name})), - onClick: ({key}) => { - if (isEqual(key, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) { - messageApi.error('只能存在1个开始节点') - return - } - if (isEqual(key, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) { - messageApi.error('只能存在1个结束节点') - return - } - - addNode({ - id: randomId(10), - type: key, - position: {x: 100, y: 100}, - data: { - getDataById, - setDataById, - removeNode, - editNode, - }, - }) - }, - }} - > - - - - - {currentNodeForm} - - def.key), - key => find(nodeDef, def => isEqual(key, def.key))!.component) - } - > - - - - - +
Test
) } diff --git a/service-web/client/src/pages/ai/flow/FlowEditor.tsx b/service-web/client/src/pages/ai/flow/FlowEditor.tsx new file mode 100644 index 0000000..20bcac2 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/FlowEditor.tsx @@ -0,0 +1,255 @@ +import {PlusCircleFilled, SaveFilled} from '@ant-design/icons' +import { + Background, + BackgroundVariant, + Controls, + type Edge, + MiniMap, + type Node, + type NodeProps, + ReactFlow, +} from '@xyflow/react' +import {useMount} from 'ahooks' +import type {Schema} from 'amis' +import {Button, Drawer, Dropdown, message, Space} from 'antd' +import {arrToMap, find, findIdx, isEqual, isNil, randomId} from 'licia' +import {type JSX, useState} from 'react' +import styled from 'styled-components' +import '@xyflow/react/dist/style.css' +import {amisRender, commonInfo} from '../../../util/amis.tsx' +import StartNode from './node/StartNode.tsx' +import EndNode from './node/EndNode.tsx' +import LlmNode from './node/LlmNode.tsx' +import {useDataStore} from './store/DataStore.ts' +import {useFlowStore} from './store/FlowStore.ts' + +const FlowableDiv = styled.div` + height: 93vh; + + .toolbar { + z-index: 999; + position: absolute; + } + + .node-card { + cursor: grab; + + .card-container { + cursor: default; + } + } +` + +const initialNodes: Node[] = [ + { + id: 'BMFP3Eov94', + type: 'start-amis-node', + position: {x: 10, y: 100}, + data: {}, + }, + { + id: 'PYK8LjduQ1', + type: 'end-amis-node', + position: {x: 500, y: 100}, + data: {}, + }, +] +const initialEdges: Edge[] = [] + +function FlowEditor() { + const [messageApi, contextHolder] = message.useMessage() + const [nodeDef] = useState<{ + key: string, + name: string, + component: (props: NodeProps) => JSX.Element + }[]>([ + { + key: 'start-amis-node', + name: '开始', + component: StartNode, + }, + { + key: 'end-amis-node', + name: '结束', + component: EndNode, + }, + { + key: 'llm-amis-node', + name: '大模型', + component: LlmNode, + }, + ]) + const [open, setOpen] = useState(false) + + const {getData, getDataById, setDataById} = useDataStore() + const { + nodes, + addNode, + removeNode, + setNodes, + onNodesChange, + edges, + setEdges, + onEdgesChange, + onConnect, + } = useFlowStore() + + const [currentNodeForm, setCurrentNodeForm] = useState() + const editNode = (id: string, name: string, description: string, columnSchema?: Schema[]) => { + if (!isNil(columnSchema)) { + setCurrentNodeForm( + amisRender( + { + type: 'wrapper', + size: 'none', + body: [ + { + type: 'tpl', + className: 'text-secondary', + tpl: description, + }, + { + debug: commonInfo.debug, + title: name, + type: 'form', + wrapWithPanel: false, + onEvent: { + submitSucc: { + actions: [ + { + actionType: 'custom', + // @ts-ignore + script: (context, action, event) => { + setDataById(id, context.props.data) + setOpen(false) + }, + }, + ], + }, + }, + body: [ + ...(columnSchema ?? []), + { + type: 'wrapper', + size: 'none', + className: 'space-x-1 float-right', + body: [ + { + type: 'action', + label: '取消', + onEvent: { + click: { + actions: [ + { + actionType: 'custom', + // @ts-ignore + script: (context, action, event) => { + setOpen(false) + }, + }, + ], + }, + }, + }, + { + type: 'submit', + label: '保存', + level: 'primary', + }, + ], + }, + ], + }, + ] + }, + getDataById(id), + ), + ) + setOpen(true) + } + } + + useMount(() => { + for (let node of initialNodes) { + node.data = { + getDataById, + setDataById, + removeNode, + editNode, + } + } + setNodes(initialNodes) + setEdges(initialEdges) + }) + + return ( + + {contextHolder} + + + ({key: def.key, label: def.name})), + onClick: ({key}) => { + if (isEqual(key, 'start-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) { + messageApi.error('只能存在1个开始节点') + return + } + if (isEqual(key, 'end-amis-node') && findIdx(nodes, (node: Node) => isEqual(key, node.type)) > -1) { + messageApi.error('只能存在1个结束节点') + return + } + + addNode({ + id: randomId(10), + type: key, + position: {x: 100, y: 100}, + data: { + getDataById, + setDataById, + removeNode, + editNode, + }, + }) + }, + }} + > + + + + + {currentNodeForm} + + def.key), + key => find(nodeDef, def => isEqual(key, def.key))!.component) + } + > + + + + + + ) +} + +export default FlowEditor \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/node/AmisNode.tsx b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx new file mode 100644 index 0000000..e9416ba --- /dev/null +++ b/service-web/client/src/pages/ai/flow/node/AmisNode.tsx @@ -0,0 +1,69 @@ +import {Handle, type NodeProps, Position} from '@xyflow/react' +import type {Schema} from 'amis' +import {Card, Dropdown} from 'antd' +import {DeleteFilled, EditFilled} from '@ant-design/icons' +import {isEqual} from 'licia' + +export type AmisNodeType = 'normal' | 'start' | 'end' + +const AmisNode = ( + props: NodeProps, + type: AmisNodeType, + name: String, + description?: String, + columnSchema?: Schema[], +) => { + const {id, data} = props + const {getDataById, removeNode, editNode} = data + return ( +
+ + , + }, + { + key: 'remove', + label: '删除', + icon: , + }, + ], + onClick: menu => { + switch (menu.key) { + case 'edit': + // @ts-ignore + editNode(id, name, description, columnSchema) + break + case 'remove': + // @ts-ignore + removeNode(id) + break + } + }, + }} + > +
+ {description} +
+
+
+ {isEqual(type, 'start') || isEqual(type, 'normal') + ? : undefined} + {isEqual(type, 'end') || isEqual(type, 'normal') + ? : undefined} +
+ ) +} + +export default AmisNode diff --git a/service-web/client/src/pages/ai/flow/node/EndNode.tsx b/service-web/client/src/pages/ai/flow/node/EndNode.tsx new file mode 100644 index 0000000..05cbcc5 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/node/EndNode.tsx @@ -0,0 +1,32 @@ +import type {NodeProps} from '@xyflow/react' +import AmisNode from './AmisNode.tsx' + +const EndNode = (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: [], + }, + ], + }, + ], +) + +export default EndNode \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/node/LlmNode.tsx b/service-web/client/src/pages/ai/flow/node/LlmNode.tsx new file mode 100644 index 0000000..9bbec7c --- /dev/null +++ b/service-web/client/src/pages/ai/flow/node/LlmNode.tsx @@ -0,0 +1,36 @@ +import type {NodeProps} from '@xyflow/react' +import AmisNode from './AmisNode.tsx' + +const LlmNode = (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, + }, + ], +) + +export default LlmNode \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/node/StartNode.tsx b/service-web/client/src/pages/ai/flow/node/StartNode.tsx new file mode 100644 index 0000000..617e85e --- /dev/null +++ b/service-web/client/src/pages/ai/flow/node/StartNode.tsx @@ -0,0 +1,50 @@ +import type {NodeProps} from '@xyflow/react' +import AmisNode from './AmisNode.tsx' + +const StartNode = (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', + }, + ], + }, + ], + }, + ], +) + +export default StartNode \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/store/DataStore.ts b/service-web/client/src/pages/ai/flow/store/DataStore.ts new file mode 100644 index 0000000..25e3aa6 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/store/DataStore.ts @@ -0,0 +1,21 @@ +import {create} from 'zustand/react' + +export const useDataStore = 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, + }) + }, +})) \ No newline at end of file diff --git a/service-web/client/src/pages/ai/flow/store/FlowStore.ts b/service-web/client/src/pages/ai/flow/store/FlowStore.ts new file mode 100644 index 0000000..a1425d4 --- /dev/null +++ b/service-web/client/src/pages/ai/flow/store/FlowStore.ts @@ -0,0 +1,54 @@ +import {create} from 'zustand/react' +import { + addEdge, + applyEdgeChanges, + applyNodeChanges, + type Edge, + type Node, + type OnConnect, + type OnEdgesChange, + type OnNodesChange +} from '@xyflow/react' +import {filter, isEqual} from 'licia' + +export const useFlowStore = create<{ + nodes: Node[], + onNodesChange: OnNodesChange, + addNode: (node: Node) => void, + removeNode: (id: string) => 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)}), + removeNode: id => { + set({ + nodes: filter(get().nodes, node => !isEqual(node.id, id)), + }) + }, + 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), + }) + }, +})) diff --git a/service-web/client/src/route.tsx b/service-web/client/src/route.tsx index 9ec7ce7..b9a6185 100644 --- a/service-web/client/src/route.tsx +++ b/service-web/client/src/route.tsx @@ -4,6 +4,7 @@ import { ClusterOutlined, CompressOutlined, DatabaseOutlined, + GatewayOutlined, InfoCircleOutlined, OpenAIOutlined, QuestionOutlined, @@ -32,6 +33,7 @@ import Yarn from './pages/overview/Yarn.tsx' import YarnCluster from './pages/overview/YarnCluster.tsx' import Test from './pages/Test.tsx' import {commonInfo} from './util/amis.tsx' +import FlowEditor from './pages/ai/flow/FlowEditor.tsx' export const routes: RouteObject[] = [ { @@ -109,6 +111,10 @@ export const routes: RouteObject[] = [ path: 'knowledge/detail/:knowledge_id/segment/:group_id', Component: DataSegment, }, + { + path: 'flowable', + Component: FlowEditor, + }, ], }, { @@ -211,6 +217,11 @@ export const menus = { name: '知识库', icon: , }, + { + path: '/ai/flowable', + name: '流程编排', + icon: , + }, ], }, ],