chore: import clean Agent Desk snapshot (6/7)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user