mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
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:
@@ -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
File diff suppressed because one or more lines are too long
+35
-23
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user