Add @xyflow/react dependency and integrate workflow features in AssistantPage

Updated package.json and package-lock.json to include the @xyflow/react library. Enhanced the AssistantPage with a new AssistantWorkflowPage component for creating and managing workflows, allowing users to visually connect nodes. Removed the deprecated WorkflowPage component to streamline the codebase. Updated global styles to incorporate @xyflow/react styles for consistent UI.
This commit is contained in:
Xin Wang
2026-06-09 10:31:49 +08:00
parent efb3c606a3
commit f7fd2bb53e
8 changed files with 380 additions and 143 deletions

231
package-lock.json generated
View File

@@ -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
}
}
}
}
}

View File

@@ -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",

View File

@@ -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 *));

View File

@@ -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" && <HistoryPage />}
{active === "dashboard" && <DashboardPage />}
{active === "test" && <TestPage />}
{active === "workflow" && <WorkflowPage />}
{active === "profile" && <ProfilePage />}
</div>
</main>
</div>
);
}
}

View File

@@ -12,7 +12,6 @@ import {
Home,
PlayCircle,
Video,
Workflow,
} from "lucide-react";
import type { NavKey } from "./AppShell";

View File

@@ -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: <Workflow size={20} />,
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 (
<AssistantWorkflowPage
workflowName={draftName.trim() || "未命名工作流助手"}
onBack={() => setView("list")}
/>
);
}
if (view === "create-dify") {
return (
<div className="-mt-6 flex h-full flex-col gap-4">

View File

@@ -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 (
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-8">
<div>
<h1 className="font-display display-lg text-ink"></h1>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-muted-foreground">
AI
</p>
</div>
<section className="relative overflow-hidden rounded-3xl border border-hairline bg-canvas-soft p-10">
<div
aria-hidden
className="pointer-events-none absolute -right-20 -top-24 h-72 w-72 rounded-full opacity-55 blur-3xl"
style={{
backgroundImage:
"radial-gradient(circle, color-mix(in srgb, var(--gradient-mint) 55%, transparent), transparent 70%)",
}}
/>
<div className="relative flex items-start gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-surface-strong text-foreground">
<Workflow size={30} />
</div>
<div className="flex-1">
<div className="caption-label inline-flex rounded-full bg-surface-strong px-3 py-1 text-muted-foreground">
</div>
<h2 className="font-display display-sm mt-5 text-ink">
</h2>
<p className="mt-3 max-w-2xl text-[15px] leading-7 text-body">
</p>
<div className="mt-7 flex gap-3">
<Button size="lg" className="gap-2">
<Plus size={16} />
</Button>
<Button
variant="outline"
size="lg"
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
>
<ArrowRight size={16} />
</Button>
</div>
</div>
</div>
</section>
<section className="grid grid-cols-3 gap-4">
<FeatureCard
icon={<GitBranch size={20} />}
title="节点编排"
description="通过拖拽节点组织多轮对话、判断分支和任务流转。"
/>
<FeatureCard
icon={<Boxes size={20} />}
title="组件复用"
description="复用模型、知识库、语音识别、声音资源和工具插件。"
/>
<FeatureCard
icon={<Workflow size={20} />}
title="流程调试"
description="支持逐节点测试、查看输入输出和定位失败原因。"
/>
</section>
<section className="rounded-2xl border border-hairline bg-card p-6 shadow-sm">
<div className="mb-5">
<h2 className="font-display display-sm text-ink"></h2>
<div className="flex h-[calc(100vh-160px)] min-h-[600px] flex-col gap-4">
<header className="flex items-center justify-between">
<div>
<h1 className="font-display display-lg text-ink">{workflowName}</h1>
<p className="mt-1 text-sm text-muted-foreground">
React Flow
线
</p>
</div>
<div className="flex items-center gap-3 overflow-x-auto rounded-2xl border border-hairline bg-canvas-soft p-5">
{["开始", "意图识别", "知识库检索", "模型回答", "工具调用", "结束"].map(
(item, index) => (
<div key={item} className="flex items-center gap-3">
<div className="min-w-[128px] rounded-xl border border-hairline bg-card p-4 text-center shadow-sm">
<div className="text-sm font-medium text-foreground">
{item}
</div>
<div className="mt-1 text-xs text-muted-soft">
Node {index + 1}
</div>
</div>
<div className="flex gap-2">
{onBack ? (
<Button variant="outline" onClick={onBack}>
<ChevronLeft size={16} />
</Button>
) : null}
{index < 5 && (
<ArrowRight size={18} className="shrink-0 text-muted-soft" />
)}
</div>
),
)}
<Button variant="outline" onClick={handleAddNode}>
<Plus size={16} />
</Button>
<Button onClick={handleSave}>
<Save size={16} />
</Button>
</div>
</section>
</div>
);
}
</header>
function FeatureCard({
icon,
title,
description,
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="rounded-2xl border border-hairline bg-card p-6 shadow-sm transition-shadow hover:shadow-md">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
<div className="min-h-0 flex-1 overflow-hidden rounded-2xl border border-hairline bg-card">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={handleConnect}
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</div>
<div className="font-medium text-foreground">{title}</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{description}
</p>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { PlaceholderPage } from "./PlaceholderPage";
export function WorkflowPage() {
return (
<PlaceholderPage
title="工作流"
description="管理与编排可复用的助手工作流,支持多轮任务与工具调用。"
/>
);
}