@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
package/main.js ADDED
@@ -0,0 +1,968 @@
1
+ const { app, BrowserWindow, ipcMain, shell, globalShortcut, clipboard, Tray, Menu, nativeImage, desktopCapturer, screen, powerMonitor } = require('electron');
2
+ const path = require('path');
3
+ require('dotenv').config();
4
+
5
+ const { handleChat, resetChat, getChatTranscript, translateImageContent, refreshApiKeyFromConfig } = require('./src/AI_Brain/Gemini_API');
6
+ const { openApp } = require('./src/Automation_Layer/open_app');
7
+ const { openWebsite, openSearch } = require('./src/Automation_Layer/open_website');
8
+ const { performWebAutomation } = require('./src/Automation_Layer/browser_automation');
9
+ const { createFolder, openFile, deleteFile } = require('./src/Automation_Layer/file_operations');
10
+ const { getSystemInfo, getWeather } = require('./src/System/system_info');
11
+ const { readConfig, writeConfig } = require('./src/System/config_manager');
12
+ const { parseCommand } = require('./src/Command_Parser/parser');
13
+ const pluginManager = require('./src/Plugins/plugin_manager');
14
+ const { analyzeAndSuggest } = require('./src/AI_Brain/proactive_engine');
15
+ const { recordBehavior, getBehaviorSummary } = require('./src/AI_Brain/behavior_memory');
16
+ const { indexFile } = require('./src/AI_Brain/knowledge_base');
17
+ const SystemAutomation = require('./src/System/system_automation');
18
+ const systemEvents = require('./src/System/system_events');
19
+ const customWorkflows = require('./src/System/custom_workflows');
20
+ const googleTTS = require('google-tts-api');
21
+
22
+ let mainWindow;
23
+ let settingsWindow = null;
24
+ let screenPickerWindow = null;
25
+ let spotlightWindow = null;
26
+ let floatingWindow = null;
27
+ let widgetWindow = null;
28
+ let proactiveGlowWindow = null;
29
+ let tray = null;
30
+ let floatingUnreadCount = 0;
31
+
32
+ // =====================
33
+ // Proactive Loop
34
+ // =====================
35
+ let proactiveIntervalHandle = null;
36
+
37
+ async function runProactiveCycle() {
38
+ if (!mainWindow || mainWindow.isDestroyed()) return;
39
+ try {
40
+ // Show Glow
41
+ showProactiveGlow();
42
+
43
+ // Silent screen capture
44
+ const primaryDisplay = screen.getPrimaryDisplay();
45
+ // Downscale to 50% for performance
46
+ const width = Math.floor(primaryDisplay.size.width * 0.5);
47
+ const height = Math.floor(primaryDisplay.size.height * 0.5);
48
+
49
+ const sources = await desktopCapturer.getSources({
50
+ types: ['screen'],
51
+ thumbnailSize: { width, height }
52
+ });
53
+ const primarySource = sources[0];
54
+ if (!primarySource || !primarySource.thumbnail) return;
55
+
56
+ // Compress image to JPEG to save huge base64 payload size and memory
57
+ // .toJPEG(quality) where quality is 0-100
58
+ const base64Image = primarySource.thumbnail.toJPEG(60).toString('base64');
59
+ const behaviorSummary = getBehaviorSummary();
60
+
61
+ const result = await analyzeAndSuggest(base64Image, behaviorSummary);
62
+
63
+ if (result && result.message && Array.isArray(result.suggestions)) {
64
+ // Record the observed context into behavior memory
65
+ if (result.context) recordBehavior(result.context);
66
+
67
+ // Push suggestion to the renderer
68
+ if (mainWindow && !mainWindow.isDestroyed()) {
69
+ mainWindow.webContents.send('proactive-suggestion', result);
70
+ }
71
+ }
72
+
73
+ // Hide Glow after analysis
74
+ hideProactiveGlow();
75
+ } catch (err) {
76
+ console.error('[Proactive] Cycle error:', err.message);
77
+ hideProactiveGlow();
78
+ }
79
+ }
80
+
81
+ function createProactiveGlowWindow() {
82
+ if (proactiveGlowWindow) return;
83
+ const { width, height } = screen.getPrimaryDisplay().bounds;
84
+
85
+ proactiveGlowWindow = new BrowserWindow({
86
+ width, height,
87
+ x: 0, y: 0,
88
+ frame: false,
89
+ transparent: true,
90
+ alwaysOnTop: true,
91
+ skipTaskbar: true,
92
+ focusable: false,
93
+ show: false,
94
+ webPreferences: {
95
+ nodeIntegration: false,
96
+ contextIsolation: true
97
+ }
98
+ });
99
+
100
+ proactiveGlowWindow.setIgnoreMouseEvents(true);
101
+ proactiveGlowWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
102
+ proactiveGlowWindow.setAlwaysOnTop(true, 'screen-saver');
103
+ proactiveGlowWindow.loadFile('src/UI/proactive-glow.html');
104
+
105
+ proactiveGlowWindow.on('closed', () => { proactiveGlowWindow = null; });
106
+ }
107
+
108
+ function showProactiveGlow() {
109
+ if (!proactiveGlowWindow) createProactiveGlowWindow();
110
+ if (proactiveGlowWindow) proactiveGlowWindow.showInactive();
111
+ }
112
+
113
+ function hideProactiveGlow() {
114
+ if (proactiveGlowWindow && !proactiveGlowWindow.isDestroyed()) {
115
+ proactiveGlowWindow.hide();
116
+ }
117
+ }
118
+
119
+ function startProactiveLoop(intervalSec) {
120
+ // Stop existing loop first
121
+ if (proactiveIntervalHandle) {
122
+ clearInterval(proactiveIntervalHandle);
123
+ proactiveIntervalHandle = null;
124
+ }
125
+ // Read from config if not provided
126
+ const cfg = readConfig();
127
+ const ms = (intervalSec || cfg.proactiveInterval || 60) * 1000;
128
+ console.log(`[Proactive] Starting loop — interval: ${ms / 1000}s`);
129
+ proactiveIntervalHandle = setInterval(runProactiveCycle, ms);
130
+ }
131
+
132
+ function stopProactiveLoop() {
133
+ if (proactiveIntervalHandle) {
134
+ clearInterval(proactiveIntervalHandle);
135
+ proactiveIntervalHandle = null;
136
+ console.log('[Proactive] Stopped proactive loop.');
137
+ }
138
+ }
139
+
140
+ // Check idle state every minute to pause/resume background tasks
141
+ const IDLE_THRESHOLD_SEC = 300; // 5 minutes
142
+ setInterval(() => {
143
+ if (!proactiveIntervalHandle) return; // Only manage if it's supposed to be active (Smart Context is ON)
144
+
145
+ // powerMonitor.getSystemIdleTime() is available after app is ready
146
+ if (app.isReady()) {
147
+ const idleSec = powerMonitor.getSystemIdleTime();
148
+ if (idleSec >= IDLE_THRESHOLD_SEC) {
149
+ console.log(`[System Idle] User idle for ${idleSec}s. Pausing Proactive loop to save resources.`);
150
+ stopProactiveLoop();
151
+
152
+ // Wait for user to come back
153
+ const resumeChecker = setInterval(() => {
154
+ if (powerMonitor.getSystemIdleTime() < 10) {
155
+ console.log('[System Idle] User returned. Resuming Proactive loop.');
156
+ clearInterval(resumeChecker);
157
+ startProactiveLoop();
158
+ }
159
+ }, 5000);
160
+ }
161
+ }
162
+ }, 60000);
163
+
164
+ function createWindow() {
165
+ mainWindow = new BrowserWindow({
166
+ width: 600,
167
+ height: 800,
168
+ icon: path.join(__dirname, 'assets', 'icon.png'),
169
+ webPreferences: {
170
+ preload: path.join(__dirname, 'preload.js'),
171
+ nodeIntegration: false,
172
+ contextIsolation: true,
173
+ },
174
+ frame: false,
175
+ transparent: true,
176
+ resizable: true,
177
+ show: false
178
+ });
179
+
180
+ mainWindow.loadFile('src/UI/index.html');
181
+
182
+ mainWindow.on('ready-to-show', () => {
183
+ mainWindow.show();
184
+ });
185
+
186
+ mainWindow.on('close', function (event) {
187
+ if (!app.isQuiting) {
188
+ event.preventDefault();
189
+ mainWindow.hide();
190
+ }
191
+ return false;
192
+ });
193
+
194
+ mainWindow.on('focus', () => {
195
+ clearFloatingUnread();
196
+ });
197
+ }
198
+
199
+ function createTray() {
200
+ const iconPath = path.join(__dirname, 'assets', 'icon.png');
201
+ let icon = nativeImage.createFromPath(iconPath);
202
+ // Optional: resize it for the tray if needed, but Electron handles this natively on many OSs
203
+ icon = icon.resize({ width: 16, height: 16 });
204
+
205
+ tray = new Tray(icon);
206
+ tray.setToolTip('Mint AI Assistant');
207
+
208
+ const contextMenu = Menu.buildFromTemplate([
209
+ { label: 'Show App', click: () => { if (mainWindow) mainWindow.show(); } },
210
+ { label: 'Settings', click: () => { createSettingsWindow(); } },
211
+ { type: 'separator' },
212
+ { label: 'Quit', click: () => {
213
+ app.isQuiting = true;
214
+ app.quit();
215
+ }}
216
+ ]);
217
+
218
+ tray.setContextMenu(contextMenu);
219
+
220
+ tray.on('click', () => {
221
+ if (mainWindow) {
222
+ if (mainWindow.isVisible()) {
223
+ mainWindow.hide();
224
+ } else {
225
+ mainWindow.show();
226
+ }
227
+ }
228
+ });
229
+ }
230
+
231
+ function createSettingsWindow() {
232
+ if (settingsWindow) {
233
+ settingsWindow.focus();
234
+ return;
235
+ }
236
+ settingsWindow = new BrowserWindow({
237
+ width: 720,
238
+ height: 620,
239
+ minWidth: 640,
240
+ minHeight: 560,
241
+ icon: path.join(__dirname, 'assets', 'icon.png'),
242
+ webPreferences: {
243
+ preload: path.join(__dirname, 'preload-settings.js'),
244
+ nodeIntegration: false,
245
+ contextIsolation: true,
246
+ },
247
+ frame: false,
248
+ transparent: true,
249
+ resizable: true,
250
+ parent: mainWindow,
251
+ });
252
+ settingsWindow.loadFile('src/UI/settings.html');
253
+ settingsWindow.on('closed', () => { settingsWindow = null; });
254
+ }
255
+
256
+ function createSpotlightWindow() {
257
+ if (spotlightWindow) {
258
+ spotlightWindow.show();
259
+ return;
260
+ }
261
+
262
+ const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
263
+ const windowWidth = 600;
264
+ const windowHeight = 80; // Starts small, expands if results show
265
+
266
+ spotlightWindow = new BrowserWindow({
267
+ width: windowWidth,
268
+ height: windowHeight,
269
+ x: Math.floor((screenWidth - windowWidth) / 2),
270
+ y: Math.floor(screenHeight * 0.25), // 25% from top
271
+ frame: false,
272
+ transparent: true,
273
+ alwaysOnTop: true,
274
+ skipTaskbar: true,
275
+ show: false,
276
+ webPreferences: {
277
+ preload: path.join(__dirname, 'src/UI/preload-spotlight.js'),
278
+ nodeIntegration: false,
279
+ contextIsolation: true,
280
+ }
281
+ });
282
+
283
+ spotlightWindow.loadFile('src/UI/spotlight.html');
284
+
285
+ spotlightWindow.on('blur', () => {
286
+ spotlightWindow.hide();
287
+ });
288
+
289
+ spotlightWindow.on('closed', () => {
290
+ spotlightWindow = null;
291
+ });
292
+ }
293
+
294
+ function createFloatingWindow() {
295
+ if (floatingWindow) return;
296
+ const display = screen.getPrimaryDisplay();
297
+ const { x, y, width, height } = display.workArea;
298
+ const size = 72;
299
+ const margin = 20;
300
+ const posX = x + width - size - margin;
301
+ const posY = y + height - size - margin - 40;
302
+
303
+ floatingWindow = new BrowserWindow({
304
+ width: size,
305
+ height: size,
306
+ x: posX,
307
+ y: posY,
308
+ frame: false,
309
+ transparent: true,
310
+ resizable: false,
311
+ alwaysOnTop: true,
312
+ skipTaskbar: true,
313
+ show: true,
314
+ webPreferences: {
315
+ preload: path.join(__dirname, 'src/UI/preload-floating.js'),
316
+ nodeIntegration: false,
317
+ contextIsolation: true
318
+ }
319
+ });
320
+
321
+ floatingWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
322
+ floatingWindow.setAlwaysOnTop(true, 'floating');
323
+ floatingWindow.loadFile('src/UI/floating.html');
324
+
325
+ floatingWindow.on('closed', () => {
326
+ floatingWindow = null;
327
+ });
328
+ }
329
+
330
+ function createWidgetWindow() {
331
+ if (widgetWindow) return;
332
+
333
+ widgetWindow = new BrowserWindow({
334
+ width: 150,
335
+ height: 150,
336
+ frame: false,
337
+ transparent: true,
338
+ resizable: false,
339
+ alwaysOnTop: true,
340
+ skipTaskbar: true,
341
+ show: true,
342
+ webPreferences: {
343
+ preload: require('path').join(__dirname, 'src/UI/preload-widget.js'),
344
+ nodeIntegration: false,
345
+ contextIsolation: true
346
+ }
347
+ });
348
+
349
+ widgetWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
350
+ widgetWindow.setAlwaysOnTop(true, 'floating');
351
+
352
+ // Position Top-Right loosely
353
+ try {
354
+ const primaryDisplay = require('electron').screen.getPrimaryDisplay();
355
+ const { width, height, x, y } = primaryDisplay.workArea;
356
+ widgetWindow.setPosition(x + width - 150 - 40, y + 40);
357
+ } catch (e) {}
358
+
359
+ widgetWindow.loadFile('src/UI/widget.html');
360
+
361
+ widgetWindow.on('closed', () => {
362
+ widgetWindow = null;
363
+ });
364
+ }
365
+
366
+ function updateFloatingUnread() {
367
+ if (floatingWindow && !floatingWindow.isDestroyed()) {
368
+ floatingWindow.webContents.send('floating-notify', floatingUnreadCount);
369
+ }
370
+ }
371
+
372
+ function clearFloatingUnread() {
373
+ if (floatingUnreadCount === 0) return;
374
+ floatingUnreadCount = 0;
375
+ updateFloatingUnread();
376
+ }
377
+
378
+ app.whenReady().then(() => {
379
+ const config = readConfig();
380
+ createWindow();
381
+ createTray();
382
+ createFloatingWindow();
383
+
384
+ // Only show AI widget if enabled in settings
385
+ if (config.showDesktopWidget !== false) {
386
+ createWidgetWindow();
387
+ }
388
+
389
+ // Start monitoring system events (battery, wifi, etc.)
390
+ systemEvents.startMonitoring();
391
+ if (config.enableCustomWorkflows !== false) {
392
+ customWorkflows.startMonitoring(mainWindow.webContents);
393
+ }
394
+
395
+ systemEvents.on('low-battery', (level) => {
396
+ if (mainWindow && !mainWindow.isDestroyed()) {
397
+ mainWindow.webContents.send('proactive-notification', {
398
+ message: `⚠️ แบตเตอรี่เหลือน้อยแล้วนะคะ (${level}%) อย่าลืมเสียบสายชาร์จนะค๊า ✨`,
399
+ type: 'warning'
400
+ });
401
+ }
402
+ });
403
+
404
+ systemEvents.on('connection-change', (isOnline) => {
405
+ if (mainWindow && !mainWindow.isDestroyed()) {
406
+ const msg = isOnline ? '✅ เชื่อมต่ออินเทอร์เน็ตได้แล้วค่ะ ✨' : '❌ การเชื่อมต่ออินเทอร์เน็ตขาดหายไปนะคะ';
407
+ mainWindow.webContents.send('proactive-notification', { message: msg, type: 'info' });
408
+ }
409
+ });
410
+
411
+ globalShortcut.register('CommandOrControl+Shift+Space', () => {
412
+ if (mainWindow) {
413
+ if (mainWindow.isVisible()) {
414
+ mainWindow.hide();
415
+ } else {
416
+ mainWindow.show();
417
+ }
418
+ }
419
+ });
420
+
421
+ globalShortcut.register('Alt+Space', () => {
422
+ if (spotlightWindow && spotlightWindow.isVisible()) {
423
+ spotlightWindow.hide();
424
+ } else {
425
+ createSpotlightWindow();
426
+ spotlightWindow.show();
427
+ }
428
+ });
429
+
430
+ app.on('activate', function () {
431
+ if (BrowserWindow.getAllWindows().length === 0) createWindow();
432
+ });
433
+ });
434
+
435
+ app.on('window-all-closed', (e) => {
436
+ e.preventDefault();
437
+ });
438
+
439
+ app.on('will-quit', () => {
440
+ globalShortcut.unregisterAll();
441
+ });
442
+
443
+ // =====================
444
+ // IPC Handlers — Chat
445
+ // =====================
446
+ ipcMain.handle('chat-message', async (event, message, base64Image = null, base64Audio = null) => {
447
+ try {
448
+ const rawResponse = await handleChat(message, base64Image, base64Audio);
449
+ const aiResponse = parseCommand(rawResponse);
450
+
451
+ if (aiResponse.action && aiResponse.action.type !== 'none') {
452
+ try {
453
+ const actionResult = await executeAction(aiResponse.action);
454
+ // If the action returned a string result (e.g. from Web Automation), append it.
455
+ if (actionResult && typeof actionResult === 'string') {
456
+ aiResponse.response += `\n\n${actionResult}`;
457
+ }
458
+ } catch (err) {
459
+ console.error("Action execution error:", err);
460
+ aiResponse.response += "\n\n(Note: I tried to execute the action, but an error occurred.)";
461
+ }
462
+ }
463
+
464
+ return aiResponse;
465
+ } catch (error) {
466
+ console.error('Chat error:', error);
467
+ return { response: 'Error communicating with Gemini API. Check your console and API key.', action: { type: 'none' } };
468
+ }
469
+ });
470
+
471
+ ipcMain.on('close-window', (event) => {
472
+ if (mainWindow) mainWindow.hide();
473
+ });
474
+
475
+ ipcMain.on('minimize-window', (event) => {
476
+ if (mainWindow) mainWindow.hide();
477
+ });
478
+
479
+ ipcMain.on('quit-app', (event) => {
480
+ app.isQuiting = true;
481
+ app.quit();
482
+ });
483
+
484
+ ipcMain.on('maximize-window', (event) => {
485
+ if (mainWindow) {
486
+ if (mainWindow.isMaximized()) {
487
+ mainWindow.unmaximize();
488
+ } else {
489
+ mainWindow.maximize();
490
+ }
491
+ }
492
+ });
493
+
494
+ ipcMain.handle('reset-chat', () => {
495
+ resetChat();
496
+ return { success: true };
497
+ });
498
+
499
+ ipcMain.handle('get-chat-history', () => {
500
+ return getChatTranscript();
501
+ });
502
+
503
+ // =====================
504
+ // IPC Handlers — Settings
505
+ // =====================
506
+ ipcMain.handle('open-settings', () => {
507
+ createSettingsWindow();
508
+ });
509
+
510
+ ipcMain.handle('get-settings', () => {
511
+ return readConfig();
512
+ });
513
+
514
+ ipcMain.handle('save-settings', (event, config) => {
515
+ const result = writeConfig(config);
516
+ // Refresh API key if user updated it in settings
517
+ refreshApiKeyFromConfig();
518
+ // 🔔 Notify main chat window immediately
519
+ if (mainWindow && !mainWindow.isDestroyed()) {
520
+ mainWindow.webContents.send('settings-changed', config);
521
+ }
522
+ // Restart proactive loop with new interval if it's running
523
+ if (proactiveIntervalHandle) {
524
+ startProactiveLoop(config.proactiveInterval);
525
+ }
526
+ if (config.enableCustomWorkflows !== false) {
527
+ customWorkflows.startMonitoring(mainWindow.webContents);
528
+ } else {
529
+ customWorkflows.stopMonitoring();
530
+ }
531
+
532
+ // Toggle Desktop Widget
533
+ if (config.showDesktopWidget === false) {
534
+ if (widgetWindow && !widgetWindow.isDestroyed()) {
535
+ widgetWindow.close();
536
+ widgetWindow = null;
537
+ }
538
+ } else {
539
+ if (!widgetWindow || widgetWindow.isDestroyed()) {
540
+ createWidgetWindow();
541
+ }
542
+ }
543
+
544
+ return result;
545
+ });
546
+
547
+ ipcMain.on('set-ai-state', (event, state) => {
548
+ if (widgetWindow && !widgetWindow.isDestroyed()) {
549
+ widgetWindow.webContents.send('widget-state', state);
550
+ }
551
+ });
552
+
553
+ ipcMain.on('close-settings', () => {
554
+ if (settingsWindow) settingsWindow.close();
555
+ });
556
+
557
+ ipcMain.on('quit-app', () => {
558
+ app.isQuiting = true;
559
+ app.quit();
560
+ });
561
+
562
+ ipcMain.handle('open-custom-workflows', () => {
563
+ customWorkflows.openConfigFile();
564
+ });
565
+
566
+ ipcMain.handle('reload-custom-workflows', () => {
567
+ customWorkflows.loadWorkflows();
568
+ return { success: true };
569
+ });
570
+
571
+ // =====================
572
+ // IPC Handlers — Spotlight
573
+ // =====================
574
+ ipcMain.on('spotlight-close', () => {
575
+ if (spotlightWindow) spotlightWindow.close();
576
+ });
577
+
578
+ ipcMain.on('spotlight-hide', () => {
579
+ if (spotlightWindow) spotlightWindow.hide();
580
+ });
581
+
582
+ ipcMain.on('spotlight-submit', async (event, query) => {
583
+ console.log('[Spotlight] Submit:', query);
584
+ if (spotlightWindow) spotlightWindow.hide();
585
+
586
+ // Show main window and send the message
587
+ if (mainWindow) {
588
+ mainWindow.show();
589
+ // We can either just show it or actually trigger the chat
590
+ mainWindow.webContents.send('spotlight-to-chat', query);
591
+ }
592
+ });
593
+
594
+ ipcMain.on('spotlight-resize', (event, width, height) => {
595
+ if (spotlightWindow) {
596
+ spotlightWindow.setSize(width, height);
597
+ }
598
+ });
599
+
600
+ // =====================
601
+ // IPC — Floating Icon
602
+ // =====================
603
+ ipcMain.on('floating-click', () => {
604
+ if (mainWindow) {
605
+ mainWindow.show();
606
+ mainWindow.focus();
607
+ }
608
+ clearFloatingUnread();
609
+ });
610
+
611
+ ipcMain.on('ai-notify', () => {
612
+ floatingUnreadCount += 1;
613
+ updateFloatingUnread();
614
+ });
615
+
616
+ ipcMain.on('ai-notify-clear', () => {
617
+ clearFloatingUnread();
618
+ });
619
+
620
+ ipcMain.on('floating-drag-move', (_event, x, y) => {
621
+ if (!floatingWindow || floatingWindow.isDestroyed()) return;
622
+ const [width, height] = floatingWindow.getSize();
623
+ const posX = Math.round(x - width / 2);
624
+ const posY = Math.round(y - height / 2);
625
+ floatingWindow.setBounds({ x: posX, y: posY, width, height });
626
+ });
627
+
628
+ ipcMain.handle('open-external', (event, url) => {
629
+ shell.openExternal(url);
630
+ });
631
+
632
+ // =====================
633
+ // IPC Handlers — Clipboard
634
+ // =====================
635
+ ipcMain.handle('clipboard-read', () => {
636
+ return clipboard.readText();
637
+ });
638
+
639
+ ipcMain.handle('clipboard-write', (event, text) => {
640
+ clipboard.writeText(text);
641
+ return { success: true };
642
+ });
643
+
644
+ // =====================
645
+ // IPC Handlers — TTS
646
+ // =====================
647
+ ipcMain.handle('get-tts-urls', async (event, text) => {
648
+ try {
649
+ const isThai = /[\u0E00-\u0E7F]/.test(text);
650
+ const lang = isThai ? 'th' : 'en';
651
+
652
+ const results = googleTTS.getAllAudioUrls(text, {
653
+ lang: lang,
654
+ slow: false,
655
+ host: 'https://translate.google.com',
656
+ splitPunct: ',.?!;:',
657
+ });
658
+ return results;
659
+ } catch (e) {
660
+ console.error("TTS Error:", e);
661
+ return [];
662
+ }
663
+ });
664
+
665
+ // =====================
666
+ // IPC Handlers — System Info
667
+ // =====================
668
+ ipcMain.handle('get-system-info', async () => {
669
+ return getSystemInfo();
670
+ });
671
+
672
+ ipcMain.handle('get-weather', async (event, city) => {
673
+ return getWeather(city);
674
+ });
675
+
676
+ // =====================
677
+ // IPC Handlers — Screen Vision
678
+ // =====================
679
+ ipcMain.handle('start-screen-capture', async () => {
680
+ if (screenPickerWindow) return; // Prevent multiple windows
681
+
682
+ try {
683
+ // Capture primary display
684
+ const primaryDisplay = screen.getPrimaryDisplay();
685
+ const { width, height } = primaryDisplay.size;
686
+
687
+ const sources = await desktopCapturer.getSources({
688
+ types: ['screen'],
689
+ thumbnailSize: { width, height }
690
+ });
691
+
692
+ // Find the full primary screen (usually "Screen 1" or "Entire screen")
693
+ const primarySource = sources[0]; // Assuming single/primary monitor is the first
694
+
695
+ // Create transparent, borderless screen picker window
696
+ screenPickerWindow = new BrowserWindow({
697
+ width, height,
698
+ x: primaryDisplay.bounds.x, y: primaryDisplay.bounds.y,
699
+ fullscreen: true,
700
+ transparent: true,
701
+ frame: false,
702
+ alwaysOnTop: true,
703
+ skipTaskbar: true,
704
+ webPreferences: {
705
+ preload: path.join(__dirname, 'preload-picker.js'),
706
+ nodeIntegration: false,
707
+ contextIsolation: true
708
+ }
709
+ });
710
+
711
+ await screenPickerWindow.loadFile('src/UI/screenPicker.html');
712
+
713
+ // Ensure image data isn't missing
714
+ if(primarySource && primarySource.thumbnail) {
715
+ screenPickerWindow.webContents.send('screenshot-data', primarySource.thumbnail.toDataURL());
716
+ }
717
+
718
+ screenPickerWindow.on('closed', () => { screenPickerWindow = null; });
719
+ } catch (err) {
720
+ console.error("Error starting screen capture:", err);
721
+ }
722
+ });
723
+
724
+ // Received selection from the picker
725
+ ipcMain.on('vision-selection', (event, base64Image) => {
726
+ if (screenPickerWindow) screenPickerWindow.close();
727
+
728
+ // Relay image back to main window interface
729
+ if (mainWindow) {
730
+ mainWindow.webContents.send('vision-ready', base64Image);
731
+ mainWindow.show(); // Bring chat back to focus
732
+ }
733
+ });
734
+
735
+ let translateIntervalHandle = null;
736
+ let isTranslateRequestInFlight = false;
737
+ let translateCooldownUntil = 0;
738
+ const TRANSLATE_REFRESH_MS = 3000;
739
+ const TRANSLATE_FAILURE_COOLDOWN_MS = 15000;
740
+
741
+ // Start Continuous Live Translate
742
+ ipcMain.on('vision-translate-start', async (event, rect) => {
743
+ if (!screenPickerWindow) return;
744
+
745
+ screenPickerWindow.setIgnoreMouseEvents(true, { forward: true });
746
+ isTranslateRequestInFlight = false;
747
+ translateCooldownUntil = 0;
748
+
749
+ // Stop any existing loop
750
+ if (translateIntervalHandle) clearInterval(translateIntervalHandle);
751
+
752
+ // Initial capture immediately
753
+ captureAndTranslate(rect);
754
+
755
+ // Then start ticking every 3 seconds
756
+ translateIntervalHandle = setInterval(() => {
757
+ captureAndTranslate(rect);
758
+ }, TRANSLATE_REFRESH_MS);
759
+ });
760
+
761
+ // Stop Continuous Live Translate
762
+ ipcMain.on('vision-translate-stop', () => {
763
+ if (translateIntervalHandle) {
764
+ clearInterval(translateIntervalHandle);
765
+ translateIntervalHandle = null;
766
+ }
767
+ if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
768
+ screenPickerWindow.setIgnoreMouseEvents(false);
769
+ }
770
+ isTranslateRequestInFlight = false;
771
+ translateCooldownUntil = 0;
772
+ });
773
+
774
+ ipcMain.on('vision-overlay-interactable', (event, isInteractable) => {
775
+ if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
776
+ screenPickerWindow.setIgnoreMouseEvents(!isInteractable, { forward: true });
777
+ });
778
+
779
+ async function captureAndTranslate(rect) {
780
+ if (!screenPickerWindow || screenPickerWindow.isDestroyed()) return;
781
+ if (isTranslateRequestInFlight) return;
782
+ if (Date.now() < translateCooldownUntil) return;
783
+
784
+ try {
785
+ isTranslateRequestInFlight = true;
786
+ const primaryDisplay = screen.getPrimaryDisplay();
787
+ const sources = await desktopCapturer.getSources({
788
+ types: ['screen'],
789
+ thumbnailSize: {
790
+ width: primaryDisplay.size.width,
791
+ height: primaryDisplay.size.height
792
+ }
793
+ });
794
+
795
+ if (sources.length > 0) {
796
+ const screenImage = sources[0].thumbnail;
797
+
798
+ // Crop the specific rect out of the full screen image
799
+ // We use standard scale (assuming 1:1 scale for exact rect, although hidpi might vary.
800
+ // In a robust implementation we might need `Math.floor(rect.x * display.scaleFactor)`)
801
+ const croppedImage = screenImage.crop({
802
+ x: Math.round(rect.x),
803
+ y: Math.round(rect.y),
804
+ width: Math.round(rect.width),
805
+ height: Math.round(rect.height)
806
+ });
807
+
808
+ // Convert to JPEG Base64 for faster processing
809
+ const base64Crop = croppedImage.toJPEG(70).toString('base64');
810
+ const dataUri = `data:image/jpeg;base64,${base64Crop}`;
811
+
812
+ const translationResult = await translateImageContent(dataUri);
813
+ if (translationResult.retryableFailure) {
814
+ translateCooldownUntil = Date.now() + TRANSLATE_FAILURE_COOLDOWN_MS;
815
+ console.warn(`Live translation cooldown active for ${TRANSLATE_FAILURE_COOLDOWN_MS / 1000}s after retryable API failure.`);
816
+ } else {
817
+ translateCooldownUntil = 0;
818
+ }
819
+
820
+ if (screenPickerWindow && !screenPickerWindow.isDestroyed()) {
821
+ screenPickerWindow.webContents.send('vision-translate-result', translationResult.text);
822
+ }
823
+ }
824
+ } catch (err) {
825
+ console.error("Continuous translation loop failed:", err);
826
+ } finally {
827
+ isTranslateRequestInFlight = false;
828
+ }
829
+ }
830
+
831
+ ipcMain.on('vision-cancel', () => {
832
+ if (translateIntervalHandle) {
833
+ clearInterval(translateIntervalHandle);
834
+ translateIntervalHandle = null;
835
+ }
836
+ isTranslateRequestInFlight = false;
837
+ translateCooldownUntil = 0;
838
+ if (screenPickerWindow) screenPickerWindow.close();
839
+ if (mainWindow) mainWindow.show();
840
+ });
841
+
842
+ // =====================
843
+ // IPC Handlers — Proactive Assistant
844
+ // =====================
845
+ ipcMain.on('toggle-proactive', (event, isOn) => {
846
+ if (isOn) {
847
+ startProactiveLoop(); // reads interval from config automatically
848
+ } else {
849
+ stopProactiveLoop();
850
+ }
851
+ });
852
+
853
+ ipcMain.on('record-behavior', (event, contextDescription) => {
854
+ recordBehavior(contextDescription);
855
+ });
856
+
857
+ // Direct action executor for proactive Accept (no Gemini involved)
858
+ ipcMain.handle('execute-proactive-action', async (event, action) => {
859
+ if (!action || action.type === 'none') {
860
+ return { success: false, message: 'ไม่มี action ที่จะดำเนินการค่ะ' };
861
+ }
862
+ try {
863
+ const result = await executeAction(action);
864
+ // Build a friendly confirmation message
865
+ const messages = {
866
+ open_url: `เปิดเว็บไซต์ให้แล้วค่ะ 🌐`,
867
+ open_app: `เปิดแอป ${action.target} ให้แล้วค่ะ 🚀`,
868
+ search: `ค้นหา "${action.target}" ให้แล้วค่ะ 🔍`,
869
+ web_automation: result || 'ดำเนินการเสร็จแล้วค่ะ ✅',
870
+ create_folder: `สร้างโฟลเดอร์ "${action.target}" แล้วค่ะ 📁`,
871
+ clipboard_write: `คัดลอกข้อความแล้วค่ะ 📋`,
872
+ learn_file: result || `เรียนรู้เอกสารเรียบร้อยค่ะ 📚`,
873
+ };
874
+ return {
875
+ success: true,
876
+ message: messages[action.type] || 'ดำเนินการเสร็จแล้วค่ะ ✅'
877
+ };
878
+ } catch (err) {
879
+ console.error('[ProactiveAction] Error:', err);
880
+ return { success: false, message: `เกิดข้อผิดพลาด: ${err.message}` };
881
+ }
882
+ });
883
+
884
+ ipcMain.handle('capture-silent-screen', async () => {
885
+ try {
886
+ const primaryDisplay = screen.getPrimaryDisplay();
887
+ const { width, height } = primaryDisplay.size;
888
+
889
+ const sources = await desktopCapturer.getSources({
890
+ types: ['screen'],
891
+ thumbnailSize: { width, height }
892
+ });
893
+
894
+ const primarySource = sources[0];
895
+ if (primarySource && primarySource.thumbnail) {
896
+ return primarySource.thumbnail.toDataURL();
897
+ }
898
+ return null;
899
+ } catch (err) {
900
+ console.error("Error silently capturing screen:", err);
901
+ return null;
902
+ }
903
+ });
904
+
905
+ // =====================
906
+ // Action Executor
907
+ // =====================
908
+ async function executeAction(action) {
909
+ console.log("Executing action:", action);
910
+
911
+ switch (action.type) {
912
+ case 'open_url':
913
+ openWebsite(action.target);
914
+ break;
915
+ case 'search':
916
+ openSearch(action.target);
917
+ break;
918
+ case 'open_app':
919
+ openApp(action.target);
920
+ break;
921
+ case 'web_automation':
922
+ return await performWebAutomation(action.target);
923
+ case 'create_folder':
924
+ createFolder(action.target);
925
+ break;
926
+ case 'open_file':
927
+ await openFile(action.target);
928
+ break;
929
+ case 'delete_file':
930
+ await deleteFile(action.target);
931
+ break;
932
+ case 'clipboard_write':
933
+ clipboard.writeText(action.target);
934
+ break;
935
+ case 'learn_file':
936
+ return await indexFile(action.target);
937
+ case 'plugin':
938
+ return await pluginManager.executePlugin(action.pluginName, action.target);
939
+ case 'system_automation':
940
+ return await handleSystemAutomation(action.target);
941
+ }
942
+ }
943
+
944
+ async function handleSystemAutomation(target) {
945
+ const [cmd, value] = target.split(':');
946
+ switch (cmd) {
947
+ case 'volume':
948
+ return await SystemAutomation.setVolume(parseInt(value));
949
+ case 'mute':
950
+ return await SystemAutomation.mute();
951
+ case 'brightness':
952
+ return await SystemAutomation.setBrightness(parseInt(value));
953
+ case 'sleep':
954
+ return await SystemAutomation.sleep();
955
+ case 'restart':
956
+ return await SystemAutomation.restart();
957
+ case 'shutdown':
958
+ return await SystemAutomation.shutdown();
959
+ case 'minimize_all':
960
+ return await SystemAutomation.minimizeAll();
961
+ default:
962
+ // Handle cases where target is just the command (like 'sleep')
963
+ if (SystemAutomation[target]) {
964
+ return await SystemAutomation[target]();
965
+ }
966
+ throw new Error(`Unknown system automation command: ${target}`);
967
+ }
968
+ }