diff --git a/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx b/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx index fa4e942..23c6a6e 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-workbench.tsx @@ -1,20 +1,13 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" +import { PencilIcon } from "lucide-react" 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 { Textarea } from "@/components/ui/textarea" import { createAIWorkflow, fetchAIWorkflow, @@ -69,9 +62,12 @@ export function WorkflowWorkbench({ const [saving, setSaving] = useState(false) const [dirty, setDirty] = useState(false) const [loaded, setLoaded] = useState(false) - const [metadataOpen, setMetadataOpen] = useState(false) - const [metadataName, setMetadataName] = useState("") - const [metadataDescription, setMetadataDescription] = useState("") + const [editingField, setEditingField] = useState< + "name" | "description" | null + >(null) + const editSnapshotRef = useRef({ value: "", dirty: false }) + const cancellingEditRef = useRef(false) + const savingRef = useRef(false) const load = useCallback(async () => { setLoaded(false) @@ -104,26 +100,99 @@ export function WorkflowWorkbench({ ) }, [load]) - function openMetadata() { - setMetadataName(name) - setMetadataDescription(description) - setMetadataOpen(true) + function beginEditing(field: "name" | "description") { + cancellingEditRef.current = false + editSnapshotRef.current = { + value: field === "name" ? name : description, + dirty, + } + setEditingField(field) } - function applyMetadata() { - setName(metadataName) - setDescription(metadataDescription) - setDirty(true) - setMetadataOpen(false) + async function finishEditing(field: "name" | "description") { + if (cancellingEditRef.current) { + cancellingEditRef.current = false + return + } + setEditingField(null) + const currentValue = field === "name" ? name.trim() : description.trim() + if (currentValue === editSnapshotRef.current.value) { + setDirty(editSnapshotRef.current.dirty) + return + } + if (field === "name" && !currentValue) { + setName(editSnapshotRef.current.value) + setDirty(editSnapshotRef.current.dirty) + toast.error("请填写工作流名称") + return + } + + const nextName = field === "name" ? currentValue : name.trim() + const nextDescription = + field === "description" ? currentValue : description.trim() + if (savingRef.current) return + savingRef.current = true + setSaving(true) + try { + if (active) { + await updateAIWorkflow({ + id: active.id, + name: nextName, + description: nextDescription, + definition: active.draftDefinition, + }) + setActive((current) => + current + ? { + ...current, + name: nextName, + description: nextDescription, + updatedAt: new Date().toISOString(), + } + : current + ) + } else { + const created = await createAIWorkflow({ + name: nextName, + description: nextDescription, + definition, + }) + setActive(created) + } + setName(nextName) + setDescription(nextDescription) + setDirty(active ? editSnapshotRef.current.dirty : false) + onSaved?.() + toast.success(field === "name" ? "名称已保存" : "描述已保存") + } catch (error) { + setDirty(true) + toast.error(error instanceof Error ? error.message : "保存失败") + } finally { + savingRef.current = false + setSaving(false) + } + } + + function cancelEditing(field: "name" | "description") { + cancellingEditRef.current = true + if (field === "name") { + setName(editSnapshotRef.current.value) + } else { + setDescription(editSnapshotRef.current.value) + } + setDirty(editSnapshotRef.current.dirty) + setEditingField(null) } async function save() { + if (savingRef.current) return if (!name.trim()) { toast.error("请填写工作流名称") return } const savedName = name.trim() const savedDescription = description.trim() + savingRef.current = true setSaving(true) try { if (active) { @@ -163,15 +232,18 @@ export function WorkflowWorkbench({ } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败") } finally { + savingRef.current = false setSaving(false) } } async function publish() { + if (savingRef.current) return if (!active) { toast.error("请先保存") return } + savingRef.current = true setSaving(true) try { const version = await publishAIWorkflow(active.id, definition) @@ -191,6 +263,7 @@ export function WorkflowWorkbench({ } catch (error) { toast.error(error instanceof Error ? error.message : "发布失败") } finally { + savingRef.current = false setSaving(false) } } @@ -201,9 +274,40 @@ export function WorkflowWorkbench({
- {description || "暂未填写业务说明"} -
+ {editingField === "description" ? ( + event.currentTarget.select()} + onChange={(event) => { + setDescription(event.target.value) + setDirty(true) + }} + onBlur={() => void finishEditing("description")} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault() + event.currentTarget.blur() + } else if (event.key === "Escape") { + event.preventDefault() + event.stopPropagation() + cancelEditing("description") + } + }} + placeholder="添加工作流描述" + className="mt-0.5 h-6 max-w-2xl px-1 text-sm font-normal" + /> + ) : ( + + )}