"use client" import "@xyflow/react/dist/style.css" import { Background, BaseEdge, Controls, Handle, MarkerType, Position, ReactFlow, getBezierPath, type Edge, type EdgeProps, type Node, type NodeProps, } from "@xyflow/react" import { AlertTriangleIcon, CheckCircle2Icon, GitBranchIcon, InfoIcon, TimerIcon, } from "lucide-react" import { useMemo, useState } from "react" import { JsonTreeViewer } from "@/components/json-tree-viewer" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import { cn } from "@/lib/utils" import type { AIWorkflowDefinition, AIWorkflowNodeRun, AIWorkflowRun, } from "@/lib/api/admin" type AuditNodeData = Record & { nodeId: string nodeType: string name: string executed: boolean statusName?: string durationMs?: number errorMessage?: string selected?: boolean } type AuditNode = Node type AuditEdge = Edge<{ executed?: boolean }> type BranchDecision = { selectedEdgeId?: string selectedBranchId?: string selectedBranchName?: string selectedTargetNodeId?: string reason?: string evaluations?: BranchEvaluation[] } type BranchEvaluation = { edgeId?: string branchId?: string branchName?: string targetNodeId?: string sourceNodeId?: string sourceField?: string operator?: string leftValue?: unknown rightValue?: unknown matched?: boolean } const auditNodeTypes = { auditNode: AuditCanvasNode, } const auditEdgeTypes = { auditEdge: AuditCanvasEdge, } const fitViewOptions = { padding: 0.12, minZoom: 0.32, maxZoom: 1, } const defaultEdgeOptions = { type: "auditEdge", markerEnd: { type: MarkerType.ArrowClosed, }, } const auditLayoutScale = { x: 1.35, y: 1.15, } export function WorkflowRunAuditGraph({ run }: { run: AIWorkflowRun }) { const nodeRuns = run.nodes ?? [] const nodeRunByNodeId = useMemo(() => { const map = new Map() for (const node of nodeRuns) { map.set(node.nodeId, node) } return map }, [nodeRuns]) const activeEdgeIds = useMemo(() => buildActiveEdgeIds(run.definition, nodeRuns), [run.definition, nodeRuns]) const firstExecutedNodeId = nodeRuns[0]?.nodeId ?? run.definition?.entryNodeId ?? "" const [selectedNodeId, setSelectedNodeId] = useState(firstExecutedNodeId) const nodes = useMemo(() => { return (run.definition?.nodes ?? []).map((node) => { const nodeRun = nodeRunByNodeId.get(node.id) return { id: node.id, type: "auditNode", position: scaleAuditPosition(node.position), data: { nodeId: node.id, nodeType: node.type, name: node.name || node.id, executed: Boolean(nodeRun), statusName: nodeRun?.statusName, durationMs: nodeRun?.durationMs, errorMessage: nodeRun?.errorMessage, selected: selectedNodeId === node.id, }, } }) }, [nodeRunByNodeId, run.definition?.nodes, selectedNodeId]) const edges = useMemo(() => { return (run.definition?.edges ?? []).map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, type: "auditEdge", data: { executed: activeEdgeIds.has(edge.id), }, })) }, [activeEdgeIds, run.definition?.edges]) const selectedNodeRun = selectedNodeId ? nodeRunByNodeId.get(selectedNodeId) : undefined const selectedDefinitionNode = run.definition?.nodes?.find((node) => node.id === selectedNodeId) if (!run.definition?.nodes?.length) { return (
流程定义快照缺失,仍可查看下方节点运行明细。
) } return (
setSelectedNodeId(node.id)} >
) } function buildActiveEdgeIds(definition: AIWorkflowDefinition | undefined, nodeRuns: AIWorkflowNodeRun[]) { const active = new Set() const edges = definition?.edges ?? [] for (let i = 0; i < nodeRuns.length - 1; i += 1) { const source = nodeRuns[i]?.nodeId const target = nodeRuns[i + 1]?.nodeId const edge = edges.find((item) => item.source === source && item.target === target) if (edge) { active.add(edge.id) } } return active } function AuditCanvasEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, data, }: EdgeProps) { const [edgePath] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, curvature: 0.18, }) const executed = Boolean(data?.executed) return ( ) } function AuditCanvasNode({ data }: NodeProps) { const executed = Boolean(data.executed) const failed = data.statusName === "failed" || Boolean(data.errorMessage) const interrupted = data.statusName === "interrupted" const selected = Boolean(data.selected) const condition = data.nodeType === "condition" const toneClass = failed ? "border-destructive bg-destructive/5 text-destructive" : interrupted ? "border-amber-500 bg-amber-500/10 text-amber-700" : executed ? "border-emerald-500 bg-emerald-500/10 text-emerald-700" : "border-border bg-muted/40 text-muted-foreground" if (condition) { return (
{data.name}
{data.statusName || "未执行"}
) } return (
{failed ? : }
{data.name}
{data.nodeType}
{data.statusName || "未执行"} {executed ? ( {data.durationMs ?? 0} ms ) : null}
) } function AuditSidePanel({ definitionNode, nodeRun, }: { definitionNode?: AIWorkflowDefinition["nodes"][number] nodeRun?: AIWorkflowNodeRun }) { const inputValue = safeParseJSON(nodeRun?.inputPreview ?? "") const outputValue = safeParseJSON(nodeRun?.outputPreview ?? "") const branchDecision = extractBranchDecision(outputValue) return (

{definitionNode?.name || nodeRun?.nodeId || "节点详情"}

{definitionNode?.id || nodeRun?.nodeId || "-"} · {definitionNode?.type || nodeRun?.nodeType || "unknown"}
{nodeRun ? (
) : (
本次运行没有执行该节点。
)} {nodeRun?.errorMessage ? (
{nodeRun.errorMessage}
) : null} {branchDecision ? : null}
) } function AuditMeta({ label, value }: { label: string; value: string }) { return (
{label}
{value}
) } function scaleAuditPosition(position: AIWorkflowDefinition["nodes"][number]["position"] | undefined) { return { x: Math.round((position?.x ?? 0) * auditLayoutScale.x), y: Math.round((position?.y ?? 0) * auditLayoutScale.y), } } function BranchDecisionBlock({ decision }: { decision: BranchDecision }) { return (
分支决策
{decision.selectedBranchName || decision.selectedBranchId || "default"}
目标节点:{decision.selectedTargetNodeId || "-"}
原因:{decision.reason || "-"}
{decision.evaluations?.length ? (
{decision.evaluations.map((item, index) => (
{item.branchName || item.branchId || item.edgeId || `条件 ${index + 1}`} {item.matched ? "命中" : "未命中"}
{item.sourceNodeId}.{item.sourceField} {item.operator} {formatUnknown(item.rightValue)}
实际值:{formatUnknown(item.leftValue)}
))}
) : null}
) } function PreviewBlock({ title, raw, value }: { title: string; raw: string; value: unknown }) { return (
{title}
{value !== null ? ( ) : raw.trim() ? (
          {raw}
        
) : (
-
)}
) } function extractBranchDecision(value: unknown): BranchDecision | null { if (!value || typeof value !== "object") { return null } const record = value as Record const decision = record.branchDecision if (!decision || typeof decision !== "object") { return null } return decision as BranchDecision } function safeParseJSON(raw: string): unknown | null { const trimmed = raw.trim() if (!trimmed) { return null } try { return JSON.parse(trimmed) } catch { return null } } function formatUnknown(value: unknown) { if (typeof value === "string") { return value } if (value === null || value === undefined) { return "-" } try { return JSON.stringify(value) } catch { return String(value) } }