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 }