@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.
- package/BUILD_AND_RELEASE.md +75 -0
- package/LICENSE +654 -0
- package/README.md +165 -0
- package/assets/Agent_Mint.png +0 -0
- package/assets/CLI_Screen.png +0 -0
- package/assets/Settings.png +0 -0
- package/assets/icon.png +0 -0
- package/benchmark_ai.js +71 -0
- package/main.js +968 -0
- package/mint-cli-logic.js +71 -0
- package/mint-cli.js +239 -0
- package/package.json +60 -0
- package/preload-picker.js +11 -0
- package/preload-settings.js +11 -0
- package/preload.js +37 -0
- package/privacy.txt +1 -0
- package/src/AI_Brain/Gemini_API.js +419 -0
- package/src/AI_Brain/autonomous_brain.js +139 -0
- package/src/AI_Brain/behavior_memory.js +114 -0
- package/src/AI_Brain/headless_agent.js +120 -0
- package/src/AI_Brain/knowledge_base.js +222 -0
- package/src/AI_Brain/proactive_engine.js +168 -0
- package/src/Automation_Layer/browser_automation.js +147 -0
- package/src/Automation_Layer/file_operations.js +80 -0
- package/src/Automation_Layer/open_app.js +56 -0
- package/src/Automation_Layer/open_website.js +38 -0
- package/src/CLI/chat_ui.js +468 -0
- package/src/CLI/list_features.js +56 -0
- package/src/CLI/onboarding.js +60 -0
- package/src/Command_Parser/parser.js +34 -0
- package/src/Plugins/dev_tools.js +41 -0
- package/src/Plugins/discord.js +20 -0
- package/src/Plugins/docker.js +45 -0
- package/src/Plugins/google_calendar.js +26 -0
- package/src/Plugins/obsidian.js +54 -0
- package/src/Plugins/plugin_manager.js +81 -0
- package/src/Plugins/spotify.js +45 -0
- package/src/Plugins/system_metrics.js +31 -0
- package/src/System/chat_history_manager.js +57 -0
- package/src/System/config_manager.js +73 -0
- package/src/System/custom_workflows.js +127 -0
- package/src/System/daemon_manager.js +67 -0
- package/src/System/system_automation.js +88 -0
- package/src/System/system_events.js +79 -0
- package/src/System/system_info.js +55 -0
- package/src/System/task_manager.js +85 -0
- package/src/UI/floating.css +80 -0
- package/src/UI/floating.html +17 -0
- package/src/UI/floating.js +67 -0
- package/src/UI/index.html +126 -0
- package/src/UI/preload-floating.js +7 -0
- package/src/UI/preload-spotlight.js +10 -0
- package/src/UI/preload-widget.js +5 -0
- package/src/UI/proactive-glow.html +42 -0
- package/src/UI/renderer.js +978 -0
- package/src/UI/screenPicker.html +214 -0
- package/src/UI/screenPicker.js +262 -0
- package/src/UI/settings.css +705 -0
- package/src/UI/settings.html +396 -0
- package/src/UI/settings.js +514 -0
- package/src/UI/spotlight.css +119 -0
- package/src/UI/spotlight.html +23 -0
- package/src/UI/spotlight.js +181 -0
- package/src/UI/styles.css +627 -0
- package/src/UI/widget.css +218 -0
- package/src/UI/widget.html +29 -0
- package/src/UI/widget.js +10 -0
- package/tech_news.txt +3 -0
- 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
|
+
});
|