- Phase 4: Hugging Face Download Engine — streaming GGUF downloads with progress, cancellation, and managed-model storage - Phase 5: Model Source Validation — source-mode compatibility, path/file validation, inline error UI with recovery guidance - Hot-swap: stop runtime → update model_path → restart atomic command - Model library: folder open, delete model file, local model management - SettingsPage unified hub: local models list + HF search with 3-state action buttons - Bug fixes: send button text restored (4 failing tests), reasoning-toggle aria-label, Rust warnings - All 89 tests passing, cargo check clean
210 lines
5.9 KiB
TypeScript
210 lines
5.9 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import { createDefaultRuntimeSnapshot, createRuntimeLifecycleResult } from "./runtime-utils";
|
|
import type {
|
|
HuggingFaceDownloadProgress,
|
|
ModelLibraryState,
|
|
HuggingFaceModelResult,
|
|
RuntimeConfig,
|
|
RuntimeGpuInfo,
|
|
RuntimeModelFile,
|
|
RuntimePickDirResult,
|
|
RuntimeSnapshot,
|
|
} from "./types";
|
|
|
|
const STORAGE_KEY = "forgelm_runtime_snapshot_v1";
|
|
export const HUGGING_FACE_DOWNLOAD_EVENT = "hugging-face-download-progress";
|
|
|
|
function canUseLocalStorage(): boolean {
|
|
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
}
|
|
|
|
function readFallbackSnapshot(): RuntimeSnapshot {
|
|
if (!canUseLocalStorage()) {
|
|
return createDefaultRuntimeSnapshot();
|
|
}
|
|
|
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) {
|
|
return createDefaultRuntimeSnapshot();
|
|
}
|
|
|
|
try {
|
|
return { ...createDefaultRuntimeSnapshot(), ...JSON.parse(raw) } as RuntimeSnapshot;
|
|
} catch {
|
|
return createDefaultRuntimeSnapshot();
|
|
}
|
|
}
|
|
|
|
function writeFallbackSnapshot(snapshot: RuntimeSnapshot): RuntimeSnapshot {
|
|
if (canUseLocalStorage()) {
|
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
export async function bootstrapRuntime(): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("bootstrap_runtime_command");
|
|
} catch {
|
|
return readFallbackSnapshot();
|
|
}
|
|
}
|
|
|
|
export async function getRuntimeSnapshot(): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("get_runtime_snapshot_command");
|
|
} catch {
|
|
return readFallbackSnapshot();
|
|
}
|
|
}
|
|
|
|
export async function saveRuntimeConfig(config: RuntimeConfig): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("save_runtime_config_command_handler", { config });
|
|
} catch {
|
|
const snapshot = readFallbackSnapshot();
|
|
return writeFallbackSnapshot({
|
|
...snapshot,
|
|
active_command: "idle",
|
|
last_command_result: createRuntimeLifecycleResult("save", "Save completed."),
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function startRuntime(): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("start_runtime_command");
|
|
} catch {
|
|
const snapshot = readFallbackSnapshot();
|
|
return writeFallbackSnapshot({
|
|
...snapshot,
|
|
status: "error",
|
|
active_command: "idle",
|
|
last_command_result: createRuntimeLifecycleResult(
|
|
"start",
|
|
"Local runtime control requires the Tauri desktop app.",
|
|
"failed",
|
|
),
|
|
last_error: { code: "tauri_unavailable", message: "Local runtime control requires the Tauri desktop app." },
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function stopRuntime(): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("stop_runtime_command");
|
|
} catch {
|
|
const snapshot = readFallbackSnapshot();
|
|
return writeFallbackSnapshot({
|
|
...snapshot,
|
|
status: "stopped",
|
|
process_running: false,
|
|
ready: false,
|
|
active_command: "idle",
|
|
last_command_result: createRuntimeLifecycleResult("stop", "Local API stopped."),
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function queryRuntimeGpu(): Promise<RuntimeGpuInfo | null> {
|
|
try {
|
|
return await invoke<RuntimeGpuInfo>("query_gpu");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function scanRuntimeModels(dir: string): Promise<RuntimeModelFile[]> {
|
|
return invoke<RuntimeModelFile[]>("scan_models", { dir });
|
|
}
|
|
|
|
export async function pickRuntimeModelsDir(): Promise<RuntimePickDirResult | null> {
|
|
try {
|
|
return await invoke<RuntimePickDirResult>("pick_models_dir");
|
|
} catch (error) {
|
|
if (String(error) === "cancelled") {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function pickRuntimeModelFiles(): Promise<RuntimeModelFile[]> {
|
|
try {
|
|
return await invoke<RuntimeModelFile[]>("pick_model_files");
|
|
} catch (error) {
|
|
if (String(error) === "cancelled") {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function searchHuggingFaceModels(query: string, token = ""): Promise<HuggingFaceModelResult[]> {
|
|
return invoke<HuggingFaceModelResult[]>("search_hugging_face_models_command", {
|
|
query,
|
|
token: token.trim() || null,
|
|
});
|
|
}
|
|
|
|
export async function startHuggingFaceDownload(repo: string, fileName: string, token = ""): Promise<string> {
|
|
try {
|
|
return await invoke<string>("start_hugging_face_download_command", {
|
|
repo,
|
|
fileName,
|
|
token: token.trim() || null,
|
|
});
|
|
} catch (err) {
|
|
console.error("[api] startHuggingFaceDownload failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function cancelHuggingFaceDownload(downloadId: string): Promise<void> {
|
|
try {
|
|
return await invoke<void>("cancel_hugging_face_download_command", {
|
|
downloadId,
|
|
});
|
|
} catch (err) {
|
|
console.error("[api] cancelHuggingFaceDownload failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function getModelLibraryState(): Promise<ModelLibraryState> {
|
|
return invoke<ModelLibraryState>("get_model_library_state_command");
|
|
}
|
|
|
|
/// Hot-swap: stop runtime → update model_path → restart.
|
|
export async function swapModel(newModelPath: string): Promise<RuntimeSnapshot> {
|
|
try {
|
|
return await invoke<RuntimeSnapshot>("swap_model_command", { newModelPath });
|
|
} catch (err) {
|
|
console.error("[api] swapModel failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/// Delete a local .gguf model file from disk.
|
|
export async function deleteModelFile(filePath: string): Promise<void> {
|
|
try {
|
|
return await invoke<void>("delete_model_file", { filePath });
|
|
} catch (err) {
|
|
console.error("[api] deleteModelFile failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/// Open the folder containing a model in the native file explorer.
|
|
export async function openModelFolder(filePath: string): Promise<void> {
|
|
try {
|
|
return await invoke<void>("open_model_folder_command", { filePath });
|
|
} catch (err) {
|
|
console.error("[api] openModelFolder failed:", err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export type { HuggingFaceDownloadProgress };
|