khulnasoft commited on
Commit
b39fb97
·
verified ·
1 Parent(s): 49500c4

Create index.tsx

Browse files
Files changed (1) hide show
  1. index.tsx +1095 -0
index.tsx ADDED
@@ -0,0 +1,1095 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import '@tailwindcss/browser';
6
+
7
+ //Gemini 95 was fully vibe-coded by @ammaar and @olacombe, while we don't endorse code quality, we thought it was a fun demonstration of what's possible with the model when a Designer and PM jam.
8
+ //An homage to an OS that inspired so many of us!
9
+
10
+ // Define the dosInstances object to fix type errors
11
+ const dosInstances: Record<string, { initialized: boolean }> = {};
12
+
13
+ // --- DOM Element References ---
14
+ const desktop = document.getElementById('desktop') as HTMLDivElement;
15
+ const windows = document.querySelectorAll('.window') as NodeListOf<HTMLDivElement>;
16
+ const icons = document.querySelectorAll('.icon') as NodeListOf<HTMLDivElement>; // This is a NodeList
17
+ const startMenu = document.getElementById('start-menu') as HTMLDivElement;
18
+ const startButton = document.getElementById('start-button') as HTMLButtonElement;
19
+ const taskbarAppsContainer = document.getElementById('taskbar-apps') as HTMLDivElement;
20
+ const paintAssistant = document.getElementById('paint-assistant') as HTMLDivElement;
21
+ const assistantBubble = paintAssistant?.querySelector('.assistant-bubble') as HTMLDivElement;
22
+
23
+ // --- State Variables ---
24
+ let activeWindow: HTMLDivElement | null = null;
25
+ let highestZIndex: number = 20; // Start z-index for active windows
26
+ const openApps = new Map<string, { windowEl: HTMLDivElement; taskbarButton: HTMLDivElement }>(); // Store open apps and their elements
27
+ let geminiInstance: any | null = null; // Store the initialized Gemini AI instance
28
+ let paintCritiqueIntervalId: number | null = null; // Timer for paint critiques
29
+
30
+ // Store ResizeObservers to disconnect them later
31
+ const paintResizeObserverMap = new Map<Element, ResizeObserver>();
32
+
33
+ // --- Minesweeper Game State Variables ---
34
+ let minesweeperTimerInterval: number | null = null;
35
+ let minesweeperTimeElapsed: number = 0;
36
+ let minesweeperFlagsPlaced: number = 0;
37
+ let minesweeperGameOver: boolean = false;
38
+ let minesweeperMineCount: number = 10; // Default for 9x9
39
+ let minesweeperGridSize: { rows: number, cols: number } = { rows: 9, cols: 9 }; // Default 9x9
40
+ let minesweeperFirstClick: boolean = true; // To ensure first click is never a mine
41
+
42
+ // --- YouTube Player State ---
43
+ // @ts-ignore: YT will be defined by the YouTube API script
44
+ const youtubePlayers: Record<string, YT.Player | null> = {};
45
+ let ytApiLoaded = false;
46
+ let ytApiLoadingPromise: Promise<void> | null = null;
47
+
48
+ const DEFAULT_YOUTUBE_VIDEO_ID = 'WXuK6gekU1Y'; // Default video for GemPlayer ("Never Gonna Give You Up")
49
+
50
+ // --- Core Functions ---
51
+
52
+ /** Brings a window to the front and sets it as active */
53
+ function bringToFront(windowElement: HTMLDivElement): void {
54
+ if (activeWindow === windowElement) return; // Already active
55
+
56
+ if (activeWindow) {
57
+ activeWindow.classList.remove('active');
58
+ const appName = activeWindow.id;
59
+ if (openApps.has(appName)) {
60
+ openApps.get(appName)?.taskbarButton.classList.remove('active');
61
+ }
62
+ }
63
+
64
+ highestZIndex++;
65
+ windowElement.style.zIndex = highestZIndex.toString();
66
+ windowElement.classList.add('active');
67
+ activeWindow = windowElement;
68
+
69
+ const appNameRef = windowElement.id;
70
+ if (openApps.has(appNameRef)) {
71
+ openApps.get(appNameRef)?.taskbarButton.classList.add('active');
72
+ }
73
+ if ((appNameRef === 'doom' || appNameRef === 'wolf3d') && dosInstances[appNameRef]) {
74
+ const container = document.getElementById(`${appNameRef}-container`); // This ID might need checking
75
+ const canvas = container?.querySelector('canvas');
76
+ canvas?.focus();
77
+ }
78
+ }
79
+
80
+ /** Opens an application window */
81
+ async function openApp(appName: string): Promise<void> {
82
+ const windowElement = document.getElementById(appName) as HTMLDivElement | null;
83
+ if (!windowElement) {
84
+ console.error(`Window element not found for app: ${appName}`);
85
+ return;
86
+ }
87
+
88
+ if (openApps.has(appName)) {
89
+ bringToFront(windowElement);
90
+ windowElement.style.display = 'flex';
91
+ windowElement.classList.add('active');
92
+ return;
93
+ }
94
+
95
+ windowElement.style.display = 'flex';
96
+ windowElement.classList.add('active');
97
+ bringToFront(windowElement);
98
+
99
+ const taskbarButton = document.createElement('div');
100
+ taskbarButton.classList.add('taskbar-app');
101
+ taskbarButton.dataset.appName = appName;
102
+
103
+ let iconSrc = '';
104
+ let title = appName;
105
+ const iconElement = findIconElement(appName);
106
+ if (iconElement) {
107
+ const img = iconElement.querySelector('img');
108
+ const span = iconElement.querySelector('span');
109
+ if(img) iconSrc = img.src;
110
+ if(span) title = span.textContent || appName;
111
+ } else { // Fallback for apps opened via start menu but maybe no desktop icon
112
+ switch(appName) {
113
+ case 'myComputer': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/mycomputer.png'; title = 'My Gemtop'; break;
114
+ case 'chrome': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/chrome-icon-2.png'; title = 'Chrome'; break;
115
+ case 'notepad': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/GemNotes.png'; title = 'GemNotes'; break;
116
+ case 'paint': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/gempaint.png'; title = 'GemPaint'; break;
117
+ case 'doom': iconSrc = 'https://64.media.tumblr.com/1d89dfa76381e5c14210a2149c83790d/7a15f84c681c1cf9-c1/s540x810/86985984be99d5591e0cbc0dea6f05ffa3136dac.png'; title = 'Doom II'; break;
118
+ case 'gemini': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/GeminiChatRetro.png'; title = 'Gemini App'; break;
119
+ case 'minesweeper': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/gemsweeper.png'; title = 'GemSweeper'; break;
120
+ case 'imageViewer': iconSrc = 'https://win98icons.alexmeub.com/icons/png/display_properties-4.png'; title = 'Image Viewer'; break;
121
+ case 'mediaPlayer': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/ytmediaplayer.png'; title = 'GemPlayer'; break;
122
+ }
123
+ }
124
+
125
+ if (iconSrc) {
126
+ const img = document.createElement('img');
127
+ img.src = iconSrc;
128
+ img.alt = title;
129
+ taskbarButton.appendChild(img);
130
+ }
131
+ taskbarButton.appendChild(document.createTextNode(title));
132
+
133
+ taskbarButton.addEventListener('click', () => {
134
+ if (windowElement === activeWindow && windowElement.style.display !== 'none') {
135
+ minimizeApp(appName);
136
+ } else {
137
+ windowElement.style.display = 'flex';
138
+ bringToFront(windowElement);
139
+ }
140
+ });
141
+
142
+ taskbarAppsContainer.appendChild(taskbarButton);
143
+ openApps.set(appName, { windowEl: windowElement, taskbarButton: taskbarButton });
144
+ taskbarButton.classList.add('active');
145
+
146
+ // Initialize specific applications
147
+ if (appName === 'chrome') {
148
+ initAiBrowser(windowElement);
149
+ }
150
+ else if (appName === 'notepad') {
151
+ await initNotepadStory(windowElement);
152
+ }
153
+ else if (appName === 'paint') {
154
+ initSimplePaintApp(windowElement);
155
+ if (paintAssistant) paintAssistant.classList.add('visible');
156
+ if (assistantBubble) assistantBubble.textContent = 'Warming up my judging circuits...';
157
+ if (paintCritiqueIntervalId) clearInterval(paintCritiqueIntervalId);
158
+ paintCritiqueIntervalId = window.setInterval(critiquePaintDrawing, 15000);
159
+ }
160
+ else if (appName === 'doom' && !dosInstances['doom']) {
161
+ const doomContainer = document.getElementById('doom-content') as HTMLDivElement;
162
+ if (doomContainer) {
163
+ doomContainer.innerHTML = '<iframe src="https://js-dos.com/games/doom.exe.html" width="100%" height="100%" frameborder="0" scrolling="no" allowfullscreen></iframe>';
164
+ dosInstances['doom'] = { initialized: true };
165
+ }
166
+ } else if (appName === 'gemini') {
167
+ await initGeminiChat(windowElement);
168
+ }
169
+ else if (appName === 'minesweeper') {
170
+ initMinesweeperGame(windowElement);
171
+ }
172
+ else if (appName === 'myComputer') {
173
+ initMyComputer(windowElement);
174
+ }
175
+ else if (appName === 'mediaPlayer') {
176
+ await initMediaPlayer(windowElement);
177
+ }
178
+ }
179
+
180
+ /** Closes an application window */
181
+ function closeApp(appName: string): void {
182
+ const appData = openApps.get(appName);
183
+ if (!appData) return;
184
+
185
+ const { windowEl, taskbarButton } = appData;
186
+
187
+ windowEl.style.display = 'none';
188
+ windowEl.classList.remove('active');
189
+ taskbarButton.remove();
190
+ openApps.delete(appName);
191
+
192
+ if (dosInstances[appName]) {
193
+ console.log(`Cleaning up ${appName} instance (iframe approach)`);
194
+ const container = document.getElementById(`${appName}-content`);
195
+ if (container) container.innerHTML = '';
196
+ delete dosInstances[appName];
197
+ }
198
+
199
+ if (appName === 'paint') {
200
+ if (paintCritiqueIntervalId) {
201
+ clearInterval(paintCritiqueIntervalId);
202
+ paintCritiqueIntervalId = null;
203
+ if (paintAssistant) paintAssistant.classList.remove('visible');
204
+ }
205
+ const paintContent = appData.windowEl.querySelector('.window-content') as HTMLDivElement | null;
206
+ if (paintContent && paintResizeObserverMap.has(paintContent)) {
207
+ paintResizeObserverMap.get(paintContent)?.disconnect();
208
+ paintResizeObserverMap.delete(paintContent);
209
+ }
210
+ }
211
+
212
+ if (appName === 'minesweeper') {
213
+ if (minesweeperTimerInterval) {
214
+ clearInterval(minesweeperTimerInterval);
215
+ minesweeperTimerInterval = null;
216
+ }
217
+ }
218
+
219
+ if (appName === 'mediaPlayer') {
220
+ const player = youtubePlayers[appName];
221
+ if (player) {
222
+ try {
223
+ if (typeof player.stopVideo === 'function') player.stopVideo();
224
+ if (typeof player.destroy === 'function') player.destroy();
225
+ } catch (e) {
226
+ console.warn("Error stopping/destroying media player:", e);
227
+ }
228
+ delete youtubePlayers[appName];
229
+ console.log("Destroyed YouTube player for mediaPlayer.");
230
+ }
231
+ // Reset the player area with a message
232
+ const playerDivId = `youtube-player-${appName}`;
233
+ const playerDiv = document.getElementById(playerDivId) as HTMLDivElement | null;
234
+ if (playerDiv) {
235
+ playerDiv.innerHTML = `<p class="media-player-status-message">Player closed. Enter a YouTube URL to load.</p>`;
236
+ }
237
+ // Reset control buttons state (optional, but good practice)
238
+ const mediaPlayerWindow = document.getElementById('mediaPlayer');
239
+ if (mediaPlayerWindow) {
240
+ const playBtn = mediaPlayerWindow.querySelector('#media-player-play') as HTMLButtonElement;
241
+ const pauseBtn = mediaPlayerWindow.querySelector('#media-player-pause') as HTMLButtonElement;
242
+ const stopBtn = mediaPlayerWindow.querySelector('#media-player-stop') as HTMLButtonElement;
243
+ if (playBtn) playBtn.disabled = true;
244
+ if (pauseBtn) pauseBtn.disabled = true;
245
+ if (stopBtn) stopBtn.disabled = true;
246
+ }
247
+ }
248
+
249
+
250
+ if (activeWindow === windowEl) {
251
+ activeWindow = null;
252
+ let nextAppToActivate: HTMLDivElement | null = null;
253
+ let maxZ = -1;
254
+ openApps.forEach((data) => {
255
+ const z = parseInt(data.windowEl.style.zIndex || '0', 10);
256
+ if (z > maxZ) {
257
+ maxZ = z;
258
+ nextAppToActivate = data.windowEl;
259
+ }
260
+ });
261
+ if (nextAppToActivate) {
262
+ bringToFront(nextAppToActivate);
263
+ }
264
+ }
265
+ }
266
+
267
+ /** Minimizes an application window */
268
+ function minimizeApp(appName: string): void {
269
+ const appData = openApps.get(appName);
270
+ if (!appData) return;
271
+
272
+ const { windowEl, taskbarButton } = appData;
273
+
274
+ windowEl.style.display = 'none';
275
+ windowEl.classList.remove('active');
276
+ taskbarButton.classList.remove('active');
277
+
278
+ if (activeWindow === windowEl) {
279
+ activeWindow = null;
280
+ let nextAppToActivate: string | null = null;
281
+ let maxZ = 0;
282
+ openApps.forEach((data, name) => {
283
+ if (data.windowEl.style.display !== 'none') {
284
+ const z = parseInt(data.windowEl.style.zIndex || '0', 10);
285
+ if (z > maxZ) {
286
+ maxZ = z;
287
+ nextAppToActivate = name;
288
+ }
289
+ }
290
+ });
291
+ if (nextAppToActivate) {
292
+ bringToFront(openApps.get(nextAppToActivate)!.windowEl);
293
+ }
294
+ }
295
+ }
296
+
297
+ // --- Gemini Chat Specific Functions ---
298
+ async function initGeminiChat(windowElement: HTMLDivElement): Promise<void> {
299
+ const historyDiv = windowElement.querySelector('.gemini-chat-history') as HTMLDivElement;
300
+ const inputEl = windowElement.querySelector('.gemini-chat-input') as HTMLInputElement;
301
+ const sendButton = windowElement.querySelector('.gemini-chat-send') as HTMLButtonElement;
302
+
303
+ if (!historyDiv || !inputEl || !sendButton) {
304
+ console.error("Gemini chat elements not found in window:", windowElement.id);
305
+ return;
306
+ }
307
+
308
+ function addChatMessage(container: HTMLDivElement, text: string, className: string = '') {
309
+ const p = document.createElement('p');
310
+ if (className) p.classList.add(className);
311
+ p.textContent = text;
312
+ container.appendChild(p);
313
+ container.scrollTop = container.scrollHeight;
314
+ }
315
+
316
+ addChatMessage(historyDiv, "Initializing AI...", "system-message");
317
+
318
+ const sendMessage = async () => {
319
+ if (!geminiInstance) {
320
+ const initSuccess = await initializeGeminiIfNeeded('initGeminiChat');
321
+ if (!initSuccess) {
322
+ addChatMessage(historyDiv, "Error: Failed to initialize AI.", "error-message");
323
+ return;
324
+ }
325
+ const initMsg = Array.from(historyDiv.children).find(el => el.textContent?.includes("Initializing AI..."));
326
+ if (initMsg) initMsg.remove();
327
+ addChatMessage(historyDiv, "AI Ready.", "system-message");
328
+ }
329
+
330
+ const message = inputEl.value.trim();
331
+ if (!message) return;
332
+
333
+ addChatMessage(historyDiv, `You: ${message}`, "user-message");
334
+ inputEl.value = '';
335
+ inputEl.disabled = true;
336
+ sendButton.disabled = true;
337
+
338
+ try {
339
+ // @ts-ignore
340
+ const chat = geminiInstance.chats.create({ model: 'gemini-2.5-flash', history: [] });
341
+ // @ts-ignore
342
+ const result = await chat.sendMessageStream({message: message});
343
+ let fullResponse = "";
344
+ addChatMessage(historyDiv, "Gemini: ", "gemini-message");
345
+ const lastMessageElement = historyDiv.lastElementChild as HTMLParagraphElement | null;
346
+ for await (const chunk of result) {
347
+ const chunkText = chunk.text || "";
348
+ fullResponse += chunkText;
349
+ if (lastMessageElement) {
350
+ lastMessageElement.textContent += chunkText;
351
+ historyDiv.scrollTop = historyDiv.scrollHeight;
352
+ }
353
+ }
354
+ } catch (error: any) {
355
+ addChatMessage(historyDiv, `Error: ${error.message || 'Failed to get response.'}`, "error-message");
356
+ } finally {
357
+ inputEl.disabled = false; sendButton.disabled = false; inputEl.focus();
358
+ }
359
+ };
360
+ sendButton.onclick = sendMessage;
361
+ inputEl.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } };
362
+ inputEl.disabled = false; sendButton.disabled = false; inputEl.focus();
363
+ }
364
+
365
+ /** Handles Notepad story generation */
366
+ async function initNotepadStory(windowElement: HTMLDivElement): Promise<void> {
367
+ const textarea = windowElement.querySelector('.notepad-textarea') as HTMLTextAreaElement;
368
+ const storyButton = windowElement.querySelector('.notepad-story-button') as HTMLButtonElement;
369
+ if (!textarea || !storyButton) return;
370
+
371
+ storyButton.addEventListener('click', async () => {
372
+ const currentText = textarea.value;
373
+ textarea.value = currentText + "\n\nGenerating story... Please wait...\n\n";
374
+ textarea.scrollTop = textarea.scrollHeight;
375
+ storyButton.disabled = true; storyButton.textContent = "Working...";
376
+ try {
377
+ if (!geminiInstance) {
378
+ if (!await initializeGeminiIfNeeded('initNotepadStory')) throw new Error("Failed to initialize Gemini API.");
379
+ }
380
+ const prompt = "Write me a short creative story (250-300 words) with an unexpected twist ending. Make it engaging and suitable for all ages.";
381
+ // @ts-ignore
382
+ const result = await geminiInstance.models.generateContentStream({ model: 'gemini-2.5-flash', contents: prompt });
383
+ textarea.value = currentText + "\n\n";
384
+ for await (const chunk of result) {
385
+ textarea.value += chunk.text || "";
386
+ textarea.scrollTop = textarea.scrollHeight;
387
+ }
388
+ textarea.value += "\n\n";
389
+ } catch (error: any) {
390
+ textarea.value = currentText + "\n\nError: " + (error.message || "Failed to generate story.") + "\n\n";
391
+ } finally {
392
+ storyButton.disabled = false; storyButton.textContent = "Generate Story";
393
+ textarea.scrollTop = textarea.scrollHeight;
394
+ }
395
+ });
396
+ }
397
+
398
+ /** Initializes the AI Browser functionality with image generation */
399
+ function initAiBrowser(windowElement: HTMLDivElement): void {
400
+ const addressBar = windowElement.querySelector('.browser-address-bar') as HTMLInputElement;
401
+ const goButton = windowElement.querySelector('.browser-go-button') as HTMLButtonElement;
402
+ const iframe = windowElement.querySelector('#browser-frame') as HTMLIFrameElement;
403
+ const loadingEl = windowElement.querySelector('.browser-loading') as HTMLDivElement;
404
+ const DIAL_UP_SOUND_URL = 'https://www.soundjay.com/communication/dial-up-modem-01.mp3';
405
+ let dialUpAudio: HTMLAudioElement | null = null;
406
+
407
+ if (!addressBar || !goButton || !iframe || !loadingEl) return;
408
+
409
+ async function navigateToUrl(url: string): Promise<void> {
410
+ if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url;
411
+ try {
412
+ const urlObj = new URL(url);
413
+ const domain = urlObj.hostname;
414
+
415
+ // --- RESTORED DIALUP ANIMATION ---
416
+ loadingEl.innerHTML = `
417
+ <style>
418
+ .dialup-animation .dot {
419
+ animation: dialup-blink 1.4s infinite both;
420
+ }
421
+ .dialup-animation .dot:nth-child(2) {
422
+ animation-delay: 0.2s;
423
+ }
424
+ .dialup-animation .dot:nth-child(3) {
425
+ animation-delay: 0.4s;
426
+ }
427
+ @keyframes dialup-blink {
428
+ 0%, 80%, 100% { opacity: 0; }
429
+ 40% { opacity: 1; }
430
+ }
431
+ .browser-loading p { margin: 5px 0; }
432
+ .browser-loading .small-text { font-size: 0.8em; color: #aaa; }
433
+ </style>
434
+ <img src="https://d112y698adiu2z.cloudfront.net/photos/production/software_photos/000/948/341/datas/original.gif"/>
435
+ <p>Connecting to ${domain}<span class="dialup-animation"><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span></span></p>
436
+ <!-- Sound will play via JS -->
437
+ `;
438
+ loadingEl.style.display = 'flex';
439
+ // --- END RESTORED DIALUP ANIMATION ---
440
+
441
+ try {
442
+ if (!dialUpAudio) {
443
+ dialUpAudio = new Audio(DIAL_UP_SOUND_URL); dialUpAudio.loop = true;
444
+ }
445
+ await dialUpAudio.play();
446
+ } catch (audioError) { console.error("Dial-up sound error:", audioError); }
447
+
448
+ try {
449
+ if (!geminiInstance) {
450
+ if (!await initializeGeminiIfNeeded('initAiBrowser')) {
451
+ iframe.src = 'data:text/plain;charset=utf-8,AI Init Error';
452
+ loadingEl.style.display = 'none'; return;
453
+ }
454
+ }
455
+ const websitePrompt = `
456
+ Create a complete 90s-style website for the domain "${domain}".
457
+ MUST include: 1 relevant image, garish 90s styling (neon, comic sans, tables), content specific to "${domain}", scrolling marquee, retro emoji/ascii, blinking text, visitor counter (9000+), "Under Construction" signs. Fun, humorous, 1996 feel. Image MUST match theme. No modern design.
458
+ `;
459
+ // @ts-ignore
460
+ const result = await geminiInstance.models.generateContent({
461
+ model: 'gemini-2.0-flash-preview-image-generation',
462
+ contents: [{role: 'user', parts: [{text: websitePrompt}]}],
463
+ config: { temperature: 0.9, responseModalities: ['TEXT', 'IMAGE'] }
464
+ });
465
+ let htmlContent = ""; const images: string[] = [];
466
+ if (result.candidates?.[0]?.content) {
467
+ for (const part of result.candidates[0].content.parts) {
468
+ if (part.text) htmlContent += part.text.replace(/```html|```/g, '').trim();
469
+ else if (part.inlineData?.data) images.push(`data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`);
470
+ }
471
+ }
472
+ if (!htmlContent.includes("<html")) {
473
+ htmlContent = `<!DOCTYPE html><html><head><title>${domain}</title><style>body{font-family:"Comic Sans MS";background:lime;color:blue;}marquee{background:yellow;color:red;}img{max-width:80%; display:block; margin:10px auto; border: 3px ridge gray;}</style></head><body><marquee>Welcome to ${domain}!</marquee><h1>${domain}</h1><div>${htmlContent}</div></body></html>`;
474
+ }
475
+ if (images.length > 0) {
476
+ if (!htmlContent.includes('<img src="data:')) {
477
+ htmlContent = htmlContent.replace(/(<\/h1>)/i, `$1\n<img src="${images[0]}" alt="Site Image">`);
478
+ }
479
+ }
480
+ iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent);
481
+ addressBar.value = url;
482
+ } catch (e: any) {
483
+ iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(`<html><body>Error generating site: ${e.message}</body></html>`);
484
+ } finally {
485
+ loadingEl.style.display = 'none';
486
+ if (dialUpAudio) { dialUpAudio.pause(); dialUpAudio.currentTime = 0; }
487
+ }
488
+ } catch (e) { alert("Invalid URL"); loadingEl.style.display = 'none'; }
489
+ }
490
+ goButton.addEventListener('click', () => navigateToUrl(addressBar.value));
491
+ addressBar.addEventListener('keydown', (e) => { if (e.key === 'Enter') navigateToUrl(addressBar.value); });
492
+ addressBar.addEventListener('click', () => addressBar.select());
493
+ }
494
+ // --- Event Listeners Setup ---
495
+
496
+ icons.forEach(icon => {
497
+ icon.addEventListener('click', () => {
498
+ const appName = icon.getAttribute('data-app');
499
+ if (appName) {
500
+ openApp(appName);
501
+ startMenu.classList.remove('active');
502
+ }
503
+ });
504
+ });
505
+
506
+ document.querySelectorAll('.start-menu-item').forEach(item => {
507
+ item.addEventListener('click', () => {
508
+ const appName = (item as HTMLElement).getAttribute('data-app');
509
+ if (appName) openApp(appName);
510
+ startMenu.classList.remove('active');
511
+ });
512
+ });
513
+
514
+ startButton.addEventListener('click', (e) => {
515
+ e.stopPropagation();
516
+ startMenu.classList.toggle('active');
517
+ if (startMenu.classList.contains('active')) {
518
+ highestZIndex++;
519
+ startMenu.style.zIndex = highestZIndex.toString();
520
+ }
521
+ });
522
+
523
+ windows.forEach(windowElement => {
524
+ const titleBar = windowElement.querySelector('.window-titlebar') as HTMLDivElement | null;
525
+ const closeButton = windowElement.querySelector('.window-close') as HTMLDivElement | null;
526
+ const minimizeButton = windowElement.querySelector('.window-minimize') as HTMLDivElement | null;
527
+
528
+ windowElement.addEventListener('mousedown', () => bringToFront(windowElement), true);
529
+
530
+ if (closeButton) {
531
+ closeButton.addEventListener('click', (e) => { e.stopPropagation(); closeApp(windowElement.id); });
532
+ }
533
+ if (minimizeButton) {
534
+ minimizeButton.addEventListener('click', (e) => { e.stopPropagation(); minimizeApp(windowElement.id); });
535
+ }
536
+
537
+ if (titleBar) {
538
+ let isDragging = false;
539
+ let dragOffsetX: number, dragOffsetY: number;
540
+ const startDragging = (e: MouseEvent) => {
541
+ if (!(e.target === titleBar || titleBar.contains(e.target as Node)) || (e.target as Element).closest('.window-control-button')) {
542
+ isDragging = false; return;
543
+ }
544
+ isDragging = true; bringToFront(windowElement);
545
+ const rect = windowElement.getBoundingClientRect();
546
+ dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top;
547
+ titleBar.style.cursor = 'grabbing';
548
+ document.addEventListener('mousemove', dragWindow);
549
+ document.addEventListener('mouseup', stopDragging, { once: true });
550
+ };
551
+ const dragWindow = (e: MouseEvent) => {
552
+ if (!isDragging) return;
553
+ let x = e.clientX - dragOffsetX; let y = e.clientY - dragOffsetY;
554
+ const taskbarHeight = taskbarAppsContainer.parentElement?.offsetHeight ?? 36;
555
+ const maxX = window.innerWidth - windowElement.offsetWidth;
556
+ const maxY = window.innerHeight - windowElement.offsetHeight - taskbarHeight;
557
+ const minX = -(windowElement.offsetWidth - 40);
558
+ const maxXAdjusted = window.innerWidth - 40;
559
+ x = Math.max(minX, Math.min(x, maxXAdjusted));
560
+ y = Math.max(0, Math.min(y, maxY));
561
+ windowElement.style.left = `${x}px`; windowElement.style.top = `${y}px`;
562
+ };
563
+ const stopDragging = () => {
564
+ if (!isDragging) return;
565
+ isDragging = false; titleBar.style.cursor = 'grab';
566
+ document.removeEventListener('mousemove', dragWindow);
567
+ };
568
+ titleBar.addEventListener('mousedown', startDragging);
569
+ }
570
+
571
+ if (!openApps.has(windowElement.id)) { // Only apply random for newly opened, not for bringToFront
572
+ const randomTop = Math.random() * (window.innerHeight / 4) + 20;
573
+ const randomLeft = Math.random() * (window.innerWidth / 3) + 20;
574
+ windowElement.style.top = `${randomTop}px`;
575
+ windowElement.style.left = `${randomLeft}px`;
576
+ }
577
+ });
578
+
579
+ document.addEventListener('click', (e) => {
580
+ if (startMenu.classList.contains('active') && !startMenu.contains(e.target as Node) && !startButton.contains(e.target as Node)) {
581
+ startMenu.classList.remove('active');
582
+ }
583
+ });
584
+
585
+ function findIconElement(appName: string): HTMLDivElement | undefined {
586
+ return Array.from(icons).find(icon => icon.dataset.app === appName);
587
+ }
588
+
589
+ console.log("Gemini 95 Simulator Initialized (TS)");
590
+
591
+ async function critiquePaintDrawing(): Promise<void> {
592
+ const paintWindow = document.getElementById('paint') as HTMLDivElement | null;
593
+ if (!paintWindow || paintWindow.style.display === 'none') return;
594
+ const canvas = paintWindow.querySelector('#paint-canvas') as HTMLCanvasElement | null;
595
+ if (!canvas) { if (assistantBubble) assistantBubble.textContent = 'Error: Canvas not found!'; return; }
596
+ if (!geminiInstance) {
597
+ if (!await initializeGeminiIfNeeded('critiquePaintDrawing')) {
598
+ if (assistantBubble) assistantBubble.textContent = 'Error: AI init failed!'; return;
599
+ }
600
+ }
601
+ try {
602
+ if (assistantBubble) assistantBubble.textContent = 'Analyzing...';
603
+ const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8);
604
+ const base64Data = imageDataUrl.split(',')[1];
605
+ if (!base64Data) throw new Error("Failed to get base64 data.");
606
+ const prompt = "Critique this drawing with witty sarcasm (1-2 sentences).";
607
+ const imagePart = { inlineData: { data: base64Data, mimeType: "image/jpeg" } };
608
+ // @ts-ignore
609
+ const result = await geminiInstance.models.generateContent({ model: "gemini-2.5-pro-exp-03-25", contents: [{ role: "user", parts: [ { text: prompt }, imagePart] }] });
610
+ const critique = result?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "Is this art?";
611
+ if (assistantBubble) assistantBubble.textContent = critique;
612
+ } catch (error: any) {
613
+ if (assistantBubble) assistantBubble.textContent = `Critique Error: ${error.message}`;
614
+ }
615
+ }
616
+
617
+ function initSimplePaintApp(windowElement: HTMLDivElement): void {
618
+ const canvas = windowElement.querySelector('#paint-canvas') as HTMLCanvasElement;
619
+ const toolbar = windowElement.querySelector('.paint-toolbar') as HTMLDivElement;
620
+ const contentArea = windowElement.querySelector('.window-content') as HTMLDivElement; // This is the direct parent managing canvas size
621
+ const colorSwatches = windowElement.querySelectorAll('.paint-color-swatch') as NodeListOf<HTMLButtonElement>;
622
+ const sizeButtons = windowElement.querySelectorAll('.paint-size-button') as NodeListOf<HTMLButtonElement>;
623
+ const clearButton = windowElement.querySelector('.paint-clear-button') as HTMLButtonElement;
624
+
625
+ if (!canvas || !toolbar || !contentArea || !clearButton) { return; }
626
+ const ctx = canvas.getContext('2d');
627
+ if (!ctx) { return; }
628
+
629
+ let isDrawing = false; let lastX = 0; let lastY = 0;
630
+ ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
631
+ let currentStrokeStyle = ctx.strokeStyle; let currentLineWidth = ctx.lineWidth;
632
+
633
+ function resizeCanvas() {
634
+ const rect = contentArea.getBoundingClientRect();
635
+ const toolbarHeight = toolbar.offsetHeight;
636
+ const newWidth = Math.floor(rect.width); // Canvas width is content area width
637
+ const newHeight = Math.floor(rect.height - toolbarHeight); // Canvas height is content area height minus toolbar
638
+
639
+ if (canvas.width === newWidth && canvas.height === newHeight && newWidth > 0 && newHeight > 0) return;
640
+
641
+ canvas.width = newWidth > 0 ? newWidth : 1;
642
+ canvas.height = newHeight > 0 ? newHeight : 1;
643
+
644
+ ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height);
645
+ ctx.strokeStyle = currentStrokeStyle; ctx.lineWidth = currentLineWidth;
646
+ ctx.lineJoin = 'round'; ctx.lineCap = 'round';
647
+ }
648
+
649
+ const resizeObserver = new ResizeObserver(resizeCanvas);
650
+ resizeObserver.observe(contentArea);
651
+ paintResizeObserverMap.set(contentArea, resizeObserver);
652
+ resizeCanvas();
653
+
654
+ function getMousePos(canvasDom: HTMLCanvasElement, event: MouseEvent | TouchEvent): { x: number, y: number } {
655
+ const rect = canvasDom.getBoundingClientRect();
656
+ let clientX, clientY;
657
+ if (event instanceof MouseEvent) { clientX = event.clientX; clientY = event.clientY; }
658
+ else { clientX = event.touches[0].clientX; clientY = event.touches[0].clientY; }
659
+ return { x: clientX - rect.left, y: clientY - rect.top };
660
+ }
661
+ function startDrawing(e: MouseEvent | TouchEvent) {
662
+ isDrawing = true; const pos = getMousePos(canvas, e);
663
+ [lastX, lastY] = [pos.x, pos.y]; ctx.beginPath(); ctx.moveTo(lastX, lastY);
664
+ }
665
+ function draw(e: MouseEvent | TouchEvent) {
666
+ if (!isDrawing) return; e.preventDefault();
667
+ const pos = getMousePos(canvas, e);
668
+ ctx.lineTo(pos.x, pos.y); ctx.stroke();
669
+ [lastX, lastY] = [pos.x, pos.y];
670
+ }
671
+ function stopDrawing() { if (isDrawing) isDrawing = false; }
672
+
673
+ canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw);
674
+ canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseleave', stopDrawing);
675
+ canvas.addEventListener('touchstart', startDrawing, { passive: false });
676
+ canvas.addEventListener('touchmove', draw, { passive: false });
677
+ canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing);
678
+
679
+ colorSwatches.forEach(swatch => {
680
+ swatch.addEventListener('click', () => {
681
+ ctx.strokeStyle = swatch.dataset.color || 'black'; currentStrokeStyle = ctx.strokeStyle;
682
+ colorSwatches.forEach(s => s.classList.remove('active')); swatch.classList.add('active');
683
+ if (swatch.dataset.color === 'white') {
684
+ const largeSizeButton = Array.from(sizeButtons).find(b => b.dataset.size === '10');
685
+ if (largeSizeButton) {
686
+ ctx.lineWidth = parseInt(largeSizeButton.dataset.size || '10', 10); currentLineWidth = ctx.lineWidth;
687
+ sizeButtons.forEach(s => s.classList.remove('active')); largeSizeButton.classList.add('active');
688
+ }
689
+ } else {
690
+ const activeSizeButton = Array.from(sizeButtons).find(b => b.classList.contains('active'));
691
+ if (activeSizeButton) { ctx.lineWidth = parseInt(activeSizeButton.dataset.size || '2', 10); currentLineWidth = ctx.lineWidth; }
692
+ }
693
+ });
694
+ });
695
+ sizeButtons.forEach(button => {
696
+ button.addEventListener('click', () => {
697
+ ctx.lineWidth = parseInt(button.dataset.size || '2', 10); currentLineWidth = ctx.lineWidth;
698
+ sizeButtons.forEach(s => s.classList.remove('active')); button.classList.add('active');
699
+ const eraser = Array.from(colorSwatches).find(s => s.dataset.color === 'white');
700
+ if (!eraser?.classList.contains('active')) {
701
+ if (!Array.from(colorSwatches).some(s => s.classList.contains('active'))) {
702
+ const blackSwatch = Array.from(colorSwatches).find(s => s.dataset.color === 'black');
703
+ blackSwatch?.classList.add('active'); ctx.strokeStyle = 'black'; currentStrokeStyle = ctx.strokeStyle;
704
+ }
705
+ }
706
+ });
707
+ });
708
+ clearButton.addEventListener('click', () => {
709
+ ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height);
710
+ });
711
+ (windowElement.querySelector('.paint-color-swatch[data-color="black"]') as HTMLButtonElement)?.classList.add('active');
712
+ (windowElement.querySelector('.paint-size-button[data-size="2"]') as HTMLButtonElement)?.classList.add('active');
713
+ }
714
+
715
+ type MinesweeperCell = { isMine: boolean; isRevealed: boolean; isFlagged: boolean; adjacentMines: number; element: HTMLDivElement; row: number; col: number; };
716
+ function initMinesweeperGame(windowElement: HTMLDivElement): void {
717
+ const boardElement = windowElement.querySelector('#minesweeper-board') as HTMLDivElement;
718
+ const flagCountElement = windowElement.querySelector('.minesweeper-flag-count') as HTMLDivElement;
719
+ const timerElement = windowElement.querySelector('.minesweeper-timer') as HTMLDivElement;
720
+ const resetButton = windowElement.querySelector('.minesweeper-reset-button') as HTMLButtonElement;
721
+ const hintButton = windowElement.querySelector('.minesweeper-hint-button') as HTMLButtonElement;
722
+ const commentaryElement = windowElement.querySelector('.minesweeper-commentary') as HTMLDivElement;
723
+ if (!boardElement || !flagCountElement || !timerElement || !resetButton || !hintButton || !commentaryElement) return;
724
+ let grid: MinesweeperCell[][] = [];
725
+ function resetGame() {
726
+ if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval);
727
+ minesweeperTimerInterval = null; minesweeperTimeElapsed = 0; minesweeperFlagsPlaced = 0;
728
+ minesweeperGameOver = false; minesweeperFirstClick = true; minesweeperMineCount = 10;
729
+ minesweeperGridSize = { rows: 9, cols: 9 };
730
+ timerElement.textContent = `⏱️ 0`; flagCountElement.textContent = `🚩 ${minesweeperMineCount}`;
731
+ resetButton.textContent = '🙂'; commentaryElement.textContent = "Let's play! Click a square.";
732
+ createGrid();
733
+ }
734
+ function createGrid() {
735
+ boardElement.innerHTML = ''; grid = [];
736
+ boardElement.style.gridTemplateColumns = `repeat(${minesweeperGridSize.cols}, 20px)`;
737
+ boardElement.style.gridTemplateRows = `repeat(${minesweeperGridSize.rows}, 20px)`;
738
+ for (let r = 0; r < minesweeperGridSize.rows; r++) {
739
+ const row: MinesweeperCell[] = [];
740
+ for (let c = 0; c < minesweeperGridSize.cols; c++) {
741
+ const cellElement = document.createElement('div'); cellElement.classList.add('minesweeper-cell');
742
+ const cellData: MinesweeperCell = { isMine: false, isRevealed: false, isFlagged: false, adjacentMines: 0, element: cellElement, row: r, col: c };
743
+ cellElement.addEventListener('click', () => handleCellClick(cellData));
744
+ cellElement.addEventListener('contextmenu', (e) => { e.preventDefault(); handleCellRightClick(cellData); });
745
+ row.push(cellData); boardElement.appendChild(cellElement);
746
+ }
747
+ grid.push(row);
748
+ }
749
+ }
750
+ function placeMines(firstClickRow: number, firstClickCol: number) {
751
+ let minesPlaced = 0;
752
+ while (minesPlaced < minesweeperMineCount) {
753
+ const r = Math.floor(Math.random() * minesweeperGridSize.rows);
754
+ const c = Math.floor(Math.random() * minesweeperGridSize.cols);
755
+ if ((r === firstClickRow && c === firstClickCol) || grid[r][c].isMine) continue;
756
+ grid[r][c].isMine = true; minesPlaced++;
757
+ }
758
+ for (let r = 0; r < minesweeperGridSize.rows; r++) {
759
+ for (let c = 0; c < minesweeperGridSize.cols; c++) {
760
+ if (!grid[r][c].isMine) grid[r][c].adjacentMines = countAdjacentMines(r, c);
761
+ }
762
+ }
763
+ }
764
+ function countAdjacentMines(row: number, col: number): number {
765
+ let count = 0;
766
+ for (let dr = -1; dr <= 1; dr++) {
767
+ for (let dc = -1; dc <= 1; dc++) {
768
+ if (dr === 0 && dc === 0) continue;
769
+ const nr = row + dr; const nc = col + dc;
770
+ if (nr >= 0 && nr < minesweeperGridSize.rows && nc >= 0 && nc < minesweeperGridSize.cols && grid[nr][nc].isMine) count++;
771
+ }
772
+ }
773
+ return count;
774
+ }
775
+ function handleCellClick(cell: MinesweeperCell) {
776
+ if (minesweeperGameOver || cell.isRevealed || cell.isFlagged) return;
777
+ if (minesweeperFirstClick && !minesweeperTimerInterval) {
778
+ placeMines(cell.row, cell.col); minesweeperFirstClick = false; startTimer();
779
+ }
780
+ if (cell.isMine) gameOver(cell);
781
+ else { revealCell(cell); checkWinCondition(); }
782
+ }
783
+ function handleCellRightClick(cell: MinesweeperCell) {
784
+ if (minesweeperGameOver || cell.isRevealed || (minesweeperFirstClick && !minesweeperTimerInterval)) return;
785
+ cell.isFlagged = !cell.isFlagged; cell.element.textContent = cell.isFlagged ? '🚩' : '';
786
+ minesweeperFlagsPlaced += cell.isFlagged ? 1 : -1;
787
+ updateFlagCount(); checkWinCondition();
788
+ }
789
+ function revealCell(cell: MinesweeperCell) {
790
+ if (cell.isRevealed || cell.isFlagged || cell.isMine) return;
791
+ cell.isRevealed = true; cell.element.classList.add('revealed'); cell.element.textContent = '';
792
+ if (cell.adjacentMines > 0) {
793
+ cell.element.textContent = cell.adjacentMines.toString();
794
+ cell.element.dataset.number = cell.adjacentMines.toString();
795
+ } else {
796
+ for (let dr = -1; dr <= 1; dr++) {
797
+ for (let dc = -1; dc <= 1; dc++) {
798
+ if (dr === 0 && dc === 0) continue;
799
+ const nr = cell.row + dr; const nc = cell.col + dc;
800
+ if (nr >= 0 && nr < minesweeperGridSize.rows && nc >= 0 && nc < minesweeperGridSize.cols) {
801
+ const neighbor = grid[nr][nc];
802
+ if (!neighbor.isRevealed && !neighbor.isFlagged) revealCell(neighbor);
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+ function startTimer() {
809
+ if (minesweeperTimerInterval) return;
810
+ minesweeperTimeElapsed = 0; timerElement.textContent = `⏱️ 0`;
811
+ minesweeperTimerInterval = window.setInterval(() => {
812
+ minesweeperTimeElapsed++; timerElement.textContent = `⏱️ ${minesweeperTimeElapsed}`;
813
+ }, 1000);
814
+ }
815
+ function updateFlagCount() {
816
+ flagCountElement.textContent = `🚩 ${minesweeperMineCount - minesweeperFlagsPlaced}`;
817
+ }
818
+ function gameOver(clickedMine: MinesweeperCell) {
819
+ minesweeperGameOver = true;
820
+ if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval);
821
+ minesweeperTimerInterval = null; resetButton.textContent = '😵';
822
+ grid.forEach(row => row.forEach(cell => {
823
+ if (cell.isMine) {
824
+ cell.element.classList.add('mine', 'revealed'); cell.element.textContent = '💣';
825
+ }
826
+ if (!cell.isMine && cell.isFlagged) cell.element.textContent = '❌';
827
+ }));
828
+ clickedMine.element.classList.add('exploded'); clickedMine.element.textContent = '💥';
829
+ }
830
+ function checkWinCondition() {
831
+ if (minesweeperGameOver) return;
832
+ let revealedCount = 0; let correctlyFlaggedMines = 0;
833
+ grid.forEach(row => row.forEach(cell => {
834
+ if (cell.isRevealed && !cell.isMine) revealedCount++;
835
+ if (cell.isFlagged && cell.isMine) correctlyFlaggedMines++;
836
+ }));
837
+ const totalNonMineCells = (minesweeperGridSize.rows * minesweeperGridSize.cols) - minesweeperMineCount;
838
+ if (revealedCount === totalNonMineCells || (correctlyFlaggedMines === minesweeperMineCount && minesweeperFlagsPlaced === minesweeperMineCount)) {
839
+ minesweeperGameOver = true;
840
+ if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval);
841
+ minesweeperTimerInterval = null; resetButton.textContent = '😎';
842
+ if (revealedCount === totalNonMineCells) {
843
+ grid.forEach(row => row.forEach(cell => {
844
+ if (cell.isMine && !cell.isFlagged) { cell.isFlagged = true; cell.element.textContent = '🚩'; minesweeperFlagsPlaced++; }
845
+ })); updateFlagCount();
846
+ }
847
+ }
848
+ }
849
+ function getBoardStateAsText(): string {
850
+ let boardString = `Flags: ${minesweeperMineCount - minesweeperFlagsPlaced}, Time: ${minesweeperTimeElapsed}s\nGrid (H=Hidden,F=Flag,Num=Mines):\n`;
851
+ grid.forEach(row => {
852
+ row.forEach(cell => {
853
+ if (cell.isFlagged) boardString += " F ";
854
+ else if (!cell.isRevealed) boardString += " H ";
855
+ else if (cell.adjacentMines > 0) boardString += ` ${cell.adjacentMines} `;
856
+ else boardString += " _ ";
857
+ });
858
+ boardString += "\n";
859
+ });
860
+ return boardString;
861
+ }
862
+ async function getAiHint() {
863
+ if (minesweeperGameOver || minesweeperFirstClick) { commentaryElement.textContent = "Click a square first!"; return; }
864
+ hintButton.disabled = true; hintButton.textContent = '🤔'; commentaryElement.textContent = 'Thinking...';
865
+ if (!geminiInstance) {
866
+ if (!await initializeGeminiIfNeeded('getAiHint')) {
867
+ commentaryElement.textContent = 'AI Init Error.'; hintButton.disabled = false; hintButton.textContent = '💡 Hint'; return;
868
+ }
869
+ }
870
+ try {
871
+ const boardState = getBoardStateAsText();
872
+ const prompt = `Minesweeper state:\n${boardState}\nShort, witty hint (1-2 sentences) for a safe move or dangerous area. Don't reveal exact mines unless certain. Hint:`;
873
+ // @ts-ignore
874
+ const result = await geminiInstance.models.generateContent({ model: "gemini-2.5-flash", contents: [{role:"user", parts:[{text:prompt}]}], config: {temperature: 0.7}});
875
+ const hintText = result?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "Try clicking somewhere?";
876
+ commentaryElement.textContent = hintText;
877
+ } catch (error: any) { commentaryElement.textContent = `Hint Error: ${error.message}`;
878
+ } finally { hintButton.disabled = false; hintButton.textContent = '💡 Hint'; }
879
+ }
880
+ resetButton.addEventListener('click', resetGame);
881
+ hintButton.addEventListener('click', getAiHint);
882
+ resetGame();
883
+ }
884
+
885
+ function initMyComputer(windowElement: HTMLDivElement): void {
886
+ const cDriveIcon = windowElement.querySelector('#c-drive-icon') as HTMLDivElement;
887
+ const cDriveContent = windowElement.querySelector('#c-drive-content') as HTMLDivElement;
888
+ const secretImageIcon = windowElement.querySelector('#secret-image-icon') as HTMLDivElement;
889
+ if (!cDriveIcon || !cDriveContent || !secretImageIcon) return;
890
+ cDriveIcon.addEventListener('click', () => {
891
+ cDriveIcon.style.display = 'none'; cDriveContent.style.display = 'block';
892
+ });
893
+ secretImageIcon.addEventListener('click', () => {
894
+ const imageViewerWindow = document.getElementById('imageViewer') as HTMLDivElement | null;
895
+ const imageViewerImg = document.getElementById('image-viewer-img') as HTMLImageElement | null;
896
+ const imageViewerTitle = document.getElementById('image-viewer-title') as HTMLSpanElement | null;
897
+ if (!imageViewerWindow || !imageViewerImg || !imageViewerTitle) { alert("Image Viewer corrupted!"); return; }
898
+ imageViewerImg.src = 'https://storage.googleapis.com/gemini-95-icons/%40ammaar%2B%40olacombe.png';
899
+ imageViewerImg.alt = 'dontshowthistoanyone.jpg';
900
+ imageViewerTitle.textContent = 'dontshowthistoanyone.jpg - Image Viewer';
901
+ openApp('imageViewer');
902
+ });
903
+ cDriveIcon.style.display = 'inline-flex'; cDriveContent.style.display = 'none';
904
+ }
905
+
906
+ // --- YouTube Player (GemPlayer) Logic ---
907
+ function loadYouTubeApi(): Promise<void> {
908
+ if (ytApiLoaded) return Promise.resolve();
909
+ if (ytApiLoadingPromise) return ytApiLoadingPromise;
910
+
911
+ ytApiLoadingPromise = new Promise((resolve, reject) => {
912
+ // @ts-ignore
913
+ if (window.YT && window.YT.Player) {
914
+ ytApiLoaded = true; resolve(); return;
915
+ }
916
+ const tag = document.createElement('script');
917
+ tag.src = "https://www.youtube.com/iframe_api";
918
+ tag.onerror = (err) => {
919
+ console.error("Failed to load YouTube API script:", err);
920
+ ytApiLoadingPromise = null;
921
+ reject(new Error("YouTube API script load failed"));
922
+ };
923
+ const firstScriptTag = document.getElementsByTagName('script')[0];
924
+ firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
925
+
926
+ // @ts-ignore
927
+ window.onYouTubeIframeAPIReady = () => {
928
+ ytApiLoaded = true; ytApiLoadingPromise = null; resolve();
929
+ };
930
+ setTimeout(() => {
931
+ if (!ytApiLoaded) {
932
+ // @ts-ignore
933
+ if (window.onYouTubeIframeAPIReady) window.onYouTubeIframeAPIReady = null;
934
+ ytApiLoadingPromise = null;
935
+ reject(new Error("YouTube API load timeout"));
936
+ }
937
+ }, 10000);
938
+ });
939
+ return ytApiLoadingPromise;
940
+ }
941
+
942
+ function getYouTubeVideoId(urlOrId: string): string | null {
943
+ if (!urlOrId) return null;
944
+ if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) return urlOrId;
945
+ const regExp = /^.*(?:youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]{11}).*/;
946
+ const match = urlOrId.match(regExp);
947
+ return (match && match[1]) ? match[1] : null;
948
+ }
949
+
950
+ async function initMediaPlayer(windowElement: HTMLDivElement): Promise<void> {
951
+ const appName = windowElement.id; // 'mediaPlayer'
952
+ const urlInput = windowElement.querySelector('.media-player-input') as HTMLInputElement;
953
+ const loadButton = windowElement.querySelector('.media-player-load-button') as HTMLButtonElement;
954
+ const playerContainerDivId = `youtube-player-${appName}`;
955
+ const playerDiv = windowElement.querySelector(`#${playerContainerDivId}`) as HTMLDivElement;
956
+ const playButton = windowElement.querySelector('#media-player-play') as HTMLButtonElement;
957
+ const pauseButton = windowElement.querySelector('#media-player-pause') as HTMLButtonElement;
958
+ const stopButton = windowElement.querySelector('#media-player-stop') as HTMLButtonElement;
959
+
960
+ if (!urlInput || !loadButton || !playerDiv || !playButton || !pauseButton || !stopButton) {
961
+ console.error("Media Player elements not found for", appName);
962
+ if (playerDiv) playerDiv.innerHTML = `<p class="media-player-status-message" style="color:red;">Error: Player UI missing.</p>`;
963
+ return;
964
+ }
965
+
966
+ const updateButtonStates = (playerState?: number) => {
967
+ // @ts-ignore
968
+ const YTPlayerState = window.YT?.PlayerState;
969
+ if (!YTPlayerState) {
970
+ playButton.disabled = true; pauseButton.disabled = true; stopButton.disabled = true;
971
+ return;
972
+ }
973
+ const state = playerState !== undefined ? playerState
974
+ // @ts-ignore
975
+ : (youtubePlayers[appName] && typeof youtubePlayers[appName].getPlayerState === 'function' ? youtubePlayers[appName].getPlayerState() : YTPlayerState.UNSTARTED);
976
+
977
+ playButton.disabled = state === YTPlayerState.PLAYING || state === YTPlayerState.BUFFERING;
978
+ pauseButton.disabled = state !== YTPlayerState.PLAYING && state !== YTPlayerState.BUFFERING; // Can pause if buffering too
979
+ stopButton.disabled = state === YTPlayerState.ENDED || state === YTPlayerState.UNSTARTED || state === -1 /* UNSTARTED also seen as -1 */;
980
+ };
981
+
982
+ updateButtonStates(-1); // Initial state (unstarted)
983
+
984
+ const showPlayerMessage = (message: string, isError: boolean = false) => {
985
+ const player = youtubePlayers[appName];
986
+ if (player) {
987
+ try { if (typeof player.destroy === 'function') player.destroy(); }
988
+ catch(e) { console.warn("Minor error destroying player:", e); }
989
+ delete youtubePlayers[appName];
990
+ }
991
+ playerDiv.innerHTML = `<p class="media-player-status-message" style="color:${isError ? 'red' : '#ccc'};">${message}</p>`;
992
+ updateButtonStates(-1);
993
+ };
994
+
995
+ const initialStatusMessageEl = playerDiv.querySelector('.media-player-status-message');
996
+ if (initialStatusMessageEl) initialStatusMessageEl.textContent = 'Connecting to YouTube...';
997
+
998
+ try {
999
+ await loadYouTubeApi();
1000
+ if (initialStatusMessageEl) initialStatusMessageEl.textContent = 'YouTube API Ready. Loading default video...';
1001
+ } catch (error: any) {
1002
+ showPlayerMessage(`Error: Could not load YouTube Player API. ${error.message}`, true);
1003
+ return;
1004
+ }
1005
+
1006
+ const createPlayer = (videoId: string) => {
1007
+ const existingPlayer = youtubePlayers[appName];
1008
+ if (existingPlayer) {
1009
+ try { if (typeof existingPlayer.destroy === 'function') existingPlayer.destroy(); }
1010
+ catch(e) { console.warn("Minor error destroying previous player:", e); }
1011
+ }
1012
+ playerDiv.innerHTML = ''; // Clear previous content/message
1013
+
1014
+ try {
1015
+ // @ts-ignore
1016
+ youtubePlayers[appName] = new YT.Player(playerContainerDivId, {
1017
+ height: '100%', width: '100%', videoId: videoId,
1018
+ playerVars: { 'playsinline': 1, 'autoplay': 1, 'controls': 0, 'modestbranding': 1, 'rel': 0, 'fs': 0, 'origin': window.location.origin },
1019
+ events: {
1020
+ 'onReady': (event: any) => { /* Autoplay handles start */ updateButtonStates(event.target.getPlayerState()); },
1021
+ 'onError': (event: any) => {
1022
+ const errorMessages: { [key: number]: string } = { 2: "Invalid video ID.", 5: "HTML5 Player error.", 100: "Video not found/private.", 101: "Playback disallowed.", 150: "Playback disallowed."};
1023
+ showPlayerMessage(errorMessages[event.data] || `Playback Error (Code: ${event.data})`, true);
1024
+ },
1025
+ 'onStateChange': (event: any) => { updateButtonStates(event.data); }
1026
+ }
1027
+ });
1028
+ } catch (error: any) {
1029
+ showPlayerMessage(`Failed to create video player: ${error.message}`, true);
1030
+ }
1031
+ };
1032
+
1033
+ loadButton.addEventListener('click', () => {
1034
+ const videoUrlOrId = urlInput.value.trim();
1035
+ const videoId = getYouTubeVideoId(videoUrlOrId);
1036
+ if (videoId) {
1037
+ showPlayerMessage("Loading video..."); // Show loading message immediately
1038
+ createPlayer(videoId);
1039
+ } else {
1040
+ showPlayerMessage("Invalid YouTube URL or Video ID.", true);
1041
+ }
1042
+ });
1043
+
1044
+ playButton.addEventListener('click', () => {
1045
+ const player = youtubePlayers[appName];
1046
+ // @ts-ignore
1047
+ if (player && typeof player.playVideo === 'function') player.playVideo();
1048
+ });
1049
+ pauseButton.addEventListener('click', () => {
1050
+ const player = youtubePlayers[appName];
1051
+ // @ts-ignore
1052
+ if (player && typeof player.pauseVideo === 'function') player.pauseVideo();
1053
+ });
1054
+ stopButton.addEventListener('click', () => {
1055
+ const player = youtubePlayers[appName];
1056
+ // @ts-ignore
1057
+ if (player && typeof player.stopVideo === 'function') {
1058
+ player.stopVideo();
1059
+ // @ts-ignore - Manually set to ended for button state update
1060
+ updateButtonStates(window.YT?.PlayerState?.ENDED);
1061
+ }
1062
+ });
1063
+
1064
+ if (DEFAULT_YOUTUBE_VIDEO_ID) {
1065
+ if (initialStatusMessageEl) initialStatusMessageEl.textContent = `Loading default video...`; // Update message
1066
+ createPlayer(DEFAULT_YOUTUBE_VIDEO_ID);
1067
+ } else {
1068
+ // Message already set by HTML if no default video.
1069
+ // showPlayerMessage("Enter a YouTube URL or Video ID and click 'Load'.");
1070
+ }
1071
+ }
1072
+ // --- END YouTube Player Logic ---
1073
+
1074
+ async function initializeGeminiIfNeeded(context: string): Promise<boolean> {
1075
+ if (geminiInstance) return true;
1076
+ try {
1077
+ const module = await import('@google/genai');
1078
+ // @ts-ignore
1079
+ const GoogleAIClass = module.GoogleGenAI;
1080
+ if (typeof GoogleAIClass !== 'function') throw new Error("GoogleGenAI constructor not found.");
1081
+ // @ts-ignore
1082
+ const apiKey = process.env.GEMINI_API_KEY || "";
1083
+ if (!apiKey) {
1084
+ alert("CRITICAL ERROR: Gemini API Key missing.");
1085
+ throw new Error("API Key is missing.");
1086
+ }
1087
+ // @ts-ignore
1088
+ geminiInstance = new GoogleAIClass({apiKey: apiKey});
1089
+ return true;
1090
+ } catch (error: any) {
1091
+ console.error(`Failed Gemini initialization in ${context}:`, error);
1092
+ alert(`CRITICAL ERROR: Gemini AI failed to initialize. ${error.message}`);
1093
+ return false;
1094
+ }
1095
+ }