Skip to content

Commit e7b9b94

Browse files
committed
add initialization modal
1 parent 7c5f38a commit e7b9b94

6 files changed

Lines changed: 183 additions & 53 deletions

File tree

src/App.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { BrowserVideoRenderer, videoEvents } from './services/BrowserVideoRender
2121
import { RuntimeResourceModal, type ResourceSelection } from './components/RuntimeResourceModal';
2222
import { WebGPUInstructionsModal } from './components/WebGPUInstructionsModal';
2323
import { UnifiedInitModal } from './components/UnifiedInitModal';
24+
import { WebLLMLoadingModal } from './components/WebLLMLoadingModal';
2425
import { initWebLLM, webLlmEvents, checkWebGPUSupport } from './services/webLlmService';
2526

2627

@@ -39,7 +40,8 @@ function MainApp() {
3940
const [isTutorialOpen, setIsTutorialOpen] = useState(false);
4041
const [isResourceModalOpen, setIsResourceModalOpen] = useState(false);
4142
const [isWebGPUModalOpen, setIsWebGPUModalOpen] = useState(false);
42-
const [isWebLLMInitModalOpen, setIsWebLLMInitModalOpen] = useState(false);
43+
const [isWebLLMInitModalOpen, setIsWebLLMInitModalOpen] = useState(false); // For first-time download/setup
44+
const [isWebLLMLoadingOpen, setIsWebLLMLoadingOpen] = useState(false); // For subsequent cached loading
4345
const [preinstalledResources, setPreinstalledResources] = useState({ tts: false, ffmpeg: false, webllm: false });
4446
const [activeDownloads, setActiveDownloads] = useState({ tts: false, ffmpeg: false, webllm: false });
4547
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false);
@@ -141,8 +143,14 @@ function MainApp() {
141143
const model = settings.webLlmModel || 'gemma-2-2b-it-q4f32_1-MLC';
142144

143145
if (cached.webllm) {
144-
// Already cached, initialize silently in background
145-
initWebLLM(model, (progress) => console.log('WebLLM Init:', progress)).catch(console.error);
146+
// Already cached, initialize with loading modal
147+
setIsWebLLMLoadingOpen(true);
148+
initWebLLM(model, (progress) => console.log('WebLLM Init:', progress))
149+
.then(() => setIsWebLLMLoadingOpen(false))
150+
.catch((e) => {
151+
console.error(e);
152+
setIsWebLLMLoadingOpen(false);
153+
});
146154
} else if (!webLLMPreinitialized && !hideSetupModal) {
147155
// First time using WebLLM - show initialization modal
148156
// This ensures users see the progress and understand it's a one-time process
@@ -802,6 +810,11 @@ function MainApp() {
802810
onClose={() => setIsWebGPUModalOpen(false)}
803811
/>
804812

813+
<WebLLMLoadingModal
814+
isOpen={isWebLLMLoadingOpen}
815+
onComplete={() => setIsWebLLMLoadingOpen(false)}
816+
/>
817+
805818
{isWebLLMInitModalOpen && (
806819
<UnifiedInitModal
807820
isOpen={isWebLLMInitModalOpen}

src/components/SlideEditor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ const SortableSlideItem = ({
476476
}, slide.script, globalSettings?.aiFixScriptSystemPrompt);
477477
onUpdate(index, { script: transformed, selectionRanges: undefined, originalScript: slide.script });
478478
} catch (error) {
479+
console.error("[SlideEditor] Transformation Error:", error);
479480
showAlert('Transformation failed: ' + (error instanceof Error ? error.message : String(error)), { type: 'error', title: 'Transformation Failed' });
480481
} finally {
481482
setIsTransforming(false);

src/components/UnifiedInitModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export const UnifiedInitModal: React.FC<UnifiedInitModalProps> = ({
163163
) : (
164164
<>
165165
We're downloading some resources to your browser. This <strong className="text-white">one-time setup</strong> enables
166-
everything to work offline and privately. Future visits will be instant.
166+
everything to work offline and privately. Future visits will be much faster.
167167
</>
168168
)}
169169
</p>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { BrainCircuit } from 'lucide-react';
3+
import { webLlmEvents } from '../services/webLlmService';
4+
import type { InitProgressReport } from '@mlc-ai/web-llm';
5+
6+
interface WebLLMLoadingModalProps {
7+
isOpen: boolean;
8+
onComplete: () => void;
9+
}
10+
11+
export const WebLLMLoadingModal: React.FC<WebLLMLoadingModalProps> = ({ isOpen, onComplete }) => {
12+
const [progress, setProgress] = useState<InitProgressReport | null>(null);
13+
const [maxPercent, setMaxPercent] = useState(0);
14+
15+
useEffect(() => {
16+
if (!isOpen) {
17+
setProgress(null);
18+
setMaxPercent(0);
19+
return;
20+
}
21+
22+
const handleWebLLMProgress = (e: Event) => {
23+
const report = (e as CustomEvent<InitProgressReport>).detail;
24+
setProgress(report);
25+
26+
const newPercent = Math.round(report.progress * 100);
27+
setMaxPercent(prev => Math.max(prev, newPercent));
28+
29+
if (report.progress === 1) {
30+
// Determine if we should wait or close immediately
31+
// We'll let the parent close it via the Promise resolution in App.tsx for speed,
32+
// but keep a fallback here just in case.
33+
setTimeout(onComplete, 500);
34+
}
35+
};
36+
37+
webLlmEvents.addEventListener('webllm-init-progress', handleWebLLMProgress);
38+
// Also listen for pure completion just in case progress doesn't hit exactly 1 or event order is weird
39+
const handleComplete = () => {
40+
setTimeout(() => onComplete(), 500);
41+
};
42+
webLlmEvents.addEventListener('webllm-init-complete', handleComplete);
43+
44+
return () => {
45+
webLlmEvents.removeEventListener('webllm-init-progress', handleWebLLMProgress);
46+
webLlmEvents.removeEventListener('webllm-init-complete', handleComplete);
47+
};
48+
}, [isOpen, onComplete]);
49+
if (!isOpen) return null;
50+
51+
const text = progress?.text || 'Initializing AI Engine...';
52+
53+
// Use tracked max progress to avoid flickering/jumping backwards
54+
const percent = maxPercent;
55+
56+
// Extract a cleaner message for the user if possible
57+
let userMessage = text;
58+
// Common WebLLM strings to pretty print
59+
if (text.includes("Finish loading")) userMessage = "Finalizing AI engine...";
60+
else if (text.includes("Loading model")) userMessage = "Loading AI model into memory...";
61+
else if (text.includes("Fetching param")) userMessage = "Verifying model parameters...";
62+
63+
return (
64+
<div className="fixed inset-0 z-60 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-in fade-in duration-300">
65+
<div className="w-full max-w-md mx-4 bg-[#18181b] border border-white/10 rounded-2xl shadow-2xl p-6 flex flex-col items-center text-center">
66+
67+
<div className="relative mb-6">
68+
<div className="absolute inset-0 bg-cyan-500/20 blur-xl rounded-full animate-pulse" />
69+
<div className="relative p-4 bg-white/5 rounded-full border border-white/10">
70+
<BrainCircuit className="w-8 h-8 text-cyan-400" />
71+
</div>
72+
</div>
73+
74+
<h3 className="text-xl font-bold text-white mb-2">
75+
Starting AI Assistant
76+
</h3>
77+
78+
<p className="text-sm text-white/60 mb-6 max-w-[80%]">
79+
Loading the AI model into your device's graphics processor.
80+
</p>
81+
82+
<div className="w-full space-y-2">
83+
<div className="flex justify-between text-xs font-medium text-white/50 uppercase tracking-wider">
84+
<span>{userMessage}</span>
85+
<span>{percent}%</span>
86+
</div>
87+
88+
<div className="h-1.5 w-full bg-white/10 rounded-full overflow-hidden">
89+
<div
90+
className="h-full bg-cyan-400 transition-all duration-300 ease-out"
91+
style={{ width: `${percent}%` }}
92+
/>
93+
</div>
94+
</div>
95+
</div>
96+
</div>
97+
);
98+
};

src/services/aiService.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ Before writing anything, read the ENTIRE slide content carefully. Identify:
6868
6969
STEP 2 — WRITE THE NARRATION:
7070
Using your understanding of the slide's title and topic from Step 1, write a complete, natural spoken narration. The narration MUST:
71-
- Open by clearly stating the slide's title or subject so the listener immediately knows what the slide is about
72-
- Flow naturally from the title into the supporting content
73-
- Connect all fragmented text (titles, bullets, metadata) into coherent, conversational sentences
74-
- NOT hallucinate new facts — only "connect the dots" between what is already on the slide
71+
- ALWAYS begin with the exact title of the slide (or a concise subject summary if no title exists), followed immediately by a period.
72+
- Continue with complete sentences of the original slide data (transformed from broken up fragments) as if presenting the slide to a viewer.
73+
- Connect all fragmented text (titles, bullets, metadata) into coherent, conversational sentences.
74+
- NOT hallucinate new facts — only "connect the dots" between what is already on the slide.
7575
7676
Write in a conversational, engaging style. Use natural transitions such as:
77-
- "This slide covers..." or "In this section, we'll look at..." to open with the topic
77+
- After the title, start the next sentence with "This slide covers..." or "In this section, we'll look at..."
7878
- "As you can see" or "Notice that" when pointing out visual elements
7979
- "Let's explore" or "Moving on to" when transitioning between points
8080
- "This is important because" to highlight key concepts
@@ -115,35 +115,41 @@ Example Input:
115115
"How to Install Visual Studio Code on Windows A Complete Beginner's Guide Step-by-Step Instructions for First-Time Users Windows 10/11 ~5 Minutes Free & Open Source Download: https://code.visualstudio.com Download size: 85 MiB $ npm install ."
116116
117117
Example Output:
118-
This slide covers how to install Visual Studio Code on Windows. This is a complete beginner's guide with step-by-step instructions designed for first-time users. The guide is compatible with Windows 10 and Windows 11, and should take around 5 minutes to complete. Visual Studio Code is free and open-source software. You can download it from https colon slash slash code dot visualstudio dot com. The download size is approximately 85 mebibytes. To install dependencies, type npm install space period.`;
118+
How to Install Visual Studio Code on Windows. This slide covers installing Visual Studio Code on Windows. This is a complete beginner's guide with step-by-step instructions designed for first-time users. The guide is compatible with Windows 10 and Windows 11, and should take around 5 minutes to complete. Visual Studio Code is free and open-source software. You can download it from https colon slash slash code dot visualstudio dot com. The download size is approximately 85 mebibytes. To install dependencies, type npm install space period.`;
119119

120120
export const transformText = async (settings: LLMSettings, text: string, customSystemPrompt?: string): Promise<string> => {
121121
const systemPrompt = customSystemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
122122

123123
const userPrompt = `Slide Content (full text extracted from the slide):
124124
"${text}"
125125
126-
Read all of the above content, identify the slide's title and topic, then write the spoken narration.`;
126+
Read all of the above content. Start the narration with the slide's title/topic, then present the rest as complete sentences.`;
127127

128128
if (settings.useWebLLM) {
129129
if (!settings.webLlmModel) {
130130
throw new Error("WebLLM is enabled but no model is selected.");
131131
}
132-
try {
133-
// Check if WebLLM is already initialized (it should be from the setup modal)
134-
if (!isWebLLMLoaded()) {
135-
throw new Error("WebLLM is not initialized. Please load a model in Settings (WebLLM tab) first.");
136-
}
132+
133+
// Check if WebLLM is already initialized (it should be from the setup modal)
134+
if (!isWebLLMLoaded()) {
135+
throw new Error("WebLLM is not initialized. Please load a model in Settings (WebLLM tab) first.");
136+
}
137137

138+
try {
138139
const messages = [
139140
{ role: "system" as const, content: systemPrompt },
140141
{ role: "user" as const, content: userPrompt }
141142
];
142143

144+
console.log("[AI Service] Sending request to WebLLM...", { model: settings.webLlmModel, promptLength: userPrompt.length });
143145
const response = await generateWebLLMResponse(messages);
144-
return cleanLLMResponse(response);
146+
console.log("[AI Service] Raw WebLLM Response:", response);
147+
148+
const cleaned = cleanLLMResponse(response);
149+
console.log("[AI Service] Cleaned Response:", cleaned);
150+
return cleaned;
145151
} catch (error) {
146-
console.error("WebLLM Error:", error);
152+
console.error("WebLLM Error in aiService:", error);
147153
throw error;
148154
}
149155
}

src/services/webLlmService.ts

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const checkWebGPUSupport = async (): Promise<{ supported: boolean; hasF16
9898

9999
let engine: MLCEngine | null = null;
100100
let currentModelId: string | null = null;
101+
let pendingInitPromise: Promise<MLCEngine> | null = null;
101102

102103

103104
export const webLlmEvents = new EventTarget();
@@ -113,32 +114,28 @@ export const unloadWebLLM = async () => {
113114
export const initWebLLM = async (
114115
modelId: string,
115116
onProgress: InitProgressCallback
116-
) => {
117+
): Promise<MLCEngine> => {
117118
// If engine exists and is loaded with the same model, do nothing
118119
if (engine && currentModelId === modelId) {
119120
return engine;
120121
}
121122

123+
// If an initialization is already in progress for the same model, return that promise
124+
if (pendingInitPromise && currentModelId === modelId) {
125+
return pendingInitPromise;
126+
}
127+
122128
// Apply WebGPU patch for vision models that require higher workgroup invocations
123129
await patchWebGPU();
124130

125-
try {
126-
if (!engine) {
127-
// Wrap the progress callback to also dispatch events
128-
const wrappedCallback: InitProgressCallback = (report) => {
129-
// Call the original callback
130-
onProgress(report);
131-
// Dispatch event for UI components
132-
webLlmEvents.dispatchEvent(new CustomEvent('webllm-init-progress', { detail: report }));
133-
};
134-
135-
const { CreateMLCEngine } = await import("@mlc-ai/web-llm");
136-
engine = await CreateMLCEngine(modelId, { initProgressCallback: wrappedCallback });
137-
} else {
138-
// Reload/recreate engine if model changed
139-
// We'll create a new engine instance to ensure clean state and correct callback binding
140-
await engine.unload();
141-
engine = null; // Prevent access to unloaded engine
131+
// Start a new initialization
132+
pendingInitPromise = (async () => {
133+
try {
134+
if (engine) {
135+
// If switching models, unload first
136+
await engine.unload();
137+
engine = null;
138+
}
142139

143140
// Wrap the progress callback
144141
const wrappedCallback: InitProgressCallback = (report) => {
@@ -147,25 +144,30 @@ export const initWebLLM = async (
147144
};
148145

149146
const { CreateMLCEngine } = await import("@mlc-ai/web-llm");
150-
engine = await CreateMLCEngine(modelId, { initProgressCallback: wrappedCallback });
147+
const newEngine = await CreateMLCEngine(modelId, { initProgressCallback: wrappedCallback });
148+
149+
engine = newEngine;
150+
currentModelId = modelId;
151+
152+
// Dispatch final progress events
153+
webLlmEvents.dispatchEvent(new CustomEvent('webllm-init-progress', {
154+
detail: { progress: 1, text: 'Initialization complete' }
155+
}));
156+
webLlmEvents.dispatchEvent(new CustomEvent('webllm-init-complete', { detail: { modelId } }));
157+
158+
return engine;
159+
} catch (error) {
160+
console.error("Failed to initialize WebLLM:", error);
161+
engine = null;
162+
currentModelId = null;
163+
throw error;
164+
} finally {
165+
// Clear the pending promise so future calls can start fresh if needed
166+
pendingInitPromise = null;
151167
}
152-
currentModelId = modelId;
153-
154-
// Dispatch final progress event
155-
webLlmEvents.dispatchEvent(new CustomEvent('webllm-init-progress', {
156-
detail: { progress: 1, text: 'Initialization complete' }
157-
}));
158-
webLlmEvents.dispatchEvent(new CustomEvent('webllm-init-complete', { detail: { modelId } }));
168+
})();
159169

160-
return engine;
161-
} catch (error) {
162-
console.error("Failed to initialize WebLLM:", error);
163-
// CRITICAL: Clear the engine reference if initialization fails
164-
// This prevents the "Cannot pass deleted object" error on retry
165-
engine = null;
166-
currentModelId = null;
167-
throw error;
168-
}
170+
return pendingInitPromise;
169171
};
170172

171173
export const getWebLLMEngine = () => engine;
@@ -179,15 +181,25 @@ export const generateWebLLMResponse = async (
179181
}
180182

181183
try {
184+
// Ensure engine is ready (sometimes it might be in a weird state)
185+
if (!engine) {
186+
throw new Error("WebLLM Engine lost connection. Please try again.");
187+
}
188+
console.log("[WebLLM] Generating response with engine:", engine, "Model:", currentModelId);
182189
const reply = await engine.chat.completions.create({
183190
messages,
184191
temperature,
185192
stream: false, // For now, no streaming to keep it simple with existing architecture
186193
});
194+
console.log("[WebLLM] Raw Reply Object:", reply);
187195

188196
return reply.choices[0].message.content || "";
189197
} catch (error) {
190198
console.error("WebLLM Generation Error:", error);
199+
// Force reload next time if something critical failed
200+
// engine = null;
201+
// Actually, let's not force null immediately unless it's a specific error,
202+
// but the parent service (aiService) handles the retry logic now.
191203
throw error;
192204
}
193205
};

0 commit comments

Comments
 (0)