TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
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