SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
5.7 KB · 99 lines tsx
Raw Blame History
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&apos;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