Files
codecraft/web/src/components/UsageOverlay.tsx
T
Richard ZinkandClaude Opus 5.5 a0d0e707f2 Add Web Push, guided start screen, SVG icons, chunked uploads, rename
- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished
  notifications while no app window is visible; settings with test button
- Guided new-session flow: task, craft target (website/app/game/old
  software/fresh), clean room or deep, description, model, folder
  (new, existing or cloned git repository); originals upload to original/
- Craft phase bar (craft_phase tool) for study/specify/build/check/ready
- Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB)
- All UI glyphs and emojis replaced by themeable SVG icons
- Rename dialog with manual name or model-generated suggestion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:03:26 +01:00

201 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Icon } from "./Icon";
import { useEffect } from "react";
import { useFetch, useModels } from "../hooks";
import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, useT } from "../i18n";
import { useStore } from "../store";
import type { Aggregate, Session } from "../types";
interface SessionStats {
total: Aggregate;
by_model: (Aggregate & { model: string })[];
by_agent: (Aggregate & { key: string })[];
first: number | null;
last: number | null;
currency: string;
}
interface BudgetRow {
provider: string;
budget: number | null;
spent: number;
share: number | null;
warning: boolean;
}
function Row({ label, value, hint }: { label: string; value: React.ReactNode; hint?: string }) {
return (
<div className="kv" title={hint}>
<span>{label}</span>
<b>{value}</b>
</div>
);
}
const perMillion = (a: Aggregate) => {
const tokens = a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens;
return tokens > 0 ? (a.cost / tokens) * 1_000_000 : 0;
};
/** Everything about the models used in a session: configuration, usage, speed, cost. */
export function UsageOverlay({ session, onClose }: { session: Session; onClose: () => void }) {
const t = useT();
const { data } = useFetch<SessionStats>(`/sessions/${session.id}/stats`);
const models = useModels();
const isAdmin = useStore((s) => s.me?.role === "admin");
const budget = useFetch<{ providers: BudgetRow[]; currency: string }>(isAdmin ? "/budget" : null).data;
useEffect(() => {
const h = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, [onClose]);
const cur = data?.currency ?? "USD";
const info = (spec: string) => {
const [p, ...rest] = spec.split("/");
return models.find((x) => x.provider === p)?.models.find((m) => m.id === rest.join("/"));
};
const u = session.last_usage;
const ctxTokens = u ? u.input_tokens + u.cache_read_tokens + u.cache_write_tokens + u.output_tokens : 0;
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal wide" role="dialog" aria-label={t("usage.title")} onClick={(e) => e.stopPropagation()}>
<div className="row" style={{ marginBottom: 8 }}>
<h3 style={{ margin: 0 }}>{t("usage.title")}</h3>
<span className="spacer" />
<button className="small ghost" onClick={onClose} aria-label={t("common.close")}>
<Icon name="close" />
</button>
</div>
{!data ? (
<div className="empty">{t("common.loading")}</div>
) : (
<>
<div className="kpis">
<div className="kpi">
<div className="k-label">{t("stats.cost")}</div>
<div className="k-value">{fmtCost(data.total.cost, cur)}</div>
<div className="k-sub">{t("stats.perRequest", { v: fmtCost(data.total.requests ? data.total.cost / data.total.requests : 0, cur) })}</div>
</div>
<div className="kpi">
<div className="k-label">{t("stats.requests")}</div>
<div className="k-value">{fmtNumber(data.total.requests)}</div>
{data.total.errors > 0 && <div className="k-sub">{t("stats.errors", { n: data.total.errors })}</div>}
</div>
<div className="kpi">
<div className="k-label">{t("stats.tokens")}</div>
<div className="k-value">
{fmtCompact(data.total.input_tokens + data.total.cache_read_tokens + data.total.cache_write_tokens)}<Icon name="arrow-up" className="tiny" /> {fmtCompact(data.total.output_tokens)}<Icon name="arrow-down" className="tiny" />
</div>
<div className="k-sub">
{t("stats.cacheHit")}: {fmtPercent(data.total.cache_hit_rate)}
</div>
</div>
<div className="kpi">
<div className="k-label">{t("usage.period")}</div>
<div className="k-value" style={{ fontSize: 15 }}>
{data.first ? fmtDate(data.first) : "–"}
</div>
<div className="k-sub">{data.last ? <><Icon name="chevron-right" className="tiny" /> {fmtDate(data.last)}</> : ""}</div>
</div>
</div>
{data.by_model.map((m) => {
const mi = info(m.model);
const provider = m.model.split("/")[0];
const b = budget?.providers.find((x) => x.provider === provider);
const isCurrent = m.model === session.model;
return (
<div className="panel" key={m.model}>
<h3>
{mi?.name ?? m.model}
{isCurrent && <span className="chip">{t("usage.current")}</span>}
<span className="chip">{provider}</span>
</h3>
<div className="kv-grid">
<div>
<div className="kv-title">{t("usage.model")}</div>
<Row label="ID" value={<code>{m.model}</code>} />
<Row label={t("settings.context")} value={mi?.context ? `${fmtCompact(mi.context)} tok` : "–"} />
<Row label={t("settings.maxOutput")} value={mi?.max_output ? `${fmtCompact(mi.max_output)} tok` : "–"} />
<Row label={t("settings.reasoning")} value={mi?.reasoning ? <><Icon name="check" />{mi.effort ? ` (${mi.effort})` : ""}</> : "–"} />
<Row label={t("settings.vision")} value={mi?.vision ? <Icon name="check" /> : "–"} />
{isCurrent && mi?.context ? (
<Row label={t("usage.contextNow")} value={`${fmtCompact(ctxTokens)} / ${fmtCompact(mi.context)} (${fmtPercent(ctxTokens / mi.context)})`} />
) : null}
</div>
<div>
<div className="kv-title">{t("usage.prices")}</div>
<Row label={t("settings.priceIn")} value={mi?.input_price != null ? fmtCost(mi.input_price, cur) : "–"} />
<Row label={t("settings.priceOut")} value={mi?.output_price != null ? fmtCost(mi.output_price, cur) : "–"} />
<Row label={t("settings.priceCacheRead")} value={mi?.cache_read_price != null ? fmtCost(mi.cache_read_price, cur) : "–"} />
<Row label={t("settings.priceCacheWrite")} value={mi?.cache_write_price != null ? fmtCost(mi.cache_write_price, cur) : "–"} />
<Row label={t("stats.perMillion")} value={fmtCost(perMillion(m), cur)} hint={t("stats.perMillionHint")} />
</div>
<div>
<div className="kv-title">{t("usage.usage")}</div>
<Row label={t("stats.requests")} value={fmtNumber(m.requests)} />
<Row label={t("stats.input")} value={fmtNumber(m.input_tokens)} />
<Row label={t("stats.cacheRead")} value={fmtNumber(m.cache_read_tokens)} />
<Row label={t("usage.cacheWrite")} value={fmtNumber(m.cache_write_tokens)} />
<Row label={t("stats.output")} value={fmtNumber(m.output_tokens)} />
<Row label={t("stats.cacheHit")} value={fmtPercent(m.cache_hit_rate)} />
</div>
<div>
<div className="kv-title">{t("usage.speed")}</div>
<Row label={t("stats.ttft")} value={`${fmtNumber(m.avg_ttft_ms)} ms`} />
<Row label={t("stats.prefillSpeed")} value={`${fmtNumber(m.prefill_tps)} tok/s`} />
<Row label={t("stats.outputSpeed")} value={`${fmtNumber(m.output_tps, 1)} tok/s`} />
<Row label={t("usage.totalTime")} value={`${fmtNumber(m.duration_ms / 1000, 1)} s`} />
<div className="kv-title" style={{ marginTop: 8 }}>{t("stats.cost")}</div>
<Row label={t("usage.inSession")} value={fmtCost(m.cost, cur)} />
{b && (
<Row
label={t("usage.providerMonth")}
value={
<span style={{ color: b.warning ? "var(--accent2)" : undefined }}>
{fmtCost(b.spent, budget!.currency)}
{b.budget ? ` / ${fmtCost(b.budget, budget!.currency)}` : ""}
</span>
}
/>
)}
</div>
</div>
</div>
);
})}
{data.by_agent.length > 1 && (
<div className="panel">
<h3>{t("usage.byAgent")}</h3>
<table className="grid">
<thead>
<tr>
<th>{t("stats.agent")}</th>
<th className="num">{t("stats.requests")}</th>
<th className="num">{t("stats.tokens")}</th>
<th className="num">{t("stats.cost")}</th>
</tr>
</thead>
<tbody>
{data.by_agent.map((a) => (
<tr key={a.key}>
<td>{a.key}</td>
<td className="num">{fmtNumber(a.requests)}</td>
<td className="num">{fmtCompact(a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens)}</td>
<td className="num">{fmtCost(a.cost, cur)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)}
</div>
</div>
);
}