chore: import clean Agent Desk snapshot (6/7)

This commit is contained in:
2026-10-16 12:02:48 +00:00
parent d215bf6303
commit 756ae33d05
278 changed files with 53808 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
"use client"
import { useMemo, useState } from "react"
import { AlertTriangleIcon, CheckCircle2Icon, TimerIcon } from "lucide-react"
import { JsonTreeViewer } from "@/components/json-tree-viewer"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useI18n } from "@/i18n/provider"
import type { AIWorkflowNodeRun, AIWorkflowRun } from "@/lib/api/admin"
import { cn } from "@/lib/utils"
import { OfficialWorkflowEditor } from "../../ai-workflows/_components/official-workflow-editor"
export function WorkflowRunAuditGraph({ run }: { run: AIWorkflowRun }) {
const t = useI18n()
const nodeRuns = useMemo(() => run.nodes ?? [], [run.nodes])
const firstNodeId = nodeRuns[0]?.nodeId ?? run.definition?.nodes?.[0]?.id ?? ""
const [selectedNodeId, setSelectedNodeId] = useState(firstNodeId)
const selectedNodeRun = nodeRuns.find((item) => item.nodeId === selectedNodeId) ?? nodeRuns[0]
const executedNodeIds = useMemo(() => new Set(nodeRuns.map((item) => item.nodeId)), [nodeRuns])
return (
<div className="grid min-h-[520px] grid-cols-[minmax(0,1fr)_320px] overflow-hidden border">
<div className="relative min-w-0">
<OfficialWorkflowEditor
documentKey={`run-${run.id}`}
definition={run.definition ?? { nodes: [], edges: [] }}
onDefinitionChange={() => undefined}
readonly
/>
<div className="pointer-events-none absolute left-3 top-3 flex flex-wrap gap-2">
<Badge variant="secondary" className="gap-1">
<CheckCircle2Icon className="size-3" />
{t("aiWorkflow.executedCount", { count: String(executedNodeIds.size) })}
</Badge>
{run.errorMessage ? (
<Badge variant="destructive" className="gap-1">
<AlertTriangleIcon className="size-3" />
{t("aiWorkflow.errorStatus")}
</Badge>
) : null}
</div>
</div>
<aside className="flex min-h-0 flex-col border-l bg-background">
<div className="border-b p-3">
<div className="text-sm font-medium">{t("aiWorkflow.nodeTraceTitle")}</div>
<div className="mt-1 text-xs text-muted-foreground">{t("aiWorkflow.nodeTraceHint")}</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<div className="space-y-2 p-3">
{nodeRuns.map((node) => (
<button
key={node.id || node.nodeId}
type="button"
className={cn(
"w-full rounded-md border p-2 text-left text-xs hover:bg-muted",
selectedNodeId === node.nodeId ? "border-primary bg-primary/5" : "bg-background"
)}
onClick={() => setSelectedNodeId(node.nodeId)}
>
<div className="flex items-center justify-between gap-2">
<span className="truncate font-medium">{node.nodeId}</span>
<Badge variant={node.errorMessage ? "destructive" : "secondary"}>{node.statusName}</Badge>
</div>
<div className="mt-1 flex items-center gap-1 text-muted-foreground">
<TimerIcon className="size-3" />
{node.durationMs ?? 0}ms
</div>
</button>
))}
</div>
</ScrollArea>
<div className="max-h-72 overflow-auto border-t p-3">
<NodeRunPreview nodeRun={selectedNodeRun} t={t} />
</div>
</aside>
</div>
)
}
function NodeRunPreview({ nodeRun, t }: { nodeRun?: AIWorkflowNodeRun; t: (key: string, values?: Record<string, string | number>) => string }) {
if (!nodeRun) {
return <div className="text-xs text-muted-foreground">{t("aiWorkflow.noNodeRuns")}</div>
}
return (
<div className="space-y-3 text-xs">
{nodeRun.errorMessage ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-2 text-destructive">
{nodeRun.errorMessage}
</div>
) : null}
<PreviewBlock title={t("aiWorkflow.inputTitle")} value={nodeRun.inputPreview} emptyText={t("aiWorkflow.noneValue")} />
<PreviewBlock title={t("aiWorkflow.outputTitle")} value={nodeRun.outputPreview} emptyText={t("aiWorkflow.noneValue")} />
</div>
)
}
function PreviewBlock({ title, value, emptyText }: { title: string; value?: string; emptyText?: string }) {
return (
<div>
<div className="mb-1 font-medium">{title}</div>
{value ? <JsonTreeViewer value={parsePreview(value)} /> : <div className="text-muted-foreground">{emptyText || "None"}</div>}
</div>
)
}
function parsePreview(value: string) {
try {
return JSON.parse(value)
} catch {
return value
}
}