fix(dashboard): baseline-% denominator + multi-image thumb strip

SessionSummary: divide saved_$ by baseline_input_$ (was actual_input + output,
which produced 719% on cheap sessions). Math now matches the user-stated
analogy: baseline=10, actual=5 -> saved=5, 50%.

RecentRequests: stamp full imagePngs[] in TransformInfo so each rendered PNG
gets a ring-buffer id. Row shows a strip of 28px thumbs (one per image)
instead of a single 'view' button, so multi-image requests are visible at
a glance. Thumbs are <button>-wrapped for keyboard accessibility.
This commit is contained in:
teamchong
2026-05-24 20:45:52 -04:00
parent 1ba74c2a83
commit 34c62b78cc
6 changed files with 127 additions and 83 deletions
+7
View File
@@ -846,6 +846,11 @@ export interface TransformInfo {
/** Pixel dimensions of the first image. */
firstImageWidth?: number;
firstImageHeight?: number;
/** Every rendered PNG for this request, in render order. images[0] === firstImagePng.
* Surfaced separately so the dashboard can pin any image individually. */
imagePngs?: Uint8Array[];
/** Matching pixel dimensions for each entry in imagePngs. */
imageDims?: Array<{ width: number; height: number }>;
/** Number of images we added by compressing `<system-reminder>` blocks in
* the first user message. */
reminderImgs?: number;
@@ -2141,6 +2146,8 @@ export async function transformRequest(
info.firstImagePng = images[0]!.png;
info.firstImageWidth = images[0]!.width;
info.firstImageHeight = images[0]!.height;
info.imagePngs = images.map((i) => i.png);
info.imageDims = images.map((i) => ({ width: i.width, height: i.height }));
}
// 4. Splice images back into the request.
+33 -33
View File
File diff suppressed because one or more lines are too long
+35 -23
View File
@@ -109,6 +109,7 @@ export interface RecentRow {
* request rendered no image, or once the image has been evicted from the
* ring (the id stays on the row but no longer fetches). */
img_id?: number;
img_ids?: number[];
}
/** Aggregate over the whole session. Reset on process restart unless
@@ -367,33 +368,42 @@ export class DashboardState {
this.ccMapFn = ccMapFn ?? (() => claudeCodeMap());
}
/** Stash a rendered image into the ring (called from onRequest with the raw
* ProxyEvent before info.firstImagePng is dropped by toTrackEvent).
* Returns the assigned image id, or undefined when there's no image —
* the caller stamps it onto the RecentRow as `img_id`. */
captureImage(info: NonNullable<ProxyEvent['info']>): number | undefined {
if (!info.firstImagePng) return undefined;
const id = this.nextImageId++;
const width = info.firstImageWidth ?? 0;
const height = info.firstImageHeight ?? 0;
const kb = (info.firstImagePng.length / 1024).toFixed(1);
const meta =
`${width}×${height} · ${kb} KB · ` +
`${info.imageCount ?? 0} image${info.imageCount === 1 ? '' : 's'} total`;
this.images.push({
id,
png: info.firstImagePng,
meta,
width,
height,
ts: Date.now() / 1000,
});
/** Stash every rendered image into the ring (called from onRequest with the
* raw ProxyEvent before info.firstImagePng is dropped by toTrackEvent).
* Returns the assigned image ids in render order; empty array when there
* are no images. The caller stamps ids[0] onto the RecentRow as `img_id`
* for back-compat and the full list as `img_ids`. */
captureImage(info: NonNullable<ProxyEvent['info']>): number[] {
const pngs = info.imagePngs ?? (info.firstImagePng ? [info.firstImagePng] : []);
if (pngs.length === 0) return [];
const dims =
info.imageDims ??
(info.firstImagePng
? [{ width: info.firstImageWidth ?? 0, height: info.firstImageHeight ?? 0 }]
: []);
const ids: number[] = [];
for (let i = 0; i < pngs.length; i++) {
const id = this.nextImageId++;
const width = dims[i]?.width ?? 0;
const height = dims[i]?.height ?? 0;
const kb = (pngs[i]!.length / 1024).toFixed(1);
const meta = `${width}×${height} · ${kb} KB · image ${i + 1}/${pngs.length}`;
this.images.push({
id,
png: pngs[i]!,
meta,
width,
height,
ts: Date.now() / 1000,
});
ids.push(id);
}
// Evict the oldest entries past the cap. splice() keeps insertion order
// so images[images.length - 1] is always the latest render.
if (this.images.length > IMAGE_RING_CAP) {
this.images.splice(0, this.images.length - IMAGE_RING_CAP);
}
return id;
return ids;
}
/** Fold one event into the running totals + ring buffer.
@@ -406,7 +416,8 @@ export class DashboardState {
// Stash the image bytes before they get GC'd by the request finishing.
// The returned id (if any) is stamped onto this request's RecentRow so
// the dashboard can pull the exact image that request rendered.
const imgId = ev.info ? this.captureImage(ev.info) : undefined;
const imgIds = ev.info ? this.captureImage(ev.info) : [];
const imgId = imgIds[0];
const u = ev.usage;
const info = ev.info;
@@ -580,6 +591,7 @@ export class DashboardState {
session_saved_so_far_delta:
haveBaseline && haveUsage ? round1(baselineInputEff - actualInputEff) : undefined,
img_id: imgId,
img_ids: imgIds,
};
this.recent.push(row);
if (this.recent.length > RECENT_CAP) this.recent.splice(0, this.recent.length - RECENT_CAP);
+40 -13
View File
@@ -50,9 +50,17 @@
? '+' + numFmt(e.session_saved_so_far_delta ?? 0)
: '-'}
</td>
<td class="num">
{#if e.img_id != null}
<button class="view-btn" on:click={() => selectedImageId.set(e.img_id ?? null)}>view</button>
<td class="img-cell">
{#if (e.img_ids && e.img_ids.length > 0) || e.img_id != null}
{@const ids = e.img_ids ?? (e.img_id != null ? [e.img_id] : [])}
<div class="thumb-strip">
{#each ids as id}
<button type="button" class="thumb-btn" title="image #{id}"
on:click={() => selectedImageId.set(id)}>
<img class="thumb" src="/proxy-latest-png?id={id}" alt="img {id}" />
</button>
{/each}
</div>
{:else}
<span class="muted">-</span>
{/if}
@@ -108,16 +116,35 @@
.muted {
color: #6e7681;
}
.view-btn {
font-size: 11px;
background: #21262d;
color: #58a6ff;
border: 1px solid #30363d;
border-radius: 4px;
padding: 1px 6px;
cursor: pointer;
.thumb-strip {
display: flex;
gap: 3px;
align-items: center;
justify-content: flex-end;
}
.view-btn:hover {
background: #30363d;
.thumb-btn {
padding: 0;
border: 1px solid #30363d;
border-radius: 3px;
background: #fff;
cursor: pointer;
line-height: 0;
}
.thumb-btn:hover,
.thumb-btn:focus-visible {
border-color: #58a6ff;
outline: none;
}
.thumb {
height: 28px;
width: auto;
max-width: 28px;
object-fit: cover;
object-position: top left;
display: block;
image-rendering: pixelated;
}
.img-cell {
text-align: right;
}
</style>
+11 -14
View File
@@ -28,23 +28,20 @@
$: err = $currentSession.error;
// backend exposes raw weighted tokens; convert to $ at Opus 4.x rates.
// These MUST stay in lockstep with the server-side constants
// `ASSUMED_INPUT_USD_PER_MTOK` and `OUTPUT_TOKEN_RATE` in src/dashboard.ts.
// These MUST stay in lockstep with the server-side constant
// `ASSUMED_INPUT_USD_PER_MTOK` in src/dashboard.ts.
const INPUT_USD_PER_MTOK = 5.0;
const OUTPUT_TOKEN_RATE = 5.0;
// Numerator: honest savings over the MEASURED slice.
$: baselineTok = data?.baselineInputWeighted ?? 0;
$: actualTok = data?.actualInputWeighted ?? 0;
$: savedTok = Math.max(0, baselineTok - actualTok);
$: savedUsd = (savedTok * INPUT_USD_PER_MTOK) / 1_000_000;
// Denominator: ALL-rows session bill ($) = input + output across every
// request the proxy saw this session, measured or not.
$: allActualTok = data?.allActualInputWeighted ?? 0;
$: allOutputTok = data?.allOutputWeighted ?? 0;
$: totalBillUsd =
(allActualTok * INPUT_USD_PER_MTOK) / 1_000_000 +
(allOutputTok * OUTPUT_TOKEN_RATE) / 1_000_000;
$: savedPct = totalBillUsd > 0 ? (savedUsd / totalBillUsd) * 100 : 0;
// Denominator: BASELINE input $ over the same MEASURED slice. The proxy
// only touches input tokens — output is identical with/without proxy — so
// scoping both sides of the ratio to baseline input $ on measured rows
// gives an apples-to-apples "what fraction of the baseline did we save".
$: baselineUsd = (baselineTok * INPUT_USD_PER_MTOK) / 1_000_000;
$: savedPct = baselineUsd > 0 ? (savedUsd / baselineUsd) * 100 : 0;
$: measuredReqs = data?.baselineMeasuredCount ?? 0;
function fmtUsd(n: number): string {
@@ -58,9 +55,9 @@
<div class="line">
<span class="label">THIS SESSION</span>
— saved <span class="num">{fmtUsd(savedUsd)}</span>
of <span class="muted">{fmtUsd(totalBillUsd)}</span> total bill
(<span class="num">{savedPct.toFixed(1)}%</span>)
· <span class="muted">{measuredReqs} requests</span>
(<span class="num">{savedPct.toFixed(1)}%</span> of
<span class="muted">{fmtUsd(baselineUsd)}</span> baseline)
<span class="muted">{measuredReqs} requests</span>
</div>
{/if}
+1
View File
@@ -86,6 +86,7 @@ export interface RecentRow {
baseline_input?: number;
session_saved_so_far_delta?: number;
img_id?: number;
img_ids?: number[];
}
/** /api/sessions.json payload — bulk session aggregate + selection table. */