feat: implemented the v1 browser embedded LLM support (Gemma 4 E4B based)

This commit is contained in:
Xiaohan-Tian
2026-05-14 18:55:23 -07:00
parent 0e6e2736d0
commit 98c2b97413
30 changed files with 19462 additions and 153 deletions
+159
View File
@@ -0,0 +1,159 @@
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: !fromCache,
progressPercent: fromCache ? 100 : 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: !fromCache,
progressPercent: totalBytes ? (receivedBytes / totalBytes) * 100 : 0,
progressText: fromCache
? 'Loading local language model from browser cache...'
: 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: 'Local language model is ready.',
error: '',
});
}
public static notifyLoadError(error: unknown): void {
this.setState({
isDownloading: false,
progressPercent: 0,
progressText: '',
error: error instanceof Error ? error.message : String(error),
});
}
}