Add dd0c/route Dashboard UI: React + Vite + Tailwind SPA
- Layout with sidebar navigation (Dashboard, Rules, Keys, Settings) - Dashboard page: stat cards, cost savings area chart (Recharts), model usage table - Rules page: routing rules CRUD with modal editor, strategy/complexity/model matching - Keys page: API key generation, copy-once reveal, revocation, quick-start code snippet - Settings page: org info, provider config, danger zone - API client (SWR + fetch wrapper) with full TypeScript types - dd0c dark theme: indigo primary, cyan accent, dark surfaces - Vite proxy config for local dev against API on :3000
This commit is contained in:
45
products/01-llm-cost-router/ui/src/components/CostChart.tsx
Normal file
45
products/01-llm-cost-router/ui/src/components/CostChart.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts';
|
||||
import type { TimeseriesPoint } from '../lib/api';
|
||||
|
||||
interface Props {
|
||||
data: TimeseriesPoint[];
|
||||
}
|
||||
|
||||
export default function CostChart({ data }: Props) {
|
||||
const formatted = data.map((d) => ({
|
||||
...d,
|
||||
time: new Date(d.bucket).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
}));
|
||||
|
||||
if (formatted.length === 0) {
|
||||
return <p className="text-dd0c-muted text-sm py-8 text-center">No data yet. Start routing requests to see savings.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<AreaChart data={formatted}>
|
||||
<defs>
|
||||
<linearGradient id="savedGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e1e2e" />
|
||||
<XAxis dataKey="time" stroke="#64748b" fontSize={11} />
|
||||
<YAxis stroke="#64748b" fontSize={11} tickFormatter={(v: number) => `$${v.toFixed(2)}`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#12121a', border: '1px solid #1e1e2e', borderRadius: 8 }}
|
||||
labelStyle={{ color: '#64748b' }}
|
||||
formatter={(value: number) => [`$${value.toFixed(4)}`, 'Saved']}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cost_saved"
|
||||
stroke="#22c55e"
|
||||
fill="url(#savedGrad)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
53
products/01-llm-cost-router/ui/src/components/Layout.tsx
Normal file
53
products/01-llm-cost-router/ui/src/components/Layout.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Outlet, NavLink } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const nav = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: '📊' },
|
||||
{ to: '/rules', label: 'Routing Rules', icon: '🔀' },
|
||||
{ to: '/keys', label: 'API Keys', icon: '🔑' },
|
||||
{ to: '/settings', label: 'Settings', icon: '⚙️' },
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-56 bg-dd0c-surface border-r border-dd0c-border flex flex-col">
|
||||
<div className="p-4 border-b border-dd0c-border">
|
||||
<h1 className="text-lg font-bold">
|
||||
<span className="text-dd0c-primary">dd0c</span>
|
||||
<span className="text-dd0c-muted">/route</span>
|
||||
</h1>
|
||||
<p className="text-xs text-dd0c-muted mt-1">LLM Cost Router</p>
|
||||
</div>
|
||||
<nav className="flex-1 p-2 space-y-1" aria-label="Main navigation">
|
||||
{nav.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors',
|
||||
isActive
|
||||
? 'bg-dd0c-primary/10 text-dd0c-primary'
|
||||
: 'text-dd0c-muted hover:text-dd0c-text hover:bg-dd0c-border/50'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-dd0c-border text-xs text-dd0c-muted">
|
||||
v0.1.0
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
products/01-llm-cost-router/ui/src/components/ModelTable.tsx
Normal file
34
products/01-llm-cost-router/ui/src/components/ModelTable.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ModelBreakdown } from '../lib/api';
|
||||
|
||||
interface Props {
|
||||
data: ModelBreakdown[];
|
||||
}
|
||||
|
||||
export default function ModelTable({ data }: Props) {
|
||||
if (data.length === 0) {
|
||||
return <p className="text-dd0c-muted text-sm py-4 text-center">No model usage data yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="w-full text-sm" role="table">
|
||||
<thead>
|
||||
<tr className="text-dd0c-muted text-xs uppercase tracking-wide border-b border-dd0c-border">
|
||||
<th className="text-left py-2 px-3">Model</th>
|
||||
<th className="text-right py-2 px-3">Requests</th>
|
||||
<th className="text-right py-2 px-3">Tokens</th>
|
||||
<th className="text-right py-2 px-3">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((m) => (
|
||||
<tr key={m.model} className="border-b border-dd0c-border/50 hover:bg-dd0c-border/20">
|
||||
<td className="py-2 px-3 font-mono text-dd0c-accent">{m.model}</td>
|
||||
<td className="py-2 px-3 text-right">{m.request_count.toLocaleString()}</td>
|
||||
<td className="py-2 px-3 text-right">{m.total_tokens.toLocaleString()}</td>
|
||||
<td className="py-2 px-3 text-right text-dd0c-warning">${m.total_cost.toFixed(4)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
27
products/01-llm-cost-router/ui/src/components/StatCard.tsx
Normal file
27
products/01-llm-cost-router/ui/src/components/StatCard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
accent?: 'primary' | 'success' | 'warning' | 'danger';
|
||||
}
|
||||
|
||||
const accentColors = {
|
||||
primary: 'text-dd0c-primary',
|
||||
success: 'text-dd0c-success',
|
||||
warning: 'text-dd0c-warning',
|
||||
danger: 'text-dd0c-danger',
|
||||
};
|
||||
|
||||
export default function StatCard({ label, value, sub, accent }: Props) {
|
||||
return (
|
||||
<div className="bg-dd0c-surface border border-dd0c-border rounded-lg p-4">
|
||||
<p className="text-xs text-dd0c-muted uppercase tracking-wide">{label}</p>
|
||||
<p className={clsx('text-2xl font-bold mt-1', accent ? accentColors[accent] : 'text-dd0c-text')}>
|
||||
{value}
|
||||
</p>
|
||||
{sub && <p className="text-xs text-dd0c-muted mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user