/** * `EmbedderSettings` — memory/embedder configuration panel (L14 / lot C2). * * Pure presentation: all behaviour comes from {@link useEmbedder}. Styled with * `@/shared` design system tokens; no inline styles, no `invoke()`. * * Product spirit ("Linux: nothing imposed"): * - All strategies are listed on an equal footing: None, ONNX local, Ollama * (localServer), API, Custom. * - `e5-small` (ONNX) is only *pre-selected* as the recommended default — the * cursor sits on it, but nothing is written until the user clicks "Save". * - A strategy whose Cargo feature flag is `false` (vectorOnnxEnabled / * vectorHttpEnabled) is shown disabled with "not available in this build". * `none` is always available. * - "Back to None" is always present. * - The change takes effect at the next app start — the panel says so. */ import { useEffect, useMemo, useState } from "react"; import type { EmbedderProfile, EmbedderStrategy, RecommendedOnnxEngine, } from "@/domain"; import { Button, Field, Input, Panel, Spinner, cn } from "@/shared"; import { useEmbedder } from "./useEmbedder"; /** Default endpoint placeholder for a local embedding server (Ollama). */ const LOCAL_SERVER_PLACEHOLDER = "http://localhost:11434/api/embeddings"; const API_ENDPOINT_PLACEHOLDER = "https://api.openai.com/v1/embeddings"; /** The selectable strategies, listed on equal footing. `none` is always last. */ interface StrategyOption { strategy: EmbedderStrategy; /** Stable selection key (custom shares the `api` strategy but its own form). */ key: string; label: string; } const STRATEGY_OPTIONS: StrategyOption[] = [ { strategy: "none", key: "none", label: "None (naïve recall)" }, { strategy: "localOnnx", key: "localOnnx", label: "Local ONNX" }, { strategy: "localServer", key: "localServer", label: "Ollama (local server)" }, { strategy: "api", key: "api", label: "API" }, { strategy: "api", key: "custom", label: "Custom" }, ]; /** Editable draft of the form, before it is turned into a profile and saved. */ interface Draft { id: string; name: string; model: string; endpoint: string; apiKeyEnv: string; dimension: string; /** Selected recommended ONNX engine id (localOnnx). */ onnxEngineId: string; } const EMPTY_DRAFT: Draft = { id: "", name: "", model: "", endpoint: "", apiKeyEnv: "", dimension: "", onnxEngineId: "", }; /** Builds the initial draft for a selection key, given the available engines. */ function draftFor( key: string, recommendedOnnx: RecommendedOnnxEngine[], existing: EmbedderProfile | null, ): Draft { if (existing) { return { id: existing.id, name: existing.name, model: existing.model ?? "", endpoint: existing.endpoint ?? "", apiKeyEnv: existing.apiKeyEnv ?? "", dimension: String(existing.dimension), onnxEngineId: key === "localOnnx" ? recommendedOnnx.find((e) => e.id === existing.model)?.id ?? recommendedOnnx[0]?.id ?? "" : "", }; } switch (key) { case "localOnnx": { const reco = recommendedOnnx.find((e) => e.recommended) ?? recommendedOnnx[0]; return { ...EMPTY_DRAFT, id: "onnx-local", name: reco?.displayName ?? "Local ONNX", model: reco?.id ?? "", dimension: reco ? String(reco.dimension) : "", onnxEngineId: reco?.id ?? "", }; } case "localServer": return { ...EMPTY_DRAFT, id: "ollama", name: "Ollama", endpoint: LOCAL_SERVER_PLACEHOLDER, model: "nomic-embed-text", dimension: "768", }; case "api": return { ...EMPTY_DRAFT, id: "api", name: "API embedder", endpoint: API_ENDPOINT_PLACEHOLDER, apiKeyEnv: "OPENAI_API_KEY", model: "text-embedding-3-small", dimension: "1536", }; case "custom": return { ...EMPTY_DRAFT, id: "custom", name: "Custom embedder" }; default: return { ...EMPTY_DRAFT }; } } /** Whether a strategy is compiled into this build (None always is). */ function strategyEnabled( strategy: EmbedderStrategy, flags: { vectorOnnxEnabled: boolean; vectorHttpEnabled: boolean }, ): boolean { switch (strategy) { case "none": return true; case "localOnnx": return flags.vectorOnnxEnabled; case "localServer": case "api": return flags.vectorHttpEnabled; } } export function EmbedderSettings() { const vm = useEmbedder(); // Selection cursor; pre-selected on the recommended ONNX engine. Writing // happens only on "Save". const [selectedKey, setSelectedKey] = useState("localOnnx"); const [draft, setDraft] = useState(EMPTY_DRAFT); const recommendedOnnx = vm.engines?.recommendedOnnx ?? []; const flags = { vectorOnnxEnabled: vm.engines?.vectorOnnxEnabled ?? false, vectorHttpEnabled: vm.engines?.vectorHttpEnabled ?? false, }; // Once the engines load, seed the draft for the current selection (mirroring // an existing active profile if its strategy matches the selection). useEffect(() => { if (!vm.engines) return; const opt = STRATEGY_OPTIONS.find((o) => o.key === selectedKey); const existing = vm.active && opt && vm.active.strategy === opt.strategy ? vm.active : null; setDraft(draftFor(selectedKey, vm.engines.recommendedOnnx, existing)); // We intentionally re-seed only when engines / selection / active change. // eslint-disable-next-line react-hooks/exhaustive-deps }, [vm.engines, vm.active, selectedKey]); const selectedOption = STRATEGY_OPTIONS.find((o) => o.key === selectedKey)!; const selectedStrategy = selectedOption.strategy; function patch(p: Partial) { setDraft((d) => ({ ...d, ...p })); } function selectOnnxEngine(id: string) { const eng = recommendedOnnx.find((e) => e.id === id); patch({ onnxEngineId: id, model: eng?.id ?? id, // Dimension is auto-filled from the chosen model (e5-small ⇒ 384). dimension: eng ? String(eng.dimension) : draft.dimension, }); } // ── UI-side validation, aligned with the backend (avoid an INVALID error) ── const dimensionNum = Number.parseInt(draft.dimension, 10); const validation = useMemo(() => { if (selectedStrategy === "none") return { ok: true as const }; if (!draft.id.trim()) return { ok: false as const, msg: "Id is required." }; if (!draft.name.trim()) return { ok: false as const, msg: "Name is required." }; if (!Number.isFinite(dimensionNum) || dimensionNum <= 0) return { ok: false as const, msg: "Dimension must be a positive number." }; if (selectedStrategy === "localServer" || selectedStrategy === "api") { if (!draft.endpoint.trim()) return { ok: false as const, msg: "Endpoint is required." }; } return { ok: true as const }; }, [selectedStrategy, draft, dimensionNum]); function toProfile(): EmbedderProfile { return { id: draft.id.trim(), name: draft.name.trim(), strategy: selectedStrategy, dimension: dimensionNum, model: draft.model.trim() || undefined, endpoint: selectedStrategy === "localServer" || selectedStrategy === "api" ? draft.endpoint.trim() || undefined : undefined, apiKeyEnv: selectedStrategy === "api" ? draft.apiKeyEnv.trim() || undefined : undefined, }; } async function handleSave() { if (!validation.ok) return; await vm.saveProfile(toProfile()); } async function handleBackToNone() { // Delete every configured (non-none) profile so recall falls back to naïve. for (const p of vm.profiles) { await vm.deleteProfile(p.id); } setSelectedKey("none"); } const selectClass = cn( "h-9 w-full rounded-md bg-raised px-3 text-sm text-content", "border border-border outline-none transition-colors", "focus:border-primary disabled:cursor-not-allowed disabled:opacity-50", ); const hasActive = vm.active !== null; return ( {vm.error && (

{vm.error}

)}
{/* ── Restart notice ── */}

