/ web / src / components / ModelInfoCard.tsx
ModelInfoCard.tsx
  1  import { useEffect, useRef, useState } from "react";
  2  import { Brain, Eye, Gauge, Lightbulb, Wrench } from "lucide-react";
  3  import { Spinner } from "@nous-research/ui/ui/components/spinner";
  4  import { api } from "@/lib/api";
  5  import type { ModelInfoResponse } from "@/lib/api";
  6  import { formatTokenCount } from "@/lib/format";
  7  
  8  interface ModelInfoCardProps {
  9    /** Current model string from config state — used to detect changes */
 10    currentModel: string;
 11    /** Bumped after config saves to trigger re-fetch */
 12    refreshKey?: number;
 13  }
 14  
 15  export function ModelInfoCard({
 16    currentModel,
 17    refreshKey = 0,
 18  }: ModelInfoCardProps) {
 19    const [info, setInfo] = useState<ModelInfoResponse | null>(null);
 20    const [loading, setLoading] = useState(false);
 21    const lastFetchKeyRef = useRef("");
 22  
 23    useEffect(() => {
 24      if (!currentModel) return;
 25      // Re-fetch when model changes OR when refreshKey bumps (after save)
 26      const fetchKey = `${currentModel}:${refreshKey}`;
 27      if (fetchKey === lastFetchKeyRef.current) return;
 28      lastFetchKeyRef.current = fetchKey;
 29      setLoading(true);
 30      api
 31        .getModelInfo()
 32        .then(setInfo)
 33        .catch(() => setInfo(null))
 34        .finally(() => setLoading(false));
 35    }, [currentModel, refreshKey]);
 36  
 37    if (loading) {
 38      return (
 39        <div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
 40          <Spinner className="text-xs" />
 41          Loading model info…
 42        </div>
 43      );
 44    }
 45  
 46    if (!info || !info.model || info.effective_context_length <= 0) return null;
 47  
 48    const caps = info.capabilities;
 49    const hasCaps = caps && Object.keys(caps).length > 0;
 50  
 51    return (
 52      <div className="border border-border/60 bg-muted/30 px-3 py-2.5 space-y-2">
 53        <div className="flex items-center gap-4 text-xs">
 54          <div className="flex items-center gap-1.5 text-muted-foreground">
 55            <Gauge className="h-3.5 w-3.5" />
 56            <span className="font-medium">Context Window</span>
 57          </div>
 58          <div className="flex items-center gap-2">
 59            <span className="font-mono font-semibold text-foreground">
 60              {formatTokenCount(info.effective_context_length)}
 61            </span>
 62            {info.config_context_length > 0 ? (
 63              <span className="text-amber-500/80 text-[10px]">
 64                (override — auto: {formatTokenCount(info.auto_context_length)})
 65              </span>
 66            ) : (
 67              <span className="text-muted-foreground/60 text-[10px]">
 68                auto-detected
 69              </span>
 70            )}
 71          </div>
 72        </div>
 73  
 74        {hasCaps && caps.max_output_tokens && caps.max_output_tokens > 0 && (
 75          <div className="flex items-center gap-4 text-xs">
 76            <div className="flex items-center gap-1.5 text-muted-foreground">
 77              <Lightbulb className="h-3.5 w-3.5" />
 78              <span className="font-medium">Max Output</span>
 79            </div>
 80            <span className="font-mono font-semibold text-foreground">
 81              {formatTokenCount(caps.max_output_tokens)}
 82            </span>
 83          </div>
 84        )}
 85  
 86        {hasCaps && (
 87          <div className="flex flex-wrap items-center gap-1.5 pt-0.5">
 88            {caps.supports_tools && (
 89              <span className="inline-flex items-center gap-1 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-600 dark:text-emerald-400">
 90                <Wrench className="h-2.5 w-2.5" /> Tools
 91              </span>
 92            )}
 93            {caps.supports_vision && (
 94              <span className="inline-flex items-center gap-1 bg-blue-500/10 px-2 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
 95                <Eye className="h-2.5 w-2.5" /> Vision
 96              </span>
 97            )}
 98            {caps.supports_reasoning && (
 99              <span className="inline-flex items-center gap-1 bg-purple-500/10 px-2 py-0.5 text-[10px] font-medium text-purple-600 dark:text-purple-400">
100                <Brain className="h-2.5 w-2.5" /> Reasoning
101              </span>
102            )}
103            {caps.model_family && (
104              <span className="inline-flex items-center gap-1 bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
105                {caps.model_family}
106              </span>
107            )}
108          </div>
109        )}
110      </div>
111    );
112  }