Adds a floating popover above the chat input showing current context-window usage. Modeled on Paseo's tracker. - New hook useChatContextStats(chatId, messages) finds the latest assistant message in the chat with both ctx_used and ctx_max set, computes percent, and returns null when data unavailable. - New component ChatContextPopover renders a small card with the "Context window" label, big percent, and "used / max tokens" subline. Hidden when stats is null. - Color thresholds: <60% muted, 60-85 amber, >85 destructive. - Not a portal — absolutely positioned inside a new relative wrapper around ChatInput in ChatPane.tsx, so it's pane-local (multi-pane safe). - Live updates via the existing messages-array dependency. - No API / schema / WS changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import type { ChatContextStats } from '@/hooks/useChatContextStats';
|
||
|
||
interface Props {
|
||
stats: ChatContextStats | null;
|
||
}
|
||
|
||
/**
|
||
* Formats a token count into a compact k/m-suffix string.
|
||
* - < 1_000 → raw integer (e.g. "42")
|
||
* - 1_000–999_999 → "Nk" or "N.Nk" (e.g. "30k", "12.5k", "100k")
|
||
* - >= 1_000_000 → "Nm" or "N.Nm" (e.g. "1m", "1.5m", "100m")
|
||
*
|
||
* Drops a trailing ".0" so we get "30k" instead of "30.0k".
|
||
*/
|
||
function formatTokens(n: number): string {
|
||
if (n < 1000) return String(n);
|
||
if (n < 1_000_000) {
|
||
const k = n / 1000;
|
||
return k >= 100 ? `${Math.round(k)}k` : `${k.toFixed(1).replace(/\.0$/, '')}k`;
|
||
}
|
||
const m = n / 1_000_000;
|
||
return m >= 100 ? `${Math.round(m)}m` : `${m.toFixed(1).replace(/\.0$/, '')}m`;
|
||
}
|
||
|
||
/**
|
||
* Color thresholds:
|
||
* - > 85% → text-destructive
|
||
* - >= 60% → text-amber-500
|
||
* - else → text-muted-foreground
|
||
* (85% itself falls into the amber band.)
|
||
*/
|
||
function percentColorClass(percent: number): string {
|
||
if (percent > 85) return 'text-destructive';
|
||
if (percent >= 60) return 'text-amber-500';
|
||
return 'text-muted-foreground';
|
||
}
|
||
|
||
export function ChatContextPopover({ stats }: Props) {
|
||
if (!stats) return null;
|
||
return (
|
||
<div className="absolute bottom-full right-4 mb-4 z-20 pointer-events-none">
|
||
<div className="rounded-md border border-border bg-card text-card-foreground shadow-sm px-3 py-2 text-xs min-w-[140px]">
|
||
<div className="text-muted-foreground/80 text-[10px] uppercase tracking-wide mb-0.5">
|
||
Context window
|
||
</div>
|
||
<div className={`text-base font-medium ${percentColorClass(stats.percent)}`}>
|
||
{stats.percent}% used
|
||
</div>
|
||
<div className="text-muted-foreground text-[10px] font-mono">
|
||
{formatTokens(stats.used)} / {formatTokens(stats.max)} tokens
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|