Spaces:
Configuration error
Configuration error
| 'use client' | |
| import { useEffect, useState } from 'react' | |
| import { useAgentStore } from '@/hooks/useAgentStore' | |
| import { Cpu, Key, RefreshCw, CheckCircle2, XCircle, AlertCircle, Zap, Activity } from 'lucide-react' | |
| const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:7860' | |
| interface ProviderStat { | |
| calls: number | |
| errors: number | |
| avg_latency_ms: number | |
| available: boolean | |
| key_count: number | |
| available_keys: number | |
| priority: number | |
| } | |
| interface KeyInfo { | |
| key_preview: string | |
| available: boolean | |
| failures: number | |
| calls: number | |
| cooldown_remaining_s: number | |
| } | |
| interface PoolStatus { | |
| provider: string | |
| total_keys: number | |
| available_keys: number | |
| keys: KeyInfo[] | |
| } | |
| const PROVIDER_ICONS: Record<string, string> = { | |
| sambanova: '⚡', | |
| gemini: '✨', | |
| openai: '🤖', | |
| groq: '🦙', | |
| cerebras: '🧠', | |
| openrouter: '🔀', | |
| anthropic: '🪄', | |
| } | |
| const PROVIDER_COLORS: Record<string, string> = { | |
| sambanova: '#f97316', | |
| gemini: '#4285f4', | |
| openai: '#10a37f', | |
| groq: '#f59e0b', | |
| cerebras: '#8b5cf6', | |
| openrouter: '#6366f1', | |
| anthropic: '#d97706', | |
| } | |
| export default function AIRouterPanel() { | |
| const { locale } = useAgentStore() | |
| const [stats, setStats] = useState<Record<string, ProviderStat>>({}) | |
| const [pools, setPools] = useState<Record<string, PoolStatus>>({}) | |
| const [loading, setLoading] = useState(true) | |
| const [expandedProvider, setExpandedProvider] = useState<string | null>(null) | |
| const [lastRefresh, setLastRefresh] = useState<Date | null>(null) | |
| const load = async () => { | |
| setLoading(true) | |
| try { | |
| const [statsRes, poolRes] = await Promise.all([ | |
| fetch(`${API_URL}/api/v1/ai/stats`).then(r => r.json()).catch(() => ({})), | |
| fetch(`${API_URL}/api/v1/ai/pool-status`).then(r => r.json()).catch(() => ({})), | |
| ]) | |
| setStats(statsRes.stats || {}) | |
| setPools(poolRes.pools || {}) | |
| setLastRefresh(new Date()) | |
| } catch {} | |
| setLoading(false) | |
| } | |
| useEffect(() => { | |
| load() | |
| const interval = setInterval(load, 30000) | |
| return () => clearInterval(interval) | |
| }, []) | |
| const sortedProviders = Object.entries(stats).sort( | |
| ([, a], [, b]) => (a.priority || 99) - (b.priority || 99) | |
| ) | |
| const activeCount = sortedProviders.filter(([, s]) => s.available).length | |
| const totalCalls = sortedProviders.reduce((sum, [, s]) => sum + s.calls, 0) | |
| return ( | |
| <div className="flex flex-col h-full" style={{ background: 'var(--bg-2)' }}> | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-4 py-2.5 border-b shrink-0" | |
| style={{ borderColor: 'var(--border)', background: 'var(--bg-3)' }}> | |
| <div className="flex items-center gap-2"> | |
| <Cpu size={14} className="text-indigo-400" /> | |
| <span className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| {locale === 'my' ? 'AI Router v8' : 'AI Router v8'} | |
| </span> | |
| <span className="text-[10px] px-1.5 py-0.5 rounded-full" | |
| style={{ | |
| background: activeCount > 0 ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)', | |
| color: activeCount > 0 ? '#4ade80' : '#f87171', | |
| border: `1px solid ${activeCount > 0 ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`, | |
| }}> | |
| {activeCount} active | |
| </span> | |
| </div> | |
| <button onClick={load} disabled={loading} | |
| className="p-1.5 rounded-lg hover:bg-white/5 transition-colors" title="Refresh"> | |
| <RefreshCw size={12} style={{ color: 'var(--text-muted)' }} | |
| className={loading ? 'animate-spin' : ''} /> | |
| </button> | |
| </div> | |
| {/* Summary */} | |
| <div className="px-3 py-2 border-b flex gap-3 text-xs" | |
| style={{ borderColor: 'var(--border)', background: 'rgba(99,102,241,0.05)' }}> | |
| <div className="flex items-center gap-1"> | |
| <Activity size={10} className="text-indigo-400" /> | |
| <span style={{ color: 'var(--text-muted)' }}>Total calls:</span> | |
| <span className="font-mono font-semibold" style={{ color: 'var(--text-primary)' }}>{totalCalls}</span> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <Key size={10} className="text-indigo-400" /> | |
| <span style={{ color: 'var(--text-muted)' }}>Providers:</span> | |
| <span className="font-mono font-semibold" style={{ color: 'var(--text-primary)' }}> | |
| {activeCount}/{sortedProviders.length} | |
| </span> | |
| </div> | |
| {lastRefresh && ( | |
| <div className="ml-auto text-[10px]" style={{ color: 'var(--text-muted)' }}> | |
| {lastRefresh.toLocaleTimeString()} | |
| </div> | |
| )} | |
| </div> | |
| {/* Provider List */} | |
| <div className="flex-1 overflow-y-auto"> | |
| {loading && sortedProviders.length === 0 ? ( | |
| <div className="flex items-center justify-center h-24"> | |
| <div className="flex gap-1"> | |
| {[0, 1, 2].map(i => ( | |
| <div key={i} className="w-1.5 h-1.5 rounded-full animate-bounce" | |
| style={{ background: 'var(--brand)', animationDelay: `${i * 0.15}s` }} /> | |
| ))} | |
| </div> | |
| </div> | |
| ) : sortedProviders.length === 0 ? ( | |
| <div className="p-4 text-center text-xs" style={{ color: 'var(--text-muted)' }}> | |
| No providers configured | |
| </div> | |
| ) : ( | |
| <div className="p-2 flex flex-col gap-1.5"> | |
| {sortedProviders.map(([name, stat]) => { | |
| const pool = pools[name] | |
| const icon = PROVIDER_ICONS[name] || '🔌' | |
| const color = PROVIDER_COLORS[name] || '#6366f1' | |
| const isExpanded = expandedProvider === name | |
| const successRate = stat.calls > 0 | |
| ? Math.round(((stat.calls - stat.errors) / stat.calls) * 100) | |
| : 100 | |
| return ( | |
| <div key={name} | |
| className="rounded-xl border overflow-hidden cursor-pointer hover:border-opacity-60 transition-all" | |
| style={{ | |
| borderColor: stat.available ? `${color}40` : 'var(--border)', | |
| background: stat.available ? `${color}08` : 'var(--bg-3)', | |
| }} | |
| onClick={() => setExpandedProvider(isExpanded ? null : name)}> | |
| {/* Provider Header */} | |
| <div className="flex items-center gap-2 px-3 py-2"> | |
| <span className="text-sm">{icon}</span> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs font-semibold capitalize" style={{ color: 'var(--text-primary)' }}> | |
| {name} | |
| </span> | |
| {stat.priority <= 2 && ( | |
| <span className="text-[9px] px-1 py-0.5 rounded font-bold" | |
| style={{ background: `${color}25`, color }}> | |
| PRIMARY | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2 mt-0.5"> | |
| <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}> | |
| {stat.calls} calls · {stat.avg_latency_ms}ms avg | |
| </span> | |
| {pool && ( | |
| <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}> | |
| · {pool.available_keys}/{pool.total_keys} keys | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-1.5"> | |
| {/* Success Rate Bar */} | |
| {stat.calls > 0 && ( | |
| <div className="w-12 h-1.5 rounded-full overflow-hidden" | |
| style={{ background: 'var(--bg-0)' }}> | |
| <div className="h-full rounded-full transition-all" | |
| style={{ | |
| width: `${successRate}%`, | |
| background: successRate > 80 ? '#4ade80' : successRate > 50 ? '#fbbf24' : '#f87171', | |
| }} /> | |
| </div> | |
| )} | |
| {stat.available ? ( | |
| <CheckCircle2 size={12} style={{ color: '#4ade80' }} /> | |
| ) : ( | |
| <XCircle size={12} style={{ color: '#f87171' }} /> | |
| )} | |
| </div> | |
| </div> | |
| {/* Expanded Key Pool */} | |
| {isExpanded && pool && pool.keys.length > 0 && ( | |
| <div className="px-3 pb-3 border-t" style={{ borderColor: 'var(--border)' }}> | |
| <div className="pt-2 text-[10px] font-semibold mb-2" style={{ color: 'var(--text-muted)' }}> | |
| KEY POOL ({pool.available_keys}/{pool.total_keys} available) | |
| </div> | |
| <div className="flex flex-col gap-1"> | |
| {pool.keys.map((k, i) => ( | |
| <div key={i} className="flex items-center gap-2 px-2 py-1 rounded-lg" | |
| style={{ background: k.available ? 'rgba(34,197,94,0.05)' : 'rgba(239,68,68,0.05)' }}> | |
| <Key size={9} style={{ color: k.available ? '#4ade80' : '#f87171' }} /> | |
| <span className="font-mono text-[10px] flex-1" style={{ color: 'var(--text-secondary)' }}> | |
| {k.key_preview} | |
| </span> | |
| <span className="text-[9px]" style={{ color: 'var(--text-muted)' }}> | |
| {k.calls} calls | |
| </span> | |
| {k.failures > 0 && ( | |
| <span className="text-[9px]" style={{ color: '#fbbf24' }}> | |
| {k.failures} fails | |
| </span> | |
| )} | |
| {!k.available && k.cooldown_remaining_s > 0 && ( | |
| <span className="text-[9px] px-1 rounded" | |
| style={{ background: 'rgba(239,68,68,0.15)', color: '#f87171' }}> | |
| {Math.round(k.cooldown_remaining_s)}s | |
| </span> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| {/* Footer Info */} | |
| <div className="px-3 py-2 border-t text-[10px]" | |
| style={{ borderColor: 'var(--border)', color: 'var(--text-muted)', background: 'var(--bg-3)' }}> | |
| <div className="flex items-center gap-1"> | |
| <Zap size={9} className="text-indigo-400" /> | |
| <span>Priority: SambaNova → Gemini → OpenAI → Groq → Cerebras → Anthropic</span> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |