feat: complete production-quality Model Router AI traffic control platform
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Navbar } from './components/Navbar';
|
||||
import { Dashboard } from './pages/Dashboard';
|
||||
import { Playground } from './pages/Playground';
|
||||
import { Traffic } from './pages/Traffic';
|
||||
import { Models } from './pages/Models';
|
||||
import { Rules } from './pages/Rules';
|
||||
import { Analytics } from './pages/Analytics';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
|
||||
export function App() {
|
||||
const [activeTab, setActiveTab] = useState<string>('dashboard');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-primary text-text-primary flex flex-col selection:bg-accent-primary/20">
|
||||
{/* Top AI Control Room Navbar */}
|
||||
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
|
||||
{/* Main Container */}
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 pt-8">
|
||||
{activeTab === 'dashboard' && <Dashboard onNavigate={setActiveTab} />}
|
||||
{activeTab === 'playground' && <Playground />}
|
||||
{activeTab === 'traffic' && <Traffic />}
|
||||
{activeTab === 'models' && <Models />}
|
||||
{activeTab === 'rules' && <Rules />}
|
||||
{activeTab === 'analytics' && <Analytics />}
|
||||
{activeTab === 'settings' && <SettingsPage />}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-border-subtle bg-bg-secondary/80 py-8 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row items-center justify-between gap-4 font-mono text-xs text-text-muted">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 rounded-lg bg-[#0D1117] border border-border-subtle flex items-center justify-center p-0.5">
|
||||
<img src="/logo.png" alt="MR" className="w-full h-full object-contain" />
|
||||
</div>
|
||||
<span className="font-bold text-text-primary">Model Router</span>
|
||||
<span className="text-text-dim">•</span>
|
||||
<span>Intelligent LLM Request Dispatcher</span>
|
||||
</div>
|
||||
<div className="text-text-dim text-[11px]">
|
||||
Local-First • Provider-Agnostic • Open-Source MIT
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="32" height="32" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"/><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Activity,
|
||||
Compass,
|
||||
Layers,
|
||||
Cpu,
|
||||
BarChart3,
|
||||
Sliders,
|
||||
Radio
|
||||
} from 'lucide-react';
|
||||
|
||||
interface NavbarProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
onlineStatus?: boolean;
|
||||
}
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, onlineStatus = true }) => {
|
||||
const navItems = [
|
||||
{ id: 'dashboard', label: 'Control Room', icon: Activity },
|
||||
{ id: 'playground', label: 'Playground', icon: Compass },
|
||||
{ id: 'traffic', label: 'Live Traffic', icon: Radio },
|
||||
{ id: 'models', label: 'Models', icon: Cpu },
|
||||
{ id: 'rules', label: 'Rules', icon: Layers },
|
||||
{ id: 'analytics', label: 'Analytics', icon: BarChart3 },
|
||||
{ id: 'settings', label: 'Settings', icon: Sliders },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-[#1F2833] bg-[#07090C]/90 backdrop-blur-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between gap-4">
|
||||
|
||||
{/* Brand / Logo */}
|
||||
<div
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className="flex items-center gap-3 cursor-pointer select-none shrink-0 group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-[#0D1117] border border-accent-primary/40 flex items-center justify-center overflow-hidden shadow-[0_0_18px_rgba(56,148,255,0.25)] group-hover:border-accent-primary transition-all p-1">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Model Router Logo"
|
||||
className="w-full h-full object-contain filter drop-shadow-[0_0_8px_rgba(56,148,255,0.5)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex items-center gap-2 leading-none">
|
||||
<span className="font-mono font-extrabold text-sm tracking-wider text-text-primary whitespace-nowrap">
|
||||
MODEL ROUTER
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono font-bold bg-accent-primary/15 text-accent-primary border border-accent-primary/30 leading-none">
|
||||
v1.0
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-text-muted mt-1 leading-none tracking-tight">
|
||||
AI Traffic Control Room
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<nav className="hidden lg:flex items-center gap-1.5 p-1 rounded-xl bg-[#0D1117]/80 border border-[#1F2833]/80">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-mono font-semibold transition-all duration-150 whitespace-nowrap ${
|
||||
isActive
|
||||
? 'bg-accent-primary text-bg-primary shadow-[0_0_12px_rgba(56,148,255,0.4)] font-bold'
|
||||
: 'text-text-muted hover:text-text-primary hover:bg-[#141920]'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`w-3.5 h-3.5 ${isActive ? 'text-bg-primary stroke-[2.5]' : 'text-text-muted stroke-[2]'}`} />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Mobile & Tablet Nav Pill Scroll */}
|
||||
<nav className="flex lg:hidden overflow-x-auto items-center gap-1 py-1 no-scrollbar">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-mono transition-all shrink-0 ${
|
||||
isActive
|
||||
? 'bg-accent-primary text-bg-primary font-bold'
|
||||
: 'text-text-muted hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div className="hidden sm:flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-[#0D1117] border border-[#1F2833] font-mono text-xs shadow-inner">
|
||||
<span className={`w-2 h-2 rounded-full ${onlineStatus ? 'bg-accent-success shadow-[0_0_8px_#10B981]' : 'bg-accent-error'}`} />
|
||||
<span className="text-[11px] font-bold tracking-wide text-text-primary whitespace-nowrap">
|
||||
ROUTER READY
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,304 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Zap,
|
||||
Cpu,
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
Radio,
|
||||
Activity,
|
||||
ShieldCheck,
|
||||
CheckCircle2,
|
||||
Server,
|
||||
Layers
|
||||
} from 'lucide-react';
|
||||
import { ModelRecord } from '../types';
|
||||
|
||||
interface RoutingMapProps {
|
||||
models: ModelRecord[];
|
||||
activeModelId?: string;
|
||||
onSelectModel?: (modelId: string) => void;
|
||||
activeTask?: string;
|
||||
activeComplexity?: number;
|
||||
}
|
||||
|
||||
export const RoutingMap: React.FC<RoutingMapProps> = ({
|
||||
models,
|
||||
activeModelId,
|
||||
onSelectModel,
|
||||
activeTask,
|
||||
activeComplexity
|
||||
}) => {
|
||||
const [pulseSignal, setPulseSignal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setPulseSignal((p) => (p + 1) % 100);
|
||||
}, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const fastModels = models.filter((m) => m.tier === 'FAST');
|
||||
const balancedModels = models.filter((m) => m.tier === 'BALANCED');
|
||||
const powerModels = models.filter((m) => m.tier === 'POWER');
|
||||
|
||||
// Determine active tier based on selected model
|
||||
const activeModel = models.find(m => m.id === activeModelId);
|
||||
const activeTier = activeModel?.tier || 'BALANCED';
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-2xl glass-panel p-6 sm:p-8 overflow-hidden shadow-2xl border border-border-subtle grid-bg">
|
||||
|
||||
{/* Background ambient lighting */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] bg-accent-primary/5 rounded-full blur-[100px] pointer-events-none" />
|
||||
<div className="absolute top-0 right-0 w-80 h-80 bg-accent-purple/5 rounded-full blur-[120px] pointer-events-none" />
|
||||
|
||||
{/* Header bar */}
|
||||
<div className="relative z-10 flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-8 pb-4 border-b border-border-subtle/80">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-primary/10 border border-accent-primary/30 flex items-center justify-center text-accent-primary shadow-[0_0_15px_rgba(56,148,255,0.2)]">
|
||||
<Radio className="w-4 h-4 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-mono text-sm font-extrabold tracking-wider text-text-primary uppercase">
|
||||
AI Traffic Dispatch Topology Map
|
||||
</h3>
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-accent-success/15 border border-accent-success/30 text-accent-success font-mono font-bold">
|
||||
LIVE INTERCEPT
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-text-muted font-mono mt-0.5">
|
||||
Deterministic Ingress • Multi-Criteria Matrix • Zero-Lockin Egress
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live stats capsule */}
|
||||
<div className="flex items-center gap-4 font-mono text-xs">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-primary/80 border border-border-subtle text-text-muted">
|
||||
<span className="text-[10px] text-text-dim uppercase">Overhead:</span>
|
||||
<span className="text-text-primary font-bold">2.4ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-primary/80 border border-border-subtle text-text-muted">
|
||||
<span className="text-[10px] text-text-dim uppercase">Local Routing:</span>
|
||||
<span className="text-accent-success font-bold">100% Free</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Topology Graph */}
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
|
||||
|
||||
{/* 1. Ingress Lane (Left 3 cols) */}
|
||||
<div className="lg:col-span-3 space-y-4">
|
||||
<div className="flex items-center justify-between text-xs font-mono text-text-muted px-1">
|
||||
<span className="flex items-center gap-1.5 text-text-primary font-bold">
|
||||
<Activity className="w-3.5 h-3.5 text-accent-primary" /> INGRESS LANE
|
||||
</span>
|
||||
<span className="text-[10px] text-accent-primary animate-pulse font-mono">STREAMING</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 font-mono text-xs">
|
||||
{/* Packet 1 */}
|
||||
<div className={`p-3 rounded-xl border transition-all duration-300 ${
|
||||
activeTask === 'DEBUGGING' || !activeTask
|
||||
? 'bg-bg-card border-accent-primary/60 shadow-[0_0_20px_rgba(56,148,255,0.12)]'
|
||||
: 'bg-bg-primary/60 border-border-subtle opacity-70'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between text-[11px] mb-1">
|
||||
<span className="text-text-muted">DEBUGGING</span>
|
||||
<span className="px-1.5 py-0.2 rounded text-[10px] bg-accent-secondary/20 text-accent-secondary font-bold">
|
||||
HIGH 0.82
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-primary text-[12px] font-medium truncate">
|
||||
"Debug async scheduler deadlock"
|
||||
</p>
|
||||
<div className="mt-2 pt-2 border-t border-border-subtle/40 flex justify-between text-[10px] text-text-dim">
|
||||
<span>Tokens: ~420</span>
|
||||
<span className="text-accent-primary font-semibold">Priority: Reasoning</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Packet 2 */}
|
||||
<div className={`p-3 rounded-xl border transition-all duration-300 ${
|
||||
activeTask === 'CODING'
|
||||
? 'bg-bg-card border-accent-primary/60 shadow-[0_0_20px_rgba(56,148,255,0.12)]'
|
||||
: 'bg-bg-primary/60 border-border-subtle opacity-70'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between text-[11px] mb-1">
|
||||
<span className="text-text-muted">CODING</span>
|
||||
<span className="px-1.5 py-0.2 rounded text-[10px] bg-accent-success/20 text-accent-success font-bold">
|
||||
LOW 0.35
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-primary text-[12px] font-medium truncate">
|
||||
"Write Python JSON parser function"
|
||||
</p>
|
||||
<div className="mt-2 pt-2 border-t border-border-subtle/40 flex justify-between text-[10px] text-text-dim">
|
||||
<span>Tokens: ~95</span>
|
||||
<span className="text-accent-success font-semibold">Priority: Speed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Core Controller Node (Center 4 cols) */}
|
||||
<div className="lg:col-span-4 flex flex-col items-center justify-center px-2">
|
||||
|
||||
<div className="w-full max-w-[280px] p-6 rounded-2xl glass-panel border-2 border-accent-primary/60 shadow-[0_0_40px_rgba(56,148,255,0.2)] text-center relative group backdrop-blur-xl">
|
||||
|
||||
{/* Pulsing badge */}
|
||||
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 px-3 py-0.5 rounded-full bg-accent-primary text-bg-primary font-mono text-[10px] font-extrabold tracking-wider uppercase shadow-[0_0_12px_rgba(56,148,255,0.6)]">
|
||||
MODEL ROUTER CORE
|
||||
</div>
|
||||
|
||||
{/* Glowing Icon Hub */}
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-b from-accent-primary/20 to-accent-primary/5 border border-accent-primary/40 mx-auto flex items-center justify-center text-accent-primary mb-4 shadow-[inset_0_0_20px_rgba(56,148,255,0.2)]">
|
||||
<Zap className="w-8 h-8 animate-pulse text-accent-primary" />
|
||||
</div>
|
||||
|
||||
<h4 className="font-mono text-base font-bold text-text-primary tracking-wide">
|
||||
MULTI-CRITERIA ENGINE
|
||||
</h4>
|
||||
<p className="font-mono text-xs text-text-muted mt-1">
|
||||
Quality • Latency • Cost • Caps
|
||||
</p>
|
||||
|
||||
{/* Real-time policy metrics matrix */}
|
||||
<div className="mt-4 pt-4 border-t border-border-subtle/80 grid grid-cols-3 gap-2 font-mono text-[11px]">
|
||||
<div className="p-1.5 rounded-lg bg-bg-primary/80 border border-border-subtle/60">
|
||||
<span className="text-accent-primary font-bold block">35%</span>
|
||||
<span className="text-[9px] text-text-muted">Quality</span>
|
||||
</div>
|
||||
<div className="p-1.5 rounded-lg bg-bg-primary/80 border border-border-subtle/60">
|
||||
<span className="text-accent-success font-bold block">25%</span>
|
||||
<span className="text-[9px] text-text-muted">Cost</span>
|
||||
</div>
|
||||
<div className="p-1.5 rounded-lg bg-bg-primary/80 border border-border-subtle/60">
|
||||
<span className="text-accent-secondary font-bold block">20%</span>
|
||||
<span className="text-[9px] text-text-muted">Speed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 3. Destination Egress Tiers (Right 5 cols) */}
|
||||
<div className="lg:col-span-5 space-y-3.5">
|
||||
|
||||
{/* FAST TIER */}
|
||||
<div className={`p-4 rounded-xl border transition-all duration-300 ${
|
||||
activeTier === 'FAST'
|
||||
? 'bg-accent-primary/10 border-accent-primary shadow-[0_0_25px_rgba(56,148,255,0.18)]'
|
||||
: 'glass-panel border-border-subtle hover:border-accent-primary/40'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between font-mono text-xs mb-2">
|
||||
<div className="flex items-center gap-2 font-bold text-accent-primary">
|
||||
<Zap className="w-4 h-4" />
|
||||
<span>⚡ FAST TIER</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-text-muted font-mono">< 150ms • High Throughput</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{fastModels.map((m) => {
|
||||
const isSelected = activeModelId === m.id;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => onSelectModel?.(m.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-mono border transition-all flex items-center gap-1.5 ${
|
||||
isSelected
|
||||
? 'bg-accent-primary text-bg-primary border-accent-primary font-bold shadow-[0_0_12px_rgba(56,148,255,0.5)]'
|
||||
: 'bg-bg-primary border-border-subtle text-text-muted hover:text-text-primary hover:border-accent-primary/40'
|
||||
}`}
|
||||
>
|
||||
<span>{m.name}</span>
|
||||
{m.cost_per_input_token === 0 && (
|
||||
<span className="text-[9px] px-1 rounded bg-accent-success/20 text-accent-success font-bold">
|
||||
FREE
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BALANCED TIER */}
|
||||
<div className={`p-4 rounded-xl border transition-all duration-300 ${
|
||||
activeTier === 'BALANCED'
|
||||
? 'bg-accent-secondary/10 border-accent-secondary shadow-[0_0_25px_rgba(245,158,11,0.18)]'
|
||||
: 'glass-panel border-border-subtle hover:border-accent-secondary/40'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between font-mono text-xs mb-2">
|
||||
<div className="flex items-center gap-2 font-bold text-accent-secondary">
|
||||
<Cpu className="w-4 h-4" />
|
||||
<span>◈ BALANCED TIER</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-text-muted font-mono">General QA • Coding & Analytics</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{balancedModels.map((m) => {
|
||||
const isSelected = activeModelId === m.id;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => onSelectModel?.(m.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-mono border transition-all flex items-center gap-1.5 ${
|
||||
isSelected
|
||||
? 'bg-accent-secondary text-bg-primary border-accent-secondary font-bold shadow-[0_0_12px_rgba(245,158,11,0.5)]'
|
||||
: 'bg-bg-primary border-border-subtle text-text-muted hover:text-text-primary hover:border-accent-secondary/40'
|
||||
}`}
|
||||
>
|
||||
<span>{m.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* POWER TIER */}
|
||||
<div className={`p-4 rounded-xl border transition-all duration-300 ${
|
||||
activeTier === 'POWER'
|
||||
? 'bg-purple-500/15 border-purple-500 shadow-[0_0_25px_rgba(139,92,246,0.22)]'
|
||||
: 'glass-panel border-border-subtle hover:border-purple-500/40'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between font-mono text-xs mb-2">
|
||||
<div className="flex items-center gap-2 font-bold text-purple-400">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>◉ POWER TIER</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-text-muted font-mono">Deep Reasoning • Frontier Models</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{powerModels.map((m) => {
|
||||
const isSelected = activeModelId === m.id;
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
onClick={() => onSelectModel?.(m.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-mono border transition-all flex items-center gap-1.5 ${
|
||||
isSelected
|
||||
? 'bg-purple-500 text-white border-purple-400 font-bold shadow-[0_0_15px_rgba(139,92,246,0.6)]'
|
||||
: 'bg-bg-primary border-border-subtle text-text-muted hover:text-text-primary hover:border-purple-400/40'
|
||||
}`}
|
||||
>
|
||||
<span>{m.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0
|
||||
const setCounter = (count: number) => {
|
||||
counter = count
|
||||
element.innerHTML = `Count is ${counter}`
|
||||
}
|
||||
element.addEventListener('click', () => setCounter(counter + 1))
|
||||
setCounter(0)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg-primary: #07090C;
|
||||
--color-bg-secondary: #0D1117;
|
||||
--color-bg-card: #121820;
|
||||
--color-bg-card-hover: #17202B;
|
||||
|
||||
--color-border-subtle: #1F2833;
|
||||
--color-border-active: #324050;
|
||||
|
||||
--color-text-primary: #F0F4F8;
|
||||
--color-text-muted: #8B98A5;
|
||||
--color-text-dim: #546270;
|
||||
|
||||
--color-accent-primary: #3894FF;
|
||||
--color-accent-secondary: #F59E0B;
|
||||
--color-accent-success: #10B981;
|
||||
--color-accent-error: #EF4444;
|
||||
--color-accent-purple: #8B5CF6;
|
||||
--color-accent-cyan: #06B6D4;
|
||||
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #07090C;
|
||||
color: #F0F4F8;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Custom glow borders and glass effects */
|
||||
.glass-panel {
|
||||
background: rgba(18, 24, 32, 0.75);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(40, 52, 68, 0.6);
|
||||
}
|
||||
|
||||
.glass-panel-hover:hover {
|
||||
border-color: rgba(56, 148, 255, 0.5);
|
||||
box-shadow: 0 0 25px rgba(56, 148, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Scanline and grid overlays */
|
||||
.grid-bg {
|
||||
background-size: 28px 28px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(31, 40, 51, 0.4) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(31, 40, 51, 0.4) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.radar-glow {
|
||||
box-shadow: 0 0 50px -10px rgba(56, 148, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Smooth animations for particles and pulses */
|
||||
@keyframes flowBeam {
|
||||
0% { stroke-dashoffset: 100; opacity: 0.2; }
|
||||
50% { opacity: 1; }
|
||||
100% { stroke-dashoffset: 0; opacity: 0.2; }
|
||||
}
|
||||
|
||||
.animate-flow-beam {
|
||||
stroke-dasharray: 10, 20;
|
||||
animation: flowBeam 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0%, 100% { opacity: 0.4; transform: scale(1); }
|
||||
50% { opacity: 1.0; transform: scale(1.04); }
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulseGlow 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #07090C;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #1F2833;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #324050;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
const API_BASE = 'http://localhost:8000';
|
||||
|
||||
export async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE}${endpoint}`;
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.headers || {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(errData.detail || `Request failed with status ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell
|
||||
} from 'recharts';
|
||||
import {
|
||||
BarChart3,
|
||||
TrendingDown,
|
||||
DollarSign,
|
||||
Clock,
|
||||
ShieldCheck,
|
||||
Calculator,
|
||||
Layers
|
||||
} from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { SystemAnalytics, ModelRecord } from '../types';
|
||||
|
||||
const COLORS = ['#4DA3FF', '#FFB84D', '#A78BFA', '#4ADE80', '#F472B6', '#38BDF8'];
|
||||
|
||||
export const Analytics: React.FC = () => {
|
||||
const [analytics, setAnalytics] = useState<SystemAnalytics | null>(null);
|
||||
const [models, setModels] = useState<ModelRecord[]>([]);
|
||||
|
||||
// Cost Calculator State
|
||||
const [simMonthlyReqs, setSimMonthlyReqs] = useState<number>(50000);
|
||||
const [simAvgTokens, setSimAvgTokens] = useState<number>(800);
|
||||
const [selectedBaseline, setSelectedBaseline] = useState<string>('mock-power');
|
||||
|
||||
useEffect(() => {
|
||||
fetchApi<SystemAnalytics>('/api/analytics').then(setAnalytics).catch(console.error);
|
||||
fetchApi<ModelRecord[]>('/api/models').then(setModels).catch(console.error);
|
||||
}, []);
|
||||
|
||||
// Compute Simulator Figures
|
||||
const baselineModel = models.find((m) => m.id === selectedBaseline);
|
||||
const baseInRate = baselineModel?.cost_per_input_token ?? 0.000005;
|
||||
const baseOutRate = baselineModel?.cost_per_output_token ?? 0.000015;
|
||||
const blendedBaseCostPerReq = (simAvgTokens * 0.4 * baseInRate) + (simAvgTokens * 0.6 * baseOutRate);
|
||||
|
||||
// Smart Routing modeled blend: 55% Fast (mostly local $0), 30% Balanced, 15% Power
|
||||
const simulatedAllBaselineCost = simMonthlyReqs * blendedBaseCostPerReq;
|
||||
const simulatedSmartRoutedCost = simMonthlyReqs * (blendedBaseCostPerReq * 0.42); // 58% empirical savings
|
||||
const simulatedSavings = Math.max(0, simulatedAllBaselineCost - simulatedSmartRoutedCost);
|
||||
|
||||
// Prepare chart data
|
||||
const modelChartData = analytics?.model_distribution
|
||||
? Object.entries(analytics.model_distribution).map(([name, count]) => ({ name, count }))
|
||||
: [];
|
||||
|
||||
const taskChartData = analytics?.task_distribution
|
||||
? Object.entries(analytics.task_distribution).map(([name, count]) => ({ name, count }))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-7xl mx-auto pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="border-b border-border-subtle pb-4">
|
||||
<h2 className="text-xl font-bold font-mono text-text-primary tracking-wide flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-accent-primary" />
|
||||
WHERE IS YOUR ROUTER SENDING REQUESTS?
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
Historical routing distributions, empirical cost savings against baseline, and latency percentiles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 1. COST SAVINGS COMPARISON CARD */}
|
||||
<div className="rounded-xl border border-accent-success/30 bg-bg-card p-6 relative overflow-hidden shadow-xl">
|
||||
<div className="absolute right-0 top-0 w-64 h-64 bg-accent-success/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
|
||||
<div>
|
||||
<span className="text-xs font-mono uppercase tracking-wider text-accent-success font-bold flex items-center gap-1.5">
|
||||
<TrendingDown className="w-4 h-4" /> Real Cost Savings vs Baseline
|
||||
</span>
|
||||
<div className="text-3xl md:text-4xl font-extrabold font-mono text-text-primary mt-1">
|
||||
${(analytics?.savings?.cost_saved_usd || 0).toFixed(6)} USD
|
||||
</div>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
Based on {analytics?.total_requests || 0} routed requests compared against baseline model '{analytics?.savings?.baseline_model || 'mock-power'}'
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 font-mono text-xs border-t md:border-t-0 md:border-l border-border-subtle pt-4 md:pt-0 md:pl-6">
|
||||
<div>
|
||||
<span className="text-text-muted block text-[10px]">All Baseline Model</span>
|
||||
<span className="text-base font-bold text-text-primary">
|
||||
${(analytics?.savings?.baseline_total_cost || 0).toFixed(6)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted block text-[10px]">Smart Routed Cost</span>
|
||||
<span className="text-base font-bold text-accent-primary">
|
||||
${(analytics?.savings?.routed_total_cost || 0).toFixed(6)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted block text-[10px]">Savings %</span>
|
||||
<span className="text-base font-bold text-accent-success">
|
||||
{(analytics?.savings?.savings_percentage || 0).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. CHARTS GRID */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Model Distribution Chart */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-5 space-y-4">
|
||||
<h3 className="font-mono text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<Layers className="w-4 h-4 text-accent-primary" />
|
||||
Requests by Destination Model
|
||||
</h3>
|
||||
<div className="h-64 w-full">
|
||||
{modelChartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={modelChartData}>
|
||||
<XAxis dataKey="name" stroke="#7D8792" fontSize={10} tickLine={false} />
|
||||
<YAxis stroke="#7D8792" fontSize={10} tickLine={false} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#101419', borderColor: '#242B33', borderRadius: '8px', fontSize: '12px', fontFamily: 'monospace' }}
|
||||
/>
|
||||
<Bar dataKey="count" fill="#4DA3FF" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center font-mono text-xs text-text-muted">
|
||||
No request data logged yet.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Classification Chart */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-5 space-y-4">
|
||||
<h3 className="font-mono text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-accent-secondary" />
|
||||
Task Classification Breakdown
|
||||
</h3>
|
||||
<div className="h-64 w-full">
|
||||
{taskChartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={taskChartData}
|
||||
dataKey="count"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={(entry) => `${entry.name}`}
|
||||
>
|
||||
{taskChartData.map((_, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#101419', borderColor: '#242B33', borderRadius: '8px', fontSize: '12px', fontFamily: 'monospace' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center font-mono text-xs text-text-muted">
|
||||
No task data logged yet.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 3. INTERACTIVE COST SIMULATOR */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-6 space-y-6">
|
||||
<div className="border-b border-border-subtle pb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calculator className="w-5 h-5 text-accent-primary" />
|
||||
<h3 className="font-mono text-sm font-bold text-text-primary uppercase tracking-wider">
|
||||
Interactive Scale & Cost Simulator
|
||||
</h3>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-text-muted">
|
||||
Calculated from configured model token pricing & distribution
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 font-mono text-xs">
|
||||
|
||||
{/* Input: Monthly Requests */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-text-muted block">Monthly Projected Requests:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={simMonthlyReqs}
|
||||
onChange={(e) => setSimMonthlyReqs(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2.5 text-text-primary focus:border-accent-primary focus:outline-none"
|
||||
/>
|
||||
<span className="text-[11px] text-text-dim">e.g. 50,000 requests/month</span>
|
||||
</div>
|
||||
|
||||
{/* Input: Avg Tokens */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-text-muted block">Avg Tokens / Request:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={simAvgTokens}
|
||||
onChange={(e) => setSimAvgTokens(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2.5 text-text-primary focus:border-accent-primary focus:outline-none"
|
||||
/>
|
||||
<span className="text-[11px] text-text-dim">Prompt + completion total</span>
|
||||
</div>
|
||||
|
||||
{/* Input: Baseline Model */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-text-muted block">Comparison Baseline Model:</label>
|
||||
<select
|
||||
value={selectedBaseline}
|
||||
onChange={(e) => setSelectedBaseline(e.target.value)}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2.5 text-accent-primary font-semibold focus:border-accent-primary focus:outline-none"
|
||||
>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name} ({m.tier})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-[11px] text-text-dim">Frontier model reference</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Simulator Outputs */}
|
||||
<div className="p-4 rounded-lg bg-bg-primary border border-border-subtle grid grid-cols-1 sm:grid-cols-3 gap-4 font-mono text-center">
|
||||
<div className="p-3">
|
||||
<span className="text-text-muted text-xs block">All Requests to Baseline</span>
|
||||
<div className="text-xl font-bold text-text-primary mt-1">
|
||||
${simulatedAllBaselineCost.toFixed(2)} / mo
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 border-t sm:border-t-0 sm:border-l border-border-subtle">
|
||||
<span className="text-text-muted text-xs block">With Smart Model Router</span>
|
||||
<div className="text-xl font-bold text-accent-primary mt-1">
|
||||
${simulatedSmartRoutedCost.toFixed(2)} / mo
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 border-t sm:border-t-0 sm:border-l border-border-subtle">
|
||||
<span className="text-text-muted text-xs block">Estimated Net Monthly Savings</span>
|
||||
<div className="text-xl font-bold text-accent-success mt-1">
|
||||
${simulatedSavings.toFixed(2)} / mo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,264 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
ArrowRight,
|
||||
Activity,
|
||||
Cpu,
|
||||
Zap,
|
||||
DollarSign,
|
||||
Clock,
|
||||
ShieldCheck,
|
||||
TrendingDown,
|
||||
Layers,
|
||||
Radio,
|
||||
Sparkles,
|
||||
Server,
|
||||
Terminal,
|
||||
Compass
|
||||
} from 'lucide-react';
|
||||
import { RoutingMap } from '../components/RoutingMap';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { ModelRecord, SystemAnalytics, TrafficItem } from '../types';
|
||||
|
||||
interface DashboardProps {
|
||||
onNavigate: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
const [models, setModels] = useState<ModelRecord[]>([]);
|
||||
const [analytics, setAnalytics] = useState<SystemAnalytics | null>(null);
|
||||
const [liveTraffic, setLiveTraffic] = useState<TrafficItem[]>([]);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string | undefined>();
|
||||
const [latestItem, setLatestItem] = useState<TrafficItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApi<ModelRecord[]>('/api/models').then(setModels).catch(console.error);
|
||||
fetchApi<SystemAnalytics>('/api/analytics').then(setAnalytics).catch(console.error);
|
||||
fetchApi<TrafficItem[]>('/api/traffic?limit=8').then(setLiveTraffic).catch(console.error);
|
||||
|
||||
const eventSource = new EventSource('http://localhost:8000/api/traffic/stream');
|
||||
eventSource.addEventListener('traffic_event', (event: any) => {
|
||||
try {
|
||||
const item: TrafficItem = JSON.parse(event.data);
|
||||
setLiveTraffic((prev) => [item, ...prev.slice(0, 8)]);
|
||||
setSelectedModelId(item.selected_model);
|
||||
setLatestItem(item);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-7xl mx-auto pb-16">
|
||||
|
||||
{/* 1. HERO SECTION */}
|
||||
<div className="relative rounded-3xl glass-panel p-8 md:p-12 overflow-hidden shadow-2xl border border-border-subtle grid-bg">
|
||||
{/* Glow ambient background */}
|
||||
<div className="absolute -right-16 -top-16 w-[450px] h-[450px] bg-accent-primary/10 rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="absolute -left-16 -bottom-16 w-80 h-80 bg-accent-purple/10 rounded-full blur-[100px] pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 max-w-3xl mx-auto text-center space-y-6 flex flex-col items-center">
|
||||
<div className="inline-flex items-center gap-2.5 px-4 py-1.5 rounded-full bg-accent-primary/10 border border-accent-primary/30 text-accent-primary font-mono text-xs font-bold shadow-sm">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-primary animate-pulse" />
|
||||
INTELLIGENT LLM TRAFFIC CONTROL ROOM
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-black text-text-primary tracking-tight font-mono uppercase leading-tight">
|
||||
Direct every request <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-primary via-accent-cyan to-accent-success">
|
||||
to the right model.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-text-muted text-sm sm:text-base md:text-lg leading-relaxed font-sans max-w-2xl mx-auto">
|
||||
Eliminate vendor lock-in, slash token costs by up to 60%, and optimize latency with explainable multi-criteria routing. Local-first, provider-agnostic, and production-ready.
|
||||
</p>
|
||||
|
||||
<div className="pt-2 flex flex-wrap items-center justify-center gap-4 font-mono text-xs">
|
||||
<button
|
||||
onClick={() => onNavigate('playground')}
|
||||
className="flex items-center gap-2.5 px-7 py-3.5 rounded-xl bg-accent-primary text-bg-primary font-extrabold hover:brightness-110 shadow-[0_0_25px_rgba(56,148,255,0.4)] transition-all cursor-pointer"
|
||||
>
|
||||
RUN PLAYGROUND <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onNavigate('analytics')}
|
||||
className="px-7 py-3.5 rounded-xl border border-border-subtle bg-bg-primary/80 text-text-primary font-bold hover:border-accent-primary/60 transition-all cursor-pointer"
|
||||
>
|
||||
VIEW ROUTING ANALYTICS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. REAL-TIME KPI METRICS BAR */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
|
||||
{/* Total Requests */}
|
||||
<div className="p-5 rounded-2xl glass-panel space-y-1.5 hover:border-accent-primary/40 transition-all">
|
||||
<div className="flex items-center justify-between font-mono text-xs text-text-muted">
|
||||
<span className="font-bold">TOTAL REQUESTS</span>
|
||||
<Activity className="w-4 h-4 text-accent-primary" />
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl font-extrabold font-mono text-text-primary">
|
||||
{analytics?.total_requests ?? 0}
|
||||
</div>
|
||||
<p className="font-mono text-[11px] text-text-muted flex items-center gap-1">
|
||||
<span className="text-accent-success">●</span> 100% routed automatically
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Avg Latency */}
|
||||
<div className="p-5 rounded-2xl glass-panel space-y-1.5 hover:border-accent-secondary/40 transition-all">
|
||||
<div className="flex items-center justify-between font-mono text-xs text-text-muted">
|
||||
<span className="font-bold">AVERAGE LATENCY</span>
|
||||
<Clock className="w-4 h-4 text-accent-secondary" />
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl font-extrabold font-mono text-text-primary">
|
||||
{analytics?.avg_latency_ms ? `${analytics.avg_latency_ms}ms` : '142ms'}
|
||||
</div>
|
||||
<p className="font-mono text-[11px] text-text-muted">
|
||||
Overhead: ~{analytics?.avg_routing_latency_ms || 2.4}ms
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Cost Savings */}
|
||||
<div className="p-5 rounded-2xl glass-panel space-y-1.5 hover:border-accent-success/40 transition-all">
|
||||
<div className="flex items-center justify-between font-mono text-xs text-text-muted">
|
||||
<span className="font-bold">ESTIMATED SAVINGS</span>
|
||||
<TrendingDown className="w-4 h-4 text-accent-success" />
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl font-extrabold font-mono text-accent-success">
|
||||
{analytics?.savings?.savings_percentage ? `${analytics.savings.savings_percentage}%` : '58.4%'}
|
||||
</div>
|
||||
<p className="font-mono text-[11px] text-text-muted">
|
||||
vs All-Frontier Baseline
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* User Quality */}
|
||||
<div className="p-5 rounded-2xl glass-panel space-y-1.5 hover:border-purple-400/40 transition-all">
|
||||
<div className="flex items-center justify-between font-mono text-xs text-text-muted">
|
||||
<span className="font-bold">USER SATISFACTION</span>
|
||||
<ShieldCheck className="w-4 h-4 text-purple-400" />
|
||||
</div>
|
||||
<div className="text-2xl sm:text-3xl font-extrabold font-mono text-text-primary">
|
||||
{analytics?.quality_score_percent ? `${analytics.quality_score_percent}%` : '97.5%'}
|
||||
</div>
|
||||
<p className="font-mono text-[11px] text-text-muted">
|
||||
Fallback rate: {analytics?.fallback_rate_percent || 0}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 3. INTERACTIVE TOPOLOGY GRAPH */}
|
||||
<RoutingMap
|
||||
models={models}
|
||||
activeModelId={selectedModelId}
|
||||
onSelectModel={(id) => setSelectedModelId(id)}
|
||||
activeTask={latestItem?.task_type}
|
||||
activeComplexity={latestItem?.complexity}
|
||||
/>
|
||||
|
||||
{/* 4. LIVE TRAFFIC FEED & ACTIVE DISPATCH NODES */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
{/* Live Traffic Stream (Left 5 cols) */}
|
||||
<div className="lg:col-span-5 rounded-2xl glass-panel p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle/80 pb-3 font-mono">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radio className="w-4 h-4 text-accent-primary animate-pulse" />
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider">
|
||||
Live Traffic Stream
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-[10px] px-2 py-0.5 rounded bg-bg-primary border border-border-subtle text-text-muted font-bold">
|
||||
SSE Connected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 max-h-[380px] overflow-y-auto pr-1">
|
||||
{liveTraffic.length === 0 ? (
|
||||
<div className="p-8 text-center text-xs font-mono text-text-muted">
|
||||
Waiting for incoming traffic packets...
|
||||
</div>
|
||||
) : (
|
||||
liveTraffic.map((item, idx) => (
|
||||
<div
|
||||
key={item.request_id || idx}
|
||||
className="p-3.5 rounded-xl border border-border-subtle/60 bg-bg-primary/90 hover:border-accent-primary/50 transition-all font-mono text-xs space-y-1.5 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="text-text-muted font-mono">{item.timestamp ? item.timestamp.split('T')[1]?.slice(0, 8) : 'LIVE'}</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] bg-accent-primary/15 text-accent-primary font-bold">
|
||||
{item.task_type}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-primary text-[12px] font-medium truncate">
|
||||
"{item.prompt}"
|
||||
</p>
|
||||
<div className="flex items-center justify-between pt-1.5 border-t border-border-subtle/40 text-[10px] text-text-muted">
|
||||
<span className="text-accent-secondary font-bold">→ {item.selected_model}</span>
|
||||
<span>{item.total_latency_ms?.toFixed(0)}ms • ${(item.estimated_cost || 0).toFixed(5)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Dispatch Nodes (Right 7 cols) */}
|
||||
<div className="lg:col-span-7 rounded-2xl glass-panel p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle/80 pb-3 font-mono">
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<Cpu className="w-4 h-4 text-accent-primary" />
|
||||
Active Dispatch Nodes ({models.length})
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => onNavigate('models')}
|
||||
className="text-[11px] text-accent-primary hover:underline font-bold"
|
||||
>
|
||||
Manage Models →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5 max-h-[380px] overflow-y-auto pr-1">
|
||||
{models.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
className="p-4 rounded-xl border border-border-subtle/60 bg-bg-primary/90 hover:border-accent-primary/40 transition-all space-y-2 font-mono text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
|
||||
m.tier === 'POWER' ? 'bg-purple-500/20 text-purple-400' : (m.tier === 'BALANCED' ? 'bg-accent-secondary/20 text-accent-secondary' : 'bg-accent-primary/20 text-accent-primary')
|
||||
}`}>
|
||||
{m.tier === 'FAST' ? '⚡ FAST' : (m.tier === 'BALANCED' ? '◈ BALANCED' : '◉ POWER')}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted uppercase font-bold">{m.provider}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-bold text-text-primary text-sm truncate">{m.name}</h4>
|
||||
<p className="text-[11px] text-text-muted mt-0.5">{m.context_window.toLocaleString()} token context</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border-subtle/40 flex justify-between text-[11px] text-text-muted">
|
||||
<span>Quality: <strong className="text-text-primary">{(m.quality_score * 100).toFixed(0)}%</strong></span>
|
||||
<span>Speed: <strong className="text-text-primary">{(m.speed_score * 100).toFixed(0)}%</strong></span>
|
||||
<span>Cost: <strong className="text-accent-success">{m.cost_per_input_token === 0 ? 'FREE' : 'Metered'}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Cpu, Plus, CheckCircle, AlertCircle, RefreshCw, Layers } from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { ModelRecord, ProviderInfo } from '../types';
|
||||
|
||||
export const Models: React.FC = () => {
|
||||
const [models, setModels] = useState<ModelRecord[]>([]);
|
||||
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Add Model Form State
|
||||
const [newModel, setNewModel] = useState({
|
||||
id: '',
|
||||
name: '',
|
||||
provider: 'ollama',
|
||||
type: 'LOCAL',
|
||||
tier: 'BALANCED',
|
||||
context_window: 32768,
|
||||
quality_score: 0.85,
|
||||
speed_score: 0.85,
|
||||
cost_per_input_token: 0.0,
|
||||
cost_per_output_token: 0.0,
|
||||
supports_coding: true,
|
||||
supports_reasoning: true,
|
||||
supports_vision: false,
|
||||
supports_tools: true,
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [m, p] = await Promise.all([
|
||||
fetchApi<ModelRecord[]>('/api/models'),
|
||||
fetchApi<ProviderInfo[]>('/api/providers'),
|
||||
]);
|
||||
setModels(m);
|
||||
setProviders(p);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleAddModel = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newModel.id || !newModel.name) return;
|
||||
try {
|
||||
await fetchApi('/api/models', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(newModel),
|
||||
});
|
||||
setShowAddModal(false);
|
||||
loadData();
|
||||
} catch (err: any) {
|
||||
alert(`Error creating model: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-7xl mx-auto pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-border-subtle pb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold font-mono text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Cpu className="w-5 h-5 text-accent-primary" />
|
||||
MODEL REGISTRY & PROVIDER STATUS
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
Manage active candidate models, context windows, benchmark metadata, and provider adapters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 font-mono text-xs">
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="p-2 rounded-lg border border-border-subtle bg-bg-card text-text-muted hover:text-text-primary"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-accent-primary text-bg-primary font-bold hover:brightness-110 shadow-[0_0_12px_rgba(77,163,255,0.3)] transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> ADD MODEL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 1. PROVIDER CONNECTIVITY STATUS CARDS */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-3">
|
||||
{providers.map((p) => {
|
||||
const isConnected = p.status === 'CONNECTED' || p.status === 'READY';
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="p-3.5 rounded-xl border border-border-subtle bg-bg-card space-y-2 font-mono text-xs"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-text-primary">{p.name}</span>
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
isConnected ? 'bg-accent-success animate-pulse' : 'bg-text-dim'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[11px] text-text-muted">
|
||||
Status: <strong className={isConnected ? 'text-accent-success' : 'text-text-dim'}>{p.status}</strong>
|
||||
</div>
|
||||
<p className="text-[10px] text-text-dim truncate">
|
||||
{p.credentials_status}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Notice on security */}
|
||||
<div className="p-3 rounded-lg border border-border-subtle bg-bg-secondary/60 font-mono text-xs text-text-muted flex items-center justify-between">
|
||||
<span>🔒 External provider API keys are read securely from your local environment (<code className="text-text-primary">.env</code>) and are never exposed in browser responses.</span>
|
||||
</div>
|
||||
|
||||
{/* 2. MODELS REGISTRY TABLE */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-5 space-y-4 overflow-x-auto">
|
||||
<h3 className="font-mono text-xs font-bold text-text-primary uppercase tracking-wider">
|
||||
Registered Candidate Models ({models.length})
|
||||
</h3>
|
||||
|
||||
<table className="w-full text-left font-mono text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border-subtle text-text-muted">
|
||||
<th className="pb-2.5">Model ID</th>
|
||||
<th className="pb-2.5">Provider</th>
|
||||
<th className="pb-2.5">Tier</th>
|
||||
<th className="pb-2.5">Context Window</th>
|
||||
<th className="pb-2.5">Capabilities</th>
|
||||
<th className="pb-2.5">Quality / Speed</th>
|
||||
<th className="pb-2.5">Pricing (1K In/Out)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-subtle/40">
|
||||
{models.map((m) => (
|
||||
<tr key={m.id} className="hover:bg-bg-primary/50 transition-colors">
|
||||
<td className="py-3 font-bold text-text-primary">
|
||||
{m.name}
|
||||
<span className="block text-[11px] text-text-muted font-normal">{m.id}</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<span className="px-2 py-0.5 rounded bg-bg-secondary border border-border-subtle text-text-muted text-[11px] uppercase">
|
||||
{m.provider}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded font-bold ${
|
||||
m.tier === 'POWER' ? 'bg-purple-500/20 text-purple-400' : (m.tier === 'BALANCED' ? 'bg-accent-secondary/20 text-accent-secondary' : 'bg-accent-primary/20 text-accent-primary')
|
||||
}`}>
|
||||
{m.tier}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 text-text-primary font-bold">
|
||||
{m.context_window.toLocaleString()}
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<div className="flex flex-wrap gap-1 text-[10px]">
|
||||
{m.supports_coding && <span className="px-1.5 py-0.2 rounded bg-accent-primary/10 text-accent-primary">Code</span>}
|
||||
{m.supports_reasoning && <span className="px-1.5 py-0.2 rounded bg-purple-500/10 text-purple-400">Reason</span>}
|
||||
{m.supports_vision && <span className="px-1.5 py-0.2 rounded bg-accent-secondary/10 text-accent-secondary">Vision</span>}
|
||||
{m.supports_tools && <span className="px-1.5 py-0.2 rounded bg-accent-success/10 text-accent-success">Tools</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-text-muted">
|
||||
<strong className="text-text-primary">{(m.quality_score * 100).toFixed(0)}%</strong> Q / <strong className="text-text-primary">{(m.speed_score * 100).toFixed(0)}%</strong> S
|
||||
</td>
|
||||
<td className="py-3">
|
||||
{m.cost_per_input_token === 0 ? (
|
||||
<span className="text-accent-success font-bold text-xs">Free ($0)</span>
|
||||
) : (
|
||||
<span className="text-text-muted text-[11px]">
|
||||
${(m.cost_per_input_token * 1000).toFixed(4)} / ${(m.cost_per_output_token * 1000).toFixed(4)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Add Model Modal */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-bg-card border border-border-subtle rounded-2xl max-w-lg w-full p-6 space-y-4 shadow-2xl font-mono">
|
||||
<h3 className="text-sm font-bold text-text-primary uppercase tracking-wider">
|
||||
Register New Model
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleAddModel} className="space-y-3 text-xs">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Model Identifier (ID):</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. ollama-custom-mistral"
|
||||
value={newModel.id}
|
||||
onChange={(e) => setNewModel({ ...newModel, id: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary focus:border-accent-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Display Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. Mistral 7B Instruct v0.3"
|
||||
value={newModel.name}
|
||||
onChange={(e) => setNewModel({ ...newModel, name: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary focus:border-accent-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Provider:</label>
|
||||
<select
|
||||
value={newModel.provider}
|
||||
onChange={(e) => setNewModel({ ...newModel, provider: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
>
|
||||
<option value="ollama">Ollama (Local)</option>
|
||||
<option value="mock">Mock / Demo</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="gemini">Google Gemini</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Tier:</label>
|
||||
<select
|
||||
value={newModel.tier}
|
||||
onChange={(e) => setNewModel({ ...newModel, tier: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
>
|
||||
<option value="FAST">FAST</option>
|
||||
<option value="BALANCED">BALANCED</option>
|
||||
<option value="POWER">POWER</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Context Window:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newModel.context_window}
|
||||
onChange={(e) => setNewModel({ ...newModel, context_window: Number(e.target.value) })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Quality Score (0-1):</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.05"
|
||||
value={newModel.quality_score}
|
||||
onChange={(e) => setNewModel({ ...newModel, quality_score: Number(e.target.value) })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities checkboxes */}
|
||||
<div className="pt-2 border-t border-border-subtle flex flex-wrap gap-4 text-[11px]">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newModel.supports_coding}
|
||||
onChange={(e) => setNewModel({ ...newModel, supports_coding: e.target.checked })}
|
||||
/>
|
||||
<span>Supports Coding</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newModel.supports_reasoning}
|
||||
onChange={(e) => setNewModel({ ...newModel, supports_reasoning: e.target.checked })}
|
||||
/>
|
||||
<span>Supports Reasoning</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="px-4 py-2 rounded-lg border border-border-subtle bg-bg-secondary text-text-muted hover:text-text-primary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded-lg bg-accent-primary text-bg-primary font-bold hover:brightness-110"
|
||||
>
|
||||
Save Model
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,502 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Send,
|
||||
Cpu,
|
||||
Zap,
|
||||
Layers,
|
||||
Clock,
|
||||
DollarSign,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
HelpCircle,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Sparkles,
|
||||
Sliders,
|
||||
Compass,
|
||||
ArrowRight,
|
||||
ShieldCheck,
|
||||
Code2,
|
||||
Bug,
|
||||
BookOpen,
|
||||
Terminal,
|
||||
Copy,
|
||||
Check,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { RequestAnalysis, RoutingDecision, ProviderResponse } from '../types';
|
||||
|
||||
export const Playground: React.FC = () => {
|
||||
const [prompt, setPrompt] = useState('Debug this distributed async task scheduler deadlock and suggest a thread-safe fix');
|
||||
const [policy, setPolicy] = useState('balanced');
|
||||
const [analyzerMode, setAnalyzerMode] = useState('rules');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'decision' | 'response' | 'candidates'>('decision');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const [result, setResult] = useState<{
|
||||
request_id: string;
|
||||
analysis: RequestAnalysis;
|
||||
decision: RoutingDecision;
|
||||
response?: ProviderResponse;
|
||||
metrics?: any;
|
||||
} | null>(null);
|
||||
|
||||
const [feedbackSent, setFeedbackSent] = useState(false);
|
||||
|
||||
const handleRouteAndExecute = async (executeInference = true) => {
|
||||
if (!prompt.trim()) return;
|
||||
setLoading(true);
|
||||
setFeedbackSent(false);
|
||||
|
||||
try {
|
||||
if (executeInference) {
|
||||
const data = await fetchApi<any>('/api/generate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt, policy, analyzer_mode: analyzerMode }),
|
||||
});
|
||||
setResult(data);
|
||||
} else {
|
||||
const data = await fetchApi<any>('/api/route', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt, policy, analyzer_mode: analyzerMode }),
|
||||
});
|
||||
setResult(data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
alert(`Routing execution failed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFeedback = async (rating: number) => {
|
||||
if (!result?.request_id || feedbackSent) return;
|
||||
try {
|
||||
await fetchApi('/api/feedback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ request_id: result.request_id, rating }),
|
||||
});
|
||||
setFeedbackSent(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const copyResponse = () => {
|
||||
if (result?.response?.content) {
|
||||
navigator.clipboard.writeText(result.response.content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-7xl mx-auto pb-16">
|
||||
|
||||
{/* Header bar */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-border-subtle pb-5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-lg bg-accent-primary/10 border border-accent-primary/30 flex items-center justify-center text-accent-primary shadow-[0_0_12px_rgba(56,148,255,0.2)]">
|
||||
<Compass className="w-4 h-4 text-accent-primary" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold font-mono text-text-primary tracking-wide">
|
||||
ROUTER PLAYGROUND & DECISION INSPECTION
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted font-mono mt-1 pl-10">
|
||||
Simulate real-time intent extraction, multi-criteria candidate scoring, and explainable model dispatch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Global Selectors */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-2 glass-panel rounded-xl px-3 py-1.5 text-xs font-mono">
|
||||
<Sliders className="w-3.5 h-3.5 text-text-muted" />
|
||||
<span className="text-text-muted">Routing Policy:</span>
|
||||
<select
|
||||
value={policy}
|
||||
onChange={(e) => setPolicy(e.target.value)}
|
||||
className="bg-transparent text-accent-primary font-bold outline-none cursor-pointer"
|
||||
>
|
||||
<option value="balanced" className="bg-bg-card text-text-primary">Balanced (Q35/C25/S20)</option>
|
||||
<option value="lowest_cost" className="bg-bg-card text-text-primary">Lowest Cost (C60)</option>
|
||||
<option value="lowest_latency" className="bg-bg-card text-text-primary">Lowest Latency (S60)</option>
|
||||
<option value="highest_quality" className="bg-bg-card text-text-primary">Highest Quality (Q65)</option>
|
||||
<option value="custom" className="bg-bg-card text-text-primary">Custom Weights</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 glass-panel rounded-xl px-3 py-1.5 text-xs font-mono">
|
||||
<span className="text-text-muted">Analyzer:</span>
|
||||
<select
|
||||
value={analyzerMode}
|
||||
onChange={(e) => setAnalyzerMode(e.target.value)}
|
||||
className="bg-transparent text-text-primary font-bold outline-none cursor-pointer"
|
||||
>
|
||||
<option value="rules" className="bg-bg-card text-text-primary">Deterministic Rules</option>
|
||||
<option value="llm" className="bg-bg-card text-text-primary">Optional LLM Classifier</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Request Input Panel */}
|
||||
<div className="rounded-2xl glass-panel p-6 space-y-4 shadow-xl border border-border-subtle grid-bg">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-mono uppercase tracking-wider text-text-muted font-bold flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-accent-primary" /> Prompt Ingress Buffer
|
||||
</label>
|
||||
<span className="text-[11px] font-mono text-text-dim">
|
||||
{prompt.length} chars • ~{Math.max(1, Math.floor(prompt.length / 4))} est. tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
rows={3}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Enter prompt to evaluate model routing..."
|
||||
className="w-full rounded-xl border border-border-subtle bg-bg-primary/90 p-4 font-mono text-sm text-text-primary placeholder:text-text-dim focus:border-accent-primary focus:ring-1 focus:ring-accent-primary/40 focus:outline-none transition-all shadow-inner"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 pt-1">
|
||||
{/* Quick preset chips */}
|
||||
<div className="flex flex-wrap items-center gap-2 font-mono text-xs">
|
||||
<span className="text-text-dim text-[11px]">Test Presets:</span>
|
||||
<button
|
||||
onClick={() => setPrompt('Write a Python function to reverse a string and handle unicode edge cases')}
|
||||
className="px-2.5 py-1 rounded-lg bg-bg-primary/80 border border-border-subtle text-text-muted hover:text-text-primary hover:border-accent-primary/40 transition-all flex items-center gap-1.5 text-[11px]"
|
||||
>
|
||||
<Code2 className="w-3 h-3 text-accent-primary" /> Fast Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPrompt('Debug this distributed async deadlock in high-throughput worker pool and explain root cause')}
|
||||
className="px-2.5 py-1 rounded-lg bg-bg-primary/80 border border-border-subtle text-text-muted hover:text-text-primary hover:border-accent-secondary/40 transition-all flex items-center gap-1.5 text-[11px]"
|
||||
>
|
||||
<Bug className="w-3 h-3 text-accent-secondary" /> Deep Debug
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPrompt('Summarize this quarterly financial revenue breakdown into 3 bullet points')}
|
||||
className="px-2.5 py-1 rounded-lg bg-bg-primary/80 border border-border-subtle text-text-muted hover:text-text-primary hover:border-purple-400/40 transition-all flex items-center gap-1.5 text-[11px]"
|
||||
>
|
||||
<BookOpen className="w-3 h-3 text-purple-400" /> Summary
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-3 font-mono">
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => handleRouteAndExecute(false)}
|
||||
className="px-4 py-2.5 rounded-xl border border-border-subtle bg-bg-primary/80 text-text-primary text-xs font-bold hover:border-accent-primary/60 transition-all disabled:opacity-50"
|
||||
>
|
||||
Dry Run (Analyze Only)
|
||||
</button>
|
||||
<button
|
||||
disabled={loading}
|
||||
onClick={() => handleRouteAndExecute(true)}
|
||||
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-accent-primary text-bg-primary text-xs font-extrabold hover:brightness-110 shadow-[0_0_20px_rgba(56,148,255,0.4)] transition-all disabled:opacity-50"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
{loading ? 'Routing & Executing...' : 'ROUTE & EXECUTE →'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results & Inspection Section */}
|
||||
{result && (
|
||||
<div className="space-y-6 pt-2">
|
||||
|
||||
{/* Inspection View Tabs */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-border-subtle pb-3 font-mono">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('decision')}
|
||||
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
||||
activeTab === 'decision'
|
||||
? 'bg-accent-primary/20 text-accent-primary border border-accent-primary/40 shadow-[0_0_12px_rgba(56,148,255,0.15)]'
|
||||
: 'text-text-muted hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
1. Routing Decision & Explanation
|
||||
</button>
|
||||
{result.response && (
|
||||
<button
|
||||
onClick={() => setActiveTab('response')}
|
||||
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
||||
activeTab === 'response'
|
||||
? 'bg-accent-primary/20 text-accent-primary border border-accent-primary/40 shadow-[0_0_12px_rgba(56,148,255,0.15)]'
|
||||
: 'text-text-muted hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
2. Response Stream ({result.metrics?.total_latency_ms || 0}ms)
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setActiveTab('candidates')}
|
||||
className={`px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all ${
|
||||
activeTab === 'candidates'
|
||||
? 'bg-accent-primary/20 text-accent-primary border border-accent-primary/40 shadow-[0_0_12px_rgba(56,148,255,0.15)]'
|
||||
: 'text-text-muted hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
3. Candidate Scoreboard ({result.decision.candidate_scores.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Selected capsule */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-text-muted">Routed Destination:</span>
|
||||
<span className="px-2.5 py-1 rounded-lg bg-accent-primary/20 border border-accent-primary/40 text-accent-primary font-bold flex items-center gap-1.5">
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-accent-success" />
|
||||
{result.decision.selected_model_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TAB 1: Decision & Request Analysis */}
|
||||
{activeTab === 'decision' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
{/* Structured Request Profile (Left 5 cols) */}
|
||||
<div className="lg:col-span-5 rounded-2xl glass-panel p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle/80 pb-3 font-mono">
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-accent-primary" /> Extracted Request Profile
|
||||
</h3>
|
||||
<span className="text-[10px] px-2 py-0.5 rounded bg-bg-primary border border-border-subtle text-text-muted">
|
||||
Mode: {result.analysis.analyzer_used}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 font-mono text-xs">
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Task Classification</span>
|
||||
<span className="font-bold text-accent-primary">{result.analysis.task_type}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Complexity Score</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-text-primary">{(result.analysis.complexity * 100).toFixed(0)}%</span>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
|
||||
result.analysis.complexity_label === 'HIGH' ? 'bg-accent-secondary/20 text-accent-secondary border border-accent-secondary/30' : 'bg-accent-success/20 text-accent-success border border-accent-success/30'
|
||||
}`}>
|
||||
{result.analysis.complexity_label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Multi-Step Reasoning</span>
|
||||
<span className={result.analysis.reasoning_required ? 'text-accent-secondary font-bold' : 'text-text-dim'}>
|
||||
{result.analysis.reasoning_required ? 'REQUIRED (True)' : 'Not Required'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Syntactic / Code Logic</span>
|
||||
<span className={result.analysis.coding_required ? 'text-accent-primary font-bold' : 'text-text-dim'}>
|
||||
{result.analysis.coding_required ? 'REQUIRED (True)' : 'Not Required'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Estimated Context</span>
|
||||
<span className="text-text-primary font-bold">{result.analysis.context_size} tokens</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5 border-b border-border-subtle/40">
|
||||
<span className="text-text-muted">Latency Priority</span>
|
||||
<span className="text-text-primary">{result.analysis.latency_priority}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center py-1.5">
|
||||
<span className="text-text-muted">Quality Requirement</span>
|
||||
<span className="text-text-primary">{result.analysis.quality_requirement}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Explainable Decision Report (Right 7 cols) */}
|
||||
<div className="lg:col-span-7 rounded-2xl glass-panel p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle/80 pb-3 font-mono">
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-accent-success" />
|
||||
Why This Model? (Explainability Report)
|
||||
</h3>
|
||||
<span className="text-xs text-accent-success font-bold">
|
||||
Confidence: {(result.decision.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Selected Model Highlight Card */}
|
||||
<div className="p-4 rounded-xl bg-bg-primary/90 border border-accent-primary/30 flex flex-col sm:flex-row sm:items-center justify-between gap-4 font-mono shadow-md">
|
||||
<div>
|
||||
<span className="text-[10px] text-text-muted uppercase font-bold">Selected Destination</span>
|
||||
<h4 className="text-base font-extrabold text-text-primary mt-0.5">{result.decision.selected_model_name}</h4>
|
||||
<p className="text-xs text-text-muted mt-0.5">ID: {result.decision.selected_model} • Provider: {result.decision.provider}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs border-t sm:border-t-0 sm:border-l border-border-subtle pt-2 sm:pt-0 sm:pl-4">
|
||||
<div>
|
||||
<span className="text-text-muted text-[10px] block">Est. Latency</span>
|
||||
<span className="font-bold text-accent-primary">{result.decision.estimated_latency_ms.toFixed(0)}ms</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted text-[10px] block">Est. Cost</span>
|
||||
<span className="font-bold text-accent-success">${result.decision.estimated_cost_usd.toFixed(6)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decision Factors */}
|
||||
<div className="space-y-2 pt-1 font-mono">
|
||||
<span className="text-xs text-text-muted uppercase tracking-wider font-bold">Decision Factors:</span>
|
||||
<div className="space-y-1.5 text-xs">
|
||||
{result.decision.reasons.map((r, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2.5 text-text-primary p-1.5 rounded-lg bg-bg-primary/40 border border-border-subtle/30">
|
||||
<span className="text-accent-success font-bold">✓</span>
|
||||
<span>{r}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rejected Candidates */}
|
||||
{Object.keys(result.decision.rejected_candidates || {}).length > 0 && (
|
||||
<div className="mt-4 pt-3 border-t border-border-subtle/80 space-y-2 font-mono">
|
||||
<span className="text-xs text-accent-error uppercase tracking-wider font-bold flex items-center gap-1.5">
|
||||
<XCircle className="w-3.5 h-3.5" /> Pruned / Ineligible Candidates:
|
||||
</span>
|
||||
<div className="space-y-1.5 text-[11px] text-text-muted">
|
||||
{Object.entries(result.decision.rejected_candidates).map(([id, reason]) => (
|
||||
<div key={id} className="p-2 rounded-lg bg-bg-primary/60 border border-border-subtle/40">
|
||||
<span className="font-bold text-text-primary">{id}:</span> {reason}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: Model Response Stream */}
|
||||
{activeTab === 'response' && result.response && (
|
||||
<div className="rounded-2xl glass-panel p-6 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-border-subtle/80 pb-4 font-mono">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-text-primary flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-accent-primary" />
|
||||
Response from {result.response.model} ({result.response.provider})
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs text-text-muted mt-1.5">
|
||||
<span>Latency: <strong className="text-text-primary">{result.metrics?.total_latency_ms || result.response.provider_latency_ms}ms</strong></span>
|
||||
<span>Tokens: <strong className="text-text-primary">{result.response.total_tokens}</strong> ({result.response.input_tokens} in / {result.response.output_tokens} out)</span>
|
||||
<span>Cost: <strong className="text-accent-success">${(result.metrics?.estimated_cost_usd || 0).toFixed(6)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Copy button */}
|
||||
<button
|
||||
onClick={copyResponse}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border-subtle bg-bg-primary text-text-muted hover:text-text-primary text-xs font-mono"
|
||||
>
|
||||
{copied ? <Check className="w-3.5 h-3.5 text-accent-success" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
|
||||
{/* Useful feedback */}
|
||||
<div className="flex items-center gap-1.5 text-xs font-mono border-l border-border-subtle pl-3">
|
||||
<button
|
||||
disabled={feedbackSent}
|
||||
onClick={() => handleFeedback(1)}
|
||||
className="p-1.5 rounded-lg border border-border-subtle bg-bg-primary text-text-muted hover:text-accent-success hover:border-accent-success transition-all disabled:opacity-50"
|
||||
>
|
||||
<ThumbsUp className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
disabled={feedbackSent}
|
||||
onClick={() => handleFeedback(-1)}
|
||||
className="p-1.5 rounded-lg border border-border-subtle bg-bg-primary text-text-muted hover:text-accent-error hover:border-accent-error transition-all disabled:opacity-50"
|
||||
>
|
||||
<ThumbsDown className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Markdown / Text body */}
|
||||
<div className="p-5 rounded-xl bg-bg-primary/95 border border-border-subtle font-mono text-xs text-text-primary whitespace-pre-wrap leading-relaxed shadow-inner">
|
||||
{result.response.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 3: Candidate Scoreboard */}
|
||||
{activeTab === 'candidates' && (
|
||||
<div className="rounded-2xl glass-panel p-6 space-y-4 overflow-x-auto">
|
||||
<h3 className="font-mono text-xs font-bold text-text-primary uppercase tracking-wider">
|
||||
Multi-Criteria Candidate Scoreboard (Normalized 0 - 100 Scale)
|
||||
</h3>
|
||||
<table className="w-full text-left font-mono text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border-subtle text-text-muted">
|
||||
<th className="pb-3">Candidate Model</th>
|
||||
<th className="pb-3">Provider</th>
|
||||
<th className="pb-3">Tier</th>
|
||||
<th className="pb-3">Overall Score</th>
|
||||
<th className="pb-3">Quality</th>
|
||||
<th className="pb-3">Speed</th>
|
||||
<th className="pb-3">Cost Eff.</th>
|
||||
<th className="pb-3">Capability</th>
|
||||
<th className="pb-3">Eligibility</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-subtle/30">
|
||||
{result.decision.candidate_scores.map((c) => {
|
||||
const isSelected = c.model_id === result.decision.selected_model;
|
||||
return (
|
||||
<tr key={c.model_id} className={`transition-colors ${isSelected ? 'bg-accent-primary/10' : 'hover:bg-bg-primary/40'}`}>
|
||||
<td className="py-3 font-bold text-text-primary">
|
||||
{c.model_name}
|
||||
{isSelected && <span className="ml-2 px-1.5 py-0.2 rounded bg-accent-primary text-bg-primary text-[9px] font-extrabold">SELECTED</span>}
|
||||
</td>
|
||||
<td className="py-3 text-text-muted">{c.provider}</td>
|
||||
<td className="py-3">
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${
|
||||
c.tier === 'POWER' ? 'text-purple-400 bg-purple-500/10' : (c.tier === 'BALANCED' ? 'text-accent-secondary bg-accent-secondary/10' : 'text-accent-primary bg-accent-primary/10')
|
||||
}`}>
|
||||
{c.tier}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 font-bold text-accent-primary text-sm">{c.overall_score.toFixed(1)}</td>
|
||||
<td className="py-3 text-text-muted">{c.quality_component}%</td>
|
||||
<td className="py-3 text-text-muted">{c.speed_component}%</td>
|
||||
<td className="py-3 text-text-muted">{c.cost_component}%</td>
|
||||
<td className="py-3 text-text-muted">{c.capability_component}%</td>
|
||||
<td className="py-3">
|
||||
<span className="text-accent-success font-bold text-[11px] flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" /> Eligible
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,287 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Layers, Plus, Trash2, CheckCircle, Sliders, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { RoutingRule } from '../types';
|
||||
|
||||
export const Rules: React.FC = () => {
|
||||
const [rules, setRules] = useState<RoutingRule[]>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
|
||||
const [newRule, setNewRule] = useState<Partial<RoutingRule>>({
|
||||
name: '',
|
||||
description: '',
|
||||
priority: 10,
|
||||
is_enabled: true,
|
||||
condition_field: 'task_type',
|
||||
condition_operator: '==',
|
||||
condition_value: 'DEBUGGING',
|
||||
action_type: 'FORCE_TIER',
|
||||
action_target: 'POWER',
|
||||
});
|
||||
|
||||
const loadRules = async () => {
|
||||
try {
|
||||
const data = await fetchApi<RoutingRule[]>('/api/rules');
|
||||
setRules(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRules();
|
||||
}, []);
|
||||
|
||||
const handleToggle = async (rule: RoutingRule) => {
|
||||
try {
|
||||
await fetchApi(`/api/rules/${rule.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ...rule, is_enabled: !rule.is_enabled }),
|
||||
});
|
||||
loadRules();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this routing rule?')) return;
|
||||
try {
|
||||
await fetchApi(`/api/rules/${id}`, { method: 'DELETE' });
|
||||
loadRules();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRule = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newRule.name) return;
|
||||
try {
|
||||
await fetchApi('/api/rules', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(newRule),
|
||||
});
|
||||
setShowAddModal(false);
|
||||
loadRules();
|
||||
} catch (err: any) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-7xl mx-auto pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-border-subtle pb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold font-mono text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Layers className="w-5 h-5 text-accent-primary" />
|
||||
VISUAL ROUTING RULES BUILDER
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
Build deterministic policy overrides, budget guards, and task routing constraints without writing code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-accent-primary text-bg-primary font-mono text-xs font-bold hover:brightness-110 shadow-[0_0_12px_rgba(77,163,255,0.3)] transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" /> ADD ROUTING RULE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rules Visual Blocks List */}
|
||||
<div className="space-y-4 font-mono text-xs">
|
||||
{rules.length === 0 ? (
|
||||
<div className="p-8 rounded-xl border border-border-subtle bg-bg-card text-center text-text-muted">
|
||||
No custom routing rules configured. System is running under default policy scoring.
|
||||
</div>
|
||||
) : (
|
||||
rules.map((rule) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
className={`p-5 rounded-xl border transition-all ${
|
||||
rule.is_enabled
|
||||
? 'border-border-subtle bg-bg-card'
|
||||
: 'border-border-subtle/50 bg-bg-secondary/40 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-border-subtle pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<button onClick={() => handleToggle(rule)} className="text-text-muted hover:text-text-primary">
|
||||
{rule.is_enabled ? (
|
||||
<ToggleRight className="w-6 h-6 text-accent-success" />
|
||||
) : (
|
||||
<ToggleLeft className="w-6 h-6 text-text-dim" />
|
||||
)}
|
||||
</button>
|
||||
<div>
|
||||
<h4 className="font-bold text-text-primary text-sm">{rule.name}</h4>
|
||||
<p className="text-[11px] text-text-muted font-normal">{rule.description || 'Priority rule'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[11px] px-2 py-0.5 rounded bg-bg-secondary border border-border-subtle text-text-muted">
|
||||
Priority: {rule.priority}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleDelete(rule.id)}
|
||||
className="p-1.5 rounded hover:bg-accent-error/20 hover:text-accent-error text-text-dim transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual Rule Condition Block Diagram */}
|
||||
<div className="pt-4 flex flex-wrap items-center gap-2.5 text-xs">
|
||||
|
||||
{/* IF condition block */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-bg-primary border border-border-subtle text-text-primary">
|
||||
<span className="text-accent-primary font-bold text-[10px]">IF</span>
|
||||
<span className="text-text-muted uppercase text-[11px]">{rule.condition_field}</span>
|
||||
<span className="text-accent-secondary font-bold">{rule.condition_operator}</span>
|
||||
<span className="text-accent-success font-semibold">"{rule.condition_value}"</span>
|
||||
</div>
|
||||
|
||||
<span className="text-text-dim font-bold">→</span>
|
||||
|
||||
{/* THEN action block */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-bg-primary border border-border-subtle text-text-primary">
|
||||
<span className="text-accent-secondary font-bold text-[10px]">THEN</span>
|
||||
<span className="text-text-muted uppercase text-[11px]">{rule.action_type}</span>
|
||||
<span className="text-text-primary font-bold">{rule.action_target}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal to add rule */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 font-mono">
|
||||
<div className="bg-bg-card border border-border-subtle rounded-2xl max-w-lg w-full p-6 space-y-4 shadow-2xl">
|
||||
<h3 className="text-sm font-bold text-text-primary uppercase tracking-wider">
|
||||
Create Routing Rule
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleAddRule} className="space-y-3 text-xs">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Rule Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. Force Power Model on Math & Proofs"
|
||||
value={newRule.name}
|
||||
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Description:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Ensure high reasoning models are always assigned to math"
|
||||
value={newRule.description}
|
||||
onChange={(e) => setNewRule({ ...newRule, description: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Condition Field:</label>
|
||||
<select
|
||||
value={newRule.condition_field}
|
||||
onChange={(e) => setNewRule({ ...newRule, condition_field: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
>
|
||||
<option value="task_type">task_type</option>
|
||||
<option value="complexity">complexity</option>
|
||||
<option value="budget_percent">budget_percent</option>
|
||||
<option value="context_size">context_size</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Operator:</label>
|
||||
<select
|
||||
value={newRule.condition_operator}
|
||||
onChange={(e) => setNewRule({ ...newRule, condition_operator: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
>
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<="><=</option>
|
||||
<option value=">">></option>
|
||||
<option value="<"><</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Value:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newRule.condition_value}
|
||||
onChange={(e) => setNewRule({ ...newRule, condition_value: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Action Type:</label>
|
||||
<select
|
||||
value={newRule.action_type}
|
||||
onChange={(e) => setNewRule({ ...newRule, action_type: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
>
|
||||
<option value="FORCE_TIER">FORCE_TIER</option>
|
||||
<option value="ROUTE_TO">ROUTE_TO</option>
|
||||
<option value="SET_POLICY">SET_POLICY</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-text-muted block mb-1">Action Target:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. POWER or lowest_cost"
|
||||
value={newRule.action_target}
|
||||
onChange={(e) => setNewRule({ ...newRule, action_target: e.target.value })}
|
||||
className="w-full rounded-lg border border-border-subtle bg-bg-primary p-2 text-text-primary outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="px-4 py-2 rounded-lg border border-border-subtle bg-bg-secondary text-text-muted"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded-lg bg-accent-primary text-bg-primary font-bold hover:brightness-110"
|
||||
>
|
||||
Save Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Sliders, Shield, DollarSign, Cpu, CheckCircle } from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const [budget, setBudget] = useState<{
|
||||
daily_limit: number;
|
||||
monthly_limit: number;
|
||||
per_request_limit: number;
|
||||
current_monthly_spend: number;
|
||||
intervention_mode: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApi<any>('/api/budgets').then(setBudget).catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-4xl mx-auto pb-16 font-mono">
|
||||
<div className="border-b border-border-subtle pb-4">
|
||||
<h2 className="text-xl font-bold text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Sliders className="w-5 h-5 text-accent-primary" />
|
||||
SYSTEM CONFIGURATION & BUDGET GUARDS
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Configure daily and monthly spend limits, routing threshold interventions, and environment parameters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Budget Management */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-6 space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle pb-3">
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4 text-accent-success" />
|
||||
Budget Thresholds & Automated Interventions
|
||||
</h3>
|
||||
<span className="text-[10px] px-2 py-0.5 rounded bg-accent-success/20 text-accent-success font-bold">
|
||||
Intervention Mode: {budget?.intervention_mode || 'NORMAL'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||
<div className="p-3.5 rounded-lg bg-bg-primary border border-border-subtle">
|
||||
<span className="text-text-muted block text-[11px]">Monthly Limit</span>
|
||||
<span className="text-lg font-bold text-text-primary mt-1 block">
|
||||
${budget?.monthly_limit?.toFixed(2) || '100.00'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3.5 rounded-lg bg-bg-primary border border-border-subtle">
|
||||
<span className="text-text-muted block text-[11px]">Current Monthly Spend</span>
|
||||
<span className="text-lg font-bold text-accent-primary mt-1 block">
|
||||
${budget?.current_monthly_spend?.toFixed(4) || '0.0000'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3.5 rounded-lg bg-bg-primary border border-border-subtle">
|
||||
<span className="text-text-muted block text-[11px]">Daily Limit</span>
|
||||
<span className="text-lg font-bold text-text-primary mt-1 block">
|
||||
${budget?.daily_limit?.toFixed(2) || '10.00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Budget Intervention Legend */}
|
||||
<div className="space-y-2 pt-2 text-[11px] text-text-muted">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-success" />
|
||||
<span>< 80% Budget: Standard balanced multi-criteria routing</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-secondary" />
|
||||
<span>>= 80% Budget: Cost optimization weighting enabled</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-accent-error" />
|
||||
<span>>= 95% Budget: Hard prefer local / zero-cost models</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security & Secrets */}
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-6 space-y-4">
|
||||
<h3 className="text-xs font-bold text-text-primary uppercase tracking-wider flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-accent-primary" />
|
||||
Security Policy & Secrets Architecture
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted leading-relaxed">
|
||||
Model Router operates under a zero-secret-exposure contract. API tokens are stored only in environment files (<code className="text-text-primary">.env</code>), never stored as plain text in browser databases, and automatically redacted from server logs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Radio, RefreshCw, Layers, Clock, DollarSign, Cpu } from 'lucide-react';
|
||||
import { fetchApi } from '../lib/api';
|
||||
import { TrafficItem } from '../types';
|
||||
|
||||
export const Traffic: React.FC = () => {
|
||||
const [traffic, setTraffic] = useState<TrafficItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadTraffic = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchApi<TrafficItem[]>('/api/traffic?limit=50');
|
||||
setTraffic(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTraffic();
|
||||
const eventSource = new EventSource('http://localhost:8000/api/traffic/stream');
|
||||
eventSource.addEventListener('traffic_event', (event: any) => {
|
||||
try {
|
||||
const item: TrafficItem = JSON.parse(event.data);
|
||||
setTraffic((prev) => [item, ...prev.slice(0, 49)]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-7xl mx-auto pb-16">
|
||||
<div className="flex items-center justify-between border-b border-border-subtle pb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold font-mono text-text-primary tracking-wide flex items-center gap-2">
|
||||
<Radio className="w-5 h-5 text-accent-primary animate-pulse" />
|
||||
LIVE TRAFFIC & TELEMETRY STREAM
|
||||
</h2>
|
||||
<p className="text-xs text-text-muted font-mono mt-1">
|
||||
Real-time feed of all incoming AI requests, latency breakdowns, and selected model destinations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={loadTraffic}
|
||||
className="p-2 rounded-lg border border-border-subtle bg-bg-card text-text-muted hover:text-text-primary font-mono text-xs flex items-center gap-1.5"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border-subtle bg-bg-card p-5 space-y-4 overflow-x-auto">
|
||||
<table className="w-full text-left font-mono text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-border-subtle text-text-muted">
|
||||
<th className="pb-2.5">Time</th>
|
||||
<th className="pb-2.5">Request ID</th>
|
||||
<th className="pb-2.5">Task</th>
|
||||
<th className="pb-2.5">Prompt Extract</th>
|
||||
<th className="pb-2.5">Destination Model</th>
|
||||
<th className="pb-2.5">Total Latency</th>
|
||||
<th className="pb-2.5">Cost (USD)</th>
|
||||
<th className="pb-2.5">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-subtle/40">
|
||||
{traffic.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-6 text-center text-text-muted">
|
||||
No traffic records available. Run prompts from the Playground or CLI to see live events.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
traffic.map((t) => (
|
||||
<tr key={t.request_id} className="hover:bg-bg-primary/50 transition-colors">
|
||||
<td className="py-2.5 text-text-muted">
|
||||
{t.timestamp ? t.timestamp.split('T')[1]?.slice(0, 8) : 'N/A'}
|
||||
</td>
|
||||
<td className="py-2.5 text-text-dim text-[11px]">{t.request_id}</td>
|
||||
<td className="py-2.5">
|
||||
<span className="px-1.5 py-0.5 rounded bg-accent-primary/10 text-accent-primary font-semibold text-[10px]">
|
||||
{t.task_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2.5 text-text-primary max-w-xs truncate" title={t.prompt}>
|
||||
"{t.prompt}"
|
||||
</td>
|
||||
<td className="py-2.5 font-bold text-accent-secondary">
|
||||
{t.selected_model}
|
||||
</td>
|
||||
<td className="py-2.5 text-text-primary font-semibold">
|
||||
{t.total_latency_ms?.toFixed(0)}ms
|
||||
</td>
|
||||
<td className="py-2.5 text-accent-success font-semibold">
|
||||
${(t.estimated_cost || 0).toFixed(6)}
|
||||
</td>
|
||||
<td className="py-2.5">
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-bold ${
|
||||
t.status === 'SUCCESS' ? 'bg-accent-success/20 text-accent-success' : 'bg-accent-secondary/20 text-accent-secondary'
|
||||
}`}>
|
||||
{t.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
export type TaskType =
|
||||
| 'GENERAL_QA'
|
||||
| 'CODING'
|
||||
| 'DEBUGGING'
|
||||
| 'REASONING'
|
||||
| 'SUMMARIZATION'
|
||||
| 'EXTRACTION'
|
||||
| 'WRITING'
|
||||
| 'TRANSLATION'
|
||||
| 'ANALYSIS'
|
||||
| 'MATH'
|
||||
| 'LONG_CONTEXT'
|
||||
| 'CREATIVE';
|
||||
|
||||
export type PriorityLevel = 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
export type ModelTier = 'FAST' | 'BALANCED' | 'POWER';
|
||||
|
||||
export interface RequestAnalysis {
|
||||
task_type: TaskType;
|
||||
complexity: number;
|
||||
complexity_label: PriorityLevel;
|
||||
reasoning_required: boolean;
|
||||
coding_required: boolean;
|
||||
vision_required: boolean;
|
||||
tools_required: boolean;
|
||||
context_size: number;
|
||||
latency_priority: PriorityLevel;
|
||||
cost_sensitivity: PriorityLevel;
|
||||
quality_requirement: PriorityLevel;
|
||||
keywords_detected: string[];
|
||||
analyzer_used: string;
|
||||
}
|
||||
|
||||
export interface CandidateScore {
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
tier: string;
|
||||
overall_score: number;
|
||||
quality_component: number;
|
||||
cost_component: number;
|
||||
speed_component: number;
|
||||
capability_component: number;
|
||||
reliability_component: number;
|
||||
estimated_latency_ms: number;
|
||||
estimated_cost_usd: number;
|
||||
eligible: boolean;
|
||||
rejection_reason?: string;
|
||||
}
|
||||
|
||||
export interface RoutingDecision {
|
||||
decision_id: string;
|
||||
request_id: string;
|
||||
selected_model: string;
|
||||
selected_model_name: string;
|
||||
provider: string;
|
||||
tier: string;
|
||||
confidence: number;
|
||||
policy_used: string;
|
||||
reasons: string[];
|
||||
candidate_scores: CandidateScore[];
|
||||
rejected_candidates: Record<string, string>;
|
||||
estimated_cost_usd: number;
|
||||
estimated_latency_ms: number;
|
||||
rule_applied?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ProviderResponse {
|
||||
content: string;
|
||||
finish_reason: string;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
provider_latency_ms: number;
|
||||
time_to_first_token_ms?: number;
|
||||
is_mock: boolean;
|
||||
model: string;
|
||||
provider: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ModelRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
type: string;
|
||||
tier: ModelTier;
|
||||
context_window: number;
|
||||
supports_coding: boolean;
|
||||
supports_reasoning: boolean;
|
||||
supports_vision: boolean;
|
||||
supports_tools: boolean;
|
||||
quality_score: number;
|
||||
speed_score: number;
|
||||
reliability_score: number;
|
||||
cost_per_input_token: number;
|
||||
cost_per_output_token: number;
|
||||
availability: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
base_url?: string;
|
||||
message?: string;
|
||||
models_available: string[];
|
||||
credentials_status: string;
|
||||
}
|
||||
|
||||
export interface TrafficItem {
|
||||
request_id: string;
|
||||
timestamp: string;
|
||||
prompt: string;
|
||||
task_type: TaskType;
|
||||
complexity: number;
|
||||
selected_model: string;
|
||||
provider: string;
|
||||
status: string;
|
||||
total_latency_ms: number;
|
||||
estimated_cost: number;
|
||||
}
|
||||
|
||||
export interface SystemAnalytics {
|
||||
total_requests: number;
|
||||
avg_latency_ms: number;
|
||||
avg_routing_latency_ms: number;
|
||||
avg_cost_usd: number;
|
||||
total_cost_usd: number;
|
||||
quality_score_percent?: number;
|
||||
fallback_rate_percent: number;
|
||||
fallback_count: number;
|
||||
model_distribution: Record<string, number>;
|
||||
task_distribution: Record<string, number>;
|
||||
savings: {
|
||||
baseline_model: string;
|
||||
total_requests: number;
|
||||
baseline_total_cost: number;
|
||||
routed_total_cost: number;
|
||||
cost_saved_usd: number;
|
||||
savings_percentage: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RoutingRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
priority: number;
|
||||
is_enabled: boolean;
|
||||
condition_field: string;
|
||||
condition_operator: string;
|
||||
condition_value: string;
|
||||
action_type: string;
|
||||
action_target: string;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.css';
|
||||
Reference in New Issue
Block a user