TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { AlertTriangle, ArrowUpRight, FileText, Loader2, MessageSquarePlus } from "lucide-react";4import type { PolyModel } from "@/lib/client/types";5import type { ContextEstimate, CostEstimate } from "@/lib/client/tokens";6import { formatEstimate } from "@/lib/client/tokens";7import { Button } from "@/components/ui/button";8import { Tooltip } from "@/components/ui/tooltip";9import { cn, formatTokens } from "@/lib/utils";1011interface Props {12 estimate: ContextEstimate;13 cost: CostEstimate;14 model?: PolyModel;15 showCost?: boolean;16 onNewChat: () => void;17 onSummarize?: () => void;18 summarizing?: boolean;19 largerModel?: PolyModel | null;20 onSwitchLarger?: (key: string) => void;21 className?: string;22}2324function fmtK(n: number): string {25 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;26 if (n >= 1000) return `${Math.round(n / 1000)}K`;27 return String(n);28}2930/**31 * Compact "43K / 200K" context meter + cost estimate. Turns into an action banner at 80 % (warn),32 * 95 % (critical) and when the estimate exceeds the window.33 */34export function ContextIndicator({ estimate, cost, showCost = true, onNewChat, onSummarize, summarizing, largerModel, onSwitchLarger, className }: Props) {35 const ratio = estimate.ratio ?? 0;36 const level = estimate.level;37 const bar = level === "over" || level === "critical" ? "bg-danger" : level === "warn" ? "bg-warning" : "bg-accent";38 const pct = Math.min(100, Math.round(ratio * 100));39 const label = estimate.contextTokens ? `${fmtK(estimate.total)} / ${fmtK(estimate.contextTokens)}` : `~${fmtK(estimate.total)} tokens`;40 const costLabel = showCost ? formatEstimate(estimate.total, cost.usd) : `~${fmtK(estimate.total)} tokens`;4142 return (43 <div className={cn("space-y-1.5", className)}>44 <div className="flex min-w-0 items-center gap-2 text-[11px] tabular-nums text-fg-subtle">45 <Tooltip content={estimate.contextTokens ? `Estimated context: ${formatTokens(estimate.history)} history + ${formatTokens(estimate.draft)} draft of ${formatTokens(estimate.contextTokens)} (${pct}%)` : "Estimated tokens in this request (no known context limit)"}>46 <span className="inline-flex shrink-0 items-center gap-1.5">47 <span className={cn(level !== "ok" && (level === "warn" ? "text-warning" : "text-danger"))}>{label}</span>48 {estimate.contextTokens ? (49 <span className="relative h-1 w-14 overflow-hidden rounded-full bg-border sm:w-20" aria-hidden>50 <span className={cn("absolute inset-y-0 left-0 rounded-full transition-[width] duration-300", bar)} style={{ width: `${Math.max(2, pct)}%` }} />51 </span>52 ) : null}53 </span>54 </Tooltip>55 <span className="hidden text-border-strong sm:inline">·</span>56 <Tooltip content={cost.usd === null ? "No public pricing for this model" : `Input ≈ $${(cost.inputUsd ?? 0).toFixed(4)} · output (~${cost.outputTokens} tokens) ≈ $${(cost.outputUsd ?? 0).toFixed(4)} — provider list prices`}>57 <span className="hidden truncate sm:inline">{costLabel}</span>58 </Tooltip>59 {cost.usd !== null && showCost ? <span className="truncate sm:hidden">≈ {cost.usd < 0.01 ? `$${cost.usd.toFixed(4)}` : `$${cost.usd.toFixed(2)}`}</span> : null}60 </div>6162 {level !== "ok" ? (63 <div className={cn("flex flex-wrap items-center gap-x-3 gap-y-2 rounded-xl px-3 py-2 text-[12.5px]", level === "warn" ? "bg-warning-soft text-warning" : "bg-danger-soft text-danger")} role="status">64 <span className="inline-flex min-w-0 flex-1 items-center gap-1.5">65 <AlertTriangle className="size-3.5 shrink-0" />66 <span className="truncate">{level === "over" ? "This request exceeds the model's context window." : level === "critical" ? `Context almost full (${pct}%). Answers may be cut off.` : `Context ${pct}% full — consider trimming.`}</span>67 </span>68 <div className="flex flex-wrap items-center gap-1.5">69 <Button size="xs" variant="outline" onClick={onNewChat} className="bg-bg-elevated">70 <MessageSquarePlus /> New chat71 </Button>72 {onSummarize ? (73 <Button size="xs" variant="outline" onClick={onSummarize} disabled={summarizing} className="bg-bg-elevated">74 {summarizing ? <Loader2 className="animate-spin" /> : <FileText />} {summarizing ? "Summarizing…" : "Summarize context"}75 </Button>76 ) : null}77 {largerModel && onSwitchLarger ? (78 <Button size="xs" variant="outline" onClick={() => onSwitchLarger(largerModel.key)} className="bg-bg-elevated">79 <ArrowUpRight /> Switch to {largerModel.displayName} ({fmtK(largerModel.limits?.contextTokens ?? 0)})80 </Button>81 ) : null}82 </div>83 </div>84 ) : null}85 </div>86 );87}88