- #70: implémentation suppression modèles locaux téléchargés - #100: correction scroll OpenCode - #102: correction fit TUI après switch/layout - memory note scoping UX
209 lines
6.2 KiB
TypeScript
209 lines
6.2 KiB
TypeScript
/**
|
|
* `useModelServers` — view-model for the local model server registry (F35.2).
|
|
* Owns the declared-servers list and the CRUD actions, consuming the
|
|
* {@link ModelServerGateway} exclusively (never `invoke()`), so the UI is
|
|
* testable with a mock.
|
|
*
|
|
* Delete surfaces the backend `model_server_in_use` rejection as a dedicated,
|
|
* human-readable message (a server still referenced by a profile cannot be
|
|
* removed).
|
|
*/
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import type {
|
|
GatewayError,
|
|
LocalModelServerConfig,
|
|
ModelServerCommandPreview,
|
|
} from "@/domain";
|
|
import { useGateways } from "@/app/di";
|
|
|
|
/** What the model-server UI needs from this hook. */
|
|
export interface ModelServersViewModel {
|
|
/** The declared local model servers. */
|
|
servers: LocalModelServerConfig[];
|
|
/** Last error message, or `null`. */
|
|
error: string | null;
|
|
/** Last non-blocking success message, or `null`. */
|
|
notice: string | null;
|
|
/** Whether a request is in flight. */
|
|
busy: boolean;
|
|
/** Server id whose managed artifact is currently being deleted, or `null`. */
|
|
deletingArtifactId: string | null;
|
|
/** Reloads the server list. */
|
|
reload: () => Promise<void>;
|
|
/** Creates or updates a server; returns the persisted config (or `null` on error). */
|
|
save: (config: LocalModelServerConfig) => Promise<LocalModelServerConfig | null>;
|
|
/** Deletes a server by id; returns `true` on success. */
|
|
remove: (serverId: string) => Promise<boolean>;
|
|
/** Deletes only the managed downloaded model artifact; returns `true` on success. */
|
|
deleteArtifact: (serverId: string) => Promise<boolean>;
|
|
/**
|
|
* Asks the backend to build the `llama-server` command line for a draft
|
|
* (never reconstructed client-side). Returns `null` when the draft is
|
|
* invalid — the editor shows a placeholder instead of a stale command.
|
|
*/
|
|
preview: (
|
|
config: LocalModelServerConfig,
|
|
) => Promise<ModelServerCommandPreview | null>;
|
|
/** Clears the current error banner. */
|
|
clearError: () => void;
|
|
}
|
|
|
|
function describe(e: unknown): string {
|
|
if (e && typeof e === "object" && "message" in e) {
|
|
return String((e as GatewayError).message);
|
|
}
|
|
return String(e);
|
|
}
|
|
|
|
function codeOf(e: unknown): string | undefined {
|
|
if (e && typeof e === "object" && "code" in e) {
|
|
return String((e as GatewayError).code);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function useModelServers(): ModelServersViewModel {
|
|
const { modelServer } = useGateways();
|
|
const [servers, setServers] = useState<LocalModelServerConfig[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [notice, setNotice] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [deletingArtifactId, setDeletingArtifactId] = useState<string | null>(null);
|
|
|
|
const reload = useCallback(async () => {
|
|
setBusy(true);
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
setServers(await modelServer.listModelServers());
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}, [modelServer]);
|
|
|
|
useEffect(() => {
|
|
void reload();
|
|
}, [reload]);
|
|
|
|
const save = useCallback(
|
|
async (config: LocalModelServerConfig) => {
|
|
setBusy(true);
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
const saved = await modelServer.saveModelServer(config);
|
|
setServers((prev) => {
|
|
const i = prev.findIndex((s) => s.id === saved.id);
|
|
if (i >= 0) {
|
|
const next = prev.slice();
|
|
next[i] = saved;
|
|
return next;
|
|
}
|
|
return [...prev, saved];
|
|
});
|
|
return saved;
|
|
} catch (e) {
|
|
setError(describe(e));
|
|
return null;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[modelServer],
|
|
);
|
|
|
|
const remove = useCallback(
|
|
async (serverId: string) => {
|
|
setBusy(true);
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
await modelServer.deleteModelServer(serverId);
|
|
setServers((prev) => prev.filter((s) => s.id !== serverId));
|
|
return true;
|
|
} catch (e) {
|
|
// A referenced server cannot be deleted — turn the stable backend code
|
|
// into an actionable message rather than a raw error string.
|
|
if (codeOf(e) === "model_server_in_use") {
|
|
setError(
|
|
"Ce serveur est encore utilisé par un profil : détache-le d'abord (choisis « aucun » ou un autre serveur) avant de le supprimer.",
|
|
);
|
|
} else {
|
|
setError(describe(e));
|
|
}
|
|
return false;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[modelServer],
|
|
);
|
|
|
|
const deleteArtifact = useCallback(
|
|
async (serverId: string) => {
|
|
setBusy(true);
|
|
setDeletingArtifactId(serverId);
|
|
setError(null);
|
|
setNotice(null);
|
|
try {
|
|
await modelServer.deleteModelArtifact(serverId);
|
|
setServers(await modelServer.listModelServers());
|
|
setNotice("Modèle téléchargé supprimé. Le serveur reste configuré.");
|
|
return true;
|
|
} catch (e) {
|
|
const code = codeOf(e);
|
|
if (code === "model_server_in_use") {
|
|
setError("Impossible de supprimer ce modèle : téléchargement en cours ou agent actif.");
|
|
} else if (code === "invalid") {
|
|
setError("Aucun modèle téléchargé géré à supprimer.");
|
|
} else if (code === "not_configured") {
|
|
setError("Serveur introuvable.");
|
|
} else {
|
|
setError(describe(e));
|
|
}
|
|
return false;
|
|
} finally {
|
|
setDeletingArtifactId(null);
|
|
setBusy(false);
|
|
}
|
|
},
|
|
[modelServer],
|
|
);
|
|
|
|
const preview = useCallback(
|
|
async (config: LocalModelServerConfig) => {
|
|
try {
|
|
return await modelServer.previewModelServerCommand(config);
|
|
} catch {
|
|
// A preview is best-effort UI sugar: an invalid draft (e.g. no source)
|
|
// simply yields no command, without touching the CRUD error banner.
|
|
return null;
|
|
}
|
|
},
|
|
[modelServer],
|
|
);
|
|
|
|
const clearError = useCallback(() => {
|
|
setError(null);
|
|
setNotice(null);
|
|
}, []);
|
|
|
|
return {
|
|
servers,
|
|
error,
|
|
notice,
|
|
busy,
|
|
deletingArtifactId,
|
|
reload,
|
|
save,
|
|
remove,
|
|
deleteArtifact,
|
|
preview,
|
|
clearError,
|
|
};
|
|
}
|