+
+
- used
- {trim(formatUSD(u.shared_spent_today_micros + u.private_spent_today_micros))}
- {#if u.private_spent_today_micros > 0}
- {trim(formatUSD(u.shared_spent_today_micros))} shared + {trim(formatUSD(u.private_spent_today_micros))} private
- {/if}
+ usage
+ {trim(formatUSD(_inCap))}
-
+ {#if _ov > 0}
+
+
+ overage
+ +{trim(formatUSD(_ov))}
+
+ {/if}
+
+
+
+
floor
- {trim(formatUSD(u.shared_allowance_floor_micros))}
-
- {#if u.shared_allowance_bonus_micros > 0}
-
-
- bonus
- +{trim(formatUSD(u.shared_allowance_bonus_micros))}
+ {trim(formatUSD(_floor))}
+ {#if _bonus > 0}
+
+
+ bonus
+ +{trim(formatUSD(_bonus))}
+
{/if}
- {#if u.private_reservation_micros > 0}
-
-
- private
- +{trim(formatUSD(u.private_reservation_micros))}
-
+ {#if _private > 0}
+
+
+ private
+ +{trim(formatUSD(_private))}
+
{/if}
-
+
total
- {trim(formatUSD(u.shared_allowance_today_micros + u.private_reservation_micros))}
+ {trim(formatUSD(_sharedCap + _private))}
-
{#if u.history_days_used >= 3}~{trim(formatUSD(u.predicted_total_today_micros))} predicted · {u.history_days_used}/30 active days{u.is_donating ? ' · donating' : ''}{:else}no history ({u.history_days_used}/3 days){/if}
+
+
+ {#if u.history_days_used >= 3}
+
+
+ predicted
+ {trim(formatUSD(u.predicted_total_today_micros))}
+
+ {/if}
+
+
+ {#if u.is_donating}
+ donating slack → pool
+ {:else if _bonus > 0}
+ receiving from pool
+ {:else}
+ {u.history_days_used < 3 ? `${u.history_days_used}/3 days — building history` : 'neutral'}
+ {/if}
+
+ {#if u.history_days_used >= 3}
+
{u.history_days_used}/30 active days · bonus decays through day
+ {/if}
+
@@ -455,18 +486,41 @@ curl {typeof window !== 'undefined' ? window.location.origin : 'https://potluck.
white-space: normal;
}
.usage-cell:hover .usage-tip { opacity: 1; }
- .tip-rows { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.5rem; }
- .tip-seg { display: grid; grid-template-columns: 0.6rem 3rem 1fr; align-items: baseline; gap: 0.4rem; font-size: 0.75rem; }
- .tip-seg.tip-total { margin-top: 0.25rem; padding-top: 0.25rem; border-top: 1px solid var(--border); }
+
+ .tip-group { display: flex; flex-direction: column; gap: 0.18rem; }
+ .tip-sep {
+ height: 1px;
+ background: var(--border);
+ margin: 0.4rem 0;
+ }
+ .tip-row {
+ display: grid;
+ grid-template-columns: 8px 3.2rem 1fr;
+ align-items: baseline;
+ gap: 0.35rem;
+ font-size: 0.75rem;
+ }
+ .tip-total-row {
+ margin-top: 0.25rem;
+ padding-top: 0.25rem;
+ border-top: 1px solid var(--border);
+ }
.tip-dot { width: 6px; height: 6px; border-radius: 50%; margin-top: 2px; flex-shrink: 0; }
.dot-used { background: var(--accent); }
+ .dot-over { background: light-dark(#c2410c, #ef4444); }
.dot-floor { background: light-dark(oklch(85% 0.05 350), oklch(40% 0.08 350)); }
.dot-bonus { background: light-dark(oklch(78% 0.13 90), oklch(55% 0.15 80)); }
.dot-private { background: light-dark(oklch(60% 0.12 230), oklch(65% 0.12 230)); }
+ .dot-pred { background: light-dark(oklch(65% 0.08 270), oklch(60% 0.1 270)); }
.tip-label { color: var(--text-muted); }
- .tip-val { color: var(--text); }
- .tip-sub { grid-column: 3; font-size: 0.68rem; color: var(--text-muted); margin-top: -0.1rem; }
+ .tip-val { color: var(--text); justify-self: end; font-feature-settings: "tnum" 1; }
+ .tip-over { color: light-dark(#c2410c, #ef4444); }
+ .tip-status-row { grid-template-columns: 8px 1fr; }
+ .tip-status { color: var(--text-muted); font-size: 0.72rem; grid-column: 2; }
+ .tip-donating { color: light-dark(oklch(55% 0.14 80), oklch(72% 0.16 80)); }
+ .tip-receiving { color: var(--accent); }
.tip-meta {
+ margin-top: 0.4rem;
padding-top: 0.4rem;
border-top: 1px solid var(--border);
font-size: 0.68rem;
diff --git a/web/src/routes/usage/+page.svelte b/web/src/routes/usage/+page.svelte
index c848a62..8233f8b 100644
--- a/web/src/routes/usage/+page.svelte
+++ b/web/src/routes/usage/+page.svelte
@@ -37,10 +37,17 @@
let spendDays = $derived.by(() => {
const map = new Map(daily.map(d => [d.day, d]));
const now = Math.floor(Date.now() / 1000);
- const out: { day: number; usd: number }[] = [];
+ const out: { day: number; usd: number; micros: number; inputTok: number; outputTok: number }[] = [];
for (let i = 0; i < 30; i++) {
const d = (Math.floor((now - 29 * 86400) / 86400) + i) * 86400;
- out.push({ day: d, usd: (map.get(d)?.amount_micros ?? 0) / 1_000_000 });
+ const row = map.get(d);
+ out.push({
+ day: d,
+ usd: (row?.amount_micros ?? 0) / 1_000_000,
+ micros: row?.amount_micros ?? 0,
+ inputTok: row?.input_tokens ?? 0,
+ outputTok: row?.output_tokens ?? 0,
+ });
}
return out;
});
@@ -83,18 +90,29 @@
return m;
});
- type Seg = { model: string; micros: number; modelIdx: number; y: number; h: number };
+ let byModelIdx = $derived(new Map(byModel.map(r => [`${r.day}:${r.model}`, r])));
+
+ type Seg = { model: string; micros: number; inputTok: number; outputTok: number; modelIdx: number; y: number; h: number };
let stackBars = $derived.by(() =>
stackData.days.map(d => {
const dm = stackData.lookup.get(d);
const segs: Seg[] = [];
let stackY = PAD.top + CH;
for (let mi = 0; mi < stackData.models.length; mi++) {
+ const row = byModelIdx.get(`${d}:${stackData.models[mi]}`);
const micros = dm?.get(stackData.models[mi]) ?? 0;
if (!micros) continue;
const h = Math.max(1, (micros / maxStack) * CH);
stackY -= h;
- segs.push({ model: stackData.models[mi], micros, modelIdx: mi, y: stackY, h });
+ segs.push({
+ model: stackData.models[mi],
+ micros,
+ inputTok: row?.input_tokens ?? 0,
+ outputTok: row?.output_tokens ?? 0,
+ modelIdx: mi,
+ y: stackY,
+ h,
+ });
}
return { day: d, segs };
})
@@ -108,15 +126,22 @@
function stackBarW() { return (CW / 7) * 0.8; }
// ── tooltip ───────────────────────────────────────────────────────────────
- type Tip = { x: number; y: number; lines: string[] } | null;
- let spendTip = $state
(null);
- let stackTip = $state(null);
+ const TIP_W = 158; // spend tooltip width
+ const TIP_WS = 170; // stack tooltip width (room for model name)
+ const TIP_Y = PAD.top + 2;
+
+ type SpendTip = { x: number; day: string; inputTok: number; outputTok: number; totalMicros: number } | null;
+ type StackTip = { x: number; day: string; model: string; inputTok: number; outputTok: number; micros: number } | null;
+ let spendTip = $state(null);
+ let stackTip = $state(null);
- function showSpendTip(event: MouseEvent, lines: string[], anchorX: number) {
- spendTip = { x: Math.min(anchorX, W - 120), y: PAD.top - 4, lines };
+ function showSpendTip(_e: MouseEvent, d: typeof spendDays[0], anchorX: number) {
+ const x = Math.min(Math.max(anchorX - TIP_W / 2, PAD.left), W - TIP_W - PAD.right);
+ spendTip = { x, day: fmtDay(d.day), inputTok: d.inputTok, outputTok: d.outputTok, totalMicros: d.micros };
}
- function showStackTip(event: MouseEvent, lines: string[], anchorX: number) {
- stackTip = { x: Math.min(anchorX, W - 140), y: PAD.top - 4, lines };
+ function showStackTip(_e: MouseEvent, day: number, seg: Seg, anchorX: number) {
+ const x = Math.min(Math.max(anchorX - TIP_WS / 2, PAD.left), W - TIP_WS - PAD.right);
+ stackTip = { x, day: fmtDay(day), model: shortModel(seg.model), inputTok: seg.inputTok, outputTok: seg.outputTok, micros: seg.micros };
}
function hideSpendTip() { spendTip = null; }
function hideStackTip() { stackTip = null; }
@@ -199,7 +224,7 @@
x={bx} y={by_} width={bw} height={bh}
class="bar"
rx="2"
- onmouseenter={(e) => showSpendTip(e, [fmtDay(d.day), fmtUSD(d.usd)], bx + bw / 2)}
+ onmouseenter={(e) => showSpendTip(e, d, bx + bw / 2)}
onmouseleave={hideSpendTip}
role="img"
aria-label="{fmtDay(d.day)}: {fmtUSD(d.usd)}"
@@ -216,15 +241,19 @@
{/each}
{#if spendTip}
+ {@const tx = spendTip.x}
+ {@const ty = TIP_Y}
-
- {#each spendTip.lines as line, li}
- {line}
- {/each}
+
+ {spendTip.day}
+
+ in
+ {fmtTokens(spendTip.inputTok)}
+ out
+ {fmtTokens(spendTip.outputTok)}
+
+ spend
+ {fmtMicros(spendTip.totalMicros)}
{/if}
@@ -253,7 +282,7 @@
style="fill:{PALETTE[seg.modelIdx % PALETTE.length]}"
class="bar stacked"
rx="2"
- onmouseenter={(e) => showStackTip(e, [fmtDay(bar.day), shortModel(seg.model), fmtMicros(seg.micros)], bx + bw / 2)}
+ onmouseenter={(e) => showStackTip(e, bar.day, seg, bx + bw / 2)}
onmouseleave={hideStackTip}
role="img"
aria-label="{fmtDay(bar.day)} {seg.model}: {fmtMicros(seg.micros)}"
@@ -262,15 +291,20 @@
{fmtDay(bar.day)}
{/each}
{#if stackTip}
+ {@const tx = stackTip.x}
+ {@const ty = TIP_Y}
-
- {#each stackTip.lines as line, li}
- {line}
- {/each}
+
+ {stackTip.day}
+ {stackTip.model}
+
+ in
+ {fmtTokens(stackTip.inputTok)}
+ out
+ {fmtTokens(stackTip.outputTok)}
+
+ spend
+ {fmtMicros(stackTip.micros)}
{/if}
@@ -346,9 +380,12 @@
font-size: 9px;
pointer-events: none;
}
- .tip-title { fill: var(--text-muted); }
- .tip-model { fill: var(--text); }
- .tip-val { fill: var(--accent); }
+ .tip-title { fill: var(--text-muted); }
+ .tip-label { fill: var(--text-muted); }
+ .tip-model { fill: var(--text); }
+ .tip-val { fill: var(--accent); font-weight: 500; }
+ .tip-divider { stroke: var(--border); stroke-width: 0.75; }
+ .tooltip-group { pointer-events: none; }
/* legend */
.legend {