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%
3.4 KB · 68 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Coins } from "lucide-react";4import type { PolyModel } from "@/lib/client/types";5import type { RouteCandidate } from "@/lib/client/router";6import type { CostEstimate } from "@/lib/client/tokens";7import { ResponsiveDialog } from "@/components/ui/sheet";8import { Button } from "@/components/ui/button";9import { ProviderIcon } from "@/components/brand/provider-icon";10import { formatTokens } from "@/lib/utils";1112function usd(n: number | null): string {13  if (n === null) return "—";14  return n < 0.01 ? `$${n.toFixed(4)}` : `$${n.toFixed(2)}`;15}1617/** Shown before sending when the estimate exceeds `COST_CONFIRM_THRESHOLD_USD`; offers cheaper models with one-tap switching. */18export function CostConfirm({ open, onOpenChange, model, estimate, alternatives, onSend, onSwitch }: { open: boolean; onOpenChange: (o: boolean) => void; model: PolyModel | null | undefined; estimate: CostEstimate | null; alternatives: RouteCandidate[]; onSend: () => void; onSwitch: (modelKey: string) => void }) {19  return (20    <ResponsiveDialog21      open={open}22      onOpenChange={onOpenChange}23      title={24        <span className="inline-flex items-center gap-2">25          <Coins className="size-4 text-warning" /> This request may cost ≈ {usd(estimate?.usd ?? null)}26        </span>27      }28      description={model && estimate ? `${model.displayName} · ~${formatTokens(estimate.inputTokens)} input tokens + ~${formatTokens(estimate.outputTokens)} expected output, at provider list prices.` : undefined}29      size="sm"30      footer={31        <div className="flex gap-2 sm:justify-end">32          <Button variant="ghost" className="flex-1 sm:flex-none" onClick={() => onOpenChange(false)}>33            Cancel34          </Button>35          <Button variant="primary" className="flex-1 sm:flex-none" onClick={onSend}>36            Send anyway37          </Button>38        </div>39      }40    >41      <div className="space-y-3 pt-1">42        {alternatives.length ? (43          <div>44            <p className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-fg-subtle">Cheaper alternatives</p>45            <ul className="divide-y divide-hairline overflow-hidden rounded-xl border border-border">46              {alternatives.map((a) => (47                <li key={a.model.key}>48                  <button type="button" onClick={() => onSwitch(a.model.key)} className="flex min-h-[48px] w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-bg-subtle">49                    <ProviderIcon provider={a.model.provider} size={14} />50                    <span className="min-w-0 flex-1">51                      <span className="block truncate text-[13.5px] font-medium">{a.model.displayName}</span>52                      <span className="block truncate text-[12px] text-fg-muted">{a.reasons.length ? a.reasons.join(" · ") : "Fits this prompt"}</span>53                    </span>54                    <span className="shrink-0 text-[12.5px] font-medium tabular-nums text-success">≈ {usd(a.estimatedUsd)}</span>55                  </button>56                </li>57              ))}58            </ul>59            <p className="mt-1.5 text-[11.5px] text-fg-subtle">Tap a model to switch and send immediately.</p>60          </div>61        ) : (62          <p className="text-[13px] text-fg-muted">No cheaper connected model can take this prompt (context, vision or file requirements).</p>63        )}64      </div>65    </ResponsiveDialog>66  );67}68