diff --git a/web/app/dashboard/ai-workflows/page.tsx b/web/app/dashboard/ai-workflows/page.tsx index db486a8..7b8024d 100644 --- a/web/app/dashboard/ai-workflows/page.tsx +++ b/web/app/dashboard/ai-workflows/page.tsx @@ -6,31 +6,105 @@ import { toast } from "sonner" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Textarea } from "@/components/ui/textarea" import { cn, formatDateTime } from "@/lib/utils" -import { createAIWorkflow, deleteAIWorkflow, fetchAIWorkflow, fetchAIWorkflowNodeSpecs, fetchAIWorkflows, fetchAIWorkflowUsage, fetchAIWorkflowVersions, publishAIWorkflow, restoreAIWorkflowVersion, updateAIWorkflow, validateAIWorkflow, type AIWorkflow, type AIWorkflowDefinition, type AIWorkflowNodeSpec, type AIWorkflowUsage, type AIWorkflowVersion } from "@/lib/api/admin" +import { + createAIWorkflow, fetchAIWorkflow, fetchAIWorkflowNodeSpecs, fetchAIWorkflows, + fetchAIWorkflowUsage, fetchAIWorkflowVersions, publishAIWorkflow, restoreAIWorkflowVersion, + updateAIWorkflow, validateAIWorkflow, type AIWorkflow, type AIWorkflowDefinition, + type AIWorkflowNodeSpec, type AIWorkflowUsage, type AIWorkflowVersion, +} 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" }] } +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([]); const [nodeSpecs, setNodeSpecs] = useState([]) - const [active, setActive] = useState(null); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [definition, setDefinition] = useState(emptyDefinition) - const [versions, setVersions] = useState([]); const [usage, setUsage] = useState([]); const [query, setQuery] = useState(""); const [saving, setSaving] = useState(false); const [dirty, setDirty] = useState(false) - const loadList = useCallback(async () => { const [page, specs] = await Promise.all([fetchAIWorkflows({ limit: 100 }), fetchAIWorkflowNodeSpecs()]); setItems(page.results ?? []); setNodeSpecs(specs ?? []) }, []) - const select = useCallback(async (id: number) => { const [item, versionPage, uses] = await Promise.all([fetchAIWorkflow(id), fetchAIWorkflowVersions({ workflowId: id, limit: 50 }), fetchAIWorkflowUsage(id)]); setActive(item); setName(item.name); setDescription(item.description); setDefinition(item.draftDefinition); setVersions(versionPage.results ?? []); setUsage(uses ?? []); setDirty(false) }, []) - useEffect(() => { void loadList().then(async () => { if (!active) { const page = await fetchAIWorkflows({ limit: 1 }); if (page.results?.[0]) await select(page.results[0].id) } }).catch((e) => toast.error(e instanceof Error ? e.message : "加载工作流失败")) }, [active, loadList, select]) + const [items, setItems] = useState([]) + const [nodeSpecs, setNodeSpecs] = useState([]) + const [active, setActive] = useState(null) + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [definition, setDefinition] = useState(emptyDefinition) + const [versions, setVersions] = useState([]) + const [usage, setUsage] = useState([]) + const [query, setQuery] = useState("") + const [saving, setSaving] = useState(false) + const [dirty, setDirty] = useState(false) + const [metadataOpen, setMetadataOpen] = useState(false) + const [metadataName, setMetadataName] = useState("") + const [metadataDescription, setMetadataDescription] = useState("") + + const loadList = useCallback(async () => { + const [page, specs] = await Promise.all([fetchAIWorkflows({ limit: 100 }), fetchAIWorkflowNodeSpecs()]) + setItems(page.results ?? []) + setNodeSpecs(specs ?? []) + }, []) + + const select = useCallback(async (id: number) => { + const [item, versionPage, uses] = await Promise.all([ + fetchAIWorkflow(id), fetchAIWorkflowVersions({ workflowId: id, limit: 50 }), fetchAIWorkflowUsage(id), + ]) + setActive(item); setName(item.name); setDescription(item.description); setDefinition(item.draftDefinition) + setVersions(versionPage.results ?? []); setUsage(uses ?? []); setDirty(false) + }, []) + + useEffect(() => { + void loadList().then(async () => { + if (!active) { + const page = await fetchAIWorkflows({ limit: 1 }) + if (page.results?.[0]) await select(page.results[0].id) + } + }).catch((error) => toast.error(error instanceof Error ? error.message : "加载工作流失败")) + }, [active, loadList, select]) + const visible = useMemo(() => items.filter((item) => item.name.toLowerCase().includes(query.trim().toLowerCase())), [items, query]) const create = () => { setActive(null); setName(""); setDescription(""); setDefinition(emptyDefinition); setVersions([]); setUsage([]); setDirty(false) } - const save = async () => { if (!name.trim()) return toast.error("请填写工作流名称"); 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 loadList(); await select(created.id) }; await loadList(); toast.success("草稿已保存") } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败") } finally { setSaving(false) } } - const publish = async () => { if (!active) return toast.error("请先保存草稿"); setSaving(true); try { const version = await publishAIWorkflow(active.id, definition); await select(active.id); await loadList(); toast.success(`已发布 v${version.version}`) } catch (e) { toast.error(e instanceof Error ? e.message : "发布失败") } finally { setSaving(false) } } - const restore = async (version: AIWorkflowVersion) => { if (!active) return; try { await restoreAIWorkflowVersion(active.id, version.id); await select(active.id); toast.success(`已将 v${version.version} 恢复为草稿`) } catch (e) { toast.error(e instanceof Error ? e.message : "恢复失败") } } - const remove = async () => { if (!active || !confirm(`确认删除“${active.name}”?`)) return; try { await deleteAIWorkflow(active.id); create(); await loadList(); toast.success("工作流已删除") } catch (e) { toast.error(e instanceof Error ? e.message : "无法删除:该工作流可能仍被 Agent 使用") } } + const openMetadata = () => { setMetadataName(name); setMetadataDescription(description); setMetadataOpen(true) } + const applyMetadata = () => { setName(metadataName); setDescription(metadataDescription); setDirty(true); setMetadataOpen(false) } + + async function save() { + if (!name.trim()) return toast.error("请填写工作流名称") + 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 loadList(); await select(created.id) } + await loadList(); toast.success("草稿已保存") + } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败") } finally { setSaving(false) } + } + + async function publish() { + if (!active) return toast.error("请先保存草稿") + setSaving(true) + try { const version = await publishAIWorkflow(active.id, definition); await select(active.id); await loadList(); toast.success(`已发布 v${version.version}`) } + catch (error) { toast.error(error instanceof Error ? error.message : "发布失败") } finally { setSaving(false) } + } + + async function restore(version: AIWorkflowVersion) { + if (!active) return + try { await restoreAIWorkflowVersion(active.id, version.id); await select(active.id); toast.success(`已将 v${version.version} 恢复为草稿`) } + catch (error) { toast.error(error instanceof Error ? error.message : "恢复失败") } + } + return
- -
{active || name ? <>
{ setName(e.target.value); setDirty(true) }} className="max-w-md border-0 px-0 text-lg font-semibold shadow-none focus-visible:ring-0" placeholder="工作流名称" />{active?.publishedVersionId ? `当前已发布` : "未发布"}{dirty ? 未保存 : null}