diff --git a/web/package.json b/web/package.json index d2c254ec..d6642ac8 100644 --- a/web/package.json +++ b/web/package.json @@ -10,10 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@antv/layout": "^1.2.14-beta.8", + "@antv/x6": "^3.0.1", + "@antv/x6-react-shape": "^3.0.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@lexical/react": "^0.39.0", "antd": "^5.27.4", "axios": "^1.12.2", "clsx": "^2.1.1", @@ -23,6 +27,8 @@ "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", "i18next": "^25.6.0", + "js-yaml": "^4.1.1", + "lexical": "^0.39.0", "mermaid": "^11.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -31,7 +37,6 @@ "react-markdown": "^10.1.0", "react-router-dom": "^6.22.0", "react-syntax-highlighter": "^16.1.0", - "reactflow": "^11.11.4", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", @@ -46,6 +51,7 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.14", "@types/crypto-js": "^4.2.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.6.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 72521d92..69d27d44 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -1,8 +1,9 @@ import { request } from '@/utils/request' -import type { Application } from '@/views/ApplicationManagement/types' +import type { ApplicationModalData } from '@/views/ApplicationManagement/types' import type { Config } from '@/views/ApplicationConfig/types' import { handleSSE, type SSEMessage } from '@/utils/stream' import type { QueryParams } from '@/views/Conversation/types' +import type { WorkflowConfig } from '@/views/Workflow/types' // 应用列表 export const getApplicationListUrl = '/apps' @@ -13,20 +14,24 @@ export const getApplicationList = (data: Record) => { export const getApplicationConfig = (id: string) => { return request.get(`/apps/${id}/config`) } -// 获取集群应配置 +// 获取集群应用配置 export const getMultiAgentConfig = (id: string) => { return request.get(`/apps/${id}/multi-agent`) } +// 获取 workflow应用配置 +export const getWorkflowConfig = (id: string) => { + return request.get(`/apps/${id}/workflow`) +} // 应用详情 export const getApplication = (id: string) => { return request.get(`/apps/${id}`) } // 更新应用 -export const updateApplication = (id: string, values: Application) => { +export const updateApplication = (id: string, values: ApplicationModalData) => { return request.put(`/apps/${id}`, values) } // 创建应用 -export const addApplication = (values: Application) => { +export const addApplication = (values: ApplicationModalData) => { return request.post('/apps', values) } // 保存Agent配置 @@ -37,6 +42,10 @@ export const saveAgentConfig = (app_id: string, values: Config) => { export const saveMultiAgentConfig = (app_id: string, values: Config) => { return request.put(`/apps/${app_id}/multi-agent`, values) } +// 保存workflow配置 +export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => { + return request.put(`/apps/${app_id}/workflow`, values) +} // 模型比对试运行 export const runCompare = (app_id: string, values: Record, onMessage?: (data: SSEMessage[]) => void) => { return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage) diff --git a/web/src/assets/images/menu/apiKey.png b/web/src/assets/images/menu/apiKey.png new file mode 100644 index 00000000..53d19428 Binary files /dev/null and b/web/src/assets/images/menu/apiKey.png differ diff --git a/web/src/assets/images/menu/apiKey_active.png b/web/src/assets/images/menu/apiKey_active.png new file mode 100644 index 00000000..4f8d1cfa Binary files /dev/null and b/web/src/assets/images/menu/apiKey_active.png differ diff --git a/web/src/assets/images/menu/space_acitve.svg b/web/src/assets/images/menu/space_active.svg similarity index 100% rename from web/src/assets/images/menu/space_acitve.svg rename to web/src/assets/images/menu/space_active.svg diff --git a/web/src/assets/images/menu/tool.png b/web/src/assets/images/menu/tool.png new file mode 100644 index 00000000..669238e8 Binary files /dev/null and b/web/src/assets/images/menu/tool.png differ diff --git a/web/src/assets/images/menu/tool_active.png b/web/src/assets/images/menu/tool_active.png new file mode 100644 index 00000000..252cd702 Binary files /dev/null and b/web/src/assets/images/menu/tool_active.png differ diff --git a/web/src/assets/images/menu/userMemory_acitve.svg b/web/src/assets/images/menu/userMemory_active.svg similarity index 100% rename from web/src/assets/images/menu/userMemory_acitve.svg rename to web/src/assets/images/menu/userMemory_active.svg diff --git a/web/src/assets/images/workflow/agent_arbitration.png b/web/src/assets/images/workflow/agent_arbitration.png new file mode 100644 index 00000000..d555e3e2 Binary files /dev/null and b/web/src/assets/images/workflow/agent_arbitration.png differ diff --git a/web/src/assets/images/workflow/agent_collaboration.png b/web/src/assets/images/workflow/agent_collaboration.png new file mode 100644 index 00000000..7a92aecf Binary files /dev/null and b/web/src/assets/images/workflow/agent_collaboration.png differ diff --git a/web/src/assets/images/workflow/agent_scheduling.png b/web/src/assets/images/workflow/agent_scheduling.png new file mode 100644 index 00000000..97028422 Binary files /dev/null and b/web/src/assets/images/workflow/agent_scheduling.png differ diff --git a/web/src/assets/images/workflow/aggregator.png b/web/src/assets/images/workflow/aggregator.png new file mode 100644 index 00000000..6253733a Binary files /dev/null and b/web/src/assets/images/workflow/aggregator.png differ diff --git a/web/src/assets/images/workflow/answer.png b/web/src/assets/images/workflow/answer.png new file mode 100644 index 00000000..57f9c94d Binary files /dev/null and b/web/src/assets/images/workflow/answer.png differ diff --git a/web/src/assets/images/workflow/arrow.png b/web/src/assets/images/workflow/arrow.png new file mode 100644 index 00000000..67ce7b48 Binary files /dev/null and b/web/src/assets/images/workflow/arrow.png differ diff --git a/web/src/assets/images/workflow/classification.png b/web/src/assets/images/workflow/classification.png new file mode 100644 index 00000000..87d34bb8 Binary files /dev/null and b/web/src/assets/images/workflow/classification.png differ diff --git a/web/src/assets/images/workflow/code_execution.png b/web/src/assets/images/workflow/code_execution.png new file mode 100644 index 00000000..7f802b3c Binary files /dev/null and b/web/src/assets/images/workflow/code_execution.png differ diff --git a/web/src/assets/images/workflow/condition.png b/web/src/assets/images/workflow/condition.png new file mode 100644 index 00000000..a0bf9160 Binary files /dev/null and b/web/src/assets/images/workflow/condition.png differ diff --git a/web/src/assets/images/workflow/empty.png b/web/src/assets/images/workflow/empty.png new file mode 100644 index 00000000..58dd77b2 Binary files /dev/null and b/web/src/assets/images/workflow/empty.png differ diff --git a/web/src/assets/images/workflow/end.png b/web/src/assets/images/workflow/end.png new file mode 100644 index 00000000..7f4628c6 Binary files /dev/null and b/web/src/assets/images/workflow/end.png differ diff --git a/web/src/assets/images/workflow/http_request.png b/web/src/assets/images/workflow/http_request.png new file mode 100644 index 00000000..64e55d36 Binary files /dev/null and b/web/src/assets/images/workflow/http_request.png differ diff --git a/web/src/assets/images/workflow/iteration.png b/web/src/assets/images/workflow/iteration.png new file mode 100644 index 00000000..dd73767b Binary files /dev/null and b/web/src/assets/images/workflow/iteration.png differ diff --git a/web/src/assets/images/workflow/llm.png b/web/src/assets/images/workflow/llm.png new file mode 100644 index 00000000..5d9e7465 Binary files /dev/null and b/web/src/assets/images/workflow/llm.png differ diff --git a/web/src/assets/images/workflow/loop.png b/web/src/assets/images/workflow/loop.png new file mode 100644 index 00000000..a4313229 Binary files /dev/null and b/web/src/assets/images/workflow/loop.png differ diff --git a/web/src/assets/images/workflow/memory_enhancement.png b/web/src/assets/images/workflow/memory_enhancement.png new file mode 100644 index 00000000..998c02fe Binary files /dev/null and b/web/src/assets/images/workflow/memory_enhancement.png differ diff --git a/web/src/assets/images/workflow/model_selection.png b/web/src/assets/images/workflow/model_selection.png new file mode 100644 index 00000000..e3e93962 Binary files /dev/null and b/web/src/assets/images/workflow/model_selection.png differ diff --git a/web/src/assets/images/workflow/model_voting.png b/web/src/assets/images/workflow/model_voting.png new file mode 100644 index 00000000..8324541e Binary files /dev/null and b/web/src/assets/images/workflow/model_voting.png differ diff --git a/web/src/assets/images/workflow/output_audit.png b/web/src/assets/images/workflow/output_audit.png new file mode 100644 index 00000000..50128f82 Binary files /dev/null and b/web/src/assets/images/workflow/output_audit.png differ diff --git a/web/src/assets/images/workflow/parallel.png b/web/src/assets/images/workflow/parallel.png new file mode 100644 index 00000000..e77d79d8 Binary files /dev/null and b/web/src/assets/images/workflow/parallel.png differ diff --git a/web/src/assets/images/workflow/parameter_extraction.png b/web/src/assets/images/workflow/parameter_extraction.png new file mode 100644 index 00000000..d4b50ee0 Binary files /dev/null and b/web/src/assets/images/workflow/parameter_extraction.png differ diff --git a/web/src/assets/images/workflow/process_evolution.png b/web/src/assets/images/workflow/process_evolution.png new file mode 100644 index 00000000..8262c00d Binary files /dev/null and b/web/src/assets/images/workflow/process_evolution.png differ diff --git a/web/src/assets/images/workflow/rag.png b/web/src/assets/images/workflow/rag.png new file mode 100644 index 00000000..3749dbfa Binary files /dev/null and b/web/src/assets/images/workflow/rag.png differ diff --git a/web/src/assets/images/workflow/reasoning_control.png b/web/src/assets/images/workflow/reasoning_control.png new file mode 100644 index 00000000..649e165c Binary files /dev/null and b/web/src/assets/images/workflow/reasoning_control.png differ diff --git a/web/src/assets/images/workflow/robot-2-line@2x.png b/web/src/assets/images/workflow/robot-2-line@2x.png new file mode 100644 index 00000000..f1dc247e Binary files /dev/null and b/web/src/assets/images/workflow/robot-2-line@2x.png differ diff --git a/web/src/assets/images/workflow/self_optimization.png b/web/src/assets/images/workflow/self_optimization.png new file mode 100644 index 00000000..08ed8598 Binary files /dev/null and b/web/src/assets/images/workflow/self_optimization.png differ diff --git a/web/src/assets/images/workflow/self_reflection.png b/web/src/assets/images/workflow/self_reflection.png new file mode 100644 index 00000000..099aac60 Binary files /dev/null and b/web/src/assets/images/workflow/self_reflection.png differ diff --git a/web/src/assets/images/workflow/sensitive_detection.png b/web/src/assets/images/workflow/sensitive_detection.png new file mode 100644 index 00000000..637a4f13 Binary files /dev/null and b/web/src/assets/images/workflow/sensitive_detection.png differ diff --git a/web/src/assets/images/workflow/start.png b/web/src/assets/images/workflow/start.png new file mode 100644 index 00000000..f6828988 Binary files /dev/null and b/web/src/assets/images/workflow/start.png differ diff --git a/web/src/assets/images/workflow/task_planning.png b/web/src/assets/images/workflow/task_planning.png new file mode 100644 index 00000000..33f322fd Binary files /dev/null and b/web/src/assets/images/workflow/task_planning.png differ diff --git a/web/src/assets/images/workflow/template_rendering.png b/web/src/assets/images/workflow/template_rendering.png new file mode 100644 index 00000000..064caeb6 Binary files /dev/null and b/web/src/assets/images/workflow/template_rendering.png differ diff --git a/web/src/assets/images/workflow/tools.png b/web/src/assets/images/workflow/tools.png new file mode 100644 index 00000000..49ff2fa4 Binary files /dev/null and b/web/src/assets/images/workflow/tools.png differ diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx index 8dd19410..be9fc48d 100644 --- a/web/src/components/Chat/ChatInput.tsx +++ b/web/src/components/Chat/ChatInput.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-10 16:46:14 * @Last Modified by: ZhaoYing - * @Last Modified time: 2025-12-10 16:49:13 + * @Last Modified time: 2025-12-20 15:38:40 */ import { useEffect } from 'react' import { Flex, Input, Form } from 'antd' @@ -40,7 +40,7 @@ const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputPr return (
- + {/* 消息输入表单 */}
@@ -66,10 +66,10 @@ const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputPr {children} {/* 发送按钮 - 根据状态显示不同图标 */} {loading - ? + ? : !values || !values?.message || values?.message?.trim() === '' - ? - : + ? + : } diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index d148a346..91d3d0f1 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -21,11 +21,11 @@ import modelActiveIcon from '@/assets/images/menu/model_active.svg'; import memoryIcon from '@/assets/images/menu/memory.svg'; import memoryActiveIcon from '@/assets/images/menu/memory_active.svg'; import spaceIcon from '@/assets/images/menu/space.svg'; -import spaceActiveIcon from '@/assets/images/menu/space_acitve.svg'; +import spaceActiveIcon from '@/assets/images/menu/space_active.svg'; import userIcon from '@/assets/images/menu/user.svg'; import userActiveIcon from '@/assets/images/menu/user_active.svg'; import userMemoryIcon from '@/assets/images/menu/userMemory.svg'; -import userMemoryActiveIcon from '@/assets/images/menu/userMemory_acitve.svg'; +import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg'; import applicationIcon from '@/assets/images/menu/application.svg'; import applicationActiveIcon from '@/assets/images/menu/application_active.svg'; import knowledgeIcon from '@/assets/images/menu/knowledge.svg'; @@ -34,6 +34,10 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg' import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg'; import memberIcon from '@/assets/images/menu/member.svg'; import memberActiveIcon from '@/assets/images/menu/member_active.svg'; +import toolIcon from '@/assets/images/menu/tool.png'; +import toolActiveIcon from '@/assets/images/menu/tool_active.png'; +import apiKeyIcon from '@/assets/images/menu/apiKey.png'; +import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png'; // 图标路径映射表 const iconPathMap: Record = { @@ -57,6 +61,10 @@ const iconPathMap: Record = { 'memoryConversationActive': memoryConversationActiveIcon, 'member': memberIcon, 'memberActive': memberActiveIcon, + 'tool': toolIcon, + 'toolActive': toolActiveIcon, + 'apiKey': apiKeyIcon, + 'apiKeyActive': apiKeyActiveIcon, }; const { Sider } = Layout; diff --git a/web/src/styles/index.css b/web/src/styles/index.css index eecd99f5..bbbe9cd9 100644 --- a/web/src/styles/index.css +++ b/web/src/styles/index.css @@ -174,4 +174,10 @@ body { } .ant-breadcrumb a:hover { background-color: transparent; +} + +/* X6 节点样式 */ +.x6-node foreignObject > body { + min-height: 100%; + max-height: 100%; } \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 7541e938..4330cd60 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useState, useRef, type Key } from 'react' +import { type FC, useEffect, useState, useRef, forwardRef, useImperativeHandle, type Key } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom'; import Card from './components/Card' @@ -11,17 +11,19 @@ import type { Config, SubAgentModalRef, ChatData, - SubAgentItem + SubAgentItem, + ClusterRef } from './types' import Chat from './components/Chat' import RbCard from '@/components/RbCard/Card' import SubAgentModal from './components/SubAgentModal' import Empty from '@/components/Empty' +import type { Application } from '@/views/ApplicationManagement/types' const tagColors = ['processing', 'warning', 'default'] const MAX_LENGTH = 5; -const Cluster: FC<{application: SubAgentItem}> = ({application}) => { +const Cluster = forwardRef(({application}, ref) => { const { t } = useTranslation() const { message } = App.useApp() const [form] = Form.useForm() @@ -113,6 +115,9 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { form.setFieldsValue({ master_agent_name: option.children }) } } + useImperativeHandle(ref, () => ({ + handleSave + })) return ( @@ -210,6 +215,6 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { /> ) -} +}) export default Cluster \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 34d85aa0..ec899a32 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -1,6 +1,6 @@ -import { type FC, useRef } from 'react'; +import { type FC, useEffect, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Layout, Tabs, Dropdown } from 'antd'; +import { Layout, Tabs, Dropdown, Button } from 'antd'; import type { MenuProps } from 'antd'; import { useTranslation } from 'react-i18next'; import styles from '../index.module.css' @@ -11,7 +11,7 @@ import exportIcon from '@/assets/images/export_hover.svg' import deleteIcon from '@/assets/images/delete_hover.svg' import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types'; import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal' -import type { CopyModalRef } from '../types' +import type { CopyModalRef, WorkflowRef } from '../types' import { deleteApplication } from '@/api/application' import CopyModal from './CopyModal' @@ -29,8 +29,12 @@ interface ConfigHeaderProps { activeTab: string; handleChangeTab: (key: string) => void; refresh: () => void; + workflowRef: React.RefObject } -const ConfigHeader: FC = ({ application, activeTab, handleChangeTab, refresh }) => { +const ConfigHeader: FC = ({ + application, activeTab, handleChangeTab, refresh, + workflowRef +}) => { const { t } = useTranslation(); const navigate = useNavigate(); const { id } = useParams(); @@ -46,7 +50,7 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha const formatMenuItems = () => { const items = ['edit', 'copy', 'delete'].map(key => ({ key, - icon: , + icon: , label: t(`common.${key}`), })) return { @@ -85,12 +89,23 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha const goToApplication = () => { navigate('/application', { replace: true }) } - + const save = () => { + workflowRef.current?.handleSave() + } + const run = () => { + workflowRef.current?.handleSave(false) + .then(() => { + workflowRef.current?.handleRun() + }) + } + const clear = () => { + workflowRef?.current?.graphRef?.current?.clearCells() + } return ( <> -
-
-
+
+
+
{application?.name[0]}
@@ -101,7 +116,7 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha placement="bottomRight" >
@@ -114,10 +129,19 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha className={styles.tabs} />
-
- + {application?.type === 'workflow' + ?
+ + + + {/* */} + +
+ :
+ {t('application.returnToApplicationList')}
+ }
{ const { id } = useParams(); const agentRef = useRef(null) + const clusterRef = useRef(null) + const workflowRef = useRef(null) const [application, setApplication] = useState(null); const [activeTab, setActiveTab] = useState('arrangement'); @@ -21,6 +24,16 @@ const ApplicationConfig: React.FC = () => { .then(() => { setActiveTab(key) }) + } else if (activeTab === 'arrangement' && application?.type === 'multi_agent' && clusterRef.current) { + clusterRef.current.handleSave(false) + .then(() => { + setActiveTab(key) + }) + } else if (activeTab === 'arrangement' && application?.type === 'workflow' && workflowRef.current) { + workflowRef.current.handleSave(false) + .then(() => { + setActiveTab(key) + }) } else { setActiveTab(key) } @@ -47,9 +60,11 @@ const ApplicationConfig: React.FC = () => { handleChangeTab={handleChangeTab} application={application as Application} refresh={getApplicationInfo} + workflowRef={workflowRef} /> {activeTab === 'arrangement' && application?.type === 'agent' && } - {activeTab === 'arrangement' && application?.type === 'multi_agent' && } + {activeTab === 'arrangement' && application?.type === 'multi_agent' && } + {activeTab === 'arrangement' && application?.type === 'workflow' && } {activeTab === 'api' && } {activeTab === 'release' && } diff --git a/web/src/views/ApplicationConfig/types.ts b/web/src/views/ApplicationConfig/types.ts index c5cda44e..c085328b 100644 --- a/web/src/views/ApplicationConfig/types.ts +++ b/web/src/views/ApplicationConfig/types.ts @@ -1,5 +1,7 @@ import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types' import type { ChatItem } from '@/components/Chat/types' +import type { GraphRef } from '@/views/Workflow/types'; +import type { ApiKey } from '@/views/ApiKeyManagement/types' export interface ModelConfig { label?: string; @@ -116,6 +118,14 @@ export interface ApplicationModalData { export interface AgentRef { handleSave: (flag?: boolean) => Promise; } +export interface ClusterRef { + handleSave: (flag?: boolean) => Promise; +} +export interface WorkflowRef { + handleSave: (flag?: boolean) => Promise; + handleRun: () => void; + graphRef: GraphRef +} export interface ApplicationModalRef { handleOpen: (application?: Config) => void; } diff --git a/web/src/views/Workflow/components/CanvasToolbar.tsx b/web/src/views/Workflow/components/CanvasToolbar.tsx new file mode 100644 index 00000000..ac1b1130 --- /dev/null +++ b/web/src/views/Workflow/components/CanvasToolbar.tsx @@ -0,0 +1,203 @@ +import type { FC } from 'react'; +import { Select, Button } from 'antd'; +import { Node } from '@antv/x6'; +import type { GraphRef } from '../types' + +interface CanvasToolbarProps { + miniMapRef: React.RefObject; + graphRef: GraphRef; + isHandMode: boolean; + setIsHandMode: React.Dispatch>; + zoomLevel: number; + canUndo: boolean; + canRedo: boolean; + onUndo: () => void; + onRedo: () => void; +} + +const CanvasToolbar: FC = ({ + miniMapRef, + graphRef, + isHandMode, + setIsHandMode, + zoomLevel, + canUndo, + canRedo, + onUndo, + onRedo, +}) => { + // 整理布局函数 + const handleLayout = () => { + if (!graphRef.current) return; + const nodes = graphRef.current.getNodes(); + const edges = graphRef.current.getEdges(); + + // 如果没有连线,使用垂直布局避免节点重叠 + if (edges.length === 0) { + nodes.forEach((node, index) => { + const nodeData = node.getData(); + const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition'; + const nodeHeight = isSpecialNode ? 220 : 50; + const xPosition = 100; + const yPosition = index * (nodeHeight + 100) + 100; + node.setPosition(xPosition, yPosition); + }); + return; + } + + // 简单的树布局算法 + const nodeMap = new Map(); + const children = new Map(); + const roots: string[] = []; + + // 初始化节点映射 + nodes.forEach(node => { + nodeMap.set(node.id, node); + children.set(node.id, []); + }); + + // 构建父子关系 + edges.forEach(edge => { + const sourceId = edge.getSourceCellId(); + const targetId = edge.getTargetCellId(); + if (sourceId && targetId) { + children.get(sourceId)?.push(targetId); + } + }); + + // 找到根节点 + const hasParent = new Set(); + edges.forEach(edge => { + const targetId = edge.getTargetCellId(); + if (targetId) hasParent.add(targetId); + }); + + nodes.forEach(node => { + if (!hasParent.has(node.id)) { + roots.push(node.id); + } + }); + + // 布局参数 + const levelWidths: number[] = []; + const baseNodeSpacing = 120; + let currentY = 100; + + // 计算每层的最大宽度 + const calculateLevelWidths = (nodeId: string, level: number) => { + const node = nodeMap.get(nodeId); + if (!node) return; + + const nodeData = node.getData(); + const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition'; + const nodeWidth = isSpecialNode ? 400 : 160; + const gap = isSpecialNode ? 150 : 100; + + levelWidths[level] = Math.max(levelWidths[level] || 0, nodeWidth + gap); + + const childIds = children.get(nodeId) || []; + childIds.forEach((childId: string) => calculateLevelWidths(childId, level + 1)); + }; + + roots.forEach(rootId => calculateLevelWidths(rootId, 0)); + + // 递归布局函数 + const layoutNode = (nodeId: string, level: number, parentY: number): number => { + const node = nodeMap.get(nodeId); + if (!node) return parentY; + + const nodeData = node.getData(); + const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'condition'; + const nodeHeight = isSpecialNode ? 220 : 50; + const verticalGap = isSpecialNode ? 80 : 40; + const spacing = baseNodeSpacing + nodeHeight + verticalGap; + + const xPosition = levelWidths.slice(0, level).reduce((sum, width) => sum + width, 100); + + const childIds = children.get(nodeId) || []; + + if (childIds.length === 0) { + // 叶子节点 + node.setPosition(xPosition, currentY); + currentY += spacing; + return currentY - spacing; + } else { + // 非叶子节点,先布局子节点 + const childPositions: number[] = []; + childIds.forEach((childId: string) => { + const childY = layoutNode(childId, level + 1, currentY); + childPositions.push(childY); + }); + + // 父节点居中,确保有足够间隙 + const minY = Math.min(...childPositions); + const maxY = Math.max(...childPositions); + const centerY = (minY + maxY) / 2; + node.setPosition(xPosition, centerY); + return centerY; + } + }; + + // 布局所有根节点 + roots.forEach(rootId => { + layoutNode(rootId, 0, currentY); + currentY += 300; // 不同树之间的间距 + }); + }; + + return ( + <> + {/* 小地图 */} +
+ {/* 缩放控制按钮 */} +
+ + + + + + +
+ + + + ) +}) + +export default Chat diff --git a/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx new file mode 100644 index 00000000..22fe8f1b --- /dev/null +++ b/web/src/views/Workflow/components/Chat/VariableConfigModal.tsx @@ -0,0 +1,98 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input, InputNumber, Checkbox } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { StartVariableItem, VariableEditModalRef } from '../../types' +import RbModal from '@/components/RbModal' + +interface VariableEditModalProps { + refresh: (values: StartVariableItem[]) => void; + variables: StartVariableItem[] +} + +const VariableConfigModal = forwardRef(({ + refresh, + variables +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm<{variables: StartVariableItem[]}>(); + const [loading, setLoading] = useState(false) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = () => { + + setVisible(true); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + form.validateFields().then((values) => { + refresh([ + ...(values?.variables ?? []), + ]) + handleClose() + }) + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+ + {(fields) => ( + <> + {fields.map(({ name }, index) => { + const field = variables[index] + return ( + + { + field.type === 'string' && + } + { + field.type === 'number' && + } + { + field.type === 'boolean' && {`${field.name}·${field.description}`} + } + + ) + })} + + )} + +
+
+ ); +}); + +export default VariableConfigModal; \ No newline at end of file diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx new file mode 100644 index 00000000..20ef8937 --- /dev/null +++ b/web/src/views/Workflow/components/NodeLibrary.tsx @@ -0,0 +1,48 @@ +import { type FC } from 'react'; +import { useTranslation } from 'react-i18next' +import { Card, Space } from 'antd' + +import { nodeLibrary } from '../constant'; + +const NodeLibrary: FC = () => { + const { t } = useTranslation() + + console.log('nodeLibrary', nodeLibrary) + + return ( +
+ + {nodeLibrary.map(category => ( + + + {category.nodes.map((node, nodeIndex) => ( +
{ + e.dataTransfer.setData('application/reactflow', node.type); + e.dataTransfer.setData('application/json', JSON.stringify(node)); + }} + > + + {t(`workflow.${node.type}`)} +
+ ))} +
+
+ ))} +
+
+ ); +}; + +export default NodeLibrary; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx new file mode 100644 index 00000000..5a84fdfa --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -0,0 +1,19 @@ +import clsx from 'clsx'; +import type { ReactShapeConfig } from '@antv/x6-react-shape'; + +const AddNode: ReactShapeConfig['component'] = ({ node }) => { + const data = node?.getData() || {} + + return ( +
+ + {data.icon} {data.label} + +
+ ); +}; + +export default AddNode; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx new file mode 100644 index 00000000..f1a48f91 --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Button } from 'antd' +import type { ReactShapeConfig } from '@antv/x6-react-shape'; + +const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { + const data = node?.getData() || {}; + + const addPort = (e: React.MouseEvent) => { + if (!node || !node.addPort) return; + e.stopPropagation(); + + const currentPorts = node.getPorts(); + const totalPorts = currentPorts.length; + + // 如果没有端口,添加第一个端口和ELSE端口 + if (totalPorts === 0) { + // 添加第一个ELIF端口 + node.addPort({ + id: 'elif_1', + group: 'right', + attrs: { + text: { + text: 'ELIF 1', + }, + }, + }); + // 添加ELSE端口 + node.addPort({ + id: 'else', + group: 'right', + attrs: { + text: { + text: 'ELSE', + }, + }, + }); + return; + } + + // 如果只有一个端口,确保它是ELSE,然后在之前添加ELIF + if (totalPorts === 1) { + const existingPort = currentPorts[0]; + + // 如果现有端口不是ELSE,先移除它 + if (node.removePort && existingPort.id !== 'else') { + node.removePort(existingPort.id as string); + + // 添加ELIF端口 + node.addPort({ + id: 'elif_1', + group: 'right', + attrs: { + text: { + text: 'ELIF 1', + }, + }, + }); + } + + // 添加或确保存在ELSE端口 + if (existingPort.id !== 'else') { + node.addPort({ + id: 'else', + group: 'right', + attrs: { + text: { + text: 'ELSE', + }, + }, + }); + } + return; + } + + // 获取最后一个端口,确保它是ELSE + let lastPort = currentPorts[totalPorts - 1]; + + // 如果最后一个端口不是ELSE,先移除它 + if (node.removePort && lastPort.id !== 'else') { + node.removePort(lastPort.id as string); + + // 添加ELSE端口作为最后一个 + node.addPort({ + id: 'else', + group: 'right', + attrs: { + text: { + text: 'ELSE', + }, + }, + }); + + // 更新currentPorts和totalPorts + const updatedPorts = node.getPorts(); + const updatedTotal = updatedPorts.length; + lastPort = updatedPorts[updatedTotal - 1]; + } + + // 计算新的ELIF端口数量(最后一个是ELSE,不算在内) + const elifCount = totalPorts - 1; + const newElifCount = elifCount + 1; + + // 如果有removePort方法,先移除最后一个端口(ELSE),添加新的ELIF端口,再添加回ELSE端口 + if (node.removePort) { + // 移除最后一个端口(ELSE) + node.removePort(lastPort.id as string); + + // 添加新的ELIF端口在倒数第二个位置 + node.addPort({ + id: `elif_${newElifCount}`, + group: 'right', + attrs: { + text: { + text: `ELIF ${newElifCount}`, + }, + }, + }); + + // 添加回ELSE端口 + node.addPort({ + id: 'else', + group: 'right', + attrs: { + text: { + text: 'ELSE', + }, + }, + }); + } + }; + + // const removeElif = (e: React.MouseEvent) => { + // e.stopPropagation(); + // }; + + return ( +
+ + + {/* 标题区域 */} +
+
+ 🔀 +
+ 条件分支 +
+
+ ); +}; + +export default ConditionNode; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx new file mode 100644 index 00000000..cd8a9c50 --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/GroupStartNode.tsx @@ -0,0 +1,19 @@ +import clsx from 'clsx'; +import type { ReactShapeConfig } from '@antv/x6-react-shape'; + +const GroupStartNode: ReactShapeConfig['component'] = ({ node }) => { + const data = node?.getData() || {} + + return ( +
+ + {data.icon} {data.label} + +
+ ); +}; + +export default GroupStartNode; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Nodes/IterationNode.tsx b/web/src/views/Workflow/components/Nodes/IterationNode.tsx new file mode 100644 index 00000000..a6c55138 --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/IterationNode.tsx @@ -0,0 +1,98 @@ +import { useEffect } from 'react'; +import clsx from 'clsx'; +import { Dropdown } from 'antd'; +import { SmallDashOutlined } from '@ant-design/icons'; +import type { ReactShapeConfig } from '@antv/x6-react-shape'; +import { graphNodeLibrary } from '../../constant'; + +interface NodeData { + isSelected?: boolean; + type?: string; + label?: string; + icon?: string; + parentId?: string; + isGroup?: boolean; +} + +const IterationNode: ReactShapeConfig['component'] = ({ node, graph }) => { + const data = node.getData() as NodeData; + + useEffect(() => { + initNodes() + }, []) + + const initNodes = () => { + // 添加默认子节点 + const parentBBox = node.getBBox(); + const centerX = parentBBox.x + 24; // 默认节点宽度的一半 + const centerY = parentBBox.y + 50; // 默认节点高度的一半 + + const childNode1 = graph.addNode({ + ...graphNodeLibrary.groupStart, + x: centerX, + y: centerY, + data: { + type: 'default', + label: '开始', + // icon: '📌', + parentId: node.id, + isDefault: true // 标记为默认节点,不可删除 + }, + }); + const childNode2 = graph.addNode({ + ...graphNodeLibrary.addStart, + x: centerX + 150, + y: centerY, + data: { + type: 'default', + label: '添加节点', + icon: '+', + parentId: node.id, + }, + }); + node.addChild(childNode1) + node.addChild(childNode2) + } + + return ( +
+ {/* 标题区域 */} +
+
+ 🔁 +
+ 迭代 +
+ + + + + {/* 画布内容区域 */} +
+
+ ); +}; + +export default IterationNode; diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx new file mode 100644 index 00000000..86e1ee5b --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -0,0 +1,98 @@ +import { useEffect } from 'react'; +import clsx from 'clsx'; +import { Dropdown } from 'antd'; +import { SmallDashOutlined } from '@ant-design/icons'; +import type { ReactShapeConfig } from '@antv/x6-react-shape'; +import { graphNodeLibrary } from '../../constant'; + +interface NodeData { + isSelected?: boolean; + type?: string; + label?: string; + icon?: string; + parentId?: string; + isGroup?: boolean; +} + +const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { + const data = node.getData() as NodeData; + + useEffect(() => { + initNodes() + }, []) + + const initNodes = () => { + // 添加默认子节点 + const parentBBox = node.getBBox(); + const centerX = parentBBox.x + 24; // 默认节点宽度的一半 + const centerY = parentBBox.y + 50; // 默认节点高度的一半 + + const childNode1 = graph.addNode({ + ...graphNodeLibrary.groupStart, + x: centerX, + y: centerY, + data: { + type: 'default', + label: '开始', + // icon: '📌', + parentId: node.id, + isDefault: true // 标记为默认节点,不可删除 + }, + }); + const childNode2 = graph.addNode({ + ...graphNodeLibrary.addStart, + x: centerX + 150, + y: centerY, + data: { + type: 'default', + label: '添加节点', + icon: '+', + parentId: node.id, + }, + }); + node.addChild(childNode1) + node.addChild(childNode2) + } + + return ( +
+ {/* 标题区域 */} +
+
+ ♻️ +
+ 循环 +
+ + + + + {/* 画布内容区域 */} +
+
+ ); +}; + +export default LoopNode; diff --git a/web/src/views/Workflow/components/Nodes/NormalNode.tsx b/web/src/views/Workflow/components/Nodes/NormalNode.tsx new file mode 100644 index 00000000..80eae888 --- /dev/null +++ b/web/src/views/Workflow/components/Nodes/NormalNode.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next' +import type { ReactShapeConfig } from '@antv/x6-react-shape'; + +const NormalNode: ReactShapeConfig['component'] = ({ node }) => { + const data = node?.getData() || {} + const { t } = useTranslation() + + return ( +
+
+
+ +
{data.name ?? t(`workflow.${data.type}`)}
+
+ +
{}} + >
+
+ +
{t('workflow.clickToConfigure')}
+
+ ); +}; + +export default NormalNode; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx new file mode 100644 index 00000000..2714e45f --- /dev/null +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -0,0 +1,89 @@ +import { type FC } from 'react'; +import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; + +interface TextareaProps { + parentName?: string; + label?: string; + placeholder?: string; + value?: string; + onChange?: (value?: string) => void; +} +const roleOptions = [ + // { label: 'SYSTEM', value: 'SYSTEM' }, + { label: 'USER', value: 'USER' }, + { label: 'ASSISTANT', value: 'ASSISTANT' }, +] +const MessageEditor: FC = ({ + parentName = 'messages', + placeholder, +}) => { + const form = Form.useFormInstance(); + const values = form.getFieldsValue() + + const handleAdd = (add: FormListOperation['add']) => { + const list = values[parentName]; + const lastRole = list[list.length - 1].role + + add({ + role: lastRole === 'USER' ? 'ASSISTANT' : 'USER', + content: undefined + }) + } + + return ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => { + const currentRole = values[parentName]?.[key].role || 'USER' + + return ( + + + + + {currentRole === 'SYSTEM' + ? + : + ({ + value: key, + label: t(`workflow.config.start.${key}`), + }))} + onChange={handleChangeType} + labelRender={(props) =>
{props.label} {variableType[props.value as keyof typeof variableType]}
} + optionRender={(props) =>
{props.label} {variableType[props.value as keyof typeof variableType]}
} + /> + + {/* 变量名称 */} + + + + + {/* 显示名称 */} + + + + + {/* 最大长度 */} + {['string'].includes(values?.type) && ( + + + + )} + {/* 默认值 */} + {['string', 'number', 'boolean'].includes(values?.type) && ( + + {['string'].includes(values.type) && } + {['number'].includes(values.type) && } + {['boolean'].includes(values.type) && { + updateNodeLabel(e.target.value); + }} + /> +
+ {configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { + const config = configs[key] || {} + + if (selectedNode.data.type === 'start' && key === 'variables' && config.type === 'define') { + return ( +
+
+
+ {t(`workflow.config.${selectedNode.data.type}.${key}`)} +
+ +
+ + + {Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) => +
+ {vo.name}·{vo.description} + +
+ {vo.required && {t('workflow.config.start.required')}} + {vo.type} +
+ +
handleEditVariable(index, vo)} + >
+
handleDeleteVariable(index, vo)} + >
+
+
+ )} + + {config.sys?.map((vo, index) => +
+
+ sys.{vo.name} +
+ {vo.type} +
+ )} +
+
+ ) + } + + if (selectedNode.data.type === 'llm' && key === 'messages' && config.type === 'define') { + return ( + + + + ) + } + + if (config.type === 'define') { + return null + } + + return ( + + {config.type === 'input' + ? + : config.type === 'textarea' + ? + : config.type === 'select' + ?