import { type JSX, Show, createContext, createMemo, useContext } from 'solid-js'; import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card'; import { cn } from '../utils/cn'; import { Button } from '../ui/button'; const ICON_RADIUS = 10; const ICON_VIEWBOX = 24; const ICON_CENTER = 12; const ICON_STROKE_WIDTH = 2; const PERCENT_MAX = 100; /** Default fraction (0–1) at which the meter transitions to the warning colour. */ export const DEFAULT_WARN_THRESHOLD = 0.7; /** Default fraction (0–1) at which the meter transitions to the danger colour. */ export const DEFAULT_DANGER_THRESHOLD = 0.9; /** * Compute the severity level for a given usage fraction and thresholds. * `pct` is a 0–1 fraction (e.g. `usedTokens / maxTokens`). */ export function computeSeverity( pct: number, warnThreshold = DEFAULT_WARN_THRESHOLD, dangerThreshold = DEFAULT_DANGER_THRESHOLD, ): ContextSeverity { if (pct > dangerThreshold) return 'danger'; if (pct > warnThreshold) return 'warn'; return 'ok'; } export type ContextSeverity = 'ok' | 'warn' | 'danger'; interface ContextSchema { usedTokens: number; maxTokens: number; inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cacheTokens?: number; estimatedCost?: number; /** Fraction (0–1) above which the meter turns yellow. Default 0.7. */ warnThreshold: number; /** Fraction (0–1) above which the meter turns red. Default 0.9. */ dangerThreshold: number; } const ContextCtx = createContext(); function useContextValue(): ContextSchema { const ctx = useContext(ContextCtx); if (!ctx) { throw new Error('Context components must be used within Context'); } return ctx; } const fmtCompact = new Intl.NumberFormat('en-US', { notation: 'compact' }); const fmtPercent = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, style: 'percent' }); const fmtCurrency = new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency' }); // --- Root provider --- export interface ContextProps { usedTokens: number; maxTokens: number; inputTokens?: number; outputTokens?: number; reasoningTokens?: number; cacheTokens?: number; estimatedCost?: number; /** * Fraction (0–1) above which the progress bar turns yellow. * Defaults to `0.7` (70%). */ warnThreshold?: number; /** * Fraction (0–1) above which the progress bar turns red. * Defaults to `0.9` (90%). */ dangerThreshold?: number; children?: JSX.Element; } export function Context(props: ContextProps) { const value = createMemo(() => ({ usedTokens: props.usedTokens, maxTokens: props.maxTokens, inputTokens: props.inputTokens, outputTokens: props.outputTokens, reasoningTokens: props.reasoningTokens, cacheTokens: props.cacheTokens, estimatedCost: props.estimatedCost, warnThreshold: props.warnThreshold ?? DEFAULT_WARN_THRESHOLD, dangerThreshold: props.dangerThreshold ?? DEFAULT_DANGER_THRESHOLD, })); return ( {props.children} ); } // --- Icon (internal) --- function ContextIcon() { const ctx = useContextValue(); const circumference = 2 * Math.PI * ICON_RADIUS; const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens); const dashOffset = createMemo(() => circumference * (1 - usedPercent())); return ( ); } // --- Trigger --- export interface ContextTriggerProps { children?: JSX.Element; class?: string; } export function ContextTrigger(props: ContextTriggerProps) { const ctx = useContextValue(); const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens); const renderedPercent = createMemo(() => fmtPercent.format(usedPercent())); return ( ); } // --- Content --- export interface ContextContentProps { class?: string; children?: JSX.Element; } export function ContextContent(props: ContextContentProps) { return ( {props.children} ); } // --- Content Header --- export interface ContextContentHeaderProps { class?: string; children?: JSX.Element; } export function ContextContentHeader(props: ContextContentHeaderProps) { const ctx = useContextValue(); const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens); const displayPct = createMemo(() => fmtPercent.format(usedPercent())); const used = createMemo(() => fmtCompact.format(ctx.usedTokens)); const total = createMemo(() => fmtCompact.format(ctx.maxTokens)); const barWidth = createMemo(() => `${Math.min(usedPercent() * PERCENT_MAX, PERCENT_MAX)}%`); const severity = createMemo(() => { const pct = usedPercent(); if (pct > ctx.dangerThreshold) return 'danger'; if (pct > ctx.warnThreshold) return 'warn'; return 'ok'; }); const colorClass = createMemo(() => { const s = severity(); if (s === 'danger') return 'bg-red-400'; if (s === 'warn') return 'bg-yellow-400'; return 'bg-primary'; }); return (

{displayPct()}

{used()} / {total()}

); } // --- Content Body --- export interface ContextContentBodyProps { class?: string; children?: JSX.Element; } export function ContextContentBody(props: ContextContentBodyProps) { return (
{props.children}
); } // --- Content Footer --- export interface ContextContentFooterProps { class?: string; children?: JSX.Element; } export function ContextContentFooter(props: ContextContentFooterProps) { const ctx = useContextValue(); const totalCost = createMemo(() => fmtCurrency.format(ctx.estimatedCost ?? 0) ); return (
Total cost {totalCost()}
); } // --- Token row helper --- function TokensDisplay(props: { tokens?: number }) { return ( {props.tokens === undefined ? '\u2014' : fmtCompact.format(props.tokens)} ); } // --- Specialized usage rows --- export interface ContextUsageRowProps { class?: string; children?: JSX.Element; } export function ContextInputUsage(props: ContextUsageRowProps) { const ctx = useContextValue(); return (
Input
); } export function ContextOutputUsage(props: ContextUsageRowProps) { const ctx = useContextValue(); return (
Output
); } export function ContextReasoningUsage(props: ContextUsageRowProps) { const ctx = useContextValue(); return (
Reasoning
); } export function ContextCacheUsage(props: ContextUsageRowProps) { const ctx = useContextValue(); return (
Cache
); }