import { useEffect, useState, type FormEvent } from "react"; import { LuCheck, LuChevronLeft, LuChevronRight, LuLayers, LuPlus, LuRefreshCw, LuTrash2 } from "react-icons/lu"; import { api, ApiError, type CustomMode } from "../api"; import { msg, t } from "../i18n"; import { MODE_PARAMS, paramName, type ModeParamKey } from "../mode-params"; import { useCached } from "../settings-cache"; import { isEnter } from "../shortcuts"; import { useFlash } from "../use-flash"; import { confirmDialog } from "./ConfirmDialog"; import { useUnsavedDraft } from "./Modal"; import { ModelChoice } from "./ModelChoice"; import { EffortPicker, Empty, Field, LoadFailed, Section, SwitchRow, btnCls, gridCls, inputCls, primaryCls } from "./SettingsUi"; import { ToolSwitches } from "./ToolSwitches"; /** * Settings → Modes: the person's own ways a chat can work, offered above the message box of a chat that has said * nothing yet, after Standard and Code mode. A list, and a page each: what the mode is called and says of itself, * what is said to the agent in its chats, what they start on, and their tools. */ export function ModesPanel({ onError }: { onError: (e: string) => void }) { const [modes, setModes] = useState(null); // Why the first read failed: "No modes of your own yet" would say that there are none. const [failed, setFailed] = useState(null); // A portal whose chats cannot be put in modes says so, in place of the list (as ToolSwitches does). const [refusal, setRefusal] = useState(""); const [openId, setOpenId] = useState(null); const [adding, setAdding] = useState(false); const load = async () => { try { const r = await api.customModes(); setFailed(null); setModes(r.modes); } catch (e) { if (e instanceof ApiError && e.body.code === "tools-unsupported") setRefusal(e.message); // A refresh of what is shown goes to the banner; with nothing read yet the page says it itself. else if (modes) onError((e as Error).message); else setFailed((e as Error).message); } }; useEffect(() => { void load(); }, []); if (refusal) return

{refusal}

; if (!modes && failed) return ; if (!modes) { return (

{t("Loading…")}

); } const open = modes.find((mode) => mode.id === openId); if (open) { return ( { setOpenId(null); // In the order of their names, which may have changed. void load(); }} onSaved={(next) => setModes(modes.map((mode) => (mode.id === next.id ? next : mode)))} onDeleted={() => { setOpenId(null); void load(); }} onError={onError} /> ); } return (
setAdding(true)} className={primaryCls}> {t("New mode")} ) } > {adding && ( { setAdding(false); setModes([...modes, made]); setOpenId(made.id); }} onCancel={() => setAdding(false)} onError={onError} /> )} {modes.length === 0 ? !adding && {t("No modes of your own yet.")} : (
    {modes.map((mode) => (
  • ))}
)}
); } /** Making a mode: its name and its line, then its own page for the rest. */ function NewMode({ onMade, onCancel, onError }: { onMade: (mode: CustomMode) => void; onCancel: () => void; onError: (e: string) => void }) { const [label, setLabel] = useState(""); const [description, setDescription] = useState(""); const [busy, setBusy] = useState(false); useUnsavedDraft(Boolean(label || description)); const create = async (e: FormEvent) => { e.preventDefault(); setBusy(true); try { onMade(await api.createMode({ label, description })); } catch (err) { onError((err as Error).message); setBusy(false); } }; return (
setLabel(e.target.value)} className={inputCls} /> setDescription(e.target.value)} className={inputCls} />
); } /** * A mode's page. Its texts are saved with Save; the rest as it is changed. What it starts its chats on is written into * a chat as it is put in the mode; its tools and its instructions reach the chats in it at once. */ function ModeEditor({ mode, onBack, onSaved, onDeleted, onError, }: { mode: CustomMode; onBack: () => void; onSaved: (mode: CustomMode) => void; onDeleted: () => void; onError: (e: string) => void; }) { const [label, setLabel] = useState(mode.label); const [description, setDescription] = useState(mode.description); const [instructions, setInstructions] = useState(mode.instructions); const [busy, setBusy] = useState(false); const [saved, flashSaved] = useFlash(); // What a chat would have without the mode: the defaults for new chats. const settings = useCached("settings", api.settings); const models = useCached("models", api.allModels); const dirty = label !== mode.label || description !== mode.description || instructions !== mode.instructions; useUnsavedDraft(dirty); const save = async () => { setBusy(true); try { const next = await api.updateMode(mode.id, { label, description, instructions }); onSaved(next); // As the server keeps them: on one line, trimmed. setLabel(next.label); setDescription(next.description); setInstructions(next.instructions); flashSaved(); } catch (e) { onError((e as Error).message); } finally { setBusy(false); } }; /** A change saved as it is made. Drawn once the server has it: the tools below are read again after the code tool's. False if it was refused. */ const change = async (patch: Parameters[1]): Promise => { try { onSaved(await api.updateMode(mode.id, patch)); return true; } catch (e) { onError((e as Error).message); return false; } }; const remove = async () => { const ok = await confirmDialog({ title: t("Delete {mode}?", { mode: mode.label }), message: t("Chats in it keep it, shown as not available, and work as in Standard mode from their next message."), confirmLabel: t("Delete"), danger: true, deletes: true, }); if (!ok) return; try { await api.deleteMode(mode.id); onDeleted(); } catch (e) { onError((e as Error).message); } }; return ( <>
setLabel(e.target.value)} className={inputCls} /> setDescription(e.target.value)} className={inputCls} />