The embedder change takes effect at the next app start.

{/* ── Active tier (current) ── */}

Current:{" "} {hasActive ? ( Vector — {vm.active!.strategy} {vm.active!.model ? ` ${vm.active!.model}` : ""} ) : ( Naïve (None) )}

{/* ── Strategy chooser ── */}
Strategy {STRATEGY_OPTIONS.map((opt) => { const enabled = strategyEnabled(opt.strategy, flags); const recommended = opt.key === "localOnnx"; return ( ); })}
{/* ── Conditional fields per strategy ── */} {selectedStrategy !== "none" && (
{/* localOnnx → model picker + auto dimension */} {selectedStrategy === "localOnnx" && ( {({ id }) => ( )} )} {/* localServer / api → endpoint */} {(selectedStrategy === "localServer" || selectedStrategy === "api") && ( {({ id }) => ( patch({ endpoint: e.target.value })} /> )} )} {/* api → apiKeyEnv (variable NAME, never the key) */} {selectedStrategy === "api" && ( {({ id, describedBy }) => ( patch({ apiKeyEnv: e.target.value })} /> )} )} {/* model (localServer / api / custom) */} {selectedStrategy !== "localOnnx" && ( {({ id }) => ( patch({ model: e.target.value })} /> )} )} {/* dimension — auto-filled for ONNX, editable everywhere */} {({ id, describedBy }) => ( patch({ dimension: e.target.value })} /> )} {/* id / name (custom & advanced) */} {({ id }) => ( patch({ id: e.target.value })} /> )} {({ id }) => ( patch({ name: e.target.value })} /> )}
)} {/* ── Actions ── */}
{vm.busy && } {selectedStrategy !== "none" && ( )}
); }