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:
2026-03-01 02:36:32 +00:00
parent 0fe25b8aa6
commit a486373d93
18 changed files with 882 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}