@pheem49/mint 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/BUILD_AND_RELEASE.md +75 -0
  2. package/LICENSE +654 -0
  3. package/README.md +165 -0
  4. package/assets/Agent_Mint.png +0 -0
  5. package/assets/CLI_Screen.png +0 -0
  6. package/assets/Settings.png +0 -0
  7. package/assets/icon.png +0 -0
  8. package/benchmark_ai.js +71 -0
  9. package/main.js +968 -0
  10. package/mint-cli-logic.js +71 -0
  11. package/mint-cli.js +239 -0
  12. package/package.json +60 -0
  13. package/preload-picker.js +11 -0
  14. package/preload-settings.js +11 -0
  15. package/preload.js +37 -0
  16. package/privacy.txt +1 -0
  17. package/src/AI_Brain/Gemini_API.js +419 -0
  18. package/src/AI_Brain/autonomous_brain.js +139 -0
  19. package/src/AI_Brain/behavior_memory.js +114 -0
  20. package/src/AI_Brain/headless_agent.js +120 -0
  21. package/src/AI_Brain/knowledge_base.js +222 -0
  22. package/src/AI_Brain/proactive_engine.js +168 -0
  23. package/src/Automation_Layer/browser_automation.js +147 -0
  24. package/src/Automation_Layer/file_operations.js +80 -0
  25. package/src/Automation_Layer/open_app.js +56 -0
  26. package/src/Automation_Layer/open_website.js +38 -0
  27. package/src/CLI/chat_ui.js +468 -0
  28. package/src/CLI/list_features.js +56 -0
  29. package/src/CLI/onboarding.js +60 -0
  30. package/src/Command_Parser/parser.js +34 -0
  31. package/src/Plugins/dev_tools.js +41 -0
  32. package/src/Plugins/discord.js +20 -0
  33. package/src/Plugins/docker.js +45 -0
  34. package/src/Plugins/google_calendar.js +26 -0
  35. package/src/Plugins/obsidian.js +54 -0
  36. package/src/Plugins/plugin_manager.js +81 -0
  37. package/src/Plugins/spotify.js +45 -0
  38. package/src/Plugins/system_metrics.js +31 -0
  39. package/src/System/chat_history_manager.js +57 -0
  40. package/src/System/config_manager.js +73 -0
  41. package/src/System/custom_workflows.js +127 -0
  42. package/src/System/daemon_manager.js +67 -0
  43. package/src/System/system_automation.js +88 -0
  44. package/src/System/system_events.js +79 -0
  45. package/src/System/system_info.js +55 -0
  46. package/src/System/task_manager.js +85 -0
  47. package/src/UI/floating.css +80 -0
  48. package/src/UI/floating.html +17 -0
  49. package/src/UI/floating.js +67 -0
  50. package/src/UI/index.html +126 -0
  51. package/src/UI/preload-floating.js +7 -0
  52. package/src/UI/preload-spotlight.js +10 -0
  53. package/src/UI/preload-widget.js +5 -0
  54. package/src/UI/proactive-glow.html +42 -0
  55. package/src/UI/renderer.js +978 -0
  56. package/src/UI/screenPicker.html +214 -0
  57. package/src/UI/screenPicker.js +262 -0
  58. package/src/UI/settings.css +705 -0
  59. package/src/UI/settings.html +396 -0
  60. package/src/UI/settings.js +514 -0
  61. package/src/UI/spotlight.css +119 -0
  62. package/src/UI/spotlight.html +23 -0
  63. package/src/UI/spotlight.js +181 -0
  64. package/src/UI/styles.css +627 -0
  65. package/src/UI/widget.css +218 -0
  66. package/src/UI/widget.html +29 -0
  67. package/src/UI/widget.js +10 -0
  68. package/tech_news.txt +3 -0
  69. package/test_knowledge.txt +3 -0
