Skip to main content
Glama
page.tsxโ€ข73.8 kB
/** * Copyright (C) 2023 the project owner * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ "use client" import { useEffect, useState, useCallback } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent } from "@/components/ui/chart" import { XAxis, YAxis, Cell, CartesianGrid, LineChart, Line, ResponsiveContainer, Tooltip, BarChart, Bar } from "recharts" interface ModelStats { calls: number tokens: number } interface UsageStats { quick: ModelStats coder: ModelStats moe: ModelStats thinking: ModelStats } interface RecentCall { timestamp: string model: string task_type: string language: string tokens: number elapsed_ms: number preview: string thinking: boolean backend_type?: string // "local" or "remote" backend?: string // "ollama" or "llamacpp" (only if not default) } interface ResponseTime { ts: string ms: number } interface EnhancedStats { task_stats: Record<string, number> recent_calls: RecentCall[] response_times: { quick: ResponseTime[] coder: ResponseTime[] moe: ResponseTime[] thinking: ResponseTime[] } } interface User { id: string email: string display_name?: string is_superuser: boolean } interface BackendModels { quick: string coder: string moe: string thinking: string } interface BackendHealth { available: boolean response_time_ms: number last_error: string loaded_models: string[] circuit_open: boolean consecutive_failures?: number } interface BackendStatus { id: string name: string provider: "ollama" | "llamacpp" | "vllm" | "openai" | "gemini" | "custom" type: "local" | "remote" url: string enabled: boolean priority: number models: BackendModels health: BackendHealth api_key?: string } interface BackendsResponse { backends: BackendStatus[] routing: { prefer_local: boolean fallback_enabled: boolean load_balance: boolean } activeBackend: string summary: { total: number enabled: number available: number local: number remote: number } timestamp: string } interface CircuitBreakerStatus { available: boolean consecutive_failures: number last_error: string circuit_open: boolean seconds_until_available: number safe_context_kb: number } interface CircuitBreakerState { ollama: CircuitBreakerStatus llamacpp: CircuitBreakerStatus active_backend: string timestamp: string } const PROVIDER_COLORS: Record<string, string> = { ollama: "#689B8A", llamacpp: "#4A7D6D", vllm: "#8BB5A6", openai: "#1E3A32", gemini: "#FF6B7A", custom: "#6B8F85", } const TYPE_COLORS: Record<string, string> = { local: "#689B8A", remote: "#FF6B7A", } const modelsConfig = { quick: { label: "Quick", color: "#8BB5A6" }, coder: { label: "Coder", color: "#689B8A" }, moe: { label: "MoE", color: "#FF6B7A" }, } satisfies ChartConfig const COLORS = { quick: "#8BB5A6", coder: "#689B8A", moe: "#FF6B7A", thinking: "#E85566", } const TASK_COLORS: Record<string, string> = { review: "#8BB5A6", analyze: "#689B8A", generate: "#4A7D6D", summarize: "#A8D4C4", critique: "#FF6B7A", quick: "#8BB5A6", plan: "#FFB3BA", think: "#E85566", ml_pipeline: "#689B8A", feature_pipeline: "#4A7D6D", other: "#6B8F85", } function WatermelonSeed({ className = "", style = {} }: { className?: string; style?: React.CSSProperties }) { return ( <svg viewBox="0 0 10 16" className={className} style={style} fill="currentColor" > <ellipse cx="5" cy="8" rx="4" ry="7" /> </svg> ) } function ScatteredSeeds({ count = 5, className = "" }: { count?: number; className?: string }) { const seededRandom = (seed: number, offset: number) => { const x = Math.sin(seed * 9999 + offset * 7777) * 10000 return x - Math.floor(x) } const seeds = Array.from({ length: count }, (_, i) => ({ id: i, left: `${10 + (i * 18) + seededRandom(i, 1) * 12}%`, top: `${25 + seededRandom(i, 2) * 50}%`, rotation: -25 + seededRandom(i, 3) * 50, scale: 0.65 + seededRandom(i, 4) * 0.35, opacity: 0.08 + seededRandom(i, 5) * 0.06, })) return ( <div className={`absolute inset-0 overflow-hidden pointer-events-none ${className}`}> {seeds.map((seed) => ( <WatermelonSeed key={seed.id} className="absolute w-3 h-5 text-foreground" style={{ left: seed.left, top: seed.top, transform: `rotate(${seed.rotation}deg) scale(${seed.scale})`, opacity: seed.opacity, }} /> ))} </div> ) } const normalizeModel = (model: string): "quick" | "coder" | "moe" | "thinking" => { const m = model.toLowerCase() if (m === "14b" || m === "quick") return "quick" if (m === "30b" || m === "coder") return "coder" if (m === "moe") return "moe" if (m === "thinking" || m.includes("think") || m.includes("reason")) return "thinking" return "quick" // default } const getModelColor = (model: string): string => { const tier = normalizeModel(model) return COLORS[tier] } export default function Dashboard() { const [stats, setStats] = useState<UsageStats | null>(null) const [enhanced, setEnhanced] = useState<EnhancedStats | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) const [theme, setTheme] = useState<"light" | "dark">("dark") const [menuOpen, setMenuOpen] = useState(false) const [activeTab, setActiveTab] = useState<"overview" | "logs">("overview") const [ollamaLogs, setOllamaLogs] = useState<string[]>([]) const [ollamaStatus, setOllamaStatus] = useState<{ model: string; size: number } | null>(null) const [isStreaming, setIsStreaming] = useState(false) const [lastUpdated, setLastUpdated] = useState<Date>(new Date()) const [backendsResponse, setBackendsResponse] = useState<BackendsResponse | null>(null) const [activeBackend, setActiveBackend] = useState<string>("") const [secondsAgo, setSecondsAgo] = useState(0) const [expandedSections, setExpandedSections] = useState({ usage: true, models: true, costs: false, }) const [showAddBackend, setShowAddBackend] = useState(false) const [editingBackend, setEditingBackend] = useState<BackendStatus | null>(null) const [circuitBreaker, setCircuitBreaker] = useState<CircuitBreakerState | null>(null) const [user, setUser] = useState<User | null>(null) const [selectedLogProvider, setSelectedLogProvider] = useState<string>("all") useEffect(() => { const timer = setInterval(() => { setSecondsAgo(Math.floor((Date.now() - lastUpdated.getTime()) / 1000)) }, 1000) return () => clearInterval(timer) }, [lastUpdated]) useEffect(() => { const stored = localStorage.getItem("delia-theme") as "light" | "dark" | null if (stored) { setTheme(stored) } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { setTheme("dark") } }, []) useEffect(() => { document.documentElement.classList.toggle("dark", theme === "dark") localStorage.setItem("delia-theme", theme) }, [theme]) const toggleTheme = () => { setTheme(prev => prev === "dark" ? "light" : "dark") } const toggleSection = (section: keyof typeof expandedSections) => { setExpandedSections(prev => ({ ...prev, [section]: !prev[section] })) } const fetchStats = useCallback(async () => { try { const res = await fetch("/api/stats") if (!res.ok) throw new Error("Failed to fetch stats") const data = await res.json() setStats(data.stats) setEnhanced(data.enhanced) setError(null) setLastUpdated(new Date()) } catch (err) { setError(err instanceof Error ? err.message : "Unknown error") } finally { setLoading(false) } }, []) const fetchOllamaStatus = useCallback(async () => { try { const res = await fetch("/api/ollama/stream?type=ps") if (res.ok) { const data = await res.json() if (data.models && data.models.length > 0) { const model = data.models[0] setOllamaStatus({ model: model.name, size: model.size }) } else { setOllamaStatus(null) } } } catch { setOllamaStatus(null) } }, []) const fetchBackends = useCallback(async () => { try { const res = await fetch("/api/backends") if (res.ok) { const data = await res.json() as BackendsResponse if (data.backends) { setBackendsResponse(data) setActiveBackend(data.activeBackend || "") } } } catch { } }, []) const fetchCircuitBreaker = useCallback(async () => { try { const res = await fetch("/api/circuit-breaker") if (res.ok) { const data = await res.json() if (data.success) { setCircuitBreaker(data) } } } catch { } }, []) const fetchUser = useCallback(async () => { try { const res = await fetch("/api/auth/me") if (res.ok) { const data = await res.json() setUser(data) } else { setUser(null) } } catch { setUser(null) } }, []) const fetchLogs = useCallback(async () => { try { const params = new URLSearchParams() if (selectedLogProvider === "all") { params.set("all", "true") } else { params.set("backend_id", selectedLogProvider) } const res = await fetch(`/api/logs?${params}`) if (res.ok) { const data = await res.json() if (data.logs && data.logs.length > 0) { const formattedLogs = data.logs.map((log: { type: string message: string provider?: string backend_id?: string backend?: string garden_msg?: string model?: string tokens?: number }) => { const displayMsg = log.garden_msg || log.message const backend = log.backend || log.backend_id || log.provider || "" const tokenInfo = log.tokens ? ` (${log.tokens} tokens)` : "" const modelInfo = log.model ? ` [${log.model}]` : "" if (backend) { return `[${backend}]${modelInfo} ${displayMsg}${tokenInfo}` } return `${displayMsg}${modelInfo}${tokenInfo}` }) setOllamaLogs(formattedLogs) setIsStreaming(data.logs.some((log: { type: string }) => log.type === "STREAM")) } else { setOllamaLogs([]) setIsStreaming(false) } } } catch { } }, [selectedLogProvider]) useEffect(() => { fetchStats() fetchOllamaStatus() fetchBackends() fetchCircuitBreaker() fetchUser() const statsInterval = setInterval(() => { fetchStats() fetchOllamaStatus() fetchCircuitBreaker() }, 5000) const backendsInterval = setInterval(() => { fetchBackends() }, 3000) return () => { clearInterval(statsInterval) clearInterval(backendsInterval) } }, [fetchStats, fetchOllamaStatus, fetchBackends, fetchCircuitBreaker, fetchUser]) useEffect(() => { if (activeTab === "logs") { fetchLogs() const interval = setInterval(fetchLogs, 1000) return () => clearInterval(interval) } }, [activeTab, fetchLogs]) if (loading) { return ( <div className="min-h-screen bg-background flex items-center justify-center relative"> <ScatteredSeeds count={8} /> <div className="flex flex-col items-center gap-4 z-10"> <div className="relative"> <div className="w-12 h-12 rounded-full border-4 border-primary border-t-accent animate-spin" /> <div className="absolute inset-2 rounded-full bg-accent/20" /> </div> <div className="text-muted-foreground">Loading dashboard...</div> <div className="flex gap-1 mt-2"> <WatermelonSeed className="w-2 h-3 text-foreground/20 animate-bounce" style={{ animationDelay: "0ms" }} /> <WatermelonSeed className="w-2 h-3 text-foreground/20 animate-bounce" style={{ animationDelay: "150ms" }} /> <WatermelonSeed className="w-2 h-3 text-foreground/20 animate-bounce" style={{ animationDelay: "300ms" }} /> </div> </div> </div> ) } if (error) { return ( <div className="min-h-screen bg-background flex items-center justify-center relative"> <ScatteredSeeds count={6} /> <Card className="max-w-md relative z-10"> <CardHeader> <CardTitle className="text-destructive flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 rotate-45" /> Connection Error </CardTitle> <CardDescription>{error}</CardDescription> </CardHeader> <CardContent> <p className="text-sm text-muted-foreground"> Make sure the Delia server is running and the .usage_stats.json file exists. </p> <button onClick={fetchStats} className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors" > Retry </button> </CardContent> </Card> </div> ) } const totalCalls = (stats?.quick?.calls || 0) + (stats?.coder?.calls || 0) + (stats?.moe?.calls || 0) + (stats?.thinking?.calls || 0) const totalTokens = (stats?.quick?.tokens || 0) + (stats?.coder?.tokens || 0) + (stats?.moe?.tokens || 0) + (stats?.thinking?.tokens || 0) const efficiencyQuick = totalCalls > 0 ? ((stats?.quick?.calls || 0) / totalCalls * 100) : 0 const usageBarData = [ { name: "Quick", calls: stats?.quick?.calls || 0, tokens: stats?.quick?.tokens || 0, color: COLORS.quick, desc: "Fast Q&A" }, { name: "Coder", calls: stats?.coder?.calls || 0, tokens: stats?.coder?.tokens || 0, color: COLORS.coder, desc: "Code tasks" }, { name: "MoE", calls: stats?.moe?.calls || 0, tokens: stats?.moe?.tokens || 0, color: COLORS.moe, desc: "Complex reasoning" }, ] const backendStats = (enhanced?.recent_calls || []).reduce((acc, call) => { const type = (call.backend_type || "local") as "local" | "remote" acc[type].calls += 1 acc[type].tokens += call.tokens || 0 acc[type].totalMs += call.elapsed_ms || 0 return acc }, { local: { calls: 0, tokens: 0, totalMs: 0 }, remote: { calls: 0, tokens: 0, totalMs: 0 } }) const localAvgMs = backendStats.local.calls > 0 ? Math.round(backendStats.local.totalMs / backendStats.local.calls) : 0 const remoteAvgMs = backendStats.remote.calls > 0 ? Math.round(backendStats.remote.totalMs / backendStats.remote.calls) : 0 const avgResponseTime = enhanced?.recent_calls?.length ? Math.round(enhanced.recent_calls.reduce((sum, c) => sum + c.elapsed_ms, 0) / enhanced.recent_calls.length) : 0 return ( <div className="min-h-screen bg-background text-foreground"> <header className="sticky top-0 z-50 border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60"> <div className="container mx-auto px-4 h-14 flex items-center justify-between"> <div className="flex items-center gap-3"> <div className="flex items-center gap-2"> {/* eslint-disable-next-line @next/next/no-img-element */} <img src="/logo.svg" alt="Delia" className="w-8 h-8" /> <span className="font-semibold text-lg hidden sm:inline">Delia</span> <span className="text-muted-foreground text-sm hidden md:inline">โ€” Local LLM Delegation</span> </div> </div> <div className="flex items-center gap-2"> <div className="flex items-center gap-3 mr-2"> <div className="flex items-center gap-2 px-2 py-1 rounded-md bg-muted/50"> {backendsResponse?.backends?.slice(0, 4).map((backend) => ( <div key={backend.id} className="flex items-center gap-1" title={`${backend.name}: ${backend.health.available ? 'online' : 'offline'}${backend.health.loaded_models?.length ? ` (${backend.health.loaded_models.length} models)` : ''}`} > <div className={`w-2 h-2 rounded-full ${ backend.health.available ? activeBackend === backend.id ? 'animate-pulse' : '' : 'opacity-30' }`} style={{ backgroundColor: PROVIDER_COLORS[backend.provider] || PROVIDER_COLORS.custom }} /> <span className={`text-xs ${activeBackend === backend.id ? 'font-medium text-foreground' : 'text-muted-foreground'}`}> {backend.type === 'local' ? 'L' : 'R'} </span> </div> ))} {backendsResponse?.summary && backendsResponse.summary.total > 4 && ( <span className="text-xs text-muted-foreground">+{backendsResponse.summary.total - 4}</span> )} </div> <span className="text-xs text-muted-foreground/60 hidden lg:inline"> ยท {secondsAgo}s ago </span> </div> <div className="hidden md:flex gap-1.5"> <Badge variant="outline" className="text-xs" style={{ backgroundColor: '#8BB5A620', color: '#8BB5A6', borderColor: '#8BB5A650' }}> Quick: {stats?.quick?.calls || 0} </Badge> <Badge variant="outline" className="text-xs" style={{ backgroundColor: '#689B8A20', color: '#689B8A', borderColor: '#689B8A50' }}> Coder: {stats?.coder?.calls || 0} </Badge> <Badge variant="outline" className="text-xs" style={{ backgroundColor: '#FF6B7A20', color: '#FF6B7A', borderColor: '#FF6B7A50' }}> MoE: {stats?.moe?.calls || 0} </Badge> <Badge variant="outline" className="text-xs" style={{ backgroundColor: efficiencyQuick >= 70 ? '#689B8A20' : efficiencyQuick >= 40 ? '#8BB5A620' : '#FF6B7A20', color: efficiencyQuick >= 70 ? '#689B8A' : efficiencyQuick >= 40 ? '#8BB5A6' : '#FF6B7A', borderColor: efficiencyQuick >= 70 ? '#689B8A50' : efficiencyQuick >= 40 ? '#8BB5A650' : '#FF6B7A50' }} > {efficiencyQuick.toFixed(0)}% quick </Badge> </div> <button onClick={toggleTheme} className="p-2 rounded-md hover:bg-accent transition-colors" title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`} > {theme === "dark" ? ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> </svg> ) : ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> </svg> )} </button> <div className="relative"> <button onClick={() => setMenuOpen(!menuOpen)} className="p-2 rounded-md hover:bg-accent transition-colors" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> </svg> </button> {menuOpen && ( <div className="absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-card border border-border"> <div className="py-1"> <button onClick={() => { setActiveTab("overview"); setMenuOpen(false) }} className={`block w-full text-left px-4 py-2 text-sm hover:bg-accent ${activeTab === "overview" ? "text-primary font-medium" : ""}`} > ๐Ÿ“Š Overview </button> <button onClick={() => { setActiveTab("logs"); setMenuOpen(false) }} className={`block w-full text-left px-4 py-2 text-sm hover:bg-accent ${activeTab === "logs" ? "text-primary font-medium" : ""}`} > ๐Ÿ–ฅ๏ธ Live Ollama </button> <hr className="my-1 border-border" /> <button onClick={fetchStats} className="block w-full text-left px-4 py-2 text-sm hover:bg-accent" > ๐Ÿ”„ Refresh </button> </div> </div> )} </div> </div> </div> </header> <main className="container mx-auto px-4 py-6"> {activeTab === "overview" && ( <div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6"> <div className="space-y-4"> <Card> <CardContent className="py-3"> <div className="flex flex-wrap items-center gap-x-6 gap-y-2"> <div className="flex items-center gap-3"> <div className="flex items-center gap-1.5"> <WatermelonSeed className="w-3 h-4 text-primary" /> <span className="text-lg font-bold">{totalCalls.toLocaleString()}</span> <span className="text-sm font-medium text-muted-foreground">calls</span> </div> <div className="w-px h-4 bg-border" /> <div className="flex items-center gap-1.5"> <span className="text-lg font-bold">{totalTokens >= 1000000 ? `${(totalTokens / 1000000).toFixed(1)}M` : totalTokens >= 1000 ? `${(totalTokens / 1000).toFixed(0)}k` : totalTokens}</span> <span className="text-sm font-medium text-muted-foreground">tokens</span> </div> </div> <div className="flex items-center gap-3"> {usageBarData.map((item) => ( <div key={item.name} className="flex items-center gap-1" title={`${item.name}: ${item.tokens.toLocaleString()} tokens`}> <WatermelonSeed className="w-2.5 h-4" style={{ color: item.color }} /> <span className="text-xs font-medium text-muted-foreground">{item.tokens >= 1000 ? `${(item.tokens/1000).toFixed(0)}k` : item.tokens || '-'}</span> </div> ))} </div> <div className="flex items-center gap-3"> <span className="text-xs font-medium flex items-center gap-1" title={`Local: ${backendStats.local.tokens.toLocaleString()} tokens`}> <span style={{ color: '#689B8A' }}>๐ŸŒฑ</span> <span className="text-muted-foreground">{backendStats.local.tokens >= 1000 ? `${(backendStats.local.tokens/1000).toFixed(0)}k` : backendStats.local.tokens}</span> </span> <span className="text-xs font-medium flex items-center gap-1" title={`Remote: ${backendStats.remote.tokens.toLocaleString()} tokens`}> <span style={{ color: '#FF6B7A' }}>โ˜๏ธ</span> <span className="text-muted-foreground">{backendStats.remote.tokens >= 1000 ? `${(backendStats.remote.tokens/1000).toFixed(0)}k` : backendStats.remote.tokens}</span> </span> </div> <div className="flex items-center gap-2 ml-auto"> <span className="text-xs font-medium text-muted-foreground">avg</span> <span className="text-sm font-semibold" style={{ color: '#689B8A' }}> {localAvgMs >= 1000 ? `${(localAvgMs / 1000).toFixed(1)}s` : `${localAvgMs}ms`} </span> <span className="text-xs font-medium text-muted-foreground">/</span> <span className="text-sm font-semibold" style={{ color: '#FF6B7A' }}> {remoteAvgMs >= 1000 ? `${(remoteAvgMs / 1000).toFixed(1)}s` : `${remoteAvgMs}ms`} </span> </div> </div> </CardContent> </Card> <Card className="lg:col-span-2"> <CardHeader className="pb-2"> <CardTitle className="text-lg flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 text-primary" /> Recent Harvests </CardTitle> </CardHeader> <CardContent> {enhanced?.recent_calls && enhanced.recent_calls.length > 0 ? ( <div className="space-y-1.5 max-h-[320px] overflow-y-auto"> {[...enhanced.recent_calls].reverse().slice(0, 15).map((call, idx) => { const tier = normalizeModel(call.model) const tierLabels = { quick: "Quick", coder: "Coder", moe: "MoE", thinking: "Thinking" } const backendType = call.backend_type || "local" const isRemote = backendType === "remote" const rowBg = idx % 2 === 0 ? "bg-[#689B8A]/10 dark:bg-[#A8D4C4]/10" : "bg-[#FF6B7A]/8 dark:bg-[#FF8A95]/10" return ( <div key={idx} className={`flex items-start gap-3 py-2 px-3 rounded-md ${rowBg} hover:bg-accent/20 transition-colors`}> <WatermelonSeed className="w-3 h-5 mt-0.5 flex-shrink-0" style={{ color: COLORS[tier] }} /> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2 flex-wrap"> <Badge variant="outline" className="text-xs font-medium px-1.5 py-0.5" style={{ borderColor: COLORS[tier], color: COLORS[tier] }}> {tierLabels[tier]} </Badge> <Badge variant="secondary" className="text-xs font-medium px-1.5 py-0.5" style={{ backgroundColor: `${TASK_COLORS[call.task_type] || TASK_COLORS.other}20`, color: TASK_COLORS[call.task_type] || TASK_COLORS.other }}> {call.task_type} </Badge> <span className="text-xs font-medium px-1.5 rounded" style={{ color: isRemote ? "#FF8A95" : "#689B8A", backgroundColor: isRemote ? "#FF6B7A15" : "#689B8A15" }} > {isRemote ? "โ˜๏ธ" : "๐ŸŒฑ"} </span> <span className="text-xs font-medium text-muted-foreground ml-auto">{call.tokens.toLocaleString()} tokens ยท {(call.elapsed_ms / 1000).toFixed(1)}s</span> </div> <p className="text-sm text-muted-foreground mt-1 truncate">{call.preview}</p> </div> </div> )})} </div> ) : ( <div className="h-[100px] flex flex-col items-center justify-center text-muted-foreground text-sm relative"> <div className="flex gap-2 mb-2"> <WatermelonSeed className="w-3 h-4 rotate-[-20deg] opacity-30" /> <WatermelonSeed className="w-2 h-3 rotate-[15deg] opacity-20" /> <WatermelonSeed className="w-3 h-4 rotate-[30deg] opacity-30" /> </div> No melons harvested yet... </div> )} </CardContent> </Card> {user && ( <Card className="mb-6"> <CardContent className="pt-4"> <div className="flex items-center gap-2"> <span className="text-sm">Logged in as {user.display_name || user.email}</span> {user.is_superuser && <Badge variant="outline">Superuser</Badge>} </div> </CardContent> </Card> )} <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <Card> <CardHeader className="pb-2"> <CardTitle className="text-base">Task Types</CardTitle> </CardHeader> <CardContent> {enhanced?.task_stats && Object.values(enhanced.task_stats).some(v => v > 0) ? ( <div className="h-[180px]"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={Object.entries(enhanced.task_stats) .filter(([, v]) => v > 0) .sort((a, b) => b[1] - a[1]) .map(([name, value]) => ({ name, value, fill: TASK_COLORS[name] || TASK_COLORS.other }))} layout="vertical" margin={{ top: 5, right: 30, bottom: 5, left: 60 }} > <XAxis type="number" tick={{ fontSize: 10 }} /> <YAxis type="category" dataKey="name" tick={{ fontSize: 10 }} width={55} /> <Tooltip formatter={(value: number) => [value, "calls"]} contentStyle={{ fontSize: 12 }} /> <Bar dataKey="value" radius={[0, 4, 4, 0]} label={{ position: 'right', fontSize: 10, fill: '#888' }} > {Object.entries(enhanced.task_stats) .filter(([, v]) => v > 0) .sort((a, b) => b[1] - a[1]) .map(([name], index) => ( <Cell key={`cell-${index}`} fill={TASK_COLORS[name] || TASK_COLORS.other} /> ))} </Bar> </BarChart> </ResponsiveContainer> </div> ) : ( <div className="h-[180px] flex flex-col items-center justify-center text-muted-foreground text-xs"> <div className="flex gap-1.5 mb-2"> <WatermelonSeed className="w-2 h-3 rotate-[-15deg] opacity-25" /> <WatermelonSeed className="w-2.5 h-3.5 rotate-[20deg] opacity-20" /> </div> No task data yet </div> )} </CardContent> </Card> <Card> <CardHeader className="pb-2"> <CardTitle className="text-base">Response Times</CardTitle> </CardHeader> <CardContent> {enhanced?.response_times && (enhanced.response_times.quick?.length > 0 || enhanced.response_times.coder?.length > 0 || enhanced.response_times.moe?.length > 0 || enhanced.response_times.thinking?.length > 0) ? ( <div className="h-[180px]"> <ResponsiveContainer width="100%" height="100%"> <LineChart data={(() => { const quickData = (enhanced.response_times.quick || []).slice(-15); const coderData = (enhanced.response_times.coder || []).slice(-15); const moeData = (enhanced.response_times.moe || []).slice(-15); const thinkingData = (enhanced.response_times.thinking || []).slice(-15); const maxLen = Math.max(quickData.length, coderData.length, moeData.length, thinkingData.length); const result: Array<{ idx: number; quick?: number; coder?: number; moe?: number; thinking?: number }> = []; for (let i = 0; i < maxLen; i++) { result.push({ idx: i, quick: quickData[i]?.ms, coder: coderData[i]?.ms, moe: moeData[i]?.ms, thinking: thinkingData[i]?.ms, }); } return result; })()}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="idx" tick={false} /> <YAxis tickFormatter={(v) => `${(v / 1000).toFixed(0)}s`} width={30} /> <Tooltip formatter={(value: number) => [`${value.toLocaleString()}ms`, ""]} /> <Line type="monotone" dataKey="quick" stroke={COLORS.quick} strokeWidth={2} dot={false} connectNulls /> <Line type="monotone" dataKey="coder" stroke={COLORS.coder} strokeWidth={2} dot={false} connectNulls /> <Line type="monotone" dataKey="moe" stroke={COLORS.moe} strokeWidth={2} dot={false} connectNulls /> <Line type="monotone" dataKey="thinking" stroke={COLORS.thinking} strokeWidth={2} dot={false} connectNulls /> </LineChart> </ResponsiveContainer> </div> ) : ( <div className="h-[180px] flex flex-col items-center justify-center text-muted-foreground text-sm"> <div className="flex gap-1.5 mb-2"> <WatermelonSeed className="w-2 h-3 rotate-[10deg] opacity-25" /> <WatermelonSeed className="w-2.5 h-3.5 rotate-[-25deg] opacity-20" /> </div> No response data yet </div> )} </CardContent> </Card> </div> </div> <div className="space-y-4"> <Card> <CardHeader className="pb-2"> <div className="flex items-center justify-between"> <div> <CardTitle className="text-base flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 rotate-[-15deg]" /> Gardens </CardTitle> <CardDescription className="text-xs"> {backendsResponse?.summary ? `${backendsResponse.summary.available}/${backendsResponse.summary.enabled} gardens thriving` : "Checking the patch..."} </CardDescription> </div> <div className="flex gap-2"> <button onClick={() => setShowAddBackend(true)} className="text-xs px-2 py-1 rounded bg-primary/10 hover:bg-primary/20 text-primary transition-colors flex items-center gap-1" > <WatermelonSeed className="w-2 h-3" /> Add </button> </div> </div> </CardHeader> <CardContent className="space-y-2"> {backendsResponse?.backends?.map((backend) => { const isActive = activeBackend === backend.id const isHealthy = backend.health.available return ( <div key={backend.id} className={`p-2.5 rounded-lg border cursor-pointer transition-colors ${ isHealthy ? isActive ? "border-accent/50 bg-accent/5" : "border-primary/30 bg-primary/5 hover:border-primary/50" : "border-border bg-muted/30" }`} onClick={() => setEditingBackend(backend)} > <div className="flex items-center gap-2"> <WatermelonSeed className={`w-3 h-4 flex-shrink-0 ${isHealthy ? "" : "opacity-40"}`} style={{ color: isActive ? '#FF6B7A' : (PROVIDER_COLORS[backend.provider] || PROVIDER_COLORS.custom) }} /> <span className="font-medium text-sm flex-1 truncate">{backend.name}</span> <span className="text-xs px-1.5 py-0.5 rounded-full" style={{ backgroundColor: isHealthy ? (isActive ? '#FF6B7A20' : '#689B8A20') : 'transparent', color: isHealthy ? (isActive ? '#FF6B7A' : '#689B8A') : 'inherit' }} > {backend.health.circuit_open ? '๐Ÿฅ€ drought' : isActive ? '๐ŸŒฑ active' : isHealthy ? 'โœ“ ready' : 'dormant'} </span> </div> <div className="flex items-center gap-2 mt-1.5 text-xs text-muted-foreground"> <span className="px-1.5 py-0.5 rounded text-[10px] font-medium" style={{ backgroundColor: `${TYPE_COLORS[backend.type]}15`, color: TYPE_COLORS[backend.type] }} > {backend.type} </span> <span className="truncate flex-1">{backend.url}</span> </div> {(backend.health.response_time_ms > 0 || backend.health.loaded_models?.length > 0) && ( <div className="flex items-center gap-3 mt-1 text-[10px] text-muted-foreground"> {backend.health.response_time_ms > 0 && ( <span>{backend.health.response_time_ms}ms</span> )} {backend.health.loaded_models?.length > 0 && ( <span>{backend.health.loaded_models.length} models</span> )} </div> )} </div> ) })} {(!backendsResponse?.backends || backendsResponse.backends.length === 0) && ( <div className="text-center py-6 text-muted-foreground"> <div className="flex justify-center gap-3 mb-3"> <WatermelonSeed className="w-4 h-6 rotate-[-20deg] opacity-20" /> <WatermelonSeed className="w-5 h-7 rotate-[5deg] opacity-30" /> <WatermelonSeed className="w-4 h-6 rotate-[25deg] opacity-20" /> </div> <p className="text-sm font-medium mb-1">No gardens configured</p> <p className="text-xs opacity-70">Add a garden to start growing melons</p> </div> )} </CardContent> </Card> {circuitBreaker && Object.values(circuitBreaker).some((status: CircuitBreakerStatus) => status.circuit_open) && ( <Card> <CardHeader className="pb-2"> <CardTitle className="text-base">Circuit Breaker</CardTitle> <CardDescription className="text-xs"> Backend failure protection and recovery </CardDescription> </CardHeader> <CardContent className="space-y-2"> {Object.entries(circuitBreaker).filter(([key]) => key !== 'active_backend' && key !== 'timestamp').map(([backend, status]: [string, CircuitBreakerStatus]) => ( status.circuit_open && ( <div key={backend} className="p-2.5 rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/20"> <div className="flex items-center justify-between mb-1"> <span className="font-medium text-sm capitalize">{backend}</span> <Badge variant="destructive" className="text-xs"> OPEN </Badge> </div> <div className="text-xs text-muted-foreground space-y-0.5"> <div>Fails: {status.consecutive_failures}</div> {status.last_error && <div>Error: {status.last_error}</div>} {status.seconds_until_available > 0 && ( <div>Recovery: {Math.ceil(status.seconds_until_available)}s</div> )} <div>Safe Context: {status.safe_context_kb}KB</div> </div> </div> ) ))} </CardContent> </Card> )} <Card> <CardHeader className="pb-2"> <CardTitle className="text-base">Routing Strategy</CardTitle> <CardDescription className="text-xs"> How requests are distributed across backends </CardDescription> </CardHeader> <CardContent className="space-y-3 text-sm"> <div className="space-y-2"> <label className="text-sm font-medium">Routing Mode</label> {(() => { const currentRoutingMode = backendsResponse?.routing?.load_balance ? "load_balance" : backendsResponse?.routing?.prefer_local ? "prefer_local" : "prefer_remote" const routingLabels = { prefer_local: "Prioritize Local GPUs", prefer_remote: "Prioritize Remote GPUs", load_balance: "Load Balance" } return ( <Select value={currentRoutingMode} onValueChange={async (value: string) => { const updates = { prefer_local: value === "prefer_local", load_balance: value === "load_balance" } try { const res = await fetch("/api/backends", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates) }) if (res.ok) { setBackendsResponse(prev => prev ? { ...prev, routing: { ...prev.routing, ...updates } } : null) } } catch (e) { console.error(e) } }} > <SelectTrigger className="w-full"> <SelectValue>{routingLabels[currentRoutingMode]}</SelectValue> </SelectTrigger> <SelectContent> <SelectItem value="prefer_local"> <div> <div className="font-medium">Prioritize Local GPUs</div> <div className="text-xs text-muted-foreground">Use local GPU first, remote only if unavailable</div> </div> </SelectItem> <SelectItem value="prefer_remote"> <div> <div className="font-medium">Prioritize Remote GPUs</div> <div className="text-xs text-muted-foreground">Use remote GPU first, local as backup</div> </div> </SelectItem> <SelectItem value="load_balance"> <div> <div className="font-medium">Load Balance</div> <div className="text-xs text-muted-foreground">Distribute across all backends by priority weight</div> </div> </SelectItem> </SelectContent> </Select> ) })()} </div> <div className="pt-2 border-t"> <div className="flex items-center justify-between"> <div> <span className="font-medium">Auto-Fallback</span> <p className="text-xs text-muted-foreground">Try next backend if primary fails</p> </div> <button onClick={async () => { const newValue = !backendsResponse?.routing?.fallback_enabled try { const res = await fetch("/api/backends", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fallback_enabled: newValue }) }) if (res.ok) { setBackendsResponse(prev => prev ? { ...prev, routing: { ...prev.routing, fallback_enabled: newValue } } : null) } } catch (e) { console.error(e) } }} className={`w-10 h-5 rounded-full transition-colors relative ${ backendsResponse?.routing?.fallback_enabled ? "bg-primary" : "bg-muted" }`} > <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all ${ backendsResponse?.routing?.fallback_enabled ? "left-5" : "left-0.5" }`} /> </button> </div> </div> </CardContent> </Card> </div> </div> )} {activeTab === "logs" && ( <div className="space-y-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <Card> <CardHeader className="pb-2"> <CardTitle className="text-base flex items-center gap-2"> <WatermelonSeed className={`w-2.5 h-3.5 rotate-[-10deg] ${ollamaStatus ? '' : 'opacity-40'}`} style={{ color: ollamaStatus ? '#689B8A' : undefined }} /> Active Vine </CardTitle> </CardHeader> <CardContent> {ollamaStatus ? ( <div className="space-y-1"> <div className="font-medium" style={{ color: '#689B8A' }}>{ollamaStatus.model}</div> <div className="text-xs text-muted-foreground"> {(ollamaStatus.size / 1024 / 1024 / 1024).toFixed(1)} GB loaded </div> </div> ) : ( <div className="text-muted-foreground text-sm flex items-center gap-2"> <WatermelonSeed className="w-2 h-3 opacity-30" /> No vine growing </div> )} </CardContent> </Card> <Card> <CardHeader className="pb-2"> <CardTitle className="text-base flex items-center gap-2"> <WatermelonSeed className="w-2.5 h-3.5 rotate-[10deg]" style={{ color: isStreaming ? '#FF6B7A' : '#689B8A' }} /> Stream Status </CardTitle> </CardHeader> <CardContent> <div className="flex items-center gap-2"> <span className={`w-2 h-2 rounded-full ${isStreaming ? 'animate-pulse' : ''}`} style={{ backgroundColor: isStreaming ? '#FF6B7A' : '#6B8F85' }} /> <span className="text-sm">{isStreaming ? 'Streaming...' : 'Idle'}</span> </div> </CardContent> </Card> <Card> <CardHeader className="pb-2"> <CardTitle className="text-base">Log Lines</CardTitle> </CardHeader> <CardContent> <div className="text-2xl font-bold" style={{ color: '#689B8A' }}>{ollamaLogs.length}</div> <button onClick={() => setOllamaLogs([])} className="text-xs text-muted-foreground hover:text-accent transition-colors" > Clear logs </button> </CardContent> </Card> </div> <Card className="mb-4"> <CardHeader className="pb-2"> <CardTitle className="text-base flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 rotate-[-10deg]" /> Select Garden </CardTitle> <CardDescription className="text-xs">Choose which garden&apos;s logs to view</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-2"> <button onClick={() => setSelectedLogProvider("all")} className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1.5 ${ selectedLogProvider === "all" ? "text-white" : "bg-muted hover:bg-muted/80 text-muted-foreground" }`} style={selectedLogProvider === "all" ? { backgroundColor: '#689B8A' } : {}} > <WatermelonSeed className="w-2 h-3" /> All Gardens </button> {backendsResponse?.backends?.map((backend) => ( <button key={backend.id} onClick={() => setSelectedLogProvider(backend.id)} className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors flex items-center gap-1.5 ${ selectedLogProvider === backend.id ? "text-white" : "bg-muted hover:bg-muted/80 text-muted-foreground" }`} style={selectedLogProvider === backend.id ? { backgroundColor: backend.type === "local" ? '#689B8A' : '#FF6B7A' } : {}} > <WatermelonSeed className="w-2 h-3" style={{ color: selectedLogProvider === backend.id ? 'white' : (PROVIDER_COLORS[backend.provider] || '#6B8F85') }} /> {backend.name} </button> ))} </div> </CardContent> </Card> <Card> <CardHeader> <CardTitle className="flex items-center justify-between"> <span className="flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 rotate-[-5deg]" /> Live Output </span> <Badge variant={isStreaming ? "default" : "secondary"} style={isStreaming ? { backgroundColor: '#FF6B7A' } : {}} > {isStreaming ? "๐Ÿ‰ RIPENING" : "๐Ÿ’ค DORMANT"} </Badge> </CardTitle> <CardDescription> Real-time output from your gardens when delegations occur </CardDescription> </CardHeader> <CardContent> <div className="rounded-lg p-4 font-mono text-sm h-[400px] overflow-y-auto" style={{ fontFamily: "'JetBrains Mono', 'Fira Code', monospace", backgroundColor: theme === 'dark' ? '#0D1A15' : '#1E3A32', }} > {ollamaLogs.length > 0 ? ( <div className="space-y-1"> {ollamaLogs.map((log, idx) => ( <div key={idx} className={`${ log.includes('[THINK]') ? 'text-pink-400' : log.includes('[MODEL]') ? 'text-emerald-400' : log.includes('[ERROR]') ? 'text-red-400' : log.includes('[INFO]') ? 'text-teal-300' : log.includes('[STREAM]') ? 'text-amber-300' : 'text-slate-300' }`} > <span className="text-slate-600 mr-2">{String(idx + 1).padStart(3, '0')}</span> {log} </div> ))} <div className="text-emerald-500 animate-pulse">โ–Œ</div> </div> ) : ( <div className="h-full flex items-center justify-center text-slate-500"> <div className="text-center"> <div className="flex justify-center gap-2 mb-3"> <WatermelonSeed className="w-3 h-4 rotate-[-15deg] text-slate-600" /> <WatermelonSeed className="w-2.5 h-3.5 rotate-[20deg] text-slate-700" /> <WatermelonSeed className="w-3 h-4 rotate-[5deg] text-slate-600" /> </div> <p className="text-slate-400">Waiting for vines to grow...</p> <p className="text-xs mt-2 text-slate-500">Logs will appear here when delegations run</p> </div> </div> )} </div> </CardContent> </Card> </div> )} </main> <footer className="border-t bg-card mt-auto relative overflow-hidden"> <div className="container mx-auto px-4 py-3 flex items-center justify-between text-xs text-muted-foreground relative z-10"> <span className="flex items-center gap-2"> <WatermelonSeed className="w-2 h-3 rotate-[-20deg] opacity-40" /> Delia Dashboard </span> <span className="flex items-center gap-2"> {totalCalls} total calls โ€ข {totalTokens.toLocaleString()} tokens <WatermelonSeed className="w-2 h-3 rotate-[15deg] opacity-40" /> </span> </div> </footer> {menuOpen && ( <div className="fixed inset-0 z-40" onClick={() => setMenuOpen(false)} /> )} {(showAddBackend || editingBackend) && ( <BackendModal backend={editingBackend} onClose={() => { setShowAddBackend(false) setEditingBackend(null) }} onSave={async (backendData) => { try { const isEdit = !!editingBackend const res = await fetch("/api/backends", { method: isEdit ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(backendData) }) if (res.ok) { setShowAddBackend(false) setEditingBackend(null) const backendsRes = await fetch("/api/backends") if (backendsRes.ok) { const data = await backendsRes.json() setBackendsResponse(data) } } } catch (err) { console.error("Failed to save backend:", err) } }} onDelete={editingBackend ? async (id) => { try { const res = await fetch(`/api/backends?id=${id}`, { method: "DELETE" }) if (res.ok) { setEditingBackend(null) const backendsRes = await fetch("/api/backends") if (backendsRes.ok) { const data = await backendsRes.json() setBackendsResponse(data) } } } catch (err) { console.error("Failed to delete backend:", err) } } : undefined} /> )} </div> ) } function BackendModal({ backend, onClose, onSave, onDelete }: { backend: BackendStatus | null onClose: () => void onSave: (data: Partial<BackendStatus>) => Promise<void> onDelete?: (id: string) => Promise<void> }) { const [formData, setFormData] = useState({ id: backend?.id || "", name: backend?.name || "", provider: backend?.provider || "ollama" as const, type: backend?.type || "local" as const, url: backend?.url || "http://localhost:11434", enabled: backend?.enabled ?? true, priority: backend?.priority ?? 0, models: backend?.models || { quick: "", coder: "", moe: "", thinking: "" }, api_key: backend?.api_key || "" }) const [saving, setSaving] = useState(false) const [discoveredModels, setDiscoveredModels] = useState<{name: string, sizeGB?: string, description?: string}[]>([]) const [loadingModels, setLoadingModels] = useState(false) const [modelError, setModelError] = useState<string | null>(null) const isGemini = formData.provider === "gemini" const pollModels = useCallback(async (url: string, provider: string, apiKey?: string) => { if (provider !== "gemini" && !url) return setLoadingModels(true) setModelError(null) try { let fetchUrl = `/api/backends/models?provider=${provider}` if (provider !== "gemini") { fetchUrl += `&url=${encodeURIComponent(url)}` } if (apiKey) { fetchUrl += `&apiKey=${encodeURIComponent(apiKey)}` } const res = await fetch(fetchUrl) const data = await res.json() if (data.success && data.models) { setDiscoveredModels(data.models) } else { setModelError(data.error || "Failed to fetch models") setDiscoveredModels([]) } } catch (err) { setModelError(err instanceof Error ? err.message : "Connection failed") setDiscoveredModels([]) } finally { setLoadingModels(false) } }, []) useEffect(() => { const timer = setTimeout(() => { if (formData.provider === "gemini") { pollModels("", formData.provider, formData.api_key) } else if (formData.url) { pollModels(formData.url, formData.provider) } }, 500) return () => clearTimeout(timer) }, [formData.url, formData.provider, formData.api_key, pollModels]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) await onSave(formData) setSaving(false) } return ( <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"> <div className="bg-card border rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto"> <div className="p-4 border-b flex items-center justify-between"> <h2 className="text-lg font-semibold flex items-center gap-2"> <WatermelonSeed className="w-3 h-4 rotate-[-10deg]" /> {backend ? "Edit Garden" : "Add New Garden"} </h2> <button onClick={onClose} className="text-muted-foreground hover:text-foreground"> โœ• </button> </div> <form onSubmit={handleSubmit} className="p-4 space-y-4"> <div> <label className="block text-sm font-medium mb-1">Garden Name</label> <input type="text" value={formData.name} onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value, id: prev.id || e.target.value.toLowerCase().replace(/\s+/g, '-') }))} className="w-full px-3 py-2 border rounded-md bg-background" placeholder="My Local GPU" required /> </div> <div> <label className="block text-sm font-medium mb-1">Provider</label> <select value={formData.provider} onChange={(e) => { const provider = e.target.value as typeof formData.provider let url = formData.url let type = formData.type if (provider === "gemini") { url = "https://generativelanguage.googleapis.com" type = "remote" } else if (provider === "ollama") { url = "http://localhost:11434" type = "local" } else if (provider === "llamacpp") { url = "http://localhost:8080" } else if (provider === "vllm") { url = "http://localhost:8000" } setFormData(prev => ({ ...prev, provider, url, type })) }} className="w-full px-3 py-2 border rounded-md bg-background" > <option value="ollama">Ollama</option> <option value="llamacpp">llama.cpp</option> <option value="vllm">vLLM</option> <option value="openai">OpenAI-compatible</option> <option value="gemini">Google Gemini</option> <option value="custom">Custom</option> </select> </div> {!isGemini && ( <div> <label className="block text-sm font-medium mb-1">Type</label> <div className="flex gap-4"> <label className="flex items-center gap-2"> <input type="radio" name="type" value="local" checked={formData.type === "local"} onChange={() => { setFormData(prev => ({ ...prev, type: "local", provider: "ollama", url: "http://localhost:11434" })) }} /> <span>Local</span> </label> <label className="flex items-center gap-2"> <input type="radio" name="type" value="remote" checked={formData.type === "remote"} onChange={() => { setFormData(prev => ({ ...prev, type: "remote", provider: "llamacpp", url: "http://localhost:8080" })) }} /> <span>Remote</span> </label> </div> </div> )} {isGemini && ( <div> <label className="block text-sm font-medium mb-1"> API Key <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="ml-2 text-xs text-primary hover:underline" > Get API key โ†’ </a> </label> <input type="password" value={formData.api_key} onChange={(e) => setFormData(prev => ({ ...prev, api_key: e.target.value }))} className="w-full px-3 py-2 border rounded-md bg-background font-mono text-sm" placeholder="AIza..." /> <p className="text-xs text-muted-foreground mt-1"> Free tier: 15 requests/minute, 1M tokens/day </p> </div> )} {!isGemini && ( <div> <label className="block text-sm font-medium mb-1">URL</label> <input type="url" value={formData.url} onChange={(e) => setFormData(prev => ({ ...prev, url: e.target.value }))} className="w-full px-3 py-2 border rounded-md bg-background" placeholder="http://localhost:11434" required /> </div> )} <div> <label className="block text-sm font-medium mb-1">Priority (lower = higher priority)</label> <input type="number" value={formData.priority} onChange={(e) => setFormData(prev => ({ ...prev, priority: parseInt(e.target.value) || 0 }))} className="w-full px-3 py-2 border rounded-md bg-background" min={0} max={100} /> </div> <div> <label className="flex items-center gap-2"> <input type="checkbox" checked={formData.enabled} onChange={(e) => setFormData(prev => ({ ...prev, enabled: e.target.checked }))} /> <span className="text-sm font-medium">Enabled</span> </label> </div> <div className="space-y-2"> <div className="flex items-center justify-between"> <label className="block text-sm font-medium">Vine Configuration</label> {loadingModels && <span className="text-xs text-muted-foreground">Loading models...</span>} {!loadingModels && discoveredModels.length > 0 && ( <span className="text-xs text-primary">{discoveredModels.length} models available</span> )} {!loadingModels && modelError && !isGemini && ( <span className="text-xs text-destructive">{modelError}</span> )} </div> <div className="space-y-2"> {(["quick", "coder", "moe", "thinking"] as const).map((tier) => ( <div key={tier} className="flex items-center gap-2"> <span className="w-16 text-sm text-muted-foreground capitalize">{tier}:</span> {discoveredModels.length > 0 ? ( <select value={formData.models[tier]} onChange={(e) => setFormData(prev => ({ ...prev, models: { ...prev.models, [tier]: e.target.value } }))} className="flex-1 px-2 py-1 text-sm border rounded bg-background" > <option value="">Select a model...</option> {discoveredModels.map((model) => ( <option key={model.name} value={model.name}> {model.name} {model.sizeGB ? ` (${model.sizeGB})` : ""} {model.description && !model.sizeGB ? ` - ${model.description}` : ""} </option> ))} </select> ) : ( <input type="text" value={formData.models[tier]} onChange={(e) => setFormData(prev => ({ ...prev, models: { ...prev.models, [tier]: e.target.value } }))} className="flex-1 px-2 py-1 text-sm border rounded bg-background" placeholder={isGemini ? "gemini-2.0-flash" : "model name"} /> )} </div> ))} </div> </div> <div className="flex items-center justify-between pt-4 border-t"> {backend && onDelete ? ( <button type="button" onClick={() => onDelete(backend.id)} className="px-3 py-2 text-sm text-accent hover:bg-accent/10 rounded transition-colors" > ๐Ÿ—‘๏ธ Remove </button> ) : ( <div /> )} <div className="flex gap-2"> <button type="button" onClick={onClose} className="px-4 py-2 text-sm border rounded hover:bg-muted transition-colors" > Cancel </button> <button type="submit" disabled={saving} className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center gap-1" > <WatermelonSeed className="w-2 h-3" /> {saving ? "Saving..." : backend ? "Save" : "Add"} </button> </div> </div> </form> </div> </div> ) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zbrdc/delia'

If you have feedback or need assistance with the MCP directory API, please join our Discord server