/** * `useEmbedder` — view-model hook for the memory/embedder settings (L14 / C2). * * Owns the configured embedder profiles and the available engines (with the * build-time feature flags). Consumes {@link EmbedderGateway} exclusively; never * touches `invoke()` or `@tauri-apps/api`, keeping the component layer testable * with mock gateways (ARCHITECTURE §1.3). * * Note: the embedder change takes effect at the *next app start* — this hook * just persists the choice; it does not hot-swap the live recall tier. */ import { useCallback, useEffect, useState } from "react"; import type { EmbedderEngines, EmbedderProfile, GatewayError } from "@/domain"; import { useGateways } from "@/app/di"; /** What the embedder settings UI needs from this hook. */ export interface EmbedderViewModel { /** The configured embedder profiles. */ profiles: EmbedderProfile[]; /** The available engines + build feature flags, or `null` until loaded. */ engines: EmbedderEngines | null; /** The single active profile (first configured), or `null` (naïve/None). */ active: EmbedderProfile | null; /** Last error message, or `null`. */ error: string | null; /** Whether a request is in flight. */ busy: boolean; /** Reloads profiles + engines. */ refresh: () => Promise; /** Persists a profile (create/replace by id) and refreshes. */ saveProfile: (profile: EmbedderProfile) => Promise; /** Deletes a profile by id and refreshes. */ deleteProfile: (embedderId: string) => Promise; } function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as GatewayError).message); } return String(e); } export function useEmbedder(): EmbedderViewModel { const { embedder } = useGateways(); const [profiles, setProfiles] = useState([]); const [engines, setEngines] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const refresh = useCallback(async () => { setBusy(true); setError(null); try { const [list, eng] = await Promise.all([ embedder.listEmbedderProfiles(), embedder.describeEmbedderEngines(), ]); setProfiles(list); setEngines(eng); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [embedder]); useEffect(() => { void refresh(); }, [refresh]); const saveProfile = useCallback( async (profile: EmbedderProfile) => { setBusy(true); setError(null); try { await embedder.saveEmbedderProfile(profile); await refresh(); } catch (e) { setError(describe(e)); setBusy(false); } }, [embedder, refresh], ); const deleteProfile = useCallback( async (embedderId: string) => { setBusy(true); setError(null); try { await embedder.deleteEmbedderProfile(embedderId); await refresh(); } catch (e) { setError(describe(e)); setBusy(false); } }, [embedder, refresh], ); // The active embedder is the first configured profile (the backend keeps a // single active embedder); `null` ⇒ naïve recall (None). const active = profiles.find((p) => p.strategy !== "none") ?? null; return { profiles, engines, active, error, busy, refresh, saveProfile, deleteProfile, }; }