Spaces:
Sleeping
Sleeping
| import { useState } from "react"; | |
| import { ChevronDown, ChevronRight, Database, Loader2, Route, Wrench } from "lucide-react"; | |
| import type { Observability, ObservabilityDataUsed } from "@/services/agenticApi"; | |
| import { formatDateTime } from "./utils"; | |
| interface MessageTraceabilityProps { | |
| observability?: Observability | null; | |
| loading?: boolean; | |
| error?: string | null; | |
| sources?: unknown[]; | |
| } | |
| function renderUnknown(value: unknown) { | |
| if (value == null) return "-"; | |
| if (typeof value === "string") return value; | |
| try { | |
| return JSON.stringify(value, null, 2); | |
| } catch { | |
| return String(value); | |
| } | |
| } | |
| function sourceLabel(source: unknown, index: number) { | |
| if (source && typeof source === "object") { | |
| const record = source as Record<string, unknown>; | |
| return String(record.name ?? record.title ?? record.source ?? record.source_id ?? record.id ?? `Source ${index + 1}`); | |
| } | |
| return String(source ?? `Source ${index + 1}`); | |
| } | |
| function DataUsedCard({ item, index }: { item: ObservabilityDataUsed; index: number }) { | |
| return ( | |
| <div className="rounded-md border border-slate-200 bg-white p-2"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <p className="font-medium text-slate-900">{item.source?.name ?? `Data source ${index + 1}`}</p> | |
| {item.source?.type && <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] uppercase text-slate-500">{item.source.type}</span>} | |
| </div> | |
| {item.tables && item.tables.length > 0 && ( | |
| <p className="mt-1.5 text-slate-600"> | |
| Tables: {item.tables.map((table) => table.name).join(", ")} | |
| </p> | |
| )} | |
| {item.columns_read && item.columns_read.length > 0 && ( | |
| <div className="mt-1.5 flex flex-wrap gap-1"> | |
| {item.columns_read.map((column) => ( | |
| <span key={column.id} className="rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[11px] text-slate-600"> | |
| {column.table ? `${column.table}.${column.name}` : column.name} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| {item.group_by && item.group_by.length > 0 && ( | |
| <p className="mt-1.5 text-slate-600">Grouped by: {item.group_by.join(", ")}</p> | |
| )} | |
| {typeof item.rows_returned === "number" && ( | |
| <p className="mt-1.5 text-slate-600">Rows returned: {item.rows_returned}</p> | |
| )} | |
| {item.query && ( | |
| <pre className="mt-2 max-h-40 overflow-auto rounded bg-slate-950 p-2 text-[11px] leading-4 text-slate-100">{item.query}</pre> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export function MessageTraceability({ observability, loading, error, sources = [] }: MessageTraceabilityProps) { | |
| const [open, setOpen] = useState(false); | |
| const [detailsOpen, setDetailsOpen] = useState(false); | |
| const traceSources = observability?.sources?.length ? observability.sources : sources; | |
| const hasTrace = Boolean(observability || loading || error || traceSources.length > 0); | |
| if (!hasTrace) return null; | |
| const toolCalls = observability?.tool_calls ?? []; | |
| const dataUsed = observability?.data_used ?? []; | |
| return ( | |
| <div className="mt-3 border-t border-slate-200 pt-3"> | |
| <button | |
| type="button" | |
| title="Show traceability for this AI answer" | |
| aria-expanded={open} | |
| onClick={() => setOpen((current) => !current)} | |
| className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-950" | |
| > | |
| {open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />} | |
| Traceability | |
| {loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />} | |
| </button> | |
| {open && ( | |
| <div className="mt-3 space-y-3 rounded-md border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700"> | |
| {loading && <p className="text-slate-500">Traceability is being prepared.</p>} | |
| {error && <p className="text-red-600">{error}</p>} | |
| {observability?.generated_at && ( | |
| <p className="text-[11px] uppercase tracking-wide text-slate-400">Generated {formatDateTime(observability.generated_at)}</p> | |
| )} | |
| {observability?.intent && ( | |
| <div> | |
| <p className="font-semibold text-slate-900">Intent</p> | |
| <p className="mt-1 leading-5">{observability.intent}</p> | |
| </div> | |
| )} | |
| {(observability?.planning || observability?.thinking) && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 font-semibold text-slate-900"> | |
| <Route className="h-3.5 w-3.5" /> | |
| Thought and planning | |
| </div> | |
| {observability.thinking && <p className="leading-5">{observability.thinking}</p>} | |
| {observability.planning?.goal_restated && <p className="leading-5">{observability.planning.goal_restated}</p>} | |
| {observability.planning?.assumptions && observability.planning.assumptions.length > 0 && ( | |
| <ul className="list-disc space-y-1 pl-4"> | |
| {observability.planning.assumptions.map((item) => <li key={item}>{item}</li>)} | |
| </ul> | |
| )} | |
| {observability.planning?.steps && observability.planning.steps.length > 0 && ( | |
| <div className="space-y-1"> | |
| {observability.planning.steps.map((step, index) => ( | |
| <div key={`${step.stage ?? "step"}-${index}`} className="rounded border border-slate-200 bg-white px-2 py-1.5"> | |
| <p className="font-medium text-slate-800">{step.stage ?? `Step ${step.step ?? index + 1}`}</p> | |
| {step.objective && <p className="mt-0.5 text-slate-600">{step.objective}</p>} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <div> | |
| <div className="flex items-center gap-2 font-semibold text-slate-900"> | |
| <Database className="h-3.5 w-3.5" /> | |
| Data used | |
| </div> | |
| {dataUsed.length === 0 ? ( | |
| <p className="mt-1 text-slate-500">No data usage recorded for this answer.</p> | |
| ) : ( | |
| <div className="mt-2 space-y-2"> | |
| {dataUsed.map((item, index) => ( | |
| <DataUsedCard key={`${item.source?.id ?? "source"}-${index}`} item={item} index={index} /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div> | |
| <button | |
| type="button" | |
| title="Show tool call details" | |
| aria-expanded={detailsOpen} | |
| onClick={() => setDetailsOpen((current) => !current)} | |
| className="inline-flex items-center gap-2 rounded-md px-1.5 py-1 text-xs font-semibold text-slate-900 hover:bg-slate-100" | |
| > | |
| {detailsOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />} | |
| <Wrench className="h-3.5 w-3.5" /> | |
| Details | |
| </button> | |
| {detailsOpen && ( | |
| toolCalls.length === 0 ? ( | |
| <p className="mt-1 pl-1.5 text-slate-500">No tool call recorded for this answer.</p> | |
| ) : ( | |
| <div className="mt-2 space-y-2 pl-1.5"> | |
| {toolCalls.map((tool, index) => ( | |
| <div key={`${tool.name}-${index}`} className="rounded-md border border-slate-200 bg-white p-2"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <p className="font-medium text-slate-900">{tool.name}</p> | |
| {tool.status && <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] uppercase text-slate-500">{tool.status}</span>} | |
| </div> | |
| {tool.summary && <p className="mt-1 text-slate-600">{tool.summary}</p>} | |
| {tool.input !== undefined && ( | |
| <pre className="mt-2 max-h-40 overflow-auto rounded bg-slate-950 p-2 text-[11px] leading-4 text-slate-100">{renderUnknown(tool.input)}</pre> | |
| )} | |
| {tool.output !== undefined && ( | |
| <pre className="mt-2 max-h-40 overflow-auto rounded bg-white p-2 text-[11px] leading-4 text-slate-600 ring-1 ring-slate-200">{renderUnknown(tool.output)}</pre> | |
| )} | |
| {tool.error && <p className="mt-2 text-red-600">{tool.error}</p>} | |
| </div> | |
| ))} | |
| </div> | |
| ) | |
| )} | |
| </div> | |
| {traceSources.length > 0 && ( | |
| <div> | |
| <p className="font-semibold text-slate-900">Citations and sources</p> | |
| <div className="mt-2 flex flex-wrap gap-1.5"> | |
| {traceSources.map((source, index) => ( | |
| <span key={`${sourceLabel(source, index)}-${index}`} className="rounded-md border border-slate-200 bg-white px-2 py-1 text-[11px] text-slate-600"> | |
| {sourceLabel(source, index)} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |