Files
KGStudio/src/util/localLLMModelManager.ts
T
2026-05-14 19:12:36 -07:00

162 lines
4.7 KiB
TypeScript

import {
detectLocalLLMRuntimeSupport,
LOCAL_LLM_LEGACY_FILENAMES,
LOCAL_LLM_MODEL_FILENAME,
LOCAL_LLM_MODEL_URL,
type LocalLLMRuntimeSupport,
} from './localLLMConfig';
import { LocalLLMModelCache } from './localLLMModelCache';
export interface LocalLLMModelState {
isCached: boolean;
isChecking: boolean;
isDownloading: boolean;
isDeleting: boolean;
progressPercent: number;
progressText: string;
error: string;
runtimeSupport: LocalLLMRuntimeSupport;
}
type Listener = (state: LocalLLMModelState) => void;
export class LocalLLMModelManager {
private static listeners = new Set<Listener>();
private static initialized = false;
private static state: LocalLLMModelState = {
isCached: false,
isChecking: false,
isDownloading: false,
isDeleting: false,
progressPercent: 0,
progressText: '',
error: '',
runtimeSupport: detectLocalLLMRuntimeSupport(),
};
public static subscribe(listener: Listener): () => void {
this.listeners.add(listener);
listener(this.getState());
if (!this.initialized) {
this.initialized = true;
void this.refresh();
}
return () => this.listeners.delete(listener);
}
public static getState(): LocalLLMModelState {
return { ...this.state, runtimeSupport: { ...this.state.runtimeSupport } };
}
public static async refresh(): Promise<void> {
this.setState({
isChecking: true,
runtimeSupport: detectLocalLLMRuntimeSupport(),
});
try {
await this.cleanupLegacyEntries();
const isCached = await LocalLLMModelCache.exists();
this.setState({ isCached, error: '' });
} catch (error) {
this.setState({ error: error instanceof Error ? error.message : String(error) });
} finally {
this.setState({ isChecking: false });
}
}
public static async ensureRuntimeSupported(): Promise<void> {
const runtimeSupport = detectLocalLLMRuntimeSupport();
this.setState({ runtimeSupport });
if (!runtimeSupport.supported) {
throw new Error(runtimeSupport.reason ?? 'Local browser LLM is not supported in this browser.');
}
await this.cleanupLegacyEntries();
}
public static async deleteCachedModel(): Promise<void> {
this.setState({ isDeleting: true, error: '' });
try {
await LocalLLMModelCache.delete();
await this.cleanupLegacyEntries();
this.setState({
isCached: false,
progressPercent: 0,
progressText: '',
});
} catch (error) {
this.setState({ error: error instanceof Error ? error.message : String(error) });
throw error;
} finally {
this.setState({ isDeleting: false });
}
}
private static setState(partial: Partial<LocalLLMModelState>): void {
this.state = {
...this.state,
...partial,
};
for (const listener of this.listeners) {
listener(this.getState());
}
}
private static async cleanupLegacyEntries(): Promise<void> {
await Promise.all(
LOCAL_LLM_LEGACY_FILENAMES.map(async legacyFilename => {
try {
await LocalLLMModelCache.delete(legacyFilename);
} catch {
// Ignore best-effort legacy cleanup failures.
}
}),
);
}
public static notifyLoadStart(fromCache: boolean): void {
this.setState({
isDownloading: true,
progressPercent: 0,
progressText: fromCache ? 'Loading local language model from browser cache...' : 'Downloading local language model...',
error: '',
});
}
public static notifyLoadProgress(receivedBytes: number, totalBytes: number | null, fromCache: boolean): void {
const receivedMb = (receivedBytes / (1024 * 1024)).toFixed(1);
const totalMb = totalBytes ? (totalBytes / (1024 * 1024)).toFixed(1) : null;
this.setState({
isDownloading: true,
progressPercent: totalBytes ? (receivedBytes / totalBytes) * 100 : 0,
progressText: fromCache
? totalMb
? `Loading local language model from browser cache... ${receivedMb} / ${totalMb} MB`
: `Loading local language model from browser cache... ${receivedMb} MB`
: totalMb
? `Downloading local language model... ${receivedMb} / ${totalMb} MB`
: `Downloading local language model... ${receivedMb} MB`,
error: '',
});
}
public static notifyCacheReady(): void {
this.setState({
isCached: true,
isDownloading: false,
progressPercent: 100,
progressText: 'Using the local browser model. No external API requests are being sent.',
error: '',
});
}
public static notifyLoadError(error: unknown): void {
this.setState({
isDownloading: false,
progressPercent: 0,
progressText: '',
error: error instanceof Error ? error.message : String(error),
});
}
}