Files
ai-agent/web/app/dashboard/ai-workflows/page.tsx
T
mlogclub 7b86fd1f09 feat: Implement AI Agent Workflow Binding functionality
- Added aiAgentWorkflowBindingRepository for managing workflow bindings associated with AI agents.
- Enhanced agentRevisionService to include workflow bindings in agent revisions.
- Updated aIAgentService to handle workflow bindings during agent creation and updates.
- Introduced ai_agent_workflow_binding_service for managing workflow binding logic.
- Created new API endpoints for fetching, creating, updating, and deleting AI workflows.
- Developed a new dashboard page for managing AI workflows.
- Updated frontend components to support workflow binding management in agent configuration.
- Added necessary tests for workflow binding functionality and updated existing tests for compatibility.
- Translated relevant UI texts and messages for workflow management.
2026-07-25 15:24:49 +08:00

100 lines
5.3 KiB
TypeScript

"use client"
import { useCallback, useEffect, useState } from "react"
import { PlusIcon } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
createAIWorkflow,
fetchAIWorkflow,
fetchAIWorkflowNodeSpecs,
fetchAIWorkflows,
publishAIWorkflow,
updateAIWorkflow,
validateAIWorkflow,
type AIWorkflow,
type AIWorkflowDefinition,
type AIWorkflowNodeSpec,
} from "@/lib/api/admin"
import { WorkflowEditor } from "./_components/workflow-editor"
const emptyDefinition: AIWorkflowDefinition = {
schemaVersion: 2,
nodes: [
{ id: "start_1", type: "start", meta: { position: { x: 0, y: 80 } }, data: { title: "开始", config: {}, inputsValues: {} } },
{ id: "end_1", type: "end", meta: { position: { x: 260, y: 80 } }, data: { title: "结束", config: {}, inputsValues: {} } },
],
edges: [{ sourceNodeID: "start_1", targetNodeID: "end_1", sourcePortID: "edge_start_end" }],
}
export default function DashboardAIWorkflowsPage() {
const [items, setItems] = useState<AIWorkflow[]>([])
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [active, setActive] = useState<AIWorkflow | null>(null)
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [definition, setDefinition] = useState<AIWorkflowDefinition>(emptyDefinition)
const [saving, setSaving] = useState(false)
const select = useCallback(async (id: number) => {
const item = await fetchAIWorkflow(id)
setActive(item)
setName(item.name)
setDescription(item.description)
setDefinition(item.draftDefinition)
}, [])
const reload = useCallback(async () => {
const [page, specs] = await Promise.all([fetchAIWorkflows({ limit: 100 }), fetchAIWorkflowNodeSpecs()])
setItems(page.results)
setNodeSpecs(specs)
if (!active && page.results[0]) await select(page.results[0].id)
}, [active, select])
useEffect(() => { void reload().catch((error) => toast.error(error instanceof Error ? error.message : "加载工作流失败")) }, [reload])
async function save() {
if (!name.trim()) { toast.error("请填写工作流名称"); return }
setSaving(true)
try {
if (active) {
await updateAIWorkflow({ id: active.id, name: name.trim(), description: description.trim(), definition })
await select(active.id)
} else {
const created = await createAIWorkflow({ name: name.trim(), description: description.trim(), definition })
await select(created.id)
}
await reload()
toast.success("工作流草稿已保存")
} catch (error) { toast.error(error instanceof Error ? error.message : "保存工作流失败") } finally { setSaving(false) }
}
async function publish() {
if (!active) { toast.error("请先保存工作流草稿"); return }
setSaving(true)
try {
const version = await publishAIWorkflow(active.id, definition)
await select(active.id)
await reload()
toast.success(`已发布工作流 v${version.version}`)
} catch (error) { toast.error(error instanceof Error ? error.message : "发布工作流失败") } finally { setSaving(false) }
}
function create() { setActive(null); setName(""); setDescription(""); setDefinition(emptyDefinition) }
return <div className="flex h-full min-h-0 bg-background">
<aside className="w-72 shrink-0 border-r bg-muted/20 p-3">
<div className="mb-3 flex items-center justify-between"><div><h1 className="font-semibold">工作流</h1><p className="text-xs text-muted-foreground">独立维护、发布后供 Agent 关联</p></div><Button size="icon" variant="outline" onClick={create}><PlusIcon className="size-4" /></Button></div>
<div className="space-y-1">{items.map((item) => <button key={item.id} type="button" onClick={() => void select(item.id)} className={`w-full rounded-md p-3 text-left ${active?.id === item.id ? "bg-background shadow-sm" : "hover:bg-background/70"}`}><div className="truncate font-medium">{item.name}</div><div className="mt-1 text-xs text-muted-foreground">{item.publishedVersionId > 0 ? `已发布版本 #${item.publishedVersionId}` : "未发布"}</div></button>)}</div>
</aside>
<section className="flex min-w-0 flex-1 flex-col">
<header className="flex shrink-0 items-center gap-3 border-b px-5 py-3"><div className="min-w-0 flex-1"><Input value={name} onChange={(event) => setName(event.target.value)} placeholder="工作流名称" className="max-w-sm" /><Textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="业务说明(可选)" className="mt-2 min-h-16 max-w-xl resize-none" /></div><Button variant="outline" disabled={saving} onClick={() => void save()}>保存草稿</Button><Button disabled={saving || !active} onClick={() => void publish()}>发布工作流</Button></header>
<div className="min-h-0 flex-1"><WorkflowEditor definition={definition} nodeSpecs={nodeSpecs} onDefinitionChange={setDefinition} onValidate={() => void validateAIWorkflow(definition).then((result) => toast[result.valid ? "success" : "error"](result.valid ? "工作流校验通过" : "工作流存在校验错误"))} validateDisabled={saving} /></div>
</section>
</div>
}