"use client"; import { FileTextIcon, Grid3x3Icon, LayoutDashboardIcon, ShrinkIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control"; import { cn } from "@/lib/utils"; function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && Boolean(prefers); } export type BudgetPart = "system" | "tools" | "memory" | "files" | "summary" | "chat" | "results" | "free"; export type BudgetView = "cells" | "treemap"; export interface BudgetFile { id: string; name: string; tokens: number; } export interface TokenBudgetProps { model?: string; limit?: number; system?: number; tools?: number; memory?: number; files?: BudgetFile[]; chat?: number; turns?: number; results?: number; keepRecent?: number; summaryRatio?: number; defaultView?: BudgetView; cells?: number; columns?: number; onRemoveFile?: (id: string) => void; onCompact?: (summary: { turns: number; summaryTokens: number; freed: number }) => void; className?: string; } const parts: Record = { system: { label: "System", cell: "bg-stone-500 dark:bg-stone-400", tile: "bg-stone-500/12 border-stone-500/30 text-stone-800 dark:bg-stone-400/14 dark:border-stone-400/30 dark:text-stone-200", dot: "bg-stone-500 dark:bg-stone-400", }, tools: { label: "Tools", cell: "bg-sky-500 dark:bg-sky-400", tile: "bg-sky-500/12 border-sky-500/30 text-sky-800 dark:bg-sky-400/14 dark:border-sky-400/30 dark:text-sky-200", dot: "bg-sky-500 dark:bg-sky-400", }, memory: { label: "Memory", cell: "bg-emerald-500 dark:bg-emerald-400", tile: "bg-emerald-500/12 border-emerald-500/30 text-emerald-800 dark:bg-emerald-400/14 dark:border-emerald-400/30 dark:text-emerald-200", dot: "bg-emerald-500 dark:bg-emerald-400", }, files: { label: "Files", cell: "bg-amber-500 dark:bg-amber-400", tile: "bg-amber-500/14 border-amber-500/35 text-amber-900 dark:bg-amber-400/14 dark:border-amber-400/30 dark:text-amber-200", dot: "bg-amber-500 dark:bg-amber-400", }, summary: { label: "Summary", cell: "bg-lime-500 dark:bg-lime-400", tile: "bg-lime-500/16 border-lime-600/40 text-lime-900 dark:bg-lime-400/16 dark:border-lime-400/35 dark:text-lime-200", dot: "bg-lime-500 dark:bg-lime-400", }, chat: { label: "Chat", cell: "bg-violet-500 dark:bg-violet-400", tile: "bg-violet-500/12 border-violet-500/30 text-violet-800 dark:bg-violet-400/14 dark:border-violet-400/30 dark:text-violet-200", dot: "bg-violet-500 dark:bg-violet-400", }, results: { label: "Tool results", cell: "bg-rose-500 dark:bg-rose-400", tile: "bg-rose-500/12 border-rose-500/30 text-rose-800 dark:bg-rose-400/14 dark:border-rose-400/30 dark:text-rose-200", dot: "bg-rose-500 dark:bg-rose-400", }, free: { label: "Free", cell: "bg-foreground/[0.07]", tile: "border-dashed border-foreground/20 text-muted-foreground", dot: "bg-foreground/15", }, }; interface Segment { id: string; part: BudgetPart; label: string; tokens: number; } interface Rect { x: number; y: number; w: number; h: number; } function orderedTreemap(items: T[], r: Rect, aspect: number, out: Map) { if (!items.length) return; if (items.length === 1) { out.set(items[0], r); return; } const total = items.reduce((s, i) => s + i.tokens, 0) || 1; let acc = 0; let k = 1; let best = Number.POSITIVE_INFINITY; for (let i = 0; i < items.length - 1; i++) { acc += items[i].tokens; const d = Math.abs(acc - total / 2); if (d < best) { best = d; k = i + 1; } } const a = items.slice(0, k); const ratio = a.reduce((s, i) => s + i.tokens, 0) / total; if (r.w * aspect >= r.h) { orderedTreemap(a, { ...r, w: r.w * ratio }, aspect, out); orderedTreemap(items.slice(k), { ...r, x: r.x + r.w * ratio, w: r.w * (1 - ratio) }, aspect, out); } else { orderedTreemap(a, { ...r, h: r.h * ratio }, aspect, out); orderedTreemap(items.slice(k), { ...r, y: r.y + r.h * ratio, h: r.h * (1 - ratio) }, aspect, out); } } const k = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 100_000 ? 0 : 1).replace(/\.0$/, "")}k` : `${Math.round(n)}`); const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; function Roll({ text, className }: { text: string; className?: string }) { const reduce = useReducedMotionAfterMount(); let d = 0; let o = 0; const glyphs = [...text] .reverse() .map((c) => ({ c, digit: c >= "0" && c <= "9", key: c >= "0" && c <= "9" ? `d${d++}` : `s${o++}` })) .reverse(); return ( {text} {glyphs.map(({ c, digit, key }) => digit ? ( 0 {DIGITS.map((n) => ( {n} ))} ) : ( {c === " " ? "\u00a0" : c} ), )} ); } type Hover = { part: BudgetPart; id?: string } | null; export function TokenBudget({ model = "orbit-2", limit = 200_000, system = 4_200, tools = 14_800, memory = 3_100, files: initialFiles = [], chat: initialChat = 0, turns: initialTurns = 0, results = 0, keepRecent = 0.2, summaryRatio = 0.06, defaultView = "cells", cells = 200, columns = 20, onRemoveFile, onCompact, className, }: TokenBudgetProps) { const reduce = useReducedMotionAfterMount(); const [view, setView] = useState(defaultView); const [files, setFiles] = useState(initialFiles); const [chat, setChat] = useState(initialChat); const [turns, setTurns] = useState(initialTurns); const [summary, setSummary] = useState(0); const [folded, setFolded] = useState(0); const [compacting, setCompacting] = useState(false); const [hover, setHover] = useState(null); const [size, setSize] = useState({ w: 400, h: 200 }); const box = useRef(null); const timer = useRef | undefined>(undefined); useEffect(() => () => clearTimeout(timer.current), []); useLayoutEffect(() => { const el = box.current; if (!el) return; const ro = new ResizeObserver(() => setSize({ w: el.clientWidth, h: el.clientHeight })); ro.observe(el); setSize({ w: el.clientWidth, h: el.clientHeight }); return () => ro.disconnect(); }, []); const segments = ( [ { id: "system", part: "system", label: "System", tokens: system }, { id: "tools", part: "tools", label: "Tools", tokens: tools }, { id: "memory", part: "memory", label: "Memory", tokens: memory }, ...files.map((f) => ({ id: f.id, part: "files" as const, label: f.name.split("/").pop() ?? f.name, tokens: f.tokens })), ...(summary > 0 ? [{ id: "summary", part: "summary" as const, label: "Summary", tokens: summary }] : []), { id: "chat", part: "chat", label: "Chat", tokens: chat }, { id: "results", part: "results", label: "Tool results", tokens: results }, ] as Segment[] ).filter((s) => s.tokens > 0); const used = segments.reduce((s, x) => s + x.tokens, 0); const free = Math.max(limit - used, 0); const pct = Math.min(used / limit, 1); const perCell = limit / cells; const totals: Record = { system, tools, memory, files: files.reduce((s, f) => s + f.tokens, 0), summary, chat, results, free, }; const owners: (Segment | null)[] = []; let si = 0; let end = segments[0]?.tokens ?? 0; for (let i = 0; i < cells; i++) { const mid = (i + 0.5) * perCell; while (si < segments.length && mid > end) { si++; end += segments[si]?.tokens ?? 0; } owners.push(si < segments.length ? segments[si] : null); } const tiles = new Map(); const all: Segment[] = [...segments, ...(free > 0 ? [{ id: "free", part: "free" as const, label: "Free", tokens: free }] : [])]; orderedTreemap(all, { x: 0, y: 0, w: 1, h: 1 }, size.w / Math.max(size.h, 1), tiles); const lit = (seg: Segment | null) => { if (!hover) return true; const part = seg?.part ?? "free"; if (hover.id) return seg?.id === hover.id; return part === hover.part; }; const removeFile = (id: string) => { setFiles((fs) => fs.filter((f) => f.id !== id)); setHover(null); onRemoveFile?.(id); }; const keptTurns = Math.max(1, Math.round(turns * keepRecent)); const foldTurns = turns - keptTurns; const foldTokens = Math.round(chat * (1 - keepRecent)); const summaryTokens = Math.round(foldTokens * summaryRatio); const canCompact = foldTurns >= 2 && !compacting; const compact = () => { if (!canCompact) return; const apply = () => { setChat((c) => c - foldTokens); setSummary((s) => s + summaryTokens); setTurns(keptTurns); setFolded((f) => f + foldTurns); setCompacting(false); onCompact?.({ turns: foldTurns, summaryTokens, freed: foldTokens - summaryTokens }); }; if (reduce) return apply(); setCompacting(true); setHover(null); timer.current = setTimeout(apply, 900); }; const focus = hover ? hover.id ? (() => { const f = files.find((x) => x.id === hover.id); return f ? { part: "files" as const, label: f.name, tokens: f.tokens } : null; })() : { part: hover.part, label: parts[hover.part].label, tokens: totals[hover.part] } : null; const legend: BudgetPart[] = ["system", "tools", "memory", "files", ...(summary > 0 ? (["summary"] as const) : []), "chat", "results", "free"]; const tone = pct > 0.85 ? "text-destructive-foreground" : pct > 0.7 ? "text-warning-foreground" : "text-foreground"; return (
Context budget {model}
{( [ ["cells", "Cells", Grid3x3Icon], ["treemap", "Treemap", LayoutDashboardIcon], ] as const ).map(([id, label, Icon]) => ( setView(id)} > {label} ))}
of {k(limit)} tokens {focus ? ( {focus.label} {k(focus.tokens)} · {Math.round((focus.tokens / limit) * 100)}% ) : ( 0.7 ? tone : "text-muted-foreground")} text={`${Math.round(pct * 100)}% full`} /> )}
setHover(null)} ref={box}>
{owners.map((seg, i) => { const row = Math.floor(i / columns); const col = i % columns; const squeeze = compacting && seg?.part === "chat"; const on = lit(seg); const alt = seg?.part === "files" && files.findIndex((f) => f.id === seg.id) % 2 === 1; return ( setHover(seg ? { part: seg.part, id: seg.part === "files" ? seg.id : undefined } : { part: "free" })} style={{ transition: reduce ? undefined : `background-color 320ms ease ${(row + col) * 8}ms` }} transition={ squeeze ? { duration: 0.5, delay: (col % 20) * 0.015, ease: [0.23, 1, 0.32, 1] } : reduce ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 30 } } /> ); })}
{all.map((seg) => { const r = tiles.get(seg); if (!r) return null; const wPx = r.w * size.w; const hPx = r.h * size.h; const on = lit(seg.part === "free" ? null : seg); const squeeze = compacting && seg.part === "chat"; return ( setHover({ part: seg.part, id: seg.part === "files" ? seg.id : undefined })} transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 300, damping: 32 }} > {wPx > 56 && hPx > 26 && {seg.label}} {wPx > 40 && hPx > 46 && {k(seg.tokens)}} ); })}
    {legend.map((part) => (
  • ))}
{files.length > 0 && (
    {files.map((file) => (
    setHover({ part: "files", id: file.id })} onPointerLeave={() => setHover(null)} > {file.name} {k(file.tokens)}
    ))}
)}
{compacting ? `Folding ${foldTurns} older turns into a summary…` : canCompact ? `${foldTurns} older turns could fold into ~${k(summaryTokens)}, freeing ${k(foldTokens - summaryTokens)}` : folded > 0 ? `${folded} turns live in a ${k(summary)} summary; the last ${turns} stay word for word` : "Nothing to compact yet"}
); }