feat(theme): dark/light mode with a Radix Colors-based palette
Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.
- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
fg.primary/secondary/muted, status.success/danger/warning. One class flip
on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
client/src onto the new tokens, so every badge/button/component pulls the
same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
constants — slate/blue/green/red/amber steps 1-12 — adopted after three
rounds of hand-picked values that kept overshooting (flat, then too dark,
then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
visual language (border + text + translucent wash of the same status
color); `current` alone stays a solid accent fill, the one state that
gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
Workspace lane-detail header already showing them.
Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
+107
-97
@@ -85,7 +85,7 @@ function ChartTooltip({ x, y, children }: { x: number; y: number; children: Reac
|
||||
const nearRight = x > window.innerWidth - 200;
|
||||
return (
|
||||
<div
|
||||
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-gray-200 pointer-events-none whitespace-nowrap"
|
||||
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-fg-secondary pointer-events-none whitespace-nowrap"
|
||||
style={{
|
||||
left: nearRight ? x - 14 : x + 14,
|
||||
top: y - 10,
|
||||
@@ -211,7 +211,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
{monthPositions.map((mp, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute text-[10px] text-gray-600 font-medium whitespace-nowrap"
|
||||
className="absolute text-[10px] text-fg-muted font-medium whitespace-nowrap"
|
||||
style={{ left: mp.col * 16 }}
|
||||
>
|
||||
{mp.label}
|
||||
@@ -224,7 +224,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
{dayLabels.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-[9px] text-gray-700 flex items-center justify-end pr-1.5"
|
||||
className="text-[9px] text-fg-muted flex items-center justify-end pr-1.5"
|
||||
style={{ height: 13 }}
|
||||
>
|
||||
{d}
|
||||
@@ -247,7 +247,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">
|
||||
<span className="text-fg-secondary">
|
||||
{dayNames[dow] ?? ""}, {cell.date}
|
||||
</span>
|
||||
<span className="ml-2 font-medium">
|
||||
@@ -273,7 +273,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
))}
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-2 mt-3 text-[11px] text-gray-600">
|
||||
<div className="flex items-center gap-2 mt-3 text-[11px] text-fg-muted">
|
||||
<span>{t("less")}</span>
|
||||
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
|
||||
const v = Math.round(f * maxCount);
|
||||
@@ -324,7 +324,7 @@ function Sparkline({
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">{date}</span>
|
||||
<span className="text-fg-secondary">{date}</span>
|
||||
<span className="ml-2 font-medium">{t("eventCountLabel", { count })}</span>
|
||||
</>
|
||||
)
|
||||
@@ -404,7 +404,7 @@ function CostTrendLine({
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">{point.date}</span>
|
||||
<span className="text-fg-secondary">{point.date}</span>
|
||||
<span className="ml-2 font-medium">{fmtCostFull(point.cost)}</span>
|
||||
</>
|
||||
)
|
||||
@@ -437,7 +437,7 @@ function BarRow({
|
||||
const width = pct !== undefined ? pct : max > 0 ? Math.round((count / max) * 100) : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 truncate flex-shrink-0" title={label}>
|
||||
<span className="text-xs text-fg-secondary w-28 truncate flex-shrink-0" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-2">
|
||||
@@ -447,7 +447,7 @@ function BarRow({
|
||||
/>
|
||||
</div>
|
||||
<Tip raw={count.toLocaleString()}>
|
||||
<span className="text-xs text-gray-500 w-10 text-right flex-shrink-0">{fmt(count)}</span>
|
||||
<span className="text-xs text-fg-muted w-10 text-right flex-shrink-0">{fmt(count)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
@@ -457,7 +457,7 @@ function CostBarRow({
|
||||
label,
|
||||
cost,
|
||||
max,
|
||||
color = "bg-emerald-400",
|
||||
color = "bg-status-success",
|
||||
}: {
|
||||
label: string;
|
||||
cost: number;
|
||||
@@ -467,7 +467,7 @@ function CostBarRow({
|
||||
const width = max > 0 ? Math.max(2, Math.round((cost / max) * 100)) : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-24 truncate flex-shrink-0" title={label}>
|
||||
<span className="text-xs text-fg-secondary w-24 truncate flex-shrink-0" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-2">
|
||||
@@ -476,7 +476,7 @@ function CostBarRow({
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-emerald-400 font-mono w-16 text-right flex-shrink-0">
|
||||
<span className="text-xs text-status-success font-mono w-16 text-right flex-shrink-0">
|
||||
<Tip raw={fmtCostFull(cost)}>{fmtCost(cost)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -495,7 +495,7 @@ function DonutChart({
|
||||
const { t } = useTranslation(["analytics", "common"]);
|
||||
const { show, move, hide, node } = useTooltip();
|
||||
const total = segments.reduce((s, g) => s + g.value, 0);
|
||||
if (total === 0) return <div className="text-xs text-gray-500">{t("common:noData")}</div>;
|
||||
if (total === 0) return <div className="text-xs text-fg-muted">{t("common:noData")}</div>;
|
||||
|
||||
const r = 52;
|
||||
const cx = 64;
|
||||
@@ -543,10 +543,10 @@ function DonutChart({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-gray-300" fontSize={11}>
|
||||
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-fg-secondary" fontSize={11}>
|
||||
{(formatTotal ?? fmt)(total)}
|
||||
</text>
|
||||
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-gray-600" fontSize={9}>
|
||||
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-fg-muted" fontSize={9}>
|
||||
{t("common:total_lower")}
|
||||
</text>
|
||||
</svg>
|
||||
@@ -557,8 +557,8 @@ function DonutChart({
|
||||
className="w-2.5 h-2.5 rounded-sm flex-shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="text-gray-400">{label}</span>
|
||||
<span className="text-gray-500 ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
|
||||
<span className="text-fg-secondary">{label}</span>
|
||||
<span className="text-fg-muted ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -588,7 +588,7 @@ function StatPill({
|
||||
return (
|
||||
<div className="card p-5 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">{label}</span>
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">{label}</span>
|
||||
<Icon className={`w-4 h-4 ${color}`} />
|
||||
</div>
|
||||
{loading ? (
|
||||
@@ -601,7 +601,7 @@ function StatPill({
|
||||
{loading ? (
|
||||
<TextSkeleton width="w-20" />
|
||||
) : (
|
||||
sub && <p className="text-[11px] text-gray-500">{sub}</p>
|
||||
sub && <p className="text-[11px] text-fg-muted">{sub}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -881,8 +881,8 @@ export function Analytics() {
|
||||
].filter((s) => s.value > 0);
|
||||
|
||||
const EVENT_TYPE_COLORS: Record<string, string> = {
|
||||
PreToolUse: "bg-emerald-400",
|
||||
PostToolUse: "bg-blue-400",
|
||||
PreToolUse: "bg-status-success",
|
||||
PostToolUse: "bg-blue-500",
|
||||
Stop: "bg-violet-400",
|
||||
SubagentStop: "bg-yellow-400",
|
||||
Notification: "bg-orange-400",
|
||||
@@ -899,22 +899,22 @@ export function Analytics() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-2">
|
||||
<p className="text-xs text-fg-muted flex items-center gap-2">
|
||||
{t("subtitle")}
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] text-gray-500 bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] text-fg-muted bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
{lastUpdate.toLocaleTimeString()}
|
||||
</span>
|
||||
@@ -942,7 +942,7 @@ export function Analytics() {
|
||||
raw={data ? data.overview.total_sessions.toLocaleString() : undefined}
|
||||
sub={data ? `${data.overview.active_sessions} ${t("common:active")}` : undefined}
|
||||
icon={FolderOpen}
|
||||
color="text-blue-400"
|
||||
color="text-blue-500"
|
||||
loading={!data}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -951,7 +951,7 @@ export function Analytics() {
|
||||
raw={data ? data.overview.total_agents.toLocaleString() : undefined}
|
||||
sub={data ? `${data.overview.active_agents} ${t("common:active")}` : undefined}
|
||||
icon={Bot}
|
||||
color="text-emerald-400"
|
||||
color="text-status-success"
|
||||
loading={!data}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -973,7 +973,7 @@ export function Analytics() {
|
||||
: undefined
|
||||
}
|
||||
icon={DollarSign}
|
||||
color="text-emerald-400"
|
||||
color="text-status-success"
|
||||
loading={!costData}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -994,7 +994,7 @@ export function Analytics() {
|
||||
{/* Activity heatmap + 30-day sparkline */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="card p-5 lg:col-span-2">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-4">{t("eventActivity")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-4">{t("eventActivity")}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="w-fit min-w-max mx-auto">
|
||||
<Heatmap weeks={weeks} />
|
||||
@@ -1002,17 +1002,17 @@ export function Analytics() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("last30Days")}</h3>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("dailyEventCount")}</p>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">{t("last30Days")}</h3>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("dailyEventCount")}</p>
|
||||
<Sparkline data={last30} />
|
||||
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
|
||||
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
|
||||
<span>{last30[0]?.date?.slice(5)}</span>
|
||||
<span>{last30[last30.length - 1]?.date?.slice(5)}</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("peakDay")}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-muted">{t("peakDay")}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={Math.max(...last30.map((d) => d.count)).toLocaleString()}>
|
||||
{fmt(Math.max(...last30.map((d) => d.count)))}
|
||||
</Tip>{" "}
|
||||
@@ -1020,8 +1020,8 @@ export function Analytics() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("total30d")}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-muted">{t("total30d")}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={last30.reduce((s, d) => s + d.count, 0).toLocaleString()}>
|
||||
{fmt(last30.reduce((s, d) => s + d.count, 0))}
|
||||
</Tip>{" "}
|
||||
@@ -1048,8 +1048,8 @@ export function Analytics() {
|
||||
onClick={() => setActiveTab(key)}
|
||||
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
activeTab === key
|
||||
? "bg-surface-4 text-gray-200"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
? "bg-surface-4 text-fg-secondary"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -1061,7 +1061,7 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Token bars */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("tokenDistribution")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -1069,12 +1069,12 @@ export function Analytics() {
|
||||
{
|
||||
label: t("common:token.input"),
|
||||
value: data?.tokens.total_input ?? 0,
|
||||
color: "bg-blue-400",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
label: t("common:token.output"),
|
||||
value: data?.tokens.total_output ?? 0,
|
||||
color: "bg-emerald-400",
|
||||
color: "bg-status-success",
|
||||
},
|
||||
{
|
||||
label: t("common:token.cacheRead"),
|
||||
@@ -1097,13 +1097,13 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("common:token.totalTokens")}</span>
|
||||
<Tip raw={totalTokens.toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">{fmt(totalTokens)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(totalTokens)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("cacheEfficiency")}</span>
|
||||
<span className="text-violet-400 font-mono">{cacheHitPct}%</span>
|
||||
</div>
|
||||
@@ -1112,18 +1112,20 @@ export function Analytics() {
|
||||
|
||||
{/* Token summary */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenBreakdown")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("tokenBreakdown")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
label: t("common:token.input"),
|
||||
value: data?.tokens.total_input ?? 0,
|
||||
color: "text-blue-400",
|
||||
color: "text-blue-500",
|
||||
},
|
||||
{
|
||||
label: t("common:token.output"),
|
||||
value: data?.tokens.total_output ?? 0,
|
||||
color: "text-emerald-400",
|
||||
color: "text-status-success",
|
||||
},
|
||||
{
|
||||
label: t("common:token.cacheRead"),
|
||||
@@ -1135,13 +1137,13 @@ export function Analytics() {
|
||||
value: data?.tokens.total_cache_write ?? 0,
|
||||
color: "text-yellow-400",
|
||||
},
|
||||
{ label: t("common:total"), value: totalTokens, color: "text-gray-100" },
|
||||
{ label: t("common:total"), value: totalTokens, color: "text-fg-primary" },
|
||||
].map(({ label, value, color }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex justify-between items-center py-2 border-b border-border last:border-0"
|
||||
>
|
||||
<span className="text-xs text-gray-400">{label}</span>
|
||||
<span className="text-xs text-fg-secondary">{label}</span>
|
||||
<span className={`text-sm font-mono font-medium ${color}`}>
|
||||
{value.toLocaleString()}
|
||||
</span>
|
||||
@@ -1149,23 +1151,23 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
{totalTokens === 0 && (
|
||||
<p className="text-[11px] text-gray-600 mt-4">{t("tokenInfo")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-4">{t("tokenInfo")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token mix donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenMix")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("tokenMix")}</h3>
|
||||
{tokenMixSegments.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("common:noData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("common:noData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<DonutChart segments={tokenMixSegments} formatTotal={(total) => fmt(total)} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-2">
|
||||
{tokenMixSegments.map((segment) => (
|
||||
<div key={segment.label} className="flex justify-between text-xs">
|
||||
<span className="text-gray-400">{segment.label}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary">{segment.label}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={segment.value.toLocaleString()}>{fmt(segment.value)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1181,29 +1183,31 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Daily cost trends */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("dailyCostTrends")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">
|
||||
{t("dailyCostTrends")}
|
||||
</h3>
|
||||
{dailyCostsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("costPerDay")}</p>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("costPerDay")}</p>
|
||||
<CostTrendLine data={dailyCostLast30} />
|
||||
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
|
||||
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
|
||||
<span>{dailyCostLast30[0]?.date?.slice(5)}</span>
|
||||
<span>{dailyCostLast30[dailyCostLast30.length - 1]?.date?.slice(5)}</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("peakCostDay")}</span>
|
||||
<span className="text-emerald-400 font-mono">
|
||||
<span className="text-fg-muted">{t("peakCostDay")}</span>
|
||||
<span className="text-status-success font-mono">
|
||||
<Tip raw={`${peakCostDay.date} • ${fmtCostFull(peakCostDay.cost)}`}>
|
||||
{fmtCost(peakCostDay.cost)}
|
||||
</Tip>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("totalCost30d")}</span>
|
||||
<span className="text-emerald-400 font-mono">
|
||||
<span className="text-fg-muted">{t("totalCost30d")}</span>
|
||||
<span className="text-status-success font-mono">
|
||||
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1214,7 +1218,7 @@ export function Analytics() {
|
||||
|
||||
{/* Cost by model */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("costByModel")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("costByModel")}</h3>
|
||||
{costBreakdown.length > 0 ? (
|
||||
<>
|
||||
<DonutChart
|
||||
@@ -1231,17 +1235,17 @@ export function Analytics() {
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-2">
|
||||
{costBreakdown.map((b) => (
|
||||
<div key={b.model} className="flex justify-between text-xs">
|
||||
<span className="text-gray-400 font-mono truncate">
|
||||
<span className="text-fg-secondary font-mono truncate">
|
||||
{formatModelName(b.model)}
|
||||
</span>
|
||||
<span className="text-emerald-400 font-mono font-medium ml-2">
|
||||
<span className="text-status-success font-mono font-medium ml-2">
|
||||
<Tip raw={fmtCostFull(b.cost)}>{fmtCost(b.cost)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between text-xs pt-2 border-t border-border">
|
||||
<span className="text-gray-300 font-medium">{t("common:total")}</span>
|
||||
<span className="text-emerald-400 font-mono font-semibold">
|
||||
<span className="text-fg-secondary font-medium">{t("common:total")}</span>
|
||||
<span className="text-status-success font-mono font-semibold">
|
||||
<Tip raw={fmtCostFull(costData?.total_cost ?? 0)}>
|
||||
{fmtCost(costData?.total_cost ?? 0)}
|
||||
</Tip>
|
||||
@@ -1250,18 +1254,20 @@ export function Analytics() {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">{t("noCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noCostData")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost by weekday */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("costByWeekday")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">
|
||||
{t("costByWeekday")}
|
||||
</h3>
|
||||
{dailyCostsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("last30Days")}</p>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("last30Days")}</p>
|
||||
<div className="space-y-3">
|
||||
{weekdayCosts.map(({ label, cost }) => (
|
||||
<CostBarRow
|
||||
@@ -1274,7 +1280,7 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border text-xs flex justify-between">
|
||||
<span className="text-gray-500">{t("common:total")}</span>
|
||||
<span className="text-fg-muted">{t("common:total")}</span>
|
||||
<span className="text-cyan-400 font-mono">
|
||||
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
|
||||
</span>
|
||||
@@ -1289,9 +1295,11 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Agent type distribution */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("subagentTypes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("subagentTypes")}
|
||||
</h3>
|
||||
{(data?.agent_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noSubagentData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
||||
@@ -1309,13 +1317,13 @@ export function Analytics() {
|
||||
|
||||
{/* Agent status donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("agentStatus")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
|
||||
<DonutChart segments={agentStatusSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalAgentsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_agents ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
@@ -1323,7 +1331,7 @@ export function Analytics() {
|
||||
{agentStatusSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-gray-500"
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -1333,7 +1341,7 @@ export function Analytics() {
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
@@ -1342,9 +1350,9 @@ export function Analytics() {
|
||||
|
||||
{/* Event type breakdown */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("eventTypes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
|
||||
{(data?.event_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noEventData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
||||
@@ -1353,7 +1361,7 @@ export function Analytics() {
|
||||
label={event_type}
|
||||
count={count}
|
||||
max={maxEventTypeCount}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-gray-400"}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1366,9 +1374,9 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Top tools */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("toolUsage")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("toolUsage")}</h3>
|
||||
{(data?.tool_usage ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noToolData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noToolData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.tool_usage ?? []).slice(0, 12).map(({ tool_name, count }) => (
|
||||
@@ -1386,13 +1394,15 @@ export function Analytics() {
|
||||
|
||||
{/* Session outcomes donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("sessionOutcomes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("sessionOutcomes")}
|
||||
</h3>
|
||||
<DonutChart segments={sessionOutcomeSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalSessionsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_sessions ?? 0).toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_sessions ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
@@ -1400,7 +1410,7 @@ export function Analytics() {
|
||||
{sessionOutcomeSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-gray-500"
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -1410,7 +1420,7 @@ export function Analytics() {
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
@@ -1419,11 +1429,11 @@ export function Analytics() {
|
||||
|
||||
{/* Daily session trends */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("dailySessionTrends")}
|
||||
</h3>
|
||||
{dailySessionsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noSessionTrendData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noSessionTrendData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<Sparkline data={dailySessionsLocal.slice(-30)} color="#6366f1" />
|
||||
@@ -1440,7 +1450,7 @@ export function Analytics() {
|
||||
);
|
||||
return (
|
||||
<div key={date} className="flex items-center gap-3">
|
||||
<span className="text-[11px] text-gray-500 font-mono w-20 flex-shrink-0">
|
||||
<span className="text-[11px] text-fg-muted font-mono w-20 flex-shrink-0">
|
||||
{date.slice(5)}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-1.5">
|
||||
@@ -1451,14 +1461,14 @@ export function Analytics() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-500 w-4 text-right">
|
||||
<span className="text-[11px] text-fg-muted w-4 text-right">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 mt-3">{t("last7Days")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-3">{t("last7Days")}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user