TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Sparkles, Wand2 } from "lucide-react";4import type { RouteResult, RouterMode } from "@/lib/client/router";5import { ROUTER_MODES, explainRoute } from "@/lib/client/router";6import { ResponsiveDialog } from "@/components/ui/sheet";7import { Button } from "@/components/ui/button";8import { Switch } from "@/components/ui/switch";9import { Segmented } from "@/components/ui/segmented";10import { ProviderIcon } from "@/components/brand/provider-icon";11import { PROVIDERS } from "@/lib/client/providers";12import { ModelPickerLauncher } from "./model-launcher";13import { cn, formatTokens } from "@/lib/utils";1415function usd(n: number | null): string {16 if (n === null) return "no public pricing";17 return n < 0.01 ? `≈ $${n.toFixed(4)}` : `≈ $${n.toFixed(2)}`;18}1920/**21 * Smart Router recommendation shown before sending when the AUTO model is selected.22 * Transparent by design: recommendation + why + estimated cost + alternatives. Nothing is sent23 * until the user taps "Use recommendation" or picks a model.24 */25export function RouterCard({ open, onOpenChange, result, mode, onModeChange, onUse, alwaysAuto, onAlwaysAutoChange }: { open: boolean; onOpenChange: (o: boolean) => void; result: RouteResult | null; mode: RouterMode; onModeChange: (m: RouterMode) => void; onUse: (modelKey: string) => void; alwaysAuto: boolean; onAlwaysAutoChange: (v: boolean) => void }) {26 const rec = result?.recommended ?? null;27 return (28 <ResponsiveDialog29 open={open}30 onOpenChange={onOpenChange}31 title={32 <span className="inline-flex items-center gap-2">33 <Wand2 className="size-4 text-accent" /> Smart Router34 </span>35 }36 description={result ? `Detected: ${result.analysis.signals.length ? result.analysis.signals.join(" · ") : `${result.analysis.task} prompt`} · ~${formatTokens(result.analysis.inputTokens)} input tokens` : undefined}37 size="md"38 footer={39 <div className="flex flex-col gap-2 sm:flex-row sm:items-center">40 <label className="flex min-h-[44px] items-center gap-2 text-[13px] text-fg-muted sm:min-h-0 sm:mr-auto">41 <Switch size="sm" checked={alwaysAuto} onCheckedChange={onAlwaysAutoChange} aria-label="Always auto-route" />42 Always auto-route (don't ask again)43 </label>44 <div className="flex gap-2">45 <ModelPickerLauncher onChange={onUse} label="Choose another" className="flex-1 sm:flex-none" variant="outline" />46 <Button variant="accent" className="flex-1 sm:flex-none" disabled={!rec} onClick={() => rec && onUse(rec.model.key)}>47 <Sparkles /> Use recommendation48 </Button>49 </div>50 </div>51 }52 >53 <div className="space-y-4 pt-1">54 <Segmented<RouterMode> value={mode} onChange={onModeChange} size="sm" ariaLabel="Routing mode" className="max-w-full" options={ROUTER_MODES.map((m) => ({ value: m.value, label: m.label }))} />55 {rec ? (56 <button type="button" onClick={() => onUse(rec.model.key)} className="flex w-full items-start gap-3 rounded-xl border border-accent/50 bg-accent-soft/40 p-3 text-left transition-colors hover:bg-accent-soft/70">57 <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border border-border bg-bg-elevated">58 <ProviderIcon provider={rec.model.provider} size={16} />59 </span>60 <span className="min-w-0 flex-1">61 <span className="block text-[11px] font-medium uppercase tracking-wide text-accent">Recommended</span>62 <span className="block truncate text-[15px] font-semibold">{rec.model.displayName}</span>63 <span className="block text-[12.5px] text-fg-muted">64 {PROVIDERS[rec.model.provider]?.name ?? rec.model.provider} · {explainRoute(rec)}65 </span>66 <span className="mt-1 block text-[12px] tabular-nums text-fg-subtle">67 Estimated cost {usd(rec.estimatedUsd)}68 {rec.model.limits?.contextTokens ? ` · ${formatTokens(rec.model.limits.contextTokens)} context` : ""}69 </span>70 </span>71 </button>72 ) : (73 <p className="rounded-xl border border-dashed border-border p-4 text-center text-[13px] text-fg-muted">No connected model fits this prompt (check vision / file / context requirements).</p>74 )}75 {result?.alternatives.length ? (76 <div>77 <p className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-fg-subtle">Alternatives</p>78 <ul className="divide-y divide-hairline overflow-hidden rounded-xl border border-border">79 {result.alternatives.map((a) => (80 <li key={a.model.key}>81 <button type="button" onClick={() => onUse(a.model.key)} className={cn("flex min-h-[48px] w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-bg-subtle")}>82 <ProviderIcon provider={a.model.provider} size={14} />83 <span className="min-w-0 flex-1">84 <span className="block truncate text-[13.5px] font-medium">{a.model.displayName}</span>85 <span className="block truncate text-[12px] text-fg-muted">{explainRoute(a)}</span>86 </span>87 <span className="shrink-0 text-[12px] tabular-nums text-fg-subtle">{usd(a.estimatedUsd)}</span>88 </button>89 </li>90 ))}91 </ul>92 </div>93 ) : null}94 <p className="text-[11.5px] text-fg-subtle">Routing is computed locally from your connected models, the prompt and the mode above — no request is sent until you confirm.</p>95 </div>96 </ResponsiveDialog>97 );98}99