mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Improve retros and usage UI: search, filters, layout tweaks
- Add search bar and smoothness dropdown filter to retros list - Replace Cost column with Frictions count on retros list - Make retro table rows fully clickable - Move Retro tab before Usage tab in run detail - Move Stage Breakdown below Open Items on retro page - Add per-model breakdown table to usage page with 3 models Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4dd2980a56
commit
162318a230
4 changed files with 452 additions and 37 deletions
134
apps/arc-web/app/routes/retros.tsx
Normal file
134
apps/arc-web/app/routes/retros.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { allRetros, smoothnessConfig, formatDuration } from "../data/retros";
|
||||
import type { Retro, SmoothnessRating } from "../data/retros";
|
||||
import type { Route } from "./+types/retros";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Retros \u2014 Arc" }];
|
||||
}
|
||||
|
||||
const smoothnessOptions: Array<{ value: SmoothnessRating; label: string }> = [
|
||||
{ value: "effortless", label: "Effortless" },
|
||||
{ value: "smooth", label: "Smooth" },
|
||||
{ value: "bumpy", label: "Bumpy" },
|
||||
{ value: "struggled", label: "Struggled" },
|
||||
{ value: "failed", label: "Failed" },
|
||||
];
|
||||
|
||||
function SmoothnesssBadge({ smoothness }: { smoothness: Retro["smoothness"] }) {
|
||||
if (!smoothness) {
|
||||
return <span className="text-xs text-navy-600">--</span>;
|
||||
}
|
||||
const config = smoothnessConfig[smoothness];
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${config.bg} ${config.text}`}>
|
||||
<span className={`size-1.5 rounded-full ${config.dot}`} />
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTimestamp(ts: string): string {
|
||||
const date = new Date(ts);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function truncate(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.slice(0, maxLength) + "\u2026";
|
||||
}
|
||||
|
||||
export default function Retros() {
|
||||
const retros = allRetros();
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState("");
|
||||
const [smoothnessFilter, setSmoothnessFilter] = useState<SmoothnessRating | "all">("all");
|
||||
|
||||
if (retros.length === 0) {
|
||||
return <p className="py-8 text-center text-sm text-navy-600">No retrospectives yet.</p>;
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const filtered = retros.filter(
|
||||
(r) =>
|
||||
(smoothnessFilter === "all" || r.smoothness === smoothnessFilter) &&
|
||||
(r.goal.toLowerCase().includes(lowerQuery) ||
|
||||
r.pipeline_name.toLowerCase().includes(lowerQuery)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search retros…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={smoothnessFilter}
|
||||
onChange={(e) => setSmoothnessFilter(e.target.value as SmoothnessRating | "all")}
|
||||
className="appearance-none rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-3 pr-8 text-sm text-ice-100 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
|
||||
>
|
||||
<option value="all">All smoothness</option>
|
||||
{smoothnessOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="px-4 py-2.5 font-medium">Pipeline</th>
|
||||
<th className="px-4 py-2.5 font-medium">Goal</th>
|
||||
<th className="px-4 py-2.5 font-medium">Smoothness</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Duration</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Frictions</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((retro) => (
|
||||
<tr key={retro.run_id} className="border-b border-white/[0.06] last:border-b-0 transition-colors hover:bg-white/[0.02] cursor-pointer" onClick={() => navigate(`/runs/${retro.run_id}/retro`)}>
|
||||
<td className="px-4 py-3 font-mono text-xs font-medium text-teal-500">
|
||||
{retro.pipeline_name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-ice-100">
|
||||
{truncate(retro.goal, 60)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<SmoothnesssBadge smoothness={retro.smoothness} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatDuration(retro.stats.total_duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{retro.friction_points?.length ?? 0}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-navy-600">
|
||||
{formatTimestamp(retro.timestamp)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ const tabs = [
|
|||
{ name: "Overview", path: "", count: null },
|
||||
{ name: "Stages", path: "/stages/detect-drift", count: 4 },
|
||||
{ name: "Files Changed", path: "/files", count: 3 },
|
||||
{ name: "Retro", path: "/retro", count: null },
|
||||
{ name: "Usage", path: "/usage", count: null },
|
||||
];
|
||||
|
||||
|
|
|
|||
225
apps/arc-web/app/routes/run-retro.tsx
Normal file
225
apps/arc-web/app/routes/run-retro.tsx
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import { Link } from "react-router";
|
||||
import {
|
||||
findRetro,
|
||||
smoothnessConfig,
|
||||
learningCategoryConfig,
|
||||
frictionKindConfig,
|
||||
openItemKindConfig,
|
||||
formatDuration,
|
||||
} from "../data/retros";
|
||||
import type { Route } from "./+types/run-retro";
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
const retro = findRetro(params.id);
|
||||
return [{ title: retro ? `Retro: ${retro.goal} \u2014 Arc` : "Retro \u2014 Arc" }];
|
||||
}
|
||||
|
||||
function formatCost(cost: number | undefined): string {
|
||||
if (cost == null) return "--";
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
export default function RunRetro({ params }: Route.ComponentProps) {
|
||||
const retro = findRetro(params.id);
|
||||
|
||||
if (!retro) {
|
||||
return <p className="py-8 text-center text-sm text-navy-600">No retrospective found for this run.</p>;
|
||||
}
|
||||
|
||||
const smoothness = retro.smoothness ? smoothnessConfig[retro.smoothness] : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Smoothness + Summary Header */}
|
||||
<div className="flex items-start gap-4">
|
||||
{smoothness && (
|
||||
<span className={`inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold ${smoothness.bg} ${smoothness.text}`}>
|
||||
<span className={`size-2.5 rounded-full ${smoothness.dot}`} />
|
||||
{smoothness.label}
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm text-ice-300">{retro.goal}</p>
|
||||
<p className="mt-1 font-mono text-xs text-navy-600">
|
||||
{retro.pipeline_name} · {new Date(retro.timestamp).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Aggregate Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Duration" value={formatDuration(retro.stats.total_duration_ms)} />
|
||||
<StatCard label="Cost" value={formatCost(retro.stats.total_cost)} />
|
||||
<StatCard label="Retries" value={String(retro.stats.total_retries)} warn={retro.stats.total_retries > 0} />
|
||||
<StatCard label="Files" value={String(retro.stats.files_touched.length)} />
|
||||
</div>
|
||||
|
||||
{/* Intent + Outcome */}
|
||||
{(retro.intent ?? retro.outcome) && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{retro.intent && (
|
||||
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 p-4">
|
||||
<h3 className="text-xs font-medium uppercase tracking-wider text-navy-600">Intent</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ice-300">{retro.intent}</p>
|
||||
</div>
|
||||
)}
|
||||
{retro.outcome && (
|
||||
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 p-4">
|
||||
<h3 className="text-xs font-medium uppercase tracking-wider text-navy-600">Outcome</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ice-300">{retro.outcome}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Learnings */}
|
||||
{retro.learnings && retro.learnings.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Learnings</h3>
|
||||
<div className="space-y-2">
|
||||
{retro.learnings.map((learning, i) => {
|
||||
const config = learningCategoryConfig[learning.category];
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
|
||||
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
|
||||
{config.label}
|
||||
</span>
|
||||
<p className="text-sm leading-relaxed text-ice-300">{learning.text}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Friction Points */}
|
||||
{retro.friction_points && retro.friction_points.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Friction Points</h3>
|
||||
<div className="space-y-2">
|
||||
{retro.friction_points.map((fp, i) => {
|
||||
const config = frictionKindConfig[fp.kind];
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
|
||||
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
|
||||
{config.label}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm leading-relaxed text-ice-300">{fp.description}</p>
|
||||
{fp.stage_id && (
|
||||
<p className="mt-1 font-mono text-xs text-navy-600">
|
||||
Stage: {retro.stages.find((s) => s.stage_id === fp.stage_id)?.stage_label ?? fp.stage_id}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Open Items */}
|
||||
{retro.open_items && retro.open_items.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Open Items</h3>
|
||||
<div className="space-y-2">
|
||||
{retro.open_items.map((item, i) => {
|
||||
const config = openItemKindConfig[item.kind];
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3 rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
|
||||
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-white/[0.06]`}>
|
||||
{config.label}
|
||||
</span>
|
||||
<p className="text-sm leading-relaxed text-ice-300">{item.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stage Breakdown */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">Stage Breakdown</h3>
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="px-4 py-2.5 font-medium">Stage</th>
|
||||
<th className="px-4 py-2.5 font-medium">Status</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Duration</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Retries</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Cost</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Files</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{retro.stages.map((stage) => (
|
||||
<tr key={stage.stage_id} className="border-b border-white/[0.06] last:border-b-0">
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
to={`/runs/${retro.run_id}/stages/${stage.stage_id}`}
|
||||
className="text-ice-100 hover:text-white"
|
||||
>
|
||||
{stage.stage_label}
|
||||
</Link>
|
||||
{stage.notes && (
|
||||
<p className="mt-1 text-xs text-navy-600">{stage.notes}</p>
|
||||
)}
|
||||
{stage.failure_reason && (
|
||||
<p className="mt-1 text-xs text-coral/80">{stage.failure_reason}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StageStatusBadge status={stage.status} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatDuration(stage.duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums">
|
||||
<span className={stage.retries > 0 ? "text-amber" : "text-ice-300"}>
|
||||
{stage.retries}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatCost(stage.cost)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{stage.files_touched.length}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
|
||||
return (
|
||||
<div className="rounded-md border border-white/[0.06] bg-navy-800/60 px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-navy-600">{label}</p>
|
||||
<p className={`mt-1 font-mono text-lg font-semibold tabular-nums ${warn ? "text-amber" : "text-white"}`}>
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StageStatusBadge({ status }: { status: string }) {
|
||||
const styles: Record<string, string> = {
|
||||
completed: "text-mint",
|
||||
running: "text-teal-500",
|
||||
pending: "text-navy-600",
|
||||
failed: "text-coral",
|
||||
};
|
||||
const colorClass = styles[status] ?? "text-ice-300";
|
||||
return (
|
||||
<span className={`text-xs font-medium capitalize ${colorClass}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
const stages = [
|
||||
{ stage: "Detect Drift", model: "Opus 4.6", inputTokens: 12_480, outputTokens: 3_210, runtime: "1m 12s", cost: 0.48 },
|
||||
{ stage: "Propose Changes", model: "Opus 4.6", inputTokens: 28_640, outputTokens: 8_750, runtime: "2m 34s", cost: 1.12 },
|
||||
{ stage: "Review Changes", model: "Opus 4.6", inputTokens: 9_120, outputTokens: 2_640, runtime: "0m 45s", cost: 0.31 },
|
||||
{ stage: "Propose Changes", model: "Gemini 3.1", inputTokens: 28_640, outputTokens: 8_750, runtime: "2m 34s", cost: 0.72 },
|
||||
{ stage: "Review Changes", model: "Codex 5.3", inputTokens: 9_120, outputTokens: 2_640, runtime: "0m 45s", cost: 0.19 },
|
||||
{ stage: "Apply Changes", model: "Opus 4.6", inputTokens: 21_300, outputTokens: 6_480, runtime: "1m 58s", cost: 0.87 },
|
||||
];
|
||||
|
||||
|
|
@ -10,48 +10,103 @@ const totalCost = stages.reduce((sum, s) => sum + s.cost, 0);
|
|||
const totalInput = stages.reduce((sum, s) => sum + s.inputTokens, 0);
|
||||
const totalOutput = stages.reduce((sum, s) => sum + s.outputTokens, 0);
|
||||
|
||||
const modelBreakdown = Object.values(
|
||||
stages.reduce<Record<string, { model: string; inputTokens: number; outputTokens: number; cost: number; stages: number }>>(
|
||||
(acc, s) => {
|
||||
const entry = acc[s.model] ?? { model: s.model, inputTokens: 0, outputTokens: 0, cost: 0, stages: 0 };
|
||||
entry.inputTokens += s.inputTokens;
|
||||
entry.outputTokens += s.outputTokens;
|
||||
entry.cost += s.cost;
|
||||
entry.stages += 1;
|
||||
acc[s.model] = entry;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
).sort((a, b) => b.cost - a.cost);
|
||||
|
||||
function formatTokens(n: number) {
|
||||
return `${(n / 1000).toFixed(1)}k`;
|
||||
}
|
||||
|
||||
export default function RunUsage() {
|
||||
return (
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="px-4 py-2.5 font-medium">Stage</th>
|
||||
<th className="px-4 py-2.5 font-medium">Model</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Run time</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stages.map((row) => (
|
||||
<tr key={row.stage} className="border-b border-white/[0.06] last:border-b-0">
|
||||
<td className="px-4 py-3 text-ice-100">{row.stage}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-ice-300">{row.model}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatTokens(row.inputTokens)} <span className="text-navy-600">/</span> {formatTokens(row.outputTokens)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">{row.runtime}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">${row.cost.toFixed(2)}</td>
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="px-4 py-2.5 font-medium">Stage</th>
|
||||
<th className="px-4 py-2.5 font-medium">Model</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Run time</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Cost</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t border-white/[0.08] bg-navy-800/40">
|
||||
<td className="px-4 py-3 font-medium text-white">Total</td>
|
||||
<td />
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">
|
||||
{formatTokens(totalInput)} <span className="text-navy-600">/</span> {formatTokens(totalOutput)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">{totalRuntime}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">${totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stages.map((row) => (
|
||||
<tr key={row.stage} className="border-b border-white/[0.06] last:border-b-0">
|
||||
<td className="px-4 py-3 text-ice-100">{row.stage}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-ice-300">{row.model}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatTokens(row.inputTokens)} <span className="text-navy-600">/</span> {formatTokens(row.outputTokens)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">{row.runtime}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">${row.cost.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t border-white/[0.08] bg-navy-800/40">
|
||||
<td className="px-4 py-3 font-medium text-white">Total</td>
|
||||
<td />
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">
|
||||
{formatTokens(totalInput)} <span className="text-navy-600">/</span> {formatTokens(totalOutput)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">{totalRuntime}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">${totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-navy-600">By Model</h3>
|
||||
<div className="rounded-md border border-white/[0.06] overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] bg-navy-800/60 text-left text-xs text-navy-600">
|
||||
<th className="px-4 py-2.5 font-medium">Model</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Stages</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{modelBreakdown.map((row) => (
|
||||
<tr key={row.model} className="border-b border-white/[0.06] last:border-b-0">
|
||||
<td className="px-4 py-3 font-mono text-xs text-ice-100">{row.model}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">{row.stages}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums text-ice-300">
|
||||
{formatTokens(row.inputTokens)} <span className="text-navy-600">/</span> {formatTokens(row.outputTokens)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-ice-300">${row.cost.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t border-white/[0.08] bg-navy-800/40">
|
||||
<td className="px-4 py-3 font-medium text-white">Total</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">{stages.length}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-white">
|
||||
{formatTokens(totalInput)} <span className="text-navy-600">/</span> {formatTokens(totalOutput)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-white">${totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue