Context Window
A four-state context-window budget bar: the assembled prompt packed part by part, the reply's reserve subtracted from the window instead of stacked on top of it, the overflow framed against the budget that actually applies, and the tokens the eviction order takes first cut out of the part that owns them.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-context-window.jsonPrompt
Build a React + TypeScript + Tailwind "ChartContextWindow" card in plain SVG
with zod. It is NOT a progress bar and NOT a generic 100% stacked bar: the
denominator is a model limit the content is allowed to exceed, the reply's
reserve is space subtracted from that limit rather than another slice of it,
and the card owes the reader a named answer to "what goes first".
Contract
- One zod schema is the source of truth:
{ status: "loading" | "empty" | "error" | "ready";
segments: { id: string; label: string; tokens: number;
evictionRank?: number }[];
limit: number; reservedOutput: number;
model: { name: string; tokenizer?: string } }.
- tokens is always supplied. The card NEVER tokenises: a BPE vocabulary is
model-specific, it belongs next to the model client, and a count computed in
the view would disagree with what the provider bills. Print whose tokenizer
the numbers came from — two models read the same prompt as 96,300 and
104,880 tokens, and a card that does not say which one it used is quoting a
number nobody can reproduce.
- evictionRank is ordered smallest-first: rank 1 is what the assembler drops
from before it touches anything else. A segment WITHOUT a rank is PINNED — it
is not in the order at all. Pinning is an absence, not a `pinned: true` flag,
because "not in the order" and "first in the order" are the same field and a
payload must not be able to claim both.
- Array order is LAYOUT order (assembly order, left to right) and is
deliberately independent of evictionRank: what sits at the front of a prompt
is rarely what leaves it first.
- refine: unique ids (a duplicate makes two parts share a React key), unique
evictionRanks (two parts cannot both be the first thing evicted),
reservedOutput <= limit. Guard every refinement against its own ragged input
— zod runs them all, so an unguarded data.segments[0].id throws out of
safeParse instead of returning { success: false }.
- Props = z.infer plus title (default "Context window" — the payload names the
MODEL; what the card itself is called is a layout decision), barHeight
(default 30, clamped 18..64), onRetry, className and the div's native props
(Omit "title"); forwardRef to the card.
- Export the maths beside the component so a test can print the numbers the
picture is made of: buildContextWindowModel(), apportionPercent().
Behavior
- SUBTRACT THE RESERVE, NEVER STACK IT. available = limit - reservedOutput. The
prompt competes for `available`, not for `limit`; reservedOutput is never a
member of segments and can never be evicted. This one line is the whole
reason the card exists: 196,140 tokens in a 200,000 window looks like a fit
and is 12,140 over once 16,000 is held back for the answer.
- over = max(0, used - available); free = max(0, available - used).
- THE PLAN IS THE POINT. When it is over, walk the included segments by
evictionRank ascending and take min(tokens, remaining) from each until the
overflow is covered; skip a ranked part with nothing in it rather than
printing "trim 0 tokens" as though that helped. Name the parts, the amounts
and what each is left with. When it FITS, state the same policy without
firing it — "nothing is evicted; if it ran over, X would go first" — so a
reader learns the rule before the day it bites. If the whole order empties
and tokens are still over, say so and name what the pinned parts alone need.
- ATTRIBUTE THE OVERFLOW; DON'T LET GEOMETRY DECIDE IT. The band that happens
to sit past the budget line is NOT the band that gets evicted. Draw two marks
carrying the same number: a dashed frame from `available` to `used` stating
the AMOUNT, and a cut hatch inside the rank-1 part stating WHERE it comes
from. Then draw a second, thinner bar of the same prompt after the plan has
run, so "who paid for it" is a visual diff rather than a claim. Put the cut
at the trailing edge of its band and say in words that the card claims an
amount, never an ordering of tokens within a part.
- CHIP = LEGEND = LEDGER = WHAT-IF. One chip per segment, a real button with
aria-pressed: swatch, name, tokens, share of the assembled prompt, rank or
"pinned", and the trim the plan takes from it. Pressing it leaves that part
out of the prompt entirely — the mix re-apportions, the plan re-runs and the
fit verdict can flip, which is exactly the question an engineer is asking.
Refuse to switch the last one off and say why in a role="status" line rather
than no-opping silently. A switched-off chip is toggled, NOT disabled: dashed
border and a struck-through name, never a blanket opacity that drags the
muted readouts under 4.5:1.
- EXACTLY 100, IN EVERY FILTER STATE. The segment mix is apportioned by largest
remainder (Hamilton): floor every share, then hand the leftover points to the
largest fractional parts, ties broken by the larger raw value then by
position. Rounding each share on its own is how five parts print 101 in one
filter state and 99 in the next. A part with nothing in it has no fraction to
claim a point, so a real zero survives as a zero.
- ONE PARTITION, ONE DENOMINATOR. A prompt / free / reserve split of the window
is only a partition while the prompt fits; once it runs over, the prompt and
the reserve overlap and three shares of a whole they no longer tile would add
past 100. Apportion that trio only in the fitting case, and fall back to lone
ratios — each with its denominator spoken out loud — in the over case.
- Four first-class branches of one card: loading (skeleton mirroring the tiles,
the bar and the chips, aria-hidden, plus an sr-only role=status line), empty
(worded so it cannot be mistaken for a failed fetch, and it explains that a
limit and a reserve are configuration known before anything is packed), error
(Try again only when onRetry was passed), ready. status "ready" with no
segments, or with every count zero, renders the empty branch and says which
of the two happened. Decide that from the RAW payload, never from the
filtered view, or a reader who switches off the last chip watches the card
and its own controls disappear.
- CLEANUP: one ResizeObserver measuring the plot column so SVG user units are
CSS pixels, disconnected on unmount and whenever the node changes. No timers,
no rAF, no window listeners.
Rendering & styling
- The scale is max(limit, used), so a prompt that has run straight past the
window still lands inside the frame. Draw the window track out to `limit`,
the reserve as a hatch occupying [available, limit] — the LAST tokens of the
window, which turns "the budget ends here" into a visible edge instead of a
second rule — and the prompt packed from 0. Mark `limit` with a rule haloed
in the card colour so it stays readable wherever it crosses a band or the
hatch in either theme, and flag it with a compact tick label.
- Band edges come from a running total in TOKEN space, so a band is always
positioned by the same number that colours and labels it. No minimum-width
floor: it would push every band after it off its own value.
- Semantic tokens only: bg-card / text-card-foreground panel, border tiles,
muted track and skeleton, muted-foreground hatch and callout text,
text-destructive for the overflow frame, the bracket and the error headline,
ring for focus. Bands take var(--chart-1..5) from one index formula shared by
the bar, the plan bar and the chip swatch; past the fifth part the slot wraps
and the fill is mixed toward --foreground so the wrapped tier stays clear of
the card surface in both themes. Never a hex, and never text painted on a
band — a chart token is dark in one theme and light in the other, so any
label sitting on one loses its contrast in the other.
- LABELS OUTSIDE, NOT INSIDE. Band names are leader-line callouts above the
bar, placed greedily highest-priority-first (limit flag, then trim amounts,
then band names by width) with a box estimated from the character count and a
minimum clear gap; a callout that would collide is dropped rather than
squeezed, and a band under ~16px is never offered one. Nothing is lost when a
callout is dropped: the chips carry every name, count and share.
- Both bars live in ONE svg. They have to share a scale, a limit rule and the
reserve hatch, and a paint server defined in a second inline svg would be a
cross-document url(#…) reference taken on for nothing.
- ACCESSIBILITY: role="img" on the svg with a full sentence label — honest,
because nothing inside the frame is a tab stop; the what-if filter lives in
real buttons and every number is repeated in an sr-only table (sr-only on the
WRAPPER div, never on the table itself: CSS width is only a lower bound for a
table box, so width:1px does not hold one back and a narrow viewport picks up
real horizontal scroll). The table is the SAME dataset the plot is drawing,
because this filter changes what is in the prompt rather than merely what is
painted; list the parts left out, marked as left out, so nothing is hidden,
and put the verdict in the caption.
- Motion: the only animation is the skeleton pulse and the chip hover; both
carry motion-reduce:animate-none / motion-reduce:transition-none.
Customization levers
- Part set: segments is open — split "Retrieved chunks" per source, add a
scratchpad or a long-term memory block, or fold the tool schemas into the
system prompt. Only the ranks are policy; the labels are yours.
- Policy: change which parts carry a rank, and in what order, to model your own
assembler. Drop every rank and the card becomes a pure budget picture that
says nothing may be evicted automatically; rank everything and nothing is
pinned.
- Density: barHeight is the bar; the two tiles and the closing paragraph are
the first things to drop for a dashboard tile, the chips the last. Delete the
after-the-plan bar for a static report and keep the sentence.
- Interaction: for a read-only report card, render the chips as plain legend
rows and delete the excluded set; for a filter shared across a page of cards,
lift it into a controlled prop and emit onIncludedChange.
- Palette: past ten parts the wrap tier repeats, so add a texture axis (an SVG
pattern per tier, mirrored as a CSS gradient on the chip swatch) rather than
reaching for a sixth colour.
- Counterfactual: the plan already computes what each part keeps — draw kept
and trimmed as a stacked pair inside one band, or emit the plan as JSON for
the assembler to execute, if the card is meant to drive the trim rather than
explain it.Concepts
- The reserve comes out of the window, not on top of it — a 200,000-token model that also has to answer is not a 200,000-token budget. Hold 16,000 back for the reply and the prompt is really competing for 184,000, so the shipped example's 196,140 tokens are 12,140 over while still looking like a comfortable 98% fit. That arithmetic is how a request fails at the provider with nothing on the client having looked wrong.
- Pinning is an absence, not a flag — a part with no
evictionRankis not in the eviction order at all, so no plan can ever reach it. Expressing that as a missing rank rather than apinned: trueboolean means a payload cannot claim that the system prompt is both never evicted and the first thing evicted; as two separate fields they would be free to say opposite things about one part. - Geometry does not decide who pays — the band sitting past the budget line is whatever assembly order happened to put there, which in the shipped example is the conversation history and the turn the user just typed. The eviction order says the tokens come out of Retrieved chunks instead. So the overflow carries two marks bearing the same number: a dashed frame stating the amount, and a cut inside the rank-1 part stating where it is taken from.
- Before and after, not before and a promise — the thin second bar is the same prompt with the plan applied, and it lands exactly on the budget edge with the reply's reserve untouched. A card that only says "trim 12,140 from Retrieved chunks" is asking to be believed; one that redraws the bar is showing its work.
- Largest remainder, so every filter state totals 100 — shares take their floor first and the leftover points go to the largest fractional parts. Rounding each share on its own is how a five-part split prints 101 in one state and 99 in the next, which on a budget card reads as a data error, because it would be one. A part measured at zero has no fraction to claim a point and survives as a real zero.
- The chip is a what-if, not a paint toggle — switching Retrieved chunks off does not hide a band, it takes that part out of the prompt: the mix re-apportions, the plan re-runs, and the verdict can flip from over to fitting. That is the question an engineer actually has, which is also why the card refuses to let the last part go and says so out loud instead of ignoring the press.
- The counts are yours, and the card says so — a token count only means something next to the tokenizer that produced it, so the model's encoding is printed beside its name. Nothing here tokenises: a count computed in a view would disagree with the one the provider bills against, and a budget card that disagrees with the invoice is worse than no card at all.
Token Latency
A four-state LLM serving-latency scatter: time to first token against time per output token for every request, nearest-rank p50/p95 crosshairs, an inclusive budget crosshair that cuts the plane into four filterable zones, and queue/prefill/decode phase bars for the median and p95 requests.
Cost-Quality Frontier
A four-state price/quality scatter for a model shortlist — Pareto frontier drawn through the non-dominated models, dominated ones greyed and named their beater, and a movable iso-value line that names the best buy at the reader's own quality-per-dollar rate.