@@ -0,0 +1,978 @@
1
+ const chatContainer = document.getElementById('chat-container');
2
+ const chatForm = document.getElementById('chat-form');
3
+ const chatInput = document.getElementById('chat-input');
4
+ const closeBtn = document.getElementById('close-btn');
5
+ const maximizeBtn = document.getElementById('maximize-btn');
6
+ const minimizeBtn = document.getElementById('minimize-btn');
7
+ const clearBtn = document.getElementById('clear-btn');
8
+ const settingsBtn = document.getElementById('settings-btn');
9
+ const micBtn = document.getElementById('mic-btn');
10
+ const visionBtn = document.getElementById('vision-btn');
11
+ const imagePreviewContainer = document.getElementById('image-preview-container');
12
+ const imagePreview = document.getElementById('image-preview');
13
+ const removeImageBtn = document.getElementById('remove-image-btn');
14
+
15
+ // Proactive Assistant elements
16
+ const proactiveBar = document.getElementById('proactive-bar');
17
+ const proactiveMessage = document.getElementById('proactive-message');
18
+ const proactiveChips = document.getElementById('proactive-chips');
19
+ const proactiveDismissBtn = document.getElementById('proactive-dismiss-btn');
20
+
21
+ let currentBase64Image = null;
22
+ let enableVoiceReply = true;
23
+ let ttsProvider = 'google';
24
+ let ttsVolume = 1.0;
25
+ let ttsSpeed = 1.0;
26
+ let ttsPitch = 1.0;
27
+
28
+ // --- Theme Loading ---
29
+ function applyTheme(theme, accentColor, systemTextColor, config = {}) {
30
+ document.documentElement.setAttribute('data-theme', theme || 'dark');
31
+ const accent = accentColor || '#8b5cf6';
32
+ const textColor = systemTextColor || '#f8fafc';
33
+ document.documentElement.style.setProperty('--accent', accent);
34
+ document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
35
+ document.documentElement.style.setProperty('--text-main', textColor);
36
+
37
+ // Dynamic UI Customizations
38
+ document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
39
+ document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
40
+
41
+ if (theme === 'custom') {
42
+ if (config.customBgStart && config.customBgEnd) {
43
+ const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
44
+ document.documentElement.style.setProperty('--bg-gradient', gradient);
45
+ }
46
+ if (config.customPanelBg) {
47
+ const rgb = hexToRgb(config.customPanelBg);
48
+ document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
49
+ }
50
+ } else {
51
+ document.documentElement.style.removeProperty('--bg-gradient');
52
+ document.documentElement.style.removeProperty('--panel-bg');
53
+ }
54
+ }
55
+
56
+ function hexToRgb(hex) {
57
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
58
+ return result ? {
59
+ r: parseInt(result[1], 16),
60
+ g: parseInt(result[2], 16),
61
+ b: parseInt(result[3], 16)
62
+ } : { r: 15, g: 23, b: 42 };
63
+ }
64
+
65
+ async function loadTheme() {
66
+ try {
67
+ const config = await window.api.getSettings();
68
+ applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
69
+ enableVoiceReply = config.enableVoiceReply !== false;
70
+ ttsProvider = config.ttsProvider || 'google';
71
+ ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
72
+ ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
73
+ ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
74
+ } catch (e) {
75
+ applyTheme('dark', '#8b5cf6', '#f8fafc');
76
+ }
77
+ }
78
+
79
+ function lightenColor(hex, amount) {
80
+ const clean = hex.replace('#', '');
81
+ if (clean.length !== 6) return hex;
82
+ const num = parseInt(clean, 16);
83
+ const r = Math.min(255, (num >> 16) + amount);
84
+ const g = Math.min(255, ((num >> 8) & 0x00FF) + amount);
85
+ const b = Math.min(255, (num & 0x0000FF) + amount);
86
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
87
+ }
88
+
89
+ // 🔔 Real-time theme sync from Settings window
90
+ window.api.onSettingsChanged((config) => {
91
+ applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
92
+ enableVoiceReply = config.enableVoiceReply !== false;
93
+ ttsProvider = config.ttsProvider || 'google';
94
+ ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
95
+ ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
96
+ ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
97
+ });
98
+
99
+ // --- Voice Input Setup ---
100
+ let mediaRecorder = null;
101
+ let audioChunks = [];
102
+ let speechRecognition = null;
103
+ let isSpeechStreaming = false;
104
+ let speechInterim = '';
105
+ let speechHadResult = false;
106
+ let speechFallbackTimer = null;
107
+ let voiceMode = null; // 'speech' | 'recorder' | null
108
+ let voiceSendQueue = Promise.resolve();
109
+ let speechPausedForReply = false;
110
+ let resumeSpeechAfterResponse = false;
111
+ const DEFAULT_PLACEHOLDER = "Type or speak a command...";
112
+ const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
113
+
114
+ function notifyAiIfNeeded() {
115
+ if (!window.api.notifyAiResponse) return;
116
+ if (!document.hasFocus() || document.hidden) {
117
+ window.api.notifyAiResponse();
118
+ } else if (window.api.clearAiNotifications) {
119
+ window.api.clearAiNotifications();
120
+ }
121
+ }
122
+
123
+ function queueVoiceTextSend(text) {
124
+ const clean = (text || '').trim();
125
+ if (!clean) return;
126
+ voiceSendQueue = voiceSendQueue.then(() => sendTextMessage(clean, { allowSmartContext: false }));
127
+ }
128
+
129
+ function pauseSpeechForReply() {
130
+ if (!speechRecognition || !isSpeechStreaming) return;
131
+ resumeSpeechAfterResponse = true;
132
+ speechPausedForReply = true;
133
+ try {
134
+ speechRecognition.stop();
135
+ } catch (_) {}
136
+ }
137
+
138
+ function resumeSpeechIfNeeded() {
139
+ if (!speechRecognition || !isSpeechStreaming) {
140
+ resumeSpeechAfterResponse = false;
141
+ speechPausedForReply = false;
142
+ return;
143
+ }
144
+ if (!resumeSpeechAfterResponse) return;
145
+ resumeSpeechAfterResponse = false;
146
+ speechPausedForReply = false;
147
+ try {
148
+ speechRecognition.start();
149
+ } catch (e) {
150
+ console.error("Speech recognition resume error:", e);
151
+ }
152
+ }
153
+
154
+ function setupSpeechRecognition() {
155
+ if (!SpeechRecognitionCtor) return;
156
+ speechRecognition = new SpeechRecognitionCtor();
157
+ speechRecognition.lang = 'th-TH';
158
+ speechRecognition.interimResults = true;
159
+ // Let the engine auto-stop on silence, then we restart if streaming is enabled.
160
+ speechRecognition.continuous = false;
161
+
162
+ speechRecognition.onstart = () => {
163
+ micBtn.classList.add('listening');
164
+ chatInput.placeholder = "Listening... (Click to stop)";
165
+ speechHadResult = false;
166
+ if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
167
+ speechFallbackTimer = setTimeout(() => {
168
+ if (isSpeechStreaming && !speechHadResult) {
169
+ fallbackToMediaRecorder();
170
+ }
171
+ }, 1500);
172
+ };
173
+
174
+ speechRecognition.onresult = (event) => {
175
+ speechHadResult = true;
176
+ let interimTranscript = '';
177
+ let finalTranscript = '';
178
+
179
+ for (let i = event.resultIndex; i < event.results.length; i++) {
180
+ const result = event.results[i];
181
+ const transcript = result[0]?.transcript || '';
182
+ if (result.isFinal) {
183
+ finalTranscript += transcript;
184
+ } else {
185
+ interimTranscript += transcript;
186
+ }
187
+ }
188
+
189
+ if (finalTranscript.trim()) {
190
+ const textToSend = finalTranscript.trim();
191
+ speechInterim = '';
192
+ chatInput.value = '';
193
+ pauseSpeechForReply();
194
+ queueVoiceTextSend(textToSend);
195
+ } else {
196
+ speechInterim = interimTranscript;
197
+ chatInput.value = speechInterim.trimStart();
198
+ }
199
+ };
200
+
201
+ speechRecognition.onerror = (err) => {
202
+ console.error("Speech recognition error:", err);
203
+ fallbackToMediaRecorder();
204
+ isSpeechStreaming = false;
205
+ resetMicUI();
206
+ };
207
+
208
+ speechRecognition.onend = () => {
209
+ if (speechFallbackTimer) {
210
+ clearTimeout(speechFallbackTimer);
211
+ speechFallbackTimer = null;
212
+ }
213
+ if (speechPausedForReply) {
214
+ return;
215
+ }
216
+ if (isSpeechStreaming && !speechHadResult) {
217
+ fallbackToMediaRecorder();
218
+ return;
219
+ }
220
+ if (isSpeechStreaming) {
221
+ try {
222
+ speechRecognition.start();
223
+ } catch (e) {
224
+ console.error("Speech recognition restart error:", e);
225
+ isSpeechStreaming = false;
226
+ resetMicUI();
227
+ }
228
+ } else {
229
+ resetMicUI();
230
+ }
231
+ };
232
+ }
233
+
234
+ async function setupMediaRecorder() {
235
+ try {
236
+ // Improved audio constraints for better quality and noise reduction
237
+ const stream = await navigator.mediaDevices.getUserMedia({
238
+ audio: {
239
+ echoCancellation: true,
240
+ noiseSuppression: true,
241
+ autoGainControl: true
242
+ }
243
+ });
244
+
245
+ // Check for supported MIME types
246
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
247
+ mediaRecorder = new MediaRecorder(stream, { mimeType });
248
+
249
+ mediaRecorder.ondataavailable = (event) => {
250
+ if (event.data.size > 0) audioChunks.push(event.data);
251
+ };
252
+
253
+ mediaRecorder.onstop = async () => {
254
+ if (audioChunks.length === 0) {
255
+ resetMicUI();
256
+ return;
257
+ }
258
+
259
+ const audioBlob = new Blob(audioChunks, { type: mimeType });
260
+ audioChunks = [];
261
+
262
+ // Convert Blob to Base64
263
+ const reader = new FileReader();
264
+ reader.readAsDataURL(audioBlob);
265
+ reader.onloadend = async () => {
266
+ const base64Audio = reader.result;
267
+ // Send to Gemini
268
+ await sendVoiceMessage(base64Audio);
269
+ };
270
+ };
271
+
272
+ mediaRecorder.onstart = () => {
273
+ micBtn.classList.add('listening');
274
+ chatInput.placeholder = "Listening... (Click to stop)";
275
+ };
276
+
277
+ } catch (err) {
278
+ console.error("Microphone access error:", err);
279
+ micBtn.style.display = 'none';
280
+ appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
281
+ }
282
+ }
283
+
284
+ function resetMicUI() {
285
+ micBtn.classList.remove('listening');
286
+ chatInput.placeholder = DEFAULT_PLACEHOLDER;
287
+ }
288
+
289
+ async function sendVoiceMessage(base64Audio) {
290
+ showTyping();
291
+ chatInput.placeholder = "Processing voice...";
292
+ try {
293
+ // Send empty text, but include the audio
294
+ const response = await window.api.sendMessage("", null, base64Audio);
295
+ removeTyping();
296
+
297
+ // Show AI response
298
+ const msgDiv = await appendAiMessages(response.response, { allowDelay: true });
299
+ await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
300
+ notifyAiIfNeeded();
301
+
302
+ if (response.action && response.action.type !== 'none') {
303
+ appendActionCard(msgDiv, response.action);
304
+ }
305
+ } catch (error) {
306
+ removeTyping();
307
+ appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
308
+ console.error(error);
309
+ resumeSpeechIfNeeded();
310
+ } finally {
311
+ resetMicUI();
312
+ }
313
+ }
314
+
315
+ function fallbackToMediaRecorder() {
316
+ if (voiceMode === 'recorder') return;
317
+ isSpeechStreaming = false;
318
+ speechPausedForReply = false;
319
+ resumeSpeechAfterResponse = false;
320
+ voiceMode = 'recorder';
321
+ try {
322
+ if (speechRecognition) {
323
+ speechRecognition.stop();
324
+ }
325
+ } catch (_) {}
326
+ if (mediaRecorder && mediaRecorder.state === 'inactive') {
327
+ audioChunks = [];
328
+ mediaRecorder.start();
329
+ }
330
+ }
331
+
332
+ // Initialize voice input
333
+ setupMediaRecorder();
334
+ if (SpeechRecognitionCtor) {
335
+ setupSpeechRecognition();
336
+ }
337
+
338
+ micBtn.addEventListener('click', (e) => {
339
+ e.preventDefault();
340
+ if (voiceMode === 'recorder') {
341
+ if (!mediaRecorder) return;
342
+ if (mediaRecorder.state === 'inactive') {
343
+ audioChunks = [];
344
+ mediaRecorder.start();
345
+ if (window.api && window.api.setAiState) window.api.setAiState('listening');
346
+ } else {
347
+ mediaRecorder.stop();
348
+ if (window.api && window.api.setAiState) window.api.setAiState('thinking');
349
+ voiceMode = null;
350
+ }
351
+ return;
352
+ }
353
+
354
+ if (speechRecognition) {
355
+ if (!isSpeechStreaming) {
356
+ isSpeechStreaming = true;
357
+ voiceMode = 'speech';
358
+ speechInterim = '';
359
+ chatInput.value = '';
360
+ try {
361
+ speechRecognition.start();
362
+ } catch (err) {
363
+ console.error("Speech recognition start error:", err);
364
+ isSpeechStreaming = false;
365
+ resetMicUI();
366
+ }
367
+ } else {
368
+ isSpeechStreaming = false;
369
+ speechRecognition.stop();
370
+ voiceMode = null;
371
+ }
372
+ return;
373
+ }
374
+
375
+ if (!mediaRecorder) return;
376
+
377
+ if (mediaRecorder.state === 'inactive') {
378
+ audioChunks = [];
379
+ mediaRecorder.start();
380
+ if (window.api && window.api.setAiState) window.api.setAiState('listening');
381
+ } else {
382
+ mediaRecorder.stop();
383
+ if (window.api && window.api.setAiState) window.api.setAiState('thinking');
384
+ }
385
+ });
386
+
387
+ // --- Speech Synthesis Setup ---
388
+ let currentAudioPlayer = null;
389
+
390
+ function speakText(text, options = {}) {
391
+ if (window.api && window.api.setAiState) window.api.setAiState('speaking');
392
+ const onEnd = typeof options.onEnd === 'function' ? options.onEnd : null;
393
+ return new Promise(async (resolve) => {
394
+ if (!enableVoiceReply) {
395
+ if (window.api && window.api.setAiState) window.api.setAiState('idle');
396
+ if (onEnd) onEnd();
397
+ return resolve();
398
+ }
399
+
400
+ // Stop any currently playing audio
401
+ if (currentAudioPlayer) {
402
+ currentAudioPlayer.pause();
403
+ currentAudioPlayer.currentTime = 0;
404
+ currentAudioPlayer = null;
405
+ }
406
+ if ('speechSynthesis' in window) {
407
+ window.speechSynthesis.cancel();
408
+ }
409
+
410
+ if (!text || !text.trim()) {
411
+ if (window.api && window.api.setAiState) window.api.setAiState('idle');
412
+ if (onEnd) onEnd();
413
+ return resolve();
414
+ }
415
+
416
+ try {
417
+ if (ttsProvider !== 'native') {
418
+ const urls = await window.api.getTtsUrls(text);
419
+ if (urls && urls.length > 0) {
420
+ let i = 0;
421
+ const playNext = () => {
422
+ if (i >= urls.length) {
423
+ if (window.api && window.api.setAiState) window.api.setAiState('idle');
424
+ if (onEnd) onEnd();
425
+ return resolve();
426
+ }
427
+ const audio = new Audio(urls[i].url);
428
+ audio.volume = ttsVolume;
429
+ audio.playbackRate = ttsSpeed;
430
+
431
+ currentAudioPlayer = audio;
432
+ audio.onended = () => {
433
+ i++;
434
+ playNext();
435
+ };
436
+ audio.onerror = () => {
437
+ console.error("TTS Audio error", urls[i]);
438
+ i++;
439
+ playNext();
440
+ };
441
+ audio.play().catch(e => {
442
+ console.error("Audio playback prevented:", e);
443
+ fallbackSpeak(text, onEnd, resolve);
444
+ });
445
+ };
446
+ playNext();
447
+ return;
448
+ }
449
+ }
450
+ } catch (err) {
451
+ console.error("Cloud TTS Error, falling back to local:", err);
452
+ }
453
+
454
+ // Fallback
455
+ fallbackSpeak(text, onEnd, resolve);
456
+ });
457
+ }
458
+
459
+ function fallbackSpeak(text, onEnd, resolve) {
460
+ if (!('speechSynthesis' in window)) {
461
+ if (onEnd) onEnd();
462
+ resolve();
463
+ return;
464
+ }
465
+
466
+ window.speechSynthesis.cancel();
467
+ const utterance = new SpeechSynthesisUtterance(text);
468
+ utterance.lang = 'th-TH';
469
+ utterance.volume = ttsVolume;
470
+ utterance.rate = ttsSpeed;
471
+ utterance.pitch = ttsPitch;
472
+
473
+ let finished = false;
474
+ const done = () => {
475
+ if (finished) return;
476
+ finished = true;
477
+ if (window.api && window.api.setAiState) window.api.setAiState('idle');
478
+ if (onEnd) onEnd();
479
+ resolve();
480
+ };
481
+
482
+ utterance.onend = done;
483
+ utterance.onerror = done;
484
+ window.speechSynthesis.speak(utterance);
485
+ }
486
+
487
+ // Minimize window handler (hides to tray)
488
+ minimizeBtn.addEventListener('click', () => {
489
+ window.api.minimizeWindow();
490
+ });
491
+
492
+ // Close window handler (quits app)
493
+ closeBtn.addEventListener('click', () => {
494
+ window.api.quitApp();
495
+ });
496
+
497
+ maximizeBtn.addEventListener('click', () => {
498
+ window.api.maximizeWindow();
499
+ });
500
+
501
+ // Settings button
502
+ settingsBtn.addEventListener('click', () => {
503
+ window.api.openSettings();
504
+ });
505
+
506
+ // Throttle utility to prevent UI spam
507
+ function throttle(func, limit) {
508
+ let inThrottle;
509
+ return function() {
510
+ const args = arguments;
511
+ const context = this;
512
+ if (!inThrottle) {
513
+ func.apply(context, args);
514
+ inThrottle = true;
515
+ setTimeout(() => inThrottle = false, limit);
516
+ }
517
+ }
518
+ }
519
+
520
+ // Vision system
521
+ visionBtn.addEventListener('click', throttle(async () => {
522
+ await window.api.startVision();
523
+ }, 1000));
524
+
525
+ window.api.onVisionReady((base64Image) => {
526
+ currentBase64Image = base64Image;
527
+ imagePreview.src = base64Image;
528
+ imagePreviewContainer.style.display = 'block';
529
+ chatInput.focus();
530
+ });
531
+
532
+ removeImageBtn.addEventListener('click', () => {
533
+ currentBase64Image = null;
534
+ imagePreview.src = '';
535
+ imagePreviewContainer.style.display = 'none';
536
+ });
537
+
538
+ // Clear chat history
539
+ clearBtn.addEventListener('click', async () => {
540
+ await window.api.resetChat();
541
+ // Remove all messages except the initial greeting
542
+ const messages = chatContainer.querySelectorAll('.message:not(.initial)');
543
+ messages.forEach(m => m.remove());
544
+ // Append a clear confirmation
545
+ appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai');
546
+ });
547
+
548
+ function appendMessage(text, sender, base64Image = null) {
549
+ const messageDiv = document.createElement('div');
550
+ messageDiv.classList.add('message', `${sender}-message`);
551
+
552
+ const bubble = document.createElement('div');
553
+ bubble.classList.add('message-bubble');
554
+
555
+ if (base64Image && sender === 'user') {
556
+ const img = document.createElement('img');
557
+ img.src = base64Image;
558
+ img.style.maxWidth = '100%';
559
+ img.style.borderRadius = '4px';
560
+ img.style.marginBottom = '8px';
561
+ img.style.display = 'block';
562
+ bubble.appendChild(img);
563
+ }
564
+
565
+ if (text) {
566
+ const textSpan = document.createElement('span');
567
+ textSpan.textContent = text;
568
+ bubble.appendChild(textSpan);
569
+ }
570
+
571
+ messageDiv.appendChild(bubble);
572
+ chatContainer.appendChild(messageDiv);
573
+ scrollToBottom();
574
+
575
+ return messageDiv; // Return it so we can append action cards if needed
576
+ }
577
+
578
+ function normalizeAiText(input) {
579
+ if (Array.isArray(input)) {
580
+ return input
581
+ .map((item) => (item == null ? '' : String(item).trim()))
582
+ .filter(Boolean)
583
+ .join('\n\n');
584
+ }
585
+ if (input == null) return '';
586
+ return String(input);
587
+ }
588
+
589
+ function splitAiMessages(text) {
590
+ const normalized = normalizeAiText(text).trim();
591
+ if (!normalized) return [];
592
+ const byBlankLine = normalized
593
+ .split(/\n\s*\n/)
594
+ .map((part) => part.trim())
595
+ .filter(Boolean);
596
+ if (byBlankLine.length > 1) return byBlankLine;
597
+ return autoChunkAiText(normalized);
598
+ }
599
+
600
+ function sleep(ms) {
601
+ return new Promise((resolve) => setTimeout(resolve, ms));
602
+ }
603
+
604
+ function estimateMessageDelay(text) {
605
+ const base = 260;
606
+ const perChar = 12;
607
+ const jitter = Math.floor(Math.random() * 120);
608
+ const scaled = base + Math.min(1200, text.length * perChar) + jitter;
609
+ return Math.min(1600, scaled);
610
+ }
611
+
612
+ async function appendAiMessages(text, options = {}) {
613
+ const allowDelay = options.allowDelay !== false;
614
+ const parts = splitAiMessages(text);
615
+ let lastDiv = null;
616
+
617
+ for (let index = 0; index < parts.length; index += 1) {
618
+ if (allowDelay && index > 0) {
619
+ showTyping();
620
+ await sleep(estimateMessageDelay(parts[index]));
621
+ removeTyping();
622
+ }
623
+ lastDiv = appendMessage(parts[index], 'ai');
624
+ }
625
+
626
+ return lastDiv;
627
+ }
628
+
629
+ function autoChunkAiText(text) {
630
+ const trimmed = text.trim();
631
+ if (trimmed.length <= 120) return [trimmed];
632
+
633
+ const sentenceMatches = trimmed.match(/[^.!?…\n]+[.!?…]+|[^.!?…\n]+$/g);
634
+ if (!sentenceMatches || sentenceMatches.length <= 1) return [trimmed];
635
+
636
+ const bubbles = [];
637
+ let current = '';
638
+ for (const sentence of sentenceMatches) {
639
+ const next = current ? `${current} ${sentence}` : sentence;
640
+ if (next.length > 180 && current) {
641
+ bubbles.push(current.trim());
642
+ current = sentence;
643
+ } else {
644
+ current = next;
645
+ }
646
+ }
647
+ if (current.trim()) bubbles.push(current.trim());
648
+
649
+ if (bubbles.length > 3) {
650
+ const merged = [bubbles[0], bubbles[1], bubbles.slice(2).join(' ').trim()];
651
+ return merged.filter(Boolean);
652
+ }
653
+
654
+ return bubbles.length > 0 ? bubbles : [trimmed];
655
+ }
656
+
657
+ function appendActionCard(messageDiv, action) {
658
+ const card = document.createElement('div');
659
+ card.classList.add('action-card');
660
+
661
+ let icon = '⚡';
662
+ let text = '';
663
+
664
+ if (action.type === 'open_url') {
665
+ icon = '🌐';
666
+ text = `Opened URL: ${action.target}`;
667
+ } else if (action.type === 'open_app') {
668
+ icon = '🚀';
669
+ text = `Launched App: ${action.target}`;
670
+ } else if (action.type === 'search') {
671
+ icon = '🔍';
672
+ text = `Searched info: ${action.target}`;
673
+ } else {
674
+ return; // Do nothing if none or unknown
675
+ }
676
+
677
+ card.textContent = `${icon} ${text}`;
678
+
679
+ // Append after the bubble
680
+ messageDiv.querySelector('.message-bubble').appendChild(card);
681
+ }
682
+
683
+ function showTyping() {
684
+ const typingDiv = document.createElement('div');
685
+ typingDiv.classList.add('message', 'ai-message', 'typing-message');
686
+ typingDiv.id = 'typing-indicator';
687
+
688
+ const indicator = document.createElement('div');
689
+ indicator.classList.add('typing-indicator');
690
+ indicator.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
691
+
692
+ typingDiv.appendChild(indicator);
693
+ chatContainer.appendChild(typingDiv);
694
+ scrollToBottom();
695
+ }
696
+
697
+ function removeTyping() {
698
+ const typingDiv = document.getElementById('typing-indicator');
699
+ if (typingDiv) {
700
+ typingDiv.remove();
701
+ }
702
+ }
703
+
704
+ function scrollToBottom() {
705
+ chatContainer.scrollTop = chatContainer.scrollHeight;
706
+ }
707
+
708
+ async function loadChatHistory() {
709
+ try {
710
+ const history = await window.api.getChatHistory();
711
+ if (!Array.isArray(history) || history.length === 0) {
712
+ return;
713
+ }
714
+
715
+ const initial = chatContainer.querySelector('.message.initial');
716
+ if (initial) {
717
+ initial.remove();
718
+ }
719
+
720
+ for (const item of history) {
721
+ if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
722
+ const sender = item.sender === 'user' ? 'user' : 'ai';
723
+ if (sender === 'ai') {
724
+ await appendAiMessages(item.text, { allowDelay: false });
725
+ } else {
726
+ appendMessage(item.text, sender);
727
+ }
728
+ }
729
+ } catch (error) {
730
+ console.error('Failed to load chat history:', error);
731
+ }
732
+ }
733
+
734
+ async function sendTextMessage(text, options = {}) {
735
+ const cleanText = (text || '').trim();
736
+ const allowSmartContext = options.allowSmartContext !== false;
737
+
738
+ // We can send either a text message, an image, or both.
739
+ if (!cleanText && !currentBase64Image) return;
740
+
741
+ // Cache the image for sending and UI, then clear
742
+ let imageToSend = currentBase64Image;
743
+
744
+ // Clear input & UI for explicit images
745
+ chatInput.value = '';
746
+ currentBase64Image = null;
747
+ imagePreviewContainer.style.display = 'none';
748
+ imagePreview.src = '';
749
+
750
+ // Show user message (with explicit image if available)
751
+ appendMessage(cleanText, 'user', imageToSend);
752
+
753
+ // Show typing early so user knows we are processing
754
+ showTyping();
755
+
756
+ // Check Smart Context Toggle
757
+ const smartToggle = document.getElementById('smart-context-toggle');
758
+ if (allowSmartContext && smartToggle && smartToggle.checked && !imageToSend) {
759
+ try {
760
+ const silentCapture = await window.api.captureSilentScreen();
761
+ if (silentCapture) {
762
+ // Set imageToSend so it gets sent to the API, but we already appended the chat bubble
763
+ imageToSend = silentCapture;
764
+ }
765
+ } catch (err) {
766
+ console.error("Smart Context capture failed:", err);
767
+ }
768
+ }
769
+
770
+ // Hide proactive bar if user is actively typing a message
771
+ hideProactiveBar();
772
+
773
+ try {
774
+ // Send to main process (text, image, audio=null)
775
+ const response = await window.api.sendMessage(cleanText, imageToSend, null);
776
+ removeTyping();
777
+
778
+ if (typeof response.response !== 'string') {
779
+ response.response = normalizeAiText(response.response);
780
+ }
781
+
782
+ // Handle system_info action: fetch data and append to AI message
783
+ if (response.action && response.action.type === 'system_info') {
784
+ const city = (response.action.target || '').trim();
785
+ // Only treat as weather if city looks like a real location name (not blank, not 'date', not 'time')
786
+ const weatherKeywords = ['date', 'time', 'วัน', 'เวลา', 'today', 'now'];
787
+ const isWeather = city && !weatherKeywords.some(k => city.toLowerCase().includes(k));
788
+
789
+ if (isWeather) {
790
+ // Weather query
791
+ const weather = await window.api.getWeather(city);
792
+ response.response += `\n\n🌡️ ${weather.data}`;
793
+ } else {
794
+ // General system info (date, time, RAM, CPU)
795
+ const info = await window.api.getSystemInfo();
796
+ response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
797
+ }
798
+ }
799
+
800
+ // Show AI response
801
+ const msgDiv = await appendAiMessages(response.response, { allowDelay: true });
802
+
803
+ // Speak AI response
804
+ await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
805
+ notifyAiIfNeeded();
806
+
807
+ // Append action card if applicable
808
+ if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
809
+ appendActionCard(msgDiv, response.action);
810
+ }
811
+ } catch (error) {
812
+ removeTyping();
813
+ appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
814
+ console.error(error);
815
+ resumeSpeechIfNeeded();
816
+ }
817
+ }
818
+
819
+ chatForm.addEventListener('submit', throttle(async (e) => {
820
+ e.preventDefault();
821
+ if (window.api && window.api.setAiState) window.api.setAiState('thinking');
822
+ const text = chatInput.value.trim();
823
+ await sendTextMessage(text);
824
+ }, 500));
825
+
826
+ // --- Image Paste and Drag-n-Drop Support ---
827
+ function handleImageFile(file) {
828
+ if (!file || !file.type.startsWith('image/')) return;
829
+ const reader = new FileReader();
830
+ reader.onload = (e) => {
831
+ currentBase64Image = e.target.result;
832
+ imagePreview.src = currentBase64Image;
833
+ imagePreviewContainer.style.display = 'block';
834
+ chatInput.focus();
835
+ };
836
+ reader.readAsDataURL(file);
837
+ }
838
+
839
+ // Paste Event
840
+ chatInput.addEventListener('paste', (e) => {
841
+ const items = (e.clipboardData || e.originalEvent.clipboardData).items;
842
+ for (let index in items) {
843
+ const item = items[index];
844
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
845
+ const blob = item.getAsFile();
846
+ handleImageFile(blob);
847
+ break; // Handle only the first image
848
+ }
849
+ }
850
+ });
851
+
852
+ // Drag and Drop Events (on the whole chat form/input area)
853
+ const inputArea = document.querySelector('.input-area');
854
+
855
+ inputArea.addEventListener('dragover', (e) => {
856
+ e.preventDefault();
857
+ e.stopPropagation();
858
+ inputArea.style.opacity = '0.7'; // Visual feedback
859
+ });
860
+
861
+ inputArea.addEventListener('dragleave', (e) => {
862
+ e.preventDefault();
863
+ e.stopPropagation();
864
+ inputArea.style.opacity = '1';
865
+ });
866
+
867
+ inputArea.addEventListener('drop', (e) => {
868
+ e.preventDefault();
869
+ e.stopPropagation();
870
+ inputArea.style.opacity = '1';
871
+
872
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
873
+ handleImageFile(e.dataTransfer.files[0]);
874
+ }
875
+ });
876
+
877
+ // Focus input on load + init theme
878
+ window.addEventListener('DOMContentLoaded', async () => {
879
+ chatInput.focus();
880
+ await loadTheme();
881
+ await loadChatHistory();
882
+ });
883
+
884
+ // Proactive OS Notifications (Battery, Network, etc.)
885
+ window.api.onProactiveNotification((data) => {
886
+ if (!data || !data.message) return;
887
+ appendMessage(data.message, 'ai');
888
+ // Also speak the notification automatically
889
+ speakText(data.message);
890
+ });
891
+
892
+ window.addEventListener('focus', () => {
893
+ if (window.api.clearAiNotifications) window.api.clearAiNotifications();
894
+ });
895
+
896
+ // =====================
897
+ // Proactive Smart Suggestion Engine
898
+ // =====================
899
+
900
+ function showProactiveBar(data) {
901
+ // Clear old chips
902
+ proactiveChips.innerHTML = '';
903
+
904
+ // Set message
905
+ proactiveMessage.textContent = data.message || '';
906
+
907
+ // Render each suggestion as a chip
908
+ data.suggestions.forEach((item, index) => {
909
+ const chip = document.createElement('button');
910
+ chip.className = 'suggestion-chip';
911
+ chip.textContent = item.label;
912
+ chip.style.animationDelay = `${index * 60}ms`;
913
+
914
+ chip.addEventListener('click', async () => {
915
+ hideProactiveBar();
916
+
917
+ if (window.api.recordBehavior) {
918
+ window.api.recordBehavior(`User picked: ${item.label}`);
919
+ }
920
+
921
+ showTyping();
922
+ try {
923
+ const result = await window.api.executeProactiveAction(item.action);
924
+ removeTyping();
925
+ const confirmText = result?.message || `เปิด ${item.label} แล้วค่ะ ✅`;
926
+ const msgDiv = appendMessage(confirmText, 'ai');
927
+ speakText(confirmText);
928
+ if (item.action && item.action.type !== 'none') {
929
+ appendActionCard(msgDiv, item.action);
930
+ }
931
+ } catch (err) {
932
+ removeTyping();
933
+ appendMessage('ขออภัยค่ะ เกิดข้อผิดพลาด', 'ai');
934
+ console.error('[Chip] Error:', err);
935
+ }
936
+ });
937
+
938
+ proactiveChips.appendChild(chip);
939
+ });
940
+
941
+ // Show bar with animation reset
942
+ proactiveBar.style.display = 'none';
943
+ requestAnimationFrame(() => {
944
+ proactiveBar.style.display = 'block';
945
+ });
946
+ }
947
+
948
+ function hideProactiveBar() {
949
+ proactiveBar.style.display = 'none';
950
+ proactiveChips.innerHTML = '';
951
+ }
952
+
953
+ // Receive multi-suggestion data from main process
954
+ window.api.onProactiveSuggestion((data) => {
955
+ if (data && data.message && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
956
+ showProactiveBar(data);
957
+ notifyAiIfNeeded();
958
+ }
959
+ });
960
+
961
+ // Dismiss button
962
+ proactiveDismissBtn.addEventListener('click', () => {
963
+ hideProactiveBar();
964
+ });
965
+
966
+ // Sync Smart Context toggle → start/stop proactive loop
967
+ const smartContextToggle = document.getElementById('smart-context-toggle');
968
+ if (smartContextToggle) {
969
+ smartContextToggle.addEventListener('change', () => {
970
+ window.api.toggleProactive(smartContextToggle.checked);
971
+ });
972
+ }
973
+
974
+ // Spotlight integration
975
+ window.api.onSpotlightToChat((query) => {
976
+ chatInput.value = query;
977
+ chatForm.dispatchEvent(new Event('submit'));
978
+ });