import React, { useMemo, useState } from 'react'; import { Activity } from 'lucide-react'; import { cn } from '@/shared/utils/cn'; export interface ContextStats { total_tokens_in: number; total_tokens_out: number; total_messages: number; text_messages: number; tool_calls: number; thinking_steps: number; last_prompt_tokens: number; model_used: string | null; context_window: number | null; context_usage_percent: number | null; } interface ContextIndicatorProps { stats: ContextStats | null; className?: string; } /** Format token count to compact form: 1234 → "1.2K", 123456 → "123K" */ function formatTokens(n: number): string { if (n === 0) return '0'; if (n < 1000) return String(n); if (n < 10000) return (n / 1000).toFixed(1) + 'K'; if (n < 1000000) return Math.round(n / 1000) + 'K'; return (n / 1000000).toFixed(1) + 'M'; } /** Get color class based on usage percentage */ function getUsageColor(percent: number | null): { bar: string; text: string; bg: string } { if (percent === null || percent === 0) return { bar: 'bg-gray-500', text: 'text-gray-400', bg: 'bg-gray-500/10' }; if (percent < 50) return { bar: 'bg-emerald-500', text: 'text-emerald-400', bg: 'bg-emerald-500/10' }; if (percent < 75) return { bar: 'bg-yellow-500', text: 'text-yellow-400', bg: 'bg-yellow-500/10' }; if (percent < 90) return { bar: 'bg-orange-500', text: 'text-orange-400', bg: 'bg-orange-500/10' }; return { bar: 'bg-red-500', text: 'text-red-400', bg: 'bg-red-500/10' }; } /** Format model name to short form */ function formatModel(model: string | null): string { if (!model) return '—'; // Remove date suffixes like "-20241022" const cleaned = model.replace(/-\d{8}$/, ''); // Short aliases const shorts: Record = { 'claude-3-5-sonnet': 'Sonnet 3.5', 'claude-3.5-sonnet': 'Sonnet 3.5', 'claude-sonnet-4': 'Sonnet 4', 'claude-4-sonnet': 'Sonnet 4', 'claude-opus-4': 'Opus 4', 'claude-4-opus': 'Opus 4', 'claude-3-opus': 'Opus 3', 'claude-3-haiku': 'Haiku 3', 'claude-3.5-haiku': 'Haiku 3.5', 'gpt-4o': 'GPT-4o', 'gpt-4o-mini': 'GPT-4o mini', 'gpt-4-turbo': 'GPT-4 Turbo', 'o1': 'o1', 'o3': 'o3', 'o3-mini': 'o3-mini', 'o4-mini': 'o4-mini', }; const lower = cleaned.toLowerCase(); return shorts[lower] || cleaned; } export const ContextIndicator: React.FC = ({ stats, className }) => { const [showTooltip, setShowTooltip] = useState(false); const { percent, colors, tokenText, totalTokens } = useMemo(() => { if (!stats) return { percent: null, colors: getUsageColor(null), tokenText: '', totalTokens: 0 }; const p = stats.context_usage_percent; const c = getUsageColor(p); const total = stats.total_tokens_in + stats.total_tokens_out; let text = ''; if (stats.last_prompt_tokens > 0 && stats.context_window) { text = `${formatTokens(stats.last_prompt_tokens)} / ${formatTokens(stats.context_window)}`; } else if (total > 0) { text = `${formatTokens(total)} tokens`; } return { percent: p, colors: c, tokenText: text, totalTokens: total }; }, [stats]); if (!stats || stats.total_messages === 0) return null; return (
setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} > {/* Compact badge */}
{/* Mini progress bar */}
{/* Percentage or token text */} {percent !== null ? ( {percent}% ) : tokenText ? ( {tokenText} ) : ( {stats.total_messages} msg )}
{/* Tooltip */} {showTooltip && (
{/* Arrow */}
{/* Title */}
Context Usage
{/* Progress bar (full) */} {percent !== null && (
{formatTokens(stats.last_prompt_tokens)} prompt {percent}%
{formatTokens(stats.context_window || 0)} window
)} {/* Stats grid */}
Tokens in
{formatTokens(stats.total_tokens_in)}
Tokens out
{formatTokens(stats.total_tokens_out)}
Total tokens
{formatTokens(totalTokens)}
Messages
{stats.text_messages}
Tool calls
{stats.tool_calls}
Thinking
{stats.thinking_steps}
{stats.model_used && ( <>
Model
{formatModel(stats.model_used)}
)}
{/* Cost estimate */} {totalTokens > 0 && (
~${((stats.total_tokens_in * 0.003 + stats.total_tokens_out * 0.015) / 1000).toFixed(3)} estimated cost
)}
)}
); };