'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 = { sambanova: 'โšก', gemini: 'โœจ', openai: '๐Ÿค–', groq: '๐Ÿฆ™', cerebras: '๐Ÿง ', openrouter: '๐Ÿ”€', anthropic: '๐Ÿช„', } const PROVIDER_COLORS: Record = { 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>({}) const [pools, setPools] = useState>({}) const [loading, setLoading] = useState(true) const [expandedProvider, setExpandedProvider] = useState(null) const [lastRefresh, setLastRefresh] = useState(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 (
{/* Header */}
{locale === 'my' ? 'AI Router v8' : 'AI Router v8'} 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
{/* Summary */}
Total calls: {totalCalls}
Providers: {activeCount}/{sortedProviders.length}
{lastRefresh && (
{lastRefresh.toLocaleTimeString()}
)}
{/* Provider List */}
{loading && sortedProviders.length === 0 ? (
{[0, 1, 2].map(i => (
))}
) : sortedProviders.length === 0 ? (
No providers configured
) : (
{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 (
setExpandedProvider(isExpanded ? null : name)}> {/* Provider Header */}
{icon}
{name} {stat.priority <= 2 && ( PRIMARY )}
{stat.calls} calls ยท {stat.avg_latency_ms}ms avg {pool && ( ยท {pool.available_keys}/{pool.total_keys} keys )}
{/* Success Rate Bar */} {stat.calls > 0 && (
80 ? '#4ade80' : successRate > 50 ? '#fbbf24' : '#f87171', }} />
)} {stat.available ? ( ) : ( )}
{/* Expanded Key Pool */} {isExpanded && pool && pool.keys.length > 0 && (
KEY POOL ({pool.available_keys}/{pool.total_keys} available)
{pool.keys.map((k, i) => (
{k.key_preview} {k.calls} calls {k.failures > 0 && ( {k.failures} fails )} {!k.available && k.cooldown_remaining_s > 0 && ( {Math.round(k.cooldown_remaining_s)}s )}
))}
)}
) })}
)}
{/* Footer Info */}
Priority: SambaNova โ†’ Gemini โ†’ OpenAI โ†’ Groq โ†’ Cerebras โ†’ Anthropic
) }