import { IconActivity, IconMessages, IconThumbUp, IconThumbDown, IconClock, IconCoin, IconTool, IconMoodSmile, IconChartBar, IconAB2, IconMessageReport, IconChevronRight, IconArrowLeft, IconLoader2, IconAlertTriangle, } from "@tabler/icons-react"; import { useState } from "react"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; import { useObservabilityOverview, useTraces, useTraceDetail, useFeedbackList, useFeedbackStats, useSatisfaction, useEvalStats, useExperiments, useExperimentDetail, useExperimentResults, type TraceSummary, type Experiment, } from "./useObservability.js"; // ─── Helpers ──────────────────────────────────────────────────────────── function formatCost(centsX100: number): string { const cents = centsX100 / 100; if (cents < 1) return `${cents.toFixed(3)}¢`; if (cents < 100) return `${cents.toFixed(2)}¢`; return `$${(cents / 100).toFixed(2)}`; } function formatCostCents(cents: number): string { if (cents < 1) return `${cents.toFixed(3)}¢`; if (cents < 100) return `${cents.toFixed(2)}¢`; return `$${(cents / 100).toFixed(2)}`; } function formatDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 60_000).toFixed(1)}m`; } function formatPercent(ratio: number): string { return `${(ratio * 100).toFixed(1)}%`; } function truncateId(id: string, len = 8): string { return id.length > len ? id.slice(0, len) + "…" : id; } function timeAgo(ts: number): string { const diff = Date.now() - ts; if (diff < 60_000) return "just now"; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; return `${Math.floor(diff / 86_400_000)}d ago`; } const RANGES = [ { value: 7, label: "7d" }, { value: 30, label: "30d" }, { value: 90, label: "90d" }, ] as const; // ─── Shared components ────────────────────────────────────────────────── function RangeSelector({ value, onChange, }: { value: number; onChange: (v: number) => void; }) { return (
{RANGES.map((r) => ( ))}
); } function MetricCard({ label, value, icon, }: { label: string; value: string; icon: React.ReactNode; }) { return (
{label} {icon}
{value}
); } function StatusBadge({ status, }: { status: "draft" | "running" | "paused" | "completed" | "success" | "error"; }) { const styles: Record = { draft: "bg-muted text-muted-foreground", running: "bg-blue-500/15 text-blue-500", paused: "bg-yellow-500/15 text-yellow-500", completed: "bg-green-500/15 text-green-500", success: "bg-green-500/15 text-green-500", error: "bg-red-500/15 text-red-500", }; return ( {status} ); } function EmptyState({ message }: { message: string }) { return (
{message}
); } function LoadingState() { return (
); } // ─── Tab: Overview ────────────────────────────────────────────────────── function OverviewTab({ days }: { days: number }) { const t = useT(); const { data, isLoading } = useObservabilityOverview(days); if (isLoading) return ; if (!data) return ; return (
} /> } /> } /> } /> } /> } />
); } // ─── Tab: Conversations ───────────────────────────────────────────────── function ConversationsTab({ days }: { days: number }) { const t = useT(); const { data: traces, isLoading } = useTraces(days); const [selectedRunId, setSelectedRunId] = useState(null); if (selectedRunId) { return ( setSelectedRunId(null)} /> ); } if (isLoading) return ; if (!traces || traces.length === 0) return ; return (
{traces.map((trace: TraceSummary) => ( setSelectedRunId(trace.runId)} className="border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/30" > ))}
{t("observability.run")} {t("observability.model")} {t("observability.duration")} {t("observability.cost")} {t("observability.tools")} {t("observability.time")}
{truncateId(trace.runId)} {trace.model || "unknown"} {formatDuration(trace.totalDurationMs)} {formatCost(trace.totalCostCentsX100)} {trace.toolCalls} {trace.failedTools > 0 && ( {t("observability.failedCount", { count: trace.failedTools, })} )} {timeAgo(trace.createdAt)}
); } function TraceDetailView({ runId, onBack, }: { runId: string; onBack: () => void; }) { const t = useT(); const { data, isLoading } = useTraceDetail(runId); return (
{isLoading && } {data && (
{t("observability.model")}
{data.summary.model || "unknown"}
{t("observability.duration")}
{formatDuration(data.summary.totalDurationMs)}
{t("observability.cost")}
{formatCost(data.summary.totalCostCentsX100)}
{t("observability.spans")}
{data.summary.totalSpans}
{data.spans.map((span) => ( ))}
{t("observability.type")} {t("observability.name")} {t("observability.duration")} {t("observability.tokens")} {t("observability.status")}
{span.spanType.replace("_", " ")} {span.name} {formatDuration(span.durationMs)} {span.inputTokens + span.outputTokens > 0 ? `${span.inputTokens} / ${span.outputTokens}` : "-"}
)}
); } // ─── Tab: Evals ───────────────────────────────────────────────────────── function EvalsTab({ days }: { days: number }) { const t = useT(); const { data, isLoading } = useEvalStats(days); if (isLoading) return ; if (!data || data.totalEvals === 0) return ; const maxCount = Math.max(...data.byCriteria.map((c) => c.count), 1); return (
} /> } />
{data.byCriteria.length > 0 && (

{t("observability.scoresByCriteria")}

{data.byCriteria.map((c) => (
{c.criteria} {c.avgScore.toFixed(2)} avg ({c.count})
))}
)}
); } // ─── Tab: Experiments ─────────────────────────────────────────────────── function ExperimentsTab() { const t = useT(); const { data: experiments, isLoading } = useExperiments(); const [selectedId, setSelectedId] = useState(null); if (selectedId) { return ( setSelectedId(null)} /> ); } if (isLoading) return ; if (!experiments || experiments.length === 0) return ; return (
{experiments.map((exp: Experiment) => ( setSelectedId(exp.id)} className="border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/30" > ))}
{t("observability.name")} {t("observability.status")} {t("observability.variants")} {t("observability.created")}
{exp.name} {exp.variants.length} {timeAgo(exp.createdAt)}
); } function ExperimentDetailView({ id, onBack, }: { id: string; onBack: () => void; }) { const t = useT(); const { data: exp, isLoading } = useExperimentDetail(id); const { data: results } = useExperimentResults(id); return (
{isLoading && } {exp && (

{exp.name}

{t("observability.variants")}
{exp.variants.length}
{t("observability.metrics")}
{exp.metrics.length}
{t("observability.level")}
{exp.assignmentLevel}
{exp.variants.length > 0 && (

{t("observability.variants")}

{exp.variants.map((v) => (
{truncateId(v.id)} Weight: {v.weight}
))}
)} {results && results.length > 0 && (

{t("observability.results")}

{results.map((r) => ( ))}
{t("observability.variant")} {t("observability.metric")} {t("observability.value")} CI N
{truncateId(r.variantId)} {r.metric} {r.value.toFixed(3)} [{r.confidenceLow.toFixed(3)},{" "} {r.confidenceHigh.toFixed(3)}] {r.sampleSize}
)}
)}
); } // ─── Tab: Feedback ────────────────────────────────────────────────────── function FeedbackTab({ days }: { days: number }) { const t = useT(); const { data: stats, isLoading: statsLoading } = useFeedbackStats(days); const { data: entries, isLoading: listLoading } = useFeedbackList(days); const { data: satisfaction } = useSatisfaction(days); const isLoading = statsLoading || listLoading; if (isLoading) return ; const thumbsTotal = (stats?.thumbsUp ?? 0) + (stats?.thumbsDown ?? 0); const thumbsUpRate = thumbsTotal > 0 ? stats!.thumbsUp / thumbsTotal : 0; const avgFrustration = satisfaction && satisfaction.length > 0 ? satisfaction.reduce((sum, s) => sum + s.frustrationScore, 0) / satisfaction.length : 0; return (
} /> } /> } /> } />
{thumbsTotal > 0 && (
{t("observability.thumbsUpRate")}
{formatPercent(thumbsUpRate)}
)} {stats?.categories && Object.keys(stats.categories).length > 0 && (

{t("observability.categories")}

{Object.entries(stats.categories).map(([cat, count]) => ( {cat} {count} ))}
)} {entries && entries.length > 0 && (

Recent feedback

{entries.map((entry) => (
{entry.feedbackType === "thumbs_up" && ( )} {entry.feedbackType === "thumbs_down" && ( )} {entry.feedbackType === "category" && ( )} {entry.feedbackType === "text" && ( )} {entry.feedbackType === "text" || entry.feedbackType === "category" ? entry.value : entry.feedbackType.replace("_", " ")} {timeAgo(entry.createdAt)}
))}
)}
); } // ─── Main Dashboard ───────────────────────────────────────────────────── const TABS = [ { id: "overview", labelKey: "observability.overview", icon: IconActivity }, { id: "conversations", labelKey: "observability.conversations", icon: IconMessages, }, { id: "evals", labelKey: "observability.evals", icon: IconChartBar }, { id: "experiments", labelKey: "observability.experiments", icon: IconAB2, }, { id: "feedback", labelKey: "observability.feedback", icon: IconMessageReport, }, ] as const; type TabId = (typeof TABS)[number]["id"]; export interface ObservabilityDashboardProps { className?: string; } export function ObservabilityDashboard({ className, }: ObservabilityDashboardProps) { const t = useT(); const [activeTab, setActiveTab] = useState("overview"); const [days, setDays] = useState(7); return (
{TABS.map((tab) => { const Icon = tab.icon; return ( ); })}
{activeTab !== "experiments" && ( )}
{activeTab === "overview" && } {activeTab === "conversations" && } {activeTab === "evals" && } {activeTab === "experiments" && } {activeTab === "feedback" && }
); }