An inference gateway over six model providers that drills its own outages: the live provider drops, the wheel reroutes, and it turns back once the provider recovers. Take one down yourself.
registry/demos/reroute-demo.tsx
"use client";
import * as React from "react";
import { Reroute, type RerouteItem, type RerouteReason } from "@/components/ui/reroute";
function Mark({ children }: { children: React.ReactNode }) {
return (
<svg viewBox="0 0 24 24" className="size-full" aria-hidden>
{children}
</svg>
);
}
// Fernwood's inference gateway and the model providers it routes between. Every name and mark is made up.
const PROVIDERS: RerouteItem[] = [
{
id: "halcyon",
name: "Halcyon 3 Turbo",
detail: "p50 182 ms · us-east",
icon: (
<Mark>
<circle cx="12" cy="12" r="9" fill="#2563eb" />
<path d="M6.5 13.2c1.8-2.4 3.6-2.4 5.5 0s3.7 2.4 5.5 0" fill="none" stroke="#fff" strokeWidth="1.9" strokeLinecap="round" />
</Mark>
),
},
{
id: "okto",
name: "Okto Large",
detail: "p50 211 ms · eu-west",
icon: (
<Mark>
<path d="M8.3 3h7.4L21 8.3v7.4L15.7 21H8.3L3 15.7V8.3z" fill="currentColor" />
<circle cx="12" cy="12" r="3.2" fill="none" strokeWidth="1.8" className="stroke-white dark:stroke-zinc-900" />
</Mark>
),
},
{
id: "brightline",
name: "Brightline 2",
detail: "p50 164 ms · us-west",
icon: (
<Mark>
<circle cx="12" cy="12" r="4.6" fill="#f59e0b" />
<g stroke="#f59e0b" strokeWidth="2" strokeLinecap="round">
<path d="M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M5.3 18.7 7 17M17 7l1.7-1.7" />
</g>
</Mark>
),
},
{
id: "vesper",
name: "Vesper Mini",
detail: "p50 96 ms · us-east",
icon: (
<Mark>
<path d="M15.8 3.4A9 9 0 1 0 20.6 15 7 7 0 0 1 15.8 3.4z" fill="#7c3aed" />
<circle cx="17.6" cy="6.4" r="1.4" fill="#c4b5fd" />
</Mark>
),
},
{
id: "corvid",
name: "Corvid R1",
detail: "p50 240 ms · ap-south",
icon: (
<Mark>
<path d="M4 18 12 4l8 14z" fill="#0f766e" />
<path d="M9.2 14.6h5.6" stroke="#5eead4" strokeWidth="1.8" strokeLinecap="round" />
</Mark>
),
},
{
id: "lumen",
name: "Lumen Edge",
detail: "p50 128 ms · eu-central",
icon: (
<Mark>
<path d="M12 2.8c.9 4.9 4.3 8.3 9.2 9.2-4.9.9-8.3 4.3-9.2 9.2-.9-4.9-4.3-8.3-9.2-9.2 4.9-.9 8.3-4.3 9.2-9.2z" fill="#e11d48" />
</Mark>
),
},
];
type Entry = { id: number; time: string; text: string; tone: "fail" | "ok" };
const clock = () =>
new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
// The outage drill: the live provider drops, comes back, then traffic moves on.
const DRILL = { down: 2400, recover: 3200, next: 2600, rest: 9000 }; // ms; `rest` is how long the drill waits after you take over
export function RerouteDemo() {
const [value, setValue] = React.useState("halcyon");
const [down, setDown] = React.useState<string[]>([]);
const [live, setLive] = React.useState<string | null>("halcyon");
const [log, setLog] = React.useState<Entry[]>([]);
const [touched, setTouched] = React.useState(0);
const seq = React.useRef(0);
const liveRef = React.useRef(live);
const prevLive = React.useRef(live);
const name = (id: string | null) => PROVIDERS.find((p) => p.id === id)?.name ?? "nothing";
const onRoute = React.useCallback((id: string | null, reason: RerouteReason) => {
const from = prevLive.current;
prevLive.current = id;
liveRef.current = id;
setLive(id);
const text =
reason === "failover"
? `${name(from)} down · rerouted to ${name(id)}`
: reason === "recover"
? `${name(id)} back · traffic returned`
: `Routing to ${name(id)}`;
seq.current += 1;
const entry: Entry = { id: seq.current, time: clock(), text, tone: reason === "failover" ? "fail" : "ok" };
setLog((l) => [entry, ...l].slice(0, 3));
}, []);
// The drill runs on its own until you press something, then waits a while.
React.useEffect(() => {
let alive = true;
const timers: number[] = [];
const wait = (ms: number) => new Promise<void>((r) => timers.push(window.setTimeout(r, ms)));
const run = async () => {
if (touched) await wait(DRILL.rest);
while (alive) {
await wait(DRILL.down);
if (!alive) return;
const victim = liveRef.current;
if (victim) setDown([victim]);
await wait(DRILL.recover);
if (!alive) return;
setDown([]);
await wait(DRILL.next);
if (!alive) return;
const at = PROVIDERS.findIndex((p) => p.id === liveRef.current);
setValue(PROVIDERS[(at + 1) % PROVIDERS.length].id);
}
};
run();
return () => {
alive = false;
timers.forEach((t) => window.clearTimeout(t));
};
}, [touched]);
const anyDown = down.length > 0;
return (
<div className="w-full max-w-xl overflow-hidden rounded-3xl border border-zinc-950/[0.08] bg-white bg-[radial-gradient(120%_55%_at_50%_0%,rgba(255,59,31,0.07),transparent)] shadow-[0_1px_2px_rgba(0,0,0,0.04)] dark:border-white/10 dark:bg-zinc-950 dark:bg-[radial-gradient(120%_55%_at_50%_0%,rgba(255,90,60,0.1),transparent)]">
<div className="flex items-center justify-between gap-3 px-5 pt-5">
<div className="min-w-0">
<p className="text-[13px] font-medium tracking-tight text-zinc-950 dark:text-white">Fernwood gateway</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400">One endpoint, six model providers</p>
</div>
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-zinc-950/[0.08] bg-white px-2.5 py-1 text-[11px] font-medium text-zinc-600 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-300">
<span className={anyDown ? "size-1.5 rounded-full bg-zinc-950 dark:bg-zinc-200" : "size-1.5 rounded-full bg-[#ff3b1f]"} />
{anyDown ? "1 provider down" : "All providers up"}
</span>
</div>
<Reroute
className="mt-2"
label="Model provider"
items={PROVIDERS}
value={value}
onValueChange={(id) => {
setValue(id);
setTouched((t) => t + 1);
}}
down={down}
onRoute={onRoute}
hub={
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M5 7h9a4 4 0 0 1 0 8H9" />
<path d="M8 12l-3 3 3 3" />
</svg>
}
/>
<div className="flex flex-col items-start gap-3 border-t border-zinc-950/[0.06] px-5 py-4 sm:flex-row sm:items-center sm:justify-between dark:border-white/[0.08]">
<ol className="w-full min-w-0 flex-1 space-y-1" aria-label="Routing log">
{log.length === 0 ? (
<li className="text-xs text-zinc-400 dark:text-zinc-500">Watching for outages…</li>
) : (
log.map((e, i) => (
<li
key={e.id}
className="flex items-center gap-2 text-xs transition-opacity duration-300"
style={{ opacity: 1 - i * 0.3 }}
>
<span className="font-mono tabular-nums text-zinc-400 dark:text-zinc-500">{e.time}</span>
<span
className={
e.tone === "fail"
? "truncate font-medium text-zinc-950 dark:text-white"
: "truncate text-zinc-600 dark:text-zinc-300"
}
>
{e.text}
</span>
</li>
))
)}
</ol>
<button
type="button"
onClick={() => {
setTouched((t) => t + 1);
setDown(anyDown ? [] : live ? [live] : []);
}}
className="h-9 shrink-0 rounded-full bg-zinc-950 px-4 text-[13px] font-medium text-white outline-offset-2 outline-[#ff3b1f] transition-transform duration-150 focus-visible:outline-2 active:scale-[0.97] dark:bg-white dark:text-zinc-950"
>
{anyDown ? `Restore ${name(down[0]).split(" ")[0]}` : `Take ${name(live).split(" ")[0]} down`}
</button>
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/reroute#reroute-demo