diff --git a/package-lock.json b/package-lock.json index d6ef976..1d0b980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "ai-video-admin-frontend", "version": "0.1.0", "dependencies": { + "@xyflow/react": "^12.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.17.0", @@ -3848,6 +3849,55 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -4557,6 +4607,48 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz", + "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.77", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.77", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz", + "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@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" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -5173,6 +5265,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5420,6 +5518,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -11646,6 +11849,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "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 + } + } } } } diff --git a/package.json b/package.json index 5de6772..cadaaed 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@xyflow/react": "^12.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.17.0", diff --git a/src/app/globals.css b/src/app/globals.css index bdec1f5..8e6077f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; +@import "@xyflow/react/dist/style.css"; @custom-variant dark (&:is(.dark *)); diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 368872f..3015138 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -12,7 +12,6 @@ import { ComponentsToolsPage } from "@/components/pages/ComponentsToolsPage"; import { HistoryPage } from "@/components/pages/HistoryPage"; import { DashboardPage } from "@/components/pages/DashboardPage"; import { TestPage } from "@/components/pages/TestPage"; -import { WorkflowPage } from "@/components/pages/WorkflowPage"; import { ProfilePage } from "@/components/pages/ProfilePage"; export type NavKey = @@ -24,7 +23,6 @@ export type NavKey = | "history" | "dashboard" | "test" - | "workflow" | "profile"; @@ -53,10 +51,9 @@ export function AppShell() { {active === "history" && } {active === "dashboard" && } {active === "test" && } - {active === "workflow" && } {active === "profile" && } ); -} \ No newline at end of file +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 4f57e85..c990801 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -12,7 +12,6 @@ import { Home, PlayCircle, Video, - Workflow, } from "lucide-react"; import type { NavKey } from "./AppShell"; diff --git a/src/components/pages/AssistantPage.tsx b/src/components/pages/AssistantPage.tsx index 97d7785..484c24b 100644 --- a/src/components/pages/AssistantPage.tsx +++ b/src/components/pages/AssistantPage.tsx @@ -53,6 +53,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { VoiceVisualizer } from "@/components/ui/voice-visualizer"; +import { AssistantWorkflowPage } from "@/components/pages/AssistantWorkflowPage"; import { Card, CardContent, @@ -120,7 +121,7 @@ type AssistantTypeOption = { label: string; description: string; icon: React.ReactNode; - /** 提示词、Dify、FastGPT 类型已落地,工作流暂时显示占位页 */ + /** 当前构建方式是否已经有可用的构建页面 */ available: boolean; }; @@ -137,7 +138,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [ label: "使用工作流构建", description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。", icon: , - available: false, + available: true, }, { type: "Dify", @@ -303,6 +304,7 @@ export function AssistantPage() { | "list" | "choose" | "create" + | "create-workflow" | "create-dify" | "create-fastgpt" | "create-opencode" @@ -338,10 +340,9 @@ export function AssistantPage() { updateOpenCodeForm("name", assistant.name); setView("create-opencode"); } else { - // 工作流:暂时显示占位页 setDraftName(assistant.name); setDraftType(assistant.type); - setView("placeholder"); + setView("create-workflow"); } } @@ -366,8 +367,9 @@ export function AssistantPage() { // OpenCode 类型:进入 OpenCode 构建表单,并把已填的名称带过去 updateOpenCodeForm("name", draftName.trim()); setView("create-opencode"); + } else if (draftType === "工作流") { + setView("create-workflow"); } else { - // 工作流:暂时显示占位页 setView("placeholder"); } } @@ -863,6 +865,15 @@ export function AssistantPage() { ); } + if (view === "create-workflow") { + return ( + setView("list")} + /> + ); + } + if (view === "create-dify") { return (
diff --git a/src/components/pages/AssistantWorkflowPage.tsx b/src/components/pages/AssistantWorkflowPage.tsx index 79cfc04..dc8e9e5 100644 --- a/src/components/pages/AssistantWorkflowPage.tsx +++ b/src/components/pages/AssistantWorkflowPage.tsx @@ -1,137 +1,144 @@ -import { ArrowRight, Boxes, GitBranch, Plus, Workflow } from "lucide-react"; +"use client"; + +import { useCallback } from "react"; +import { + addEdge, + Background, + Connection, + Controls, + Edge, + MiniMap, + Node, + ReactFlow, + useEdgesState, + useNodesState, +} from "@xyflow/react"; +import { ChevronLeft, Plus, Save } from "lucide-react"; + import { Button } from "@/components/ui/button"; -export function AssistantWorkflowPage() { +const initialNodes: Node[] = [ + { + id: "start", + type: "input", + position: { x: 80, y: 180 }, + data: { label: "开始" }, + }, + { + id: "answer", + position: { x: 340, y: 180 }, + data: { label: "模型回答" }, + }, + { + id: "end", + type: "output", + position: { x: 600, y: 180 }, + data: { label: "结束" }, + }, +]; + +const initialEdges: Edge[] = [ + { + id: "start-answer", + source: "start", + target: "answer", + }, + { + id: "answer-end", + source: "answer", + target: "end", + }, +]; + +type AssistantWorkflowPageProps = { + workflowName?: string; + onBack?: () => void; +}; + +export function AssistantWorkflowPage({ + workflowName = "工作流编辑器", + onBack, +}: AssistantWorkflowPageProps = {}) { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const handleConnect = useCallback( + (connection: Connection) => { + setEdges((currentEdges) => addEdge(connection, currentEdges)); + }, + [setEdges], + ); + + const handleAddNode = () => { + const id = crypto.randomUUID(); + + setNodes((currentNodes) => [ + ...currentNodes, + { + id, + position: { + x: 200 + currentNodes.length * 40, + y: 100 + currentNodes.length * 40, + }, + data: { + label: `新节点 ${currentNodes.length + 1}`, + }, + }, + ]); + }; + + const handleSave = () => { + const workflow = { nodes, edges }; + + localStorage.setItem("assistant-workflow", JSON.stringify(workflow)); + + console.log("Saved workflow:", workflow); + }; + return ( -
-
-

创建助手

-

- 通过节点编排方式创建复杂 AI - 视频助手流程,适合多轮任务、工具调用、知识库检索和人工协同场景。 -

-
- -
-
- -
-
- -
- -
-
- 即将开放 -
- -

- 工作流画布正在设计中 -

- -

- 后续这里会提供可视化节点画布,你可以拖拽配置开始节点、意图识别节点、知识库检索节点、 - 大模型回答节点、工具调用节点、人工接管节点和结束节点。 -

- -
- - - -
-
-
-
- -
- } - title="节点编排" - description="通过拖拽节点组织多轮对话、判断分支和任务流转。" - /> - - } - title="组件复用" - description="复用模型、知识库、语音识别、声音资源和工具插件。" - /> - - } - title="流程调试" - description="支持逐节点测试、查看输入输出和定位失败原因。" - /> -
- -
-
-

未来画布结构预览

+
+
+
+

{workflowName}

- 当前是静态占位,后续可替换为 React Flow 或自研画布组件。 + 拖动节点,并从节点连接点拖出连线。

-
- {["开始", "意图识别", "知识库检索", "模型回答", "工具调用", "结束"].map( - (item, index) => ( -
-
-
- {item} -
-
- Node {index + 1} -
-
+
+ {onBack ? ( + + ) : null} - {index < 5 && ( - - )} -
- ), - )} + + +
-
-
- ); -} + -function FeatureCard({ - icon, - title, - description, -}: { - icon: React.ReactNode; - title: string; - description: string; -}) { - return ( -
-
- {icon} +
+ + + + +
- -
{title}
-

- {description} -

); } diff --git a/src/components/pages/WorkflowPage.tsx b/src/components/pages/WorkflowPage.tsx deleted file mode 100644 index e4958fe..0000000 --- a/src/components/pages/WorkflowPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { PlaceholderPage } from "./PlaceholderPage"; - -export function WorkflowPage() { - return ( - - ); -}