"use client"; import { useMemo, useState } from "react"; import { Message, TokenUsage, RateLimitInfo } from "@/types/chat"; // ── Groq production models (primary + fallback chain) ─────────────────────── const GROQ_MODEL_CHAIN = [ { id: "llama-3.1-8b-instant", label: "Llama 3.1 · 8B", note: "Primary — fastest inference" }, { id: "llama-3.3-70b-versatile", label: "Llama 3.3 · 70B", note: "Fallback 1 — most capable" }, { id: "openai/gpt-oss-20b", label: "GPT OSS · 20B", note: "Fallback 2 — 1,000 tok/s" }, { id: "openai/gpt-oss-120b", label: "GPT OSS · 120B", note: "Fallback 3 — strongest on Groq" }, { id: "qwen/qwen3-32b", label: "Qwen3 · 32B", note: "Fallback 4 — preview" }, ]; // ── OpenRouter free models (ordered best → smallest) ──────────────────────── const OPENROUTER_MODELS = [ { id: "openai/gpt-oss-120b:free", label: "GPT OSS 120B", provider: "OpenAI" }, { id: "nvidia/nemotron-3-ultra-550b-a55b:free", label: "Nemotron Ultra 550B", provider: "NVIDIA" }, { id: "nousresearch/hermes-3-llama-3.1-405b:free", label: "Hermes 3 · 405B", provider: "Nous" }, { id: "nvidia/nemotron-3-super-120b-a12b:free", label: "Nemotron Super 120B", provider: "NVIDIA" }, { id: "meta-llama/llama-3.3-70b-instruct:free", label: "Llama 3.3 · 70B", provider: "Meta" }, { id: "qwen/qwen3-next-80b-a3b-instruct:free", label: "Qwen3 · 80B", provider: "Qwen" }, { id: "qwen/qwen3-coder:free", label: "Qwen3 Coder", provider: "Qwen" }, { id: "moonshotai/kimi-k2.6:free", label: "Kimi K2.6", provider: "Moonshot" }, { id: "google/gemma-4-31b-it:free", label: "Gemma 4 · 31B", provider: "Google" }, { id: "google/gemma-4-26b-a4b-it:free", label: "Gemma 4 · 26B MoE", provider: "Google" }, { id: "nvidia/nemotron-3-nano-30b-a3b:free", label: "Nemotron Nano 30B", provider: "NVIDIA" }, { id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", label: "Nemotron Nano 30B-R", provider: "NVIDIA" }, { id: "openai/gpt-oss-20b:free", label: "GPT OSS 20B", provider: "OpenAI" }, { id: "cognitivecomputations/dolphin-mistral-24b-venice-edition:free", label: "Dolphin Mistral 24B", provider: "CogComp" }, { id: "nvidia/nemotron-nano-9b-v2:free", label: "Nemotron Nano 9B", provider: "NVIDIA" }, { id: "meta-llama/llama-3.2-3b-instruct:free", label: "Llama 3.2 · 3B", provider: "Meta" }, { id: "poolside/laguna-m.1:free", label: "Laguna M.1", provider: "Poolside" }, { id: "poolside/laguna-xs.2:free", label: "Laguna XS.2", provider: "Poolside" }, { id: "nex-agi/nex-n2-pro:free", label: "NEX N2 Pro", provider: "NexAGI" }, ]; const ALL_MODEL_LABELS: Record = { ...Object.fromEntries(GROQ_MODEL_CHAIN.map((m) => [m.id, m.label])), ...Object.fromEntries(OPENROUTER_MODELS.map((m) => [m.id, m.label])), }; function shortLabel(model: string) { return ALL_MODEL_LABELS[model] ?? model.split("/").pop()?.replace(/:free$/, "") ?? model; } function isOpenRouterModel(id: string) { return OPENROUTER_MODELS.some((m) => m.id === id); } /** Parse Groq reset strings like "58.3s", "3m45.2s", "1h0m0s" → "58s", "3m 45s", "1h" */ function parseReset(reset: string | null | undefined): string { if (!reset || reset === "0s") return "now"; const m = reset.match(/(?:(\d+)h)?(?:(\d+)m)?(?:([\d.]+)s)?/); if (!m) return reset; const h = m[1] ? parseInt(m[1]) : 0; const min = m[2] ? parseInt(m[2]) : 0; const s = m[3] ? Math.ceil(parseFloat(m[3])) : 0; if (h > 0) return min > 0 ? `${h}h ${min}m` : `${h}h`; if (min > 0) return s > 0 ? `${min}m ${s}s` : `${min}m`; return s > 0 ? `${s}s` : "now"; } function pct(remaining: number | null, limit: number | null): number { if (!limit || remaining === null) return 0; return Math.max(0, Math.min(100, Math.round((remaining / limit) * 100))); } function barColor(remainingPct: number): string { if (remainingPct > 50) return "from-[#8FD15E] to-[#47C3A6]"; if (remainingPct > 20) return "from-[#F59E0B] to-[#FBBF24]"; return "from-[#EF4444] to-[#F87171]"; } interface Props { isOpen: boolean; onClose: () => void; messages: Message[]; preferredModel: string; setPreferredModel: (model: string) => void; } export default function SettingsPanel({ isOpen, onClose, messages, preferredModel, setPreferredModel, }: Props) { const [showOpenRouter, setShowOpenRouter] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const assistantMessages = useMemo( () => messages.filter((m) => m.role === "assistant" && m.tokenUsage), [messages] ); const sessionTotals = useMemo(() => assistantMessages.reduce( (acc, m) => ({ prompt_tokens: acc.prompt_tokens + (m.tokenUsage?.prompt_tokens ?? 0), completion_tokens: acc.completion_tokens + (m.tokenUsage?.completion_tokens ?? 0), total_tokens: acc.total_tokens + (m.tokenUsage?.total_tokens ?? 0), }), { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } ), [assistantMessages] ); const latestRateLimit = useMemo(() => { for (let i = assistantMessages.length - 1; i >= 0; i--) { if (assistantMessages[i].rateLimit) return assistantMessages[i].rateLimit!; } return undefined; }, [assistantMessages]); const latestTimestamp = useMemo(() => { if (!assistantMessages.length) return null; return assistantMessages[assistantMessages.length - 1].timestamp; }, [assistantMessages]); const inputRatio = sessionTotals.total_tokens > 0 ? Math.round((sessionTotals.prompt_tokens / sessionTotals.total_tokens) * 100) : 0; const usedModels = useMemo(() => { const set = new Set(assistantMessages.map((m) => m.modelUsed).filter(Boolean)); return Array.from(set) as string[]; }, [assistantMessages]); const reqPct = pct(latestRateLimit?.remaining_requests ?? null, latestRateLimit?.limit_requests ?? null); const tokPct = pct(latestRateLimit?.remaining_tokens ?? null, latestRateLimit?.limit_tokens ?? null); const isORActive = isOpenRouterModel(preferredModel); return ( <> {/* Backdrop */}
{/* Drawer */} ); } // ── Sub-components ─────────────────────────────────────── function RateLimitRow({ label, remaining, limit, reset, pct: pctValue, formatValue = (n) => n.toLocaleString(), }: { label: string; remaining: number | null | undefined; limit: number | null | undefined; reset: string | null | undefined; pct: number; formatValue?: (n: number) => string; }) { const color = barColor(pctValue); const hasData = remaining !== null && remaining !== undefined && limit; return (
{label}
{hasData && ( {formatValue(remaining!)} / {formatValue(limit!)} )} {reset && ( ↺ {parseReset(reset)} )}
{hasData && (
)}
{hasData && (
{pctValue}% remaining resets in {parseReset(reset)}
)}
); } function SectionLabel({ children }: { children: React.ReactNode }) { return (
{children}
); } function StatTile({ label, value, color, bold }: { label: string; value: number; color: string; bold?: boolean }) { return (
{value.toLocaleString()} {label}
); } function Chip({ children, color }: { children: React.ReactNode; color: "green" | "amber" }) { const cls = color === "green" ? "text-[#059669] bg-[#D1FAE5]" : "text-amber-600 bg-amber-50 border border-amber-200"; return ( {children} ); } function SettingsIcon() { return ( ); }