SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.9 KB · 88 lines tsx
Raw Blame History
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