Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartContextWindowData,
  ChartContextWindowSegment,
} from "./chart-context-window.contract"

export interface ChartContextWindowProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartContextWindowData {
  /**

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-context-window.json

Prompt

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 evictionRank is not in the eviction order at all, so no plan can ever reach it. Expressing that as a missing rank rather than a pinned: true boolean 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.

On This Page