"use client" import { useEffect, useMemo, useState, type ReactNode } from "react" import { AlertTriangleIcon, Clock3Icon, MessageCircleMoreIcon, MessageSquareTextIcon, WorkflowIcon, } from "lucide-react" import { toast } from "sonner" import { ConversationDetailDialog } from "@/app/dashboard/conversation-monitor/_components/detail" import { DashboardListPage } from "@/components/dashboard/list" import { JsonTreeViewer } from "@/components/json-tree-viewer" import { ProjectDialog } from "@/components/project-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { fetchAIAgentsAll, fetchAIWorkflowRun, fetchAIWorkflowRuns, fetchConversationDetail, fetchConversationMessages, type AdminConversationDetail, type AdminMessage, type AIAgent, type AIWorkflowNodeRun, type AIWorkflowRun, } from "@/lib/api/admin" import { formatDateTime } from "@/lib/utils" import { useI18n } from "@/i18n/provider" import { WorkflowRunAuditGraph } from "./_components/workflow-run-audit-graph" type TFunction = (key: string, values?: Record) => string function getStatusOptions(t: TFunction) { return [ { value: "all", label: t("workflowRun.allStatus") }, { value: "1", label: t("workflowRun.completed") }, { value: "2", label: t("workflowRun.interrupted") }, { value: "3", label: t("workflowRun.failed") }, ] } function statusBadgeVariant(statusName?: string) { switch ((statusName || "").trim()) { case "failed": return "destructive" as const case "interrupted": return "outline" as const case "completed": return "default" as const default: return "secondary" as const } } function formatRunStatus(item: Pick) { return item.statusName || (item.status ? String(item.status) : "-") } export default function DashboardAIWorkflowRunsPage() { const t = useI18n() const [agents, setAgents] = useState([]) const [detailOpen, setDetailOpen] = useState(false) const [detailLoading, setDetailLoading] = useState(false) const [activeRun, setActiveRun] = useState(null) const [conversationOpen, setConversationOpen] = useState(false) const [conversationLoading, setConversationLoading] = useState(false) const [activeConversation, setActiveConversation] = useState(null) const [conversationMessages, setConversationMessages] = useState([]) const statusOptions = useMemo(() => getStatusOptions(t), [t]) const agentOptions = useMemo( () => [ { value: "all", label: t("workflowRun.allAgents") }, ...agents.map((agent) => ({ value: String(agent.id), label: agent.name, })), ], [agents, t] ) useEffect(() => { let cancelled = false async function loadAgents() { try { const data = await fetchAIAgentsAll() if (!cancelled) { setAgents(data) } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : t("workflowRun.loadAgentsFailed")) } } } void loadAgents() return () => { cancelled = true } }, [t]) async function openDetail(runId: number) { setDetailOpen(true) setDetailLoading(true) try { const data = await fetchAIWorkflowRun(runId) setActiveRun(data) } catch (error) { toast.error(error instanceof Error ? error.message : t("workflowRun.loadDetailFailed")) setDetailOpen(false) } finally { setDetailLoading(false) } } async function openConversation(conversationId: number) { if (!conversationId) { return } setConversationOpen(true) setConversationLoading(true) try { const [detail, messagePage] = await Promise.all([ fetchConversationDetail(conversationId), fetchConversationMessages({ conversationId, limit: 20 }), ]) setActiveConversation(detail) setConversationMessages(messagePage.results ?? []) } catch (error) { toast.error(error instanceof Error ? error.message : t("workflowRun.loadConversationFailed")) setConversationOpen(false) } finally { setConversationLoading(false) } } return ( <> filters={[ { name: "conversationId", label: t("workflowRun.conversationId"), placeholder: t("workflowRun.conversationId"), defaultValue: "", valueType: "number", className: "w-full sm:w-44", }, { name: "messageId", label: t("workflowRun.messageId"), placeholder: t("workflowRun.messageId"), defaultValue: "", valueType: "number", className: "w-full sm:w-40", }, { name: "workflowVersionId", label: t("workflowRun.workflowVersionId"), placeholder: t("workflowRun.workflowVersionId"), defaultValue: "", valueType: "number", className: "w-full sm:w-48", }, { name: "aiAgentId", label: t("workflowRun.agent"), type: "select", defaultValue: "all", allValue: "all", valueType: "number", options: agentOptions, placeholder: t("workflowRun.agent"), searchPlaceholder: t("workflowRun.searchAgent"), emptyText: t("workflowRun.emptyAgent"), className: "w-full sm:w-56", }, { name: "status", label: t("workflowRun.status"), type: "select", defaultValue: "all", allValue: "all", valueType: "number", options: statusOptions, placeholder: t("workflowRun.status"), className: "w-full sm:w-44", }, ]} fetchList={fetchAIWorkflowRuns} getItemId={(item) => item.id} getRowClassName={() => "cursor-pointer"} onRowClick={(item) => void openDetail(item.id)} columns={[ { key: "time", label: t("workflowRun.startedAt"), className: "w-42 text-xs text-muted-foreground", render: (item) => formatDateTime(item.startedAt || item.createdAt), }, { key: "workflow", label: t("workflowRun.workflow"), render: (item) => (
{item.workflowName || `Workflow #${item.workflowId}`}
v{item.workflowVersion || "-"} #{item.workflowVersionId || "-"}
), }, { key: "agent", label: t("workflowRun.agent"), className: "w-48", render: (item) => item.aiAgentName || `#${item.aiAgentId}`, }, { key: "message", label: t("workflowRun.message"), className: "w-48", render: (item) => (
{t("workflowRun.messageShort", { id: item.messageId || "-" })}
), }, { key: "status", label: t("workflowRun.status"), className: "w-32", render: (item) => ( {formatRunStatus(item)} ), }, { key: "duration", label: t("workflowRun.duration"), className: "w-28 text-right", render: (item) => `${item.durationMs || 0} ms`, }, { key: "error", label: t("workflowRun.error"), className: "w-72 max-w-72", render: (item) => item.errorMessage ? ( ) : ( - ), }, ]} labels={{ refresh: t("workflowRun.refresh"), query: t("workflowRun.query"), loading: t("workflowRun.loading"), empty: t("workflowRun.empty"), loadFailed: t("workflowRun.loadFailed"), }} /> { setDetailOpen(open) if (!open) { setActiveRun(null) } }} t={t} onOpenConversation={openConversation} /> { setConversationOpen(open) if (!open) { setActiveConversation(null) setConversationMessages([]) } }} onOpenAssign={() => undefined} onDispatch={async () => undefined} onOpenTransfer={() => undefined} onRead={async () => undefined} onOpenClose={() => undefined} /> ) } function ErrorMessagePreview({ message }: { message: string }) { return ( ) } function WorkflowRunDetailDialog({ open, loading, run, onOpenChange, t, onOpenConversation, }: { open: boolean loading: boolean run: AIWorkflowRun | null onOpenChange: (open: boolean) => void t: TFunction onOpenConversation: (conversationId: number) => void | Promise }) { return ( {t("workflowRun.detailTitle")} } description={run ? `Run #${run.id}` : t("workflowRun.detailDescription")} size="xl" allowFullscreen defaultFullscreen bodyClassName="min-h-0" footer={ } > {loading ? (
{t("workflowRun.loadingDetail")}
) : run ? (
void onOpenConversation(run.conversationId)} > #{run.conversationId} } /> {run.interruptNodeId ? : null}
{run.errorMessage ? (
{run.errorMessage}
) : null}
{t("workflowRun.nodeDetails")}
{(run.nodes ?? []).map((node, index) => ( ))} {!run.nodes || run.nodes.length === 0 ? (

{t("workflowRun.emptyNodes")}

) : null}
) : (
{t("workflowRun.notFound")}
)}
) } function WorkflowNodeRunBlock({ node, t }: { node: AIWorkflowNodeRun; t: TFunction }) { const inputPreview = node.inputPreview || "" const outputPreview = node.outputPreview || "" const inputValue = safeParseJSON(inputPreview) const outputValue = safeParseJSON(outputPreview) return (
{node.nodeId || `#${node.id}`} {node.statusName || node.status || "-"}
{node.nodeType || "unknown"} {node.durationMs} ms
{node.errorMessage ? (
{node.errorMessage}
) : null}
) } function PreviewBlock({ title, raw, value, }: { title: string raw: string value: unknown }) { return (
{title}
{value !== null ? ( ) : raw.trim() ? (
          {raw}
        
) : (
-
)}
) } function DetailRow({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value || "-"}
) } function CompactRunMeta({ label, value }: { label: string; value: ReactNode }) { return ( {label} {value || "-"} ) } function safeParseJSON(raw: string): unknown | null { const trimmed = raw.trim() if (!trimmed) { return null } try { return JSON.parse(trimmed) } catch { return null } }