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 (
);
}
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 (
|
{t("observability.run")}
|
{t("observability.model")}
|
{t("observability.duration")}
|
{t("observability.cost")}
|
{t("observability.tools")}
|
{t("observability.time")}
|
|
{traces.map((trace: TraceSummary) => (
setSelectedRunId(trace.runId)}
className="border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/30"
>
|
{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}
|
{t("observability.type")}
|
{t("observability.name")}
|
{t("observability.duration")}
|
{t("observability.tokens")}
|
{t("observability.status")}
|
{data.spans.map((span) => (
|
{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 (
|
{t("observability.name")}
|
{t("observability.status")}
|
{t("observability.variants")}
|
{t("observability.created")}
|
|
{experiments.map((exp: Experiment) => (
setSelectedId(exp.id)}
className="border-b border-border last:border-b-0 cursor-pointer hover:bg-accent/30"
>
|
{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")}
|
{t("observability.variant")}
|
{t("observability.metric")}
|
{t("observability.value")}
|
CI
|
N
|
{results.map((r) => (
|
{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" &&
}
);
}