feat: enhance AIAgentConfigWorkbench with version history dialog and refactor sections

This commit is contained in:
mlogclub
2026-06-26 17:28:54 +08:00
parent 9fd503b52b
commit aa346ac717
2 changed files with 135 additions and 125 deletions
@@ -5,16 +5,11 @@ import {
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
BotMessageSquareIcon, BotMessageSquareIcon,
BrainCircuitIcon,
DatabaseIcon,
GitBranchIcon, GitBranchIcon,
HistoryIcon, HistoryIcon,
LifeBuoyIcon,
PlugIcon, PlugIcon,
SaveIcon, SaveIcon,
SendIcon,
SettingsIcon, SettingsIcon,
ShieldCheckIcon,
Trash2Icon, Trash2Icon,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
@@ -23,6 +18,12 @@ import { ContentEditor } from "@/components/content-editor"
import { OptionCombobox } from "@/components/option-combobox" import { OptionCombobox } from "@/components/option-combobox"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { import {
@@ -52,11 +53,9 @@ import {
validateAIWorkflow, validateAIWorkflow,
type AIAgent, type AIAgent,
type AIConfig, type AIConfig,
type AIWorkflow,
type AIWorkflowDefinition, type AIWorkflowDefinition,
type AIWorkflowNodeSpec, type AIWorkflowNodeSpec,
type AIWorkflowVersion, type AIWorkflowVersion,
type AIWorkflowValidationResult,
type AdminAgentTeam, type AdminAgentTeam,
type CreateAIAgentPayload, type CreateAIAgentPayload,
type KnowledgeBase, type KnowledgeBase,
@@ -85,13 +84,8 @@ type DirectToolOption = {
type SectionKey = type SectionKey =
| "basic" | "basic"
| "model" | "capabilities"
| "knowledge"
| "skills"
| "tools"
| "workflow" | "workflow"
| "handoff"
| "versions"
const fallbackDefinition: AIWorkflowDefinition = { const fallbackDefinition: AIWorkflowDefinition = {
schemaVersion: 1, schemaVersion: 1,
@@ -140,13 +134,12 @@ export function AIAgentConfigWorkbench({
const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null) const [currentAgentId, setCurrentAgentId] = useState(agentId ?? null)
const [activeSection, setActiveSection] = useState<SectionKey>("basic") const [activeSection, setActiveSection] = useState<SectionKey>("basic")
const [agent, setAgent] = useState<AIAgent | null>(null) const [agent, setAgent] = useState<AIAgent | null>(null)
const [workflow, setWorkflow] = useState<AIWorkflow | null>(null)
const [workflowVersions, setWorkflowVersions] = useState<AIWorkflowVersion[]>([]) const [workflowVersions, setWorkflowVersions] = useState<AIWorkflowVersion[]>([])
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([]) const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [validation, setValidation] = useState<AIWorkflowValidationResult | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [savingAgent, setSavingAgent] = useState(false) const [savingAgent, setSavingAgent] = useState(false)
const [savingWorkflow, setSavingWorkflow] = useState(false) const [savingWorkflow, setSavingWorkflow] = useState(false)
const [versionDialogOpen, setVersionDialogOpen] = useState(false)
const [name, setName] = useState("") const [name, setName] = useState("")
const [description, setDescription] = useState("") const [description, setDescription] = useState("")
@@ -216,7 +209,6 @@ export function AIAgentConfigWorkbench({
if (!currentAgentId || currentAgentId <= 0) { if (!currentAgentId || currentAgentId <= 0) {
setAgent(null) setAgent(null)
setWorkflow(null)
setWorkflowVersions([]) setWorkflowVersions([])
setName("") setName("")
setDescription("") setDescription("")
@@ -233,7 +225,6 @@ export function AIAgentConfigWorkbench({
setSelectedSkillIds([]) setSelectedSkillIds([])
setDirectTools([]) setDirectTools([])
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
setValidation(null)
return return
} }
@@ -243,7 +234,6 @@ export function AIAgentConfigWorkbench({
]) ])
setAgent(agentDetail) setAgent(agentDetail)
setWorkflow(workflowDetail)
if (workflowDetail?.id > 0) { if (workflowDetail?.id > 0) {
const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 }) const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 })
setWorkflowVersions(versionPage.results ?? []) setWorkflowVersions(versionPage.results ?? [])
@@ -265,7 +255,6 @@ export function AIAgentConfigWorkbench({
setSelectedSkillIds(agentDetail.skillIds ?? []) setSelectedSkillIds(agentDetail.skillIds ?? [])
setDirectTools(agentDetail.directTools ?? []) setDirectTools(agentDetail.directTools ?? [])
replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition)
setValidation(null)
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to load Agent config") toast.error(error instanceof Error ? error.message : "Failed to load Agent config")
} finally { } finally {
@@ -438,13 +427,12 @@ export function AIAgentConfigWorkbench({
if (!currentAgentId) return if (!currentAgentId) return
setSavingWorkflow(true) setSavingWorkflow(true)
try { try {
const saved = await saveAIAgentWorkflow({ await saveAIAgentWorkflow({
agentId: currentAgentId, agentId: currentAgentId,
name: "", name: "",
description: "", description: "",
definition, definition,
}) })
setWorkflow(saved)
toast.success("Workflow draft saved") toast.success("Workflow draft saved")
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to save workflow draft") toast.error(error instanceof Error ? error.message : "Failed to save workflow draft")
@@ -457,7 +445,6 @@ export function AIAgentConfigWorkbench({
setSavingWorkflow(true) setSavingWorkflow(true)
try { try {
const result = await validateAIWorkflow(definition) const result = await validateAIWorkflow(definition)
setValidation(result)
toast[result.valid ? "success" : "error"]( toast[result.valid ? "success" : "error"](
result.valid ? "Workflow is valid" : "Workflow has validation errors" result.valid ? "Workflow is valid" : "Workflow has validation errors"
) )
@@ -474,7 +461,6 @@ export function AIAgentConfigWorkbench({
try { try {
const defaultDefinition = await fetchAIWorkflowDefaultDefinition() const defaultDefinition = await fetchAIWorkflowDefaultDefinition()
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition) replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
setValidation(null)
toast.success("已恢复默认流程,保存草稿或发布后生效") toast.success("已恢复默认流程,保存草稿或发布后生效")
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "恢复默认流程失败") toast.error(error instanceof Error ? error.message : "恢复默认流程失败")
@@ -493,15 +479,11 @@ export function AIAgentConfigWorkbench({
description: "", description: "",
definition, definition,
}) })
setWorkflow(saved)
const version = await publishAIAgentWorkflow(currentAgentId, definition) const version = await publishAIAgentWorkflow(currentAgentId, definition)
toast.success(`Published version ${version.version}`) toast.success(`Published version ${version.version}`)
setAgent((current) => setAgent((current) =>
current ? { ...current, workflowVersionId: version.id } : current current ? { ...current, workflowVersionId: version.id } : current
) )
setWorkflow((current) =>
current ? { ...current, publishedVersionId: version.id } : saved
)
if (saved.id > 0) { if (saved.id > 0) {
const versionPage = await fetchAIWorkflowVersions({ const versionPage = await fetchAIWorkflowVersions({
workflowId: saved.id, workflowId: saved.id,
@@ -523,13 +505,8 @@ export function AIAgentConfigWorkbench({
const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [ const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [
{ key: "basic", title: "基础信息", icon: <SettingsIcon /> }, { key: "basic", title: "基础信息", icon: <SettingsIcon /> },
{ key: "model", title: "模型与 Prompt", icon: <BrainCircuitIcon /> }, { key: "capabilities", title: "能力来源", icon: <PlugIcon /> },
{ key: "knowledge", title: "知识库", icon: <DatabaseIcon /> },
{ key: "skills", title: "Skills", icon: <ShieldCheckIcon /> },
{ key: "tools", title: "MCP Tools", icon: <PlugIcon /> },
{ key: "workflow", title: "会话流程", icon: <GitBranchIcon /> }, { key: "workflow", title: "会话流程", icon: <GitBranchIcon /> },
{ key: "handoff", title: "转人工与兜底", icon: <LifeBuoyIcon /> },
{ key: "versions", title: "版本记录", icon: <HistoryIcon /> },
] ]
const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions) const selectedKnowledgeOptions = selectedOptions(selectedKnowledgeIds, knowledgeOptions)
@@ -569,15 +546,7 @@ export function AIAgentConfigWorkbench({
onClick={saveAgentSettings} onClick={saveAgentSettings}
> >
<SaveIcon className="size-4" /> <SaveIcon className="size-4" />
</Button>
<Button
type="button"
disabled={savingWorkflow || loading || !currentAgentId}
onClick={publishWorkflow}
>
<SendIcon className="size-4" />
</Button> </Button>
</> </>
)} )}
@@ -624,7 +593,7 @@ export function AIAgentConfigWorkbench({
... ...
</div> </div>
) : ( ) : (
<div className={activeSection === "workflow" ? "h-full min-h-0" : "w-full p-6"}> <div className={activeSection === "workflow" ? "h-full min-h-0" : "w-full space-y-6 p-6"}>
{activeSection === "basic" ? ( {activeSection === "basic" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
@@ -646,7 +615,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "model" ? ( {activeSection === "basic" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FieldBlock label="AI 配置"> <FieldBlock label="AI 配置">
@@ -683,7 +652,38 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "knowledge" ? ( {activeSection === "basic" ? (
<ConfigSection>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<FieldBlock label="转人工模式">
<OptionCombobox value={handoffMode} options={handoffModeOptions} placeholder="选择转人工模式" onChange={setHandoffMode} />
</FieldBlock>
<FieldBlock label="兜底策略">
<OptionCombobox value={fallbackMode} options={fallbackModeOptions} placeholder="选择兜底策略" onChange={setFallbackMode} />
</FieldBlock>
</div>
<AddRow
value={teamToAdd}
options={teamOptions.filter((option) => !selectedTeamIds.includes(Number(option.value)))}
placeholder="选择客服组"
onValueChange={setTeamToAdd}
onAdd={() => {
addSelected(teamToAdd, selectedTeamIds, setSelectedTeamIds)
setTeamToAdd("")
}}
/>
<BadgeList
empty="未配置客服组。"
items={selectedTeamOptions}
onRemove={(id) => setSelectedTeamIds((current) => current.filter((item) => item !== id))}
/>
<FieldBlock label="兜底文案">
<Textarea rows={5} value={fallbackMessage} onChange={(event) => setFallbackMessage(event.target.value)} />
</FieldBlock>
</ConfigSection>
) : null}
{activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<AddRow <AddRow
value={knowledgeToAdd} value={knowledgeToAdd}
@@ -736,7 +736,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "skills" ? ( {activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<AddRow <AddRow
value={skillToAdd} value={skillToAdd}
@@ -756,7 +756,7 @@ export function AIAgentConfigWorkbench({
</ConfigSection> </ConfigSection>
) : null} ) : null}
{activeSection === "tools" ? ( {activeSection === "capabilities" ? (
<ConfigSection> <ConfigSection>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[220px_minmax(0,1fr)_auto]"> <div className="grid grid-cols-1 gap-3 lg:grid-cols-[220px_minmax(0,1fr)_auto]">
<OptionCombobox <OptionCombobox
@@ -824,87 +824,32 @@ export function AIAgentConfigWorkbench({
saveDraftDisabled={savingWorkflow || loading || !currentAgentId} saveDraftDisabled={savingWorkflow || loading || !currentAgentId}
onPublish={publishWorkflow} onPublish={publishWorkflow}
publishDisabled={savingWorkflow || loading || !currentAgentId} publishDisabled={savingWorkflow || loading || !currentAgentId}
toolbarExtra={
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 rounded-none px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => setVersionDialogOpen(true)}
>
<HistoryIcon className="size-3.5" />
</Button>
}
/> />
) : null} ) : null}
{activeSection === "handoff" ? ( <Dialog open={versionDialogOpen} onOpenChange={setVersionDialogOpen}>
<ConfigSection> <DialogContent className="max-h-[80vh] overflow-hidden sm:max-w-4xl">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <DialogHeader>
<FieldBlock label="转人工模式"> <DialogTitle></DialogTitle>
<OptionCombobox value={handoffMode} options={handoffModeOptions} placeholder="选择转人工模式" onChange={setHandoffMode} /> </DialogHeader>
</FieldBlock> <VersionRecordsTable
<FieldBlock label="兜底策略"> agent={agent}
<OptionCombobox value={fallbackMode} options={fallbackModeOptions} placeholder="选择兜底策略" onChange={setFallbackMode} /> workflowVersions={workflowVersions}
</FieldBlock>
</div>
<AddRow
value={teamToAdd}
options={teamOptions.filter((option) => !selectedTeamIds.includes(Number(option.value)))}
placeholder="选择客服组"
onValueChange={setTeamToAdd}
onAdd={() => {
addSelected(teamToAdd, selectedTeamIds, setSelectedTeamIds)
setTeamToAdd("")
}}
/> />
<BadgeList </DialogContent>
empty="未配置客服组。" </Dialog>
items={selectedTeamOptions}
onRemove={(id) => setSelectedTeamIds((current) => current.filter((item) => item !== id))}
/>
<FieldBlock label="兜底文案">
<Textarea rows={5} value={fallbackMessage} onChange={(event) => setFallbackMessage(event.target.value)} />
</FieldBlock>
</ConfigSection>
) : null}
{activeSection === "versions" ? (
<ConfigSection>
<div className="overflow-hidden rounded-md border">
{workflowVersions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead className="w-28"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflowVersions.map((version) => (
<TableRow key={version.id}>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">v{version.version}</span>
{agent?.workflowVersionId === version.id ? (
<Badge variant="secondary"></Badge>
) : null}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{version.publishedAt || version.createdAt || "-"}
</TableCell>
<TableCell>{version.publishedByName || "-"}</TableCell>
<TableCell>
<Badge variant={version.status === Status.Ok ? "outline" : "secondary"}>
{version.status === Status.Ok ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell className="text-right font-mono text-xs text-muted-foreground">
{version.definitionHash ? version.definitionHash.slice(0, 8) : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="p-4 text-sm text-muted-foreground"></div>
)}
</div>
</ConfigSection>
) : null}
</div> </div>
)} )}
@@ -960,6 +905,60 @@ function AddRow({
) )
} }
function VersionRecordsTable({
agent,
workflowVersions,
}: {
agent: AIAgent | null
workflowVersions: AIWorkflowVersion[]
}) {
return (
<div className="max-h-[60vh] overflow-auto rounded-md border">
{workflowVersions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead className="w-28"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflowVersions.map((version) => (
<TableRow key={version.id}>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">v{version.version}</span>
{agent?.workflowVersionId === version.id ? (
<Badge variant="secondary"></Badge>
) : null}
</div>
</TableCell>
<TableCell className="text-muted-foreground">
{version.publishedAt || version.createdAt || "-"}
</TableCell>
<TableCell>{version.publishedByName || "-"}</TableCell>
<TableCell>
<Badge variant={version.status === Status.Ok ? "outline" : "secondary"}>
{version.status === Status.Ok ? "启用" : "禁用"}
</Badge>
</TableCell>
<TableCell className="text-right font-mono text-xs text-muted-foreground">
{version.definitionHash ? version.definitionHash.slice(0, 8) : "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="p-4 text-sm text-muted-foreground"></div>
)}
</div>
)
}
function BadgeList({ function BadgeList({
empty, empty,
items, items,
@@ -40,7 +40,7 @@ import {
SendIcon, SendIcon,
Undo2Icon, Undo2Icon,
} from "lucide-react" } from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
@@ -192,6 +192,7 @@ export function WorkflowEditor({
saveDraftDisabled = false, saveDraftDisabled = false,
onPublish, onPublish,
publishDisabled = false, publishDisabled = false,
toolbarExtra,
}: { }: {
definition: AIWorkflowDefinition definition: AIWorkflowDefinition
nodeSpecs: AIWorkflowNodeSpec[] nodeSpecs: AIWorkflowNodeSpec[]
@@ -204,6 +205,7 @@ export function WorkflowEditor({
saveDraftDisabled?: boolean saveDraftDisabled?: boolean
onPublish?: () => void onPublish?: () => void
publishDisabled?: boolean publishDisabled?: boolean
toolbarExtra?: ReactNode
}) { }) {
const [nodes, setNodes, onNodesChange] = useNodesState<WorkflowFlowNode>( const [nodes, setNodes, onNodesChange] = useNodesState<WorkflowFlowNode>(
toFlowNodes(definition) toFlowNodes(definition)
@@ -916,6 +918,7 @@ export function WorkflowEditor({
onRedo={redoWorkflowEdit} onRedo={redoWorkflowEdit}
onRestoreDefault={onRestoreDefault} onRestoreDefault={onRestoreDefault}
restoreDefaultDisabled={restoreDefaultDisabled} restoreDefaultDisabled={restoreDefaultDisabled}
toolbarExtra={toolbarExtra}
/> />
</div> </div>
{propertyPanelNode ? ( {propertyPanelNode ? (
@@ -1403,6 +1406,7 @@ function WorkflowCanvasToolbar({
onRedo, onRedo,
onRestoreDefault, onRestoreDefault,
restoreDefaultDisabled, restoreDefaultDisabled,
toolbarExtra,
}: { }: {
validationErrors: string[] validationErrors: string[]
validationValid: boolean validationValid: boolean
@@ -1418,6 +1422,7 @@ function WorkflowCanvasToolbar({
onRedo: () => void onRedo: () => void
onRestoreDefault?: () => void onRestoreDefault?: () => void
restoreDefaultDisabled?: boolean restoreDefaultDisabled?: boolean
toolbarExtra?: ReactNode
}) { }) {
return ( return (
<div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm"> <div className="flex overflow-hidden rounded-md border bg-background/95 shadow-sm">
@@ -1486,6 +1491,12 @@ function WorkflowCanvasToolbar({
</Button> </Button>
</> </>
) : null} ) : null}
{toolbarExtra ? (
<>
<WorkflowToolbarDivider />
{toolbarExtra}
</>
) : null}
<WorkflowToolbarDivider /> <WorkflowToolbarDivider />
<Button <Button
type="button" type="button"