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%
4.5 KB · 99 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { AlertCircle, Info, KeyRound, RefreshCw, Shuffle } from "lucide-react";5import { humanizeChatError, secondsLeft, type ChatErrorLike } from "@/lib/chat/humanize-error";6import { ResponsiveDialog } from "@/components/ui/sheet";7import { Button } from "@/components/ui/button";8import { CopyButton } from "@/components/ui/misc";9import { PROVIDERS } from "@/lib/client/providers";1011interface Props {12  error: ChatErrorLike;13  /** Provider id (message.provider) for the copy. */14  provider?: string | null;15  requestId?: string | null;16  /** When the error happened (ms) — drives the retry countdown. */17  since?: number;18  onRetry?: () => void;19  onSwitchModel?: () => void;20  compact?: boolean;21}2223/**24 * Humanized provider error with Retry (live countdown from `retryAfterMs`), Switch model and25 * View details (code, provider code, HTTP status, request id).26 */27export function MessageError({ error, provider, requestId, since, onRetry, onSwitchModel, compact }: Props) {28  const providerName = provider ? PROVIDERS[provider as keyof typeof PROVIDERS]?.name ?? provider : undefined;29  const h = React.useMemo(() => humanizeChatError(error, { providerName, requestId }), [error, providerName, requestId]);30  const [detailsOpen, setDetailsOpen] = React.useState(false);31  const [now, setNow] = React.useState(() => since ?? 0);32  const start = since ?? 0;33  const wait = h.retryAfterMs && since ? secondsLeft(h.retryAfterMs, start, now) : null;3435  React.useEffect(() => {36    if (!h.retryAfterMs || !since) return;37    const t = setInterval(() => setNow(Date.now()), 500);38    return () => clearInterval(t);39  }, [h.retryAfterMs, since]);4041  return (42    <div className="rounded-xl border border-danger/25 bg-danger-soft/70 px-3 py-2.5 text-[13px] text-fg" role="alert">43      <div className="flex items-start gap-2">44        <AlertCircle className="mt-0.5 size-4 shrink-0 text-danger" />45        <div className="min-w-0 flex-1">46          <p className="font-medium text-danger">{h.title}</p>47          <p className={compact ? "text-[12.5px] text-fg-muted" : "mt-0.5 text-[12.5px] text-fg-muted"}>{h.description}</p>48        </div>49      </div>50      <div className="mt-2 flex flex-wrap items-center gap-1.5 pl-6">51        {onRetry && h.canRetry ? (52          <Button size="xs" variant="outline" className="bg-bg-elevated" onClick={onRetry} disabled={wait !== null}>53            <RefreshCw /> {wait !== null ? `Retry in ${wait}s` : "Retry"}54          </Button>55        ) : null}56        {onSwitchModel && h.suggestSwitch ? (57          <Button size="xs" variant="outline" className="bg-bg-elevated" onClick={onSwitchModel}>58            <Shuffle /> Switch model59          </Button>60        ) : null}61        {h.suggestProviders ? (62          <Button asChild size="xs" variant="outline" className="bg-bg-elevated">63            <Link href="/app/settings/providers">64              <KeyRound /> Providers65            </Link>66          </Button>67        ) : null}68        <Button size="xs" variant="ghost" onClick={() => setDetailsOpen(true)}>69          <Info /> View details70        </Button>71      </div>7273      <ResponsiveDialog open={detailsOpen} onOpenChange={setDetailsOpen} title="Error details" description="Safe diagnostics — no keys or prompts are included." size="sm">74        <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 pt-1 text-[13px]">75          <Row k="Code" v={h.details.code} mono />76          <Row k="Provider" v={h.details.provider ? PROVIDERS[h.details.provider as keyof typeof PROVIDERS]?.name ?? h.details.provider : "—"} />77          <Row k="Provider code" v={h.details.providerCode ?? "—"} mono />78          <Row k="HTTP status" v={h.details.status !== null ? String(h.details.status) : "—"} mono />79          <Row k="Request id" v={h.details.requestId ?? "—"} mono />80          {h.retryAfterMs ? <Row k="Retry after" v={`${Math.ceil(h.retryAfterMs / 1000)} s`} /> : null}81          {h.details.raw ? <Row k="Provider message" v={h.details.raw} /> : null}82        </dl>83        <div className="mt-4 flex justify-end">84          <CopyButton size="sm" label="Copy details" value={JSON.stringify({ ...h.details, retryAfterMs: h.retryAfterMs }, null, 2)} />85        </div>86      </ResponsiveDialog>87    </div>88  );89}9091function Row({ k, v, mono }: { k: string; v: string; mono?: boolean }) {92  return (93    <>94      <dt className="text-fg-subtle">{k}</dt>95      <dd className={mono ? "break-all font-mono text-[12px]" : "break-words"}>{v}</dd>96    </>97  );98}99