// @ts-nocheck — vendored, see PROVENANCE.md "use client"; import { useRef } from "react"; import { Palette } from "../lib/tokens"; import { t, useLang } from "../lib/i18n"; import { AiSettings, PROVIDERS, Provider, providerSpec } from "../lib/ai"; import { Icon } from "./M3Node"; /** the message shown for a failed request, mapped from the error codes lib/ai throws */ export function aiErrorText(e: unknown, lang: ReturnType): string { const m = e instanceof Error ? e.message : String(e); if (m === "refusal") return t("aiErrorRefusal", lang); if (m === "json" || m === "empty") return t("aiErrorJson", lang); if (m === "model") return t("aiErrorModel", lang); if (m === "insecure") return t("aiErrorInsecure", lang); if (/failed to fetch|networkerror|load failed/i.test(m)) return t("aiErrorNetwork", lang); return `${t("aiError", lang)}: ${m}`; } export type AiActionKey = "behavior" | "describe"; /** The button that asks the model to write the field above it; spins while it works. */ export function AiWriteBtn({ p, busy, disabled, onClick, onCancel, label, title }: { p: Palette; busy: boolean; disabled?: boolean; onClick: () => void; onCancel: () => void; label: string; title: string }) { const lang = useLang(); const shown = busy ? t("cancel", lang) : title; return ( ); } const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const field = (p: Palette): React.CSSProperties => ({ width: "100%", height: 44, padding: "0 14px", borderRadius: 22, border: `1px solid ${p.outlineVariant}`, background: p.surface, color: p.onSurface, fontSize: 13, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", outline: "none", boxSizing: "border-box", }); function Input({ value, onChange, placeholder, p, type = "text", label }: { value: string; onChange: (v: string) => void; placeholder?: string; p: Palette; type?: string; label: string }) { return onChange(e.target.value)} placeholder={placeholder} aria-label={label} spellCheck={false} autoComplete="off" style={field(p)} />; } const Label = ({ children, p, right }: { children: React.ReactNode; p: Palette; right?: React.ReactNode }) => (
{children} {right}
); /** The providers as one connected button group; each segment shows the provider's mark in the button's color. */ function ProviderGroup({ value, onChange, p }: { value: Provider; onChange: (k: Provider) => void; p: Palette }) { const n = PROVIDERS.length; const ref = useRef(null); const move = (from: number, delta: number) => { const to = (from + delta + n) % n; onChange(PROVIDERS[to].key); (ref.current?.children[to] as HTMLElement | undefined)?.focus(); }; return (
{PROVIDERS.map((pr, i) => { const on = pr.key === value; const outer = 22; const inner = 8; const l = i === 0 ? outer : inner; const r = i === n - 1 ? outer : inner; return ( ); })}
); } /** The AI tab of the left rail: the provider settings. The actions live with each screen on the right. */ export function AiPanel({ p, settings, onSettings }: { p: Palette; settings: AiSettings; onSettings: (s: AiSettings) => void }) { const lang = useLang(); const spec = providerSpec(settings.provider); const pick = (k: Provider) => { const s = providerSpec(k); onSettings({ ...settings, provider: k, baseUrl: s.baseUrl, model: s.model }); }; return (
{t("aiSettings", lang)}
onSettings({ ...settings, model })} placeholder={spec.model || "model"} p={p} />
onSettings({ ...settings, baseUrl })} placeholder={spec.baseUrl} p={p} />
onSettings({ ...settings, key })} placeholder="sk-…" p={p} type="password" />
{t("aiKeyHint", lang)}
); }