feat(memory): config embedders (LOT C2) + suggestion contextuelle (LOT C3) + contexte projet partagé
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs (List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés, environnement local détecté, stratégies compilées). UI EmbedderSettings. - LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la mémoire dépasse le budget de recall sans embedder configuré (event EmbedderSuggested, anti-spam 1×/session, « ne plus demander »). - Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les agents/profils au lancement, avant la persona. UI ProjectContextPanel. Tests : backend workspace vert (0 échec) ; frontend 306/306. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
118
frontend/src/features/embedder/useEmbedder.ts
Normal file
118
frontend/src/features/embedder/useEmbedder.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* `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<void>;
|
||||
/** Persists a profile (create/replace by id) and refreshes. */
|
||||
saveProfile: (profile: EmbedderProfile) => Promise<void>;
|
||||
/** Deletes a profile by id and refreshes. */
|
||||
deleteProfile: (embedderId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
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<EmbedderProfile[]>([]);
|
||||
const [engines, setEngines] = useState<EmbedderEngines | null>(null);
|
||||
const [error, setError] = useState<string | null>(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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user