import { useEffect, useState } from "react"; import { enhanceKeyPresent } from "../api"; import type { EnhanceProfile, EnhanceProvider, Settings } from "../types"; interface Props { settings: Settings; onChange: (next: Settings) => void; } interface ProviderDef { id: EnhanceProvider; label: string; keyVar: string; bin: string; defaultModel: string; modelHint: string; } const API_PROVIDERS: ProviderDef[] = [ { id: "anthropic", label: "claude", keyVar: "ANTHROPIC_API_KEY", bin: "", defaultModel: "claude-opus-4-8", modelHint: "claude-opus-4-8, claude-sonnet-5, claude-haiku-4-5", }, { id: "gemini", label: "gemini", keyVar: "GEMINI_API_KEY", bin: "", defaultModel: "gemini-2.5-flash", modelHint: "gemini-2.5-flash, gemini-2.5-pro", }, { id: "openai", label: "openai compatible", keyVar: "OPENAI_API_KEY", bin: "", defaultModel: "gpt-4o-mini", modelHint: "any model id your endpoint serves", }, { id: "ollama", label: "ollama (local)", keyVar: "", bin: "", defaultModel: "llama3.2", modelHint: "any model pulled into ollama", }, ]; const CLI_PROVIDERS: ProviderDef[] = [ { id: "claude-cli", label: "claude code", keyVar: "", bin: "claude", defaultModel: "", modelHint: "empty = your CLI default, or sonnet, opus, a full model id", }, { id: "gemini-cli", label: "gemini cli", keyVar: "", bin: "gemini", defaultModel: "", modelHint: "empty = your CLI default, or gemini-2.5-pro", }, { id: "codex-cli", label: "codex", keyVar: "", bin: "codex", defaultModel: "", modelHint: "empty = your CLI default", }, { id: "custom-cli", label: "custom", keyVar: "", bin: "", defaultModel: "", modelHint: "model handling is up to your command", }, ]; function uniqueName(base: string, profiles: EnhanceProfile[]): string { const names = new Set(profiles.map((p) => p.name)); if (!names.has(base)) return base; let n = 2; while (names.has(`${base} ${n}`)) n++; return `${base} ${n}`; } export default function EnhancePage({ settings, onChange }: Props) { const cfg = settings.enhance; const profile = cfg.profiles.find((p) => p.name === cfg.active_profile) ?? cfg.profiles[0]; const [ready, setReady] = useState(null); const [nameDraft, setNameDraft] = useState(profile?.name ?? ""); useEffect(() => { setNameDraft(profile?.name ?? ""); }, [profile?.name]); useEffect(() => { enhanceKeyPresent().then(setReady).catch(() => setReady(null)); }, [ profile?.provider, profile?.api_key_env, profile?.cli_command, cfg.active_profile, ]); if (!profile) { return (

enhance

); } const isCli = profile.provider.endsWith("-cli"); const providers = isCli ? CLI_PROVIDERS : API_PROVIDERS; const providerDef = providers.find((p) => p.id === profile.provider) ?? providers[0]; const patchEnhance = (partial: Partial) => onChange({ ...settings, enhance: { ...cfg, ...partial } }); const patchProfile = (partial: Partial) => { const profiles = cfg.profiles.map((p) => p.name === profile.name ? { ...p, ...partial } : p, ); patchEnhance({ profiles, active_profile: partial.name ?? cfg.active_profile, }); }; const selectProfile = (name: string) => patchEnhance({ active_profile: name }); const addProfile = () => { const name = uniqueName("profile", cfg.profiles); patchEnhance({ profiles: [...cfg.profiles, { ...profile, name }], active_profile: name, }); }; const deleteProfile = () => { if (cfg.profiles.length < 2) return; const profiles = cfg.profiles.filter((p) => p.name !== profile.name); patchEnhance({ profiles, active_profile: profiles[0].name }); }; const commitRename = () => { const name = nameDraft.trim(); if (!name || name === profile.name) { setNameDraft(profile.name); return; } if (cfg.profiles.some((p) => p.name === name)) { setNameDraft(profile.name); // duplicate: revert return; } patchProfile({ name }); }; const switchBackend = (cli: boolean) => { if (cli === isCli) return; const next = cli ? CLI_PROVIDERS[0] : API_PROVIDERS[0]; patchProfile({ provider: next.id, model: next.defaultModel }); }; return (

enhance

Optionally pass the raw transcript through an LLM before it is typed. Profiles are presets you can switch between, from here or the tray menu.

Off by default. Dictation stays fully local when this is off.

{cfg.profiles.map((p) => ( ))}

The selected profile is what dictation uses. Everything below edits it.

setNameDraft(e.currentTarget.value)} onBlur={commitRename} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }} spellCheck={false} />

{isCli ? "Runs an installed CLI agent with your existing login. No API keys needed." : "Calls the provider HTTP API directly using a key from your environment."}

{providers.map((p) => ( ))}
{isCli ? ( providerDef.bin ? (

Uses the {providerDef.bin} binary from your PATH{" "} {ready === true ? ( — found ) : ready === false ? ( — not found in PATH ) : null}

) : (

Any shell command. The full prompt arrives on stdin; print only the rewritten text to stdout.

) ) : providerDef.keyVar ? (

Reads the key from{" "} {profile.api_key_env || providerDef.keyVar}{" "} {ready === true ? ( — found ) : ready === false ? ( — not set. Export it in your shell profile and restart mutterbox. ) : null}

) : (

Talks to a local ollama server. No key needed.

)}
{profile.provider === "custom-cli" && (
patchProfile({ cli_command: e.currentTarget.value })} spellCheck={false} />

Runs via sh -c. Must exit 0 and print only the result.

)} {profile.provider !== "custom-cli" && (
patchProfile({ model: e.currentTarget.value })} spellCheck={false} />

e.g. {providerDef.modelHint}

)} {(profile.provider === "openai" || profile.provider === "ollama") && (
patchProfile({ base_url: e.currentTarget.value })} spellCheck={false} />

Any OpenAI-compatible endpoint works: OpenAI, OpenRouter, llama.cpp server, vLLM, LM Studio.

)} {!isCli && profile.provider !== "ollama" && (
patchProfile({ api_key_env: e.currentTarget.value })} spellCheck={false} />

Name of an environment variable to read the API key from, if not the default. Keys are never stored by mutterbox.

)}