@pheem49/mint 1.5.2 → 1.5.4
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/GUIDE_TH.md +23 -11
- package/README.md +148 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/install.ps1 +64 -0
- package/install.sh +54 -0
- package/main.js +12 -0
- package/package.json +5 -3
- package/preload.js +4 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +231 -498
- package/src/AI_Brain/autonomous_brain.js +46 -19
- package/src/AI_Brain/headless_agent.js +21 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/file_operations.js +17 -5
- package/src/CLI/approval_handler.js +5 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +397 -76
- package/src/CLI/cli_colors.js +86 -3
- package/src/CLI/cli_formatters.js +6 -1
- package/src/CLI/code_agent.js +706 -273
- package/src/CLI/interactive_chat.js +311 -149
- package/src/CLI/slash_command_handler.js +2 -2
- package/src/CLI/updater.js +21 -1
- package/src/System/config_manager.js +5 -1
- package/src/System/ipc_handlers.js +95 -1
- package/src/System/picture_store.js +109 -0
- package/src/System/smart_context.js +227 -0
- package/src/System/task_manager.js +127 -0
- package/src/System/tool_registry.js +13 -0
- package/src/System/window_manager.js +16 -8
- package/src/UI/live2d_manager.js +42 -8
- package/src/UI/preload-spotlight.js +1 -0
- package/src/UI/renderer.js +837 -63
- package/src/UI/settings.css +160 -96
- package/src/UI/settings.html +9 -0
- package/src/UI/settings.js +35 -2
- package/src/UI/spotlight.js +13 -9
- package/src/UI/styles.css +1592 -165
- package/privacy.txt +0 -1
package/src/UI/renderer.js
CHANGED
|
@@ -6,14 +6,36 @@ const maximizeBtn = document.getElementById('maximize-btn');
|
|
|
6
6
|
const minimizeBtn = document.getElementById('minimize-btn');
|
|
7
7
|
const clearBtn = document.getElementById('clear-btn');
|
|
8
8
|
const settingsBtn = document.getElementById('settings-btn');
|
|
9
|
+
const sidebarNewChatBtn = document.getElementById('sidebar-new-chat');
|
|
10
|
+
const sidebarSettingsBtn = document.getElementById('sidebar-settings');
|
|
11
|
+
const sidebarToggleBtn = document.getElementById('sidebar-toggle');
|
|
12
|
+
const appBody = document.querySelector('.app-body');
|
|
13
|
+
const sidebarChatBtn = document.getElementById('sidebar-chat-btn');
|
|
14
|
+
const sidebarPicturesBtn = document.getElementById('sidebar-pictures-btn');
|
|
15
|
+
const picturesLibrary = document.getElementById('pictures-library');
|
|
16
|
+
const picturesGrid = document.getElementById('pictures-grid');
|
|
17
|
+
const picturesEmpty = document.getElementById('pictures-empty');
|
|
18
|
+
const picturesCloseBtn = document.getElementById('pictures-close-btn');
|
|
9
19
|
const micBtn = document.getElementById('mic-btn');
|
|
10
20
|
const visionBtn = document.getElementById('vision-btn');
|
|
21
|
+
const chatProviderSelect = document.getElementById('chat-provider-select');
|
|
11
22
|
const imagePreviewContainer = document.getElementById('image-preview-container');
|
|
12
23
|
const imagePreview = document.getElementById('image-preview');
|
|
13
24
|
const removeImageBtn = document.getElementById('remove-image-btn');
|
|
25
|
+
const agentModeToggle = document.getElementById('agent-mode-toggle');
|
|
14
26
|
const modelMount = document.getElementById('model-mount');
|
|
15
27
|
const modelShell = document.getElementById('model-shell');
|
|
16
28
|
const modelStatus = document.getElementById('model-status');
|
|
29
|
+
const mintStatus = document.getElementById('mint-status');
|
|
30
|
+
const mintStatusLabel = document.getElementById('mint-status-label');
|
|
31
|
+
const modelActivityBadge = document.getElementById('model-activity-badge');
|
|
32
|
+
const startupLoading = document.getElementById('startup-loading');
|
|
33
|
+
const appContainer = document.querySelector('.app-container');
|
|
34
|
+
|
|
35
|
+
if (startupLoading) {
|
|
36
|
+
startupLoading.style.background = 'var(--bg-gradient)';
|
|
37
|
+
startupLoading.style.color = 'var(--text-muted)';
|
|
38
|
+
}
|
|
17
39
|
|
|
18
40
|
// Proactive Assistant elements
|
|
19
41
|
const proactiveBar = document.getElementById('proactive-bar');
|
|
@@ -28,6 +50,92 @@ let ttsVolume = 1.0;
|
|
|
28
50
|
let ttsSpeed = 1.0;
|
|
29
51
|
let ttsPitch = 1.0;
|
|
30
52
|
let lastConversationLanguage = 'auto';
|
|
53
|
+
let mintActivityResetTimer = null;
|
|
54
|
+
let currentSettings = {};
|
|
55
|
+
|
|
56
|
+
const PROVIDER_PICKER_OPTIONS = [
|
|
57
|
+
['gemini', 'Gemini'],
|
|
58
|
+
['anthropic', 'Claude'],
|
|
59
|
+
['openai', 'OpenAI'],
|
|
60
|
+
['ollama', 'Ollama'],
|
|
61
|
+
['huggingface', 'Hugging Face'],
|
|
62
|
+
['local_openai', 'Local']
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function buildProviderPicker(settings = currentSettings) {
|
|
66
|
+
if (!chatProviderSelect) return;
|
|
67
|
+
chatProviderSelect.textContent = '';
|
|
68
|
+
PROVIDER_PICKER_OPTIONS.forEach(([value, label]) => {
|
|
69
|
+
const option = document.createElement('option');
|
|
70
|
+
option.value = value;
|
|
71
|
+
option.textContent = label;
|
|
72
|
+
chatProviderSelect.appendChild(option);
|
|
73
|
+
});
|
|
74
|
+
chatProviderSelect.value = settings.aiProvider || 'gemini';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function syncAgentModeToggle(settings = currentSettings) {
|
|
78
|
+
if (!agentModeToggle) return;
|
|
79
|
+
agentModeToggle.checked = settings.assistantMode === 'agent';
|
|
80
|
+
agentModeToggle.closest('.smart-context-control')?.classList.toggle('is-active', agentModeToggle.checked);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function changeChatProvider(provider) {
|
|
84
|
+
if (!PROVIDER_PICKER_OPTIONS.some(([value]) => value === provider)) return;
|
|
85
|
+
const nextSettings = { ...currentSettings, aiProvider: provider };
|
|
86
|
+
chatProviderSelect.disabled = true;
|
|
87
|
+
try {
|
|
88
|
+
const result = await window.api.saveSettings(nextSettings);
|
|
89
|
+
if (!result || result.success !== false) {
|
|
90
|
+
currentSettings = nextSettings;
|
|
91
|
+
buildProviderPicker(currentSettings);
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error(result.message || 'Unable to save provider setting');
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Failed to change provider:', error);
|
|
97
|
+
buildProviderPicker(currentSettings);
|
|
98
|
+
setMintActivity('error');
|
|
99
|
+
} finally {
|
|
100
|
+
chatProviderSelect.disabled = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const MINT_ACTIVITY_STATES = {
|
|
105
|
+
idle: { label: 'Idle', title: 'Mint is idle' },
|
|
106
|
+
listening: { label: 'Listening', title: 'Mint is listening' },
|
|
107
|
+
thinking: { label: 'Thinking', title: 'Mint is thinking' },
|
|
108
|
+
speaking: { label: 'Speaking', title: 'Mint is speaking' },
|
|
109
|
+
error: { label: 'Error', title: 'Mint needs attention' }
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function setMintActivity(state, options = {}) {
|
|
113
|
+
const normalizedState = MINT_ACTIVITY_STATES[state] ? state : 'idle';
|
|
114
|
+
const meta = MINT_ACTIVITY_STATES[normalizedState];
|
|
115
|
+
if (mintActivityResetTimer) {
|
|
116
|
+
clearTimeout(mintActivityResetTimer);
|
|
117
|
+
mintActivityResetTimer = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[mintStatus, modelActivityBadge].forEach((element) => {
|
|
121
|
+
if (!element) return;
|
|
122
|
+
element.dataset.state = normalizedState;
|
|
123
|
+
element.title = meta.title;
|
|
124
|
+
const label = element.querySelector('.mint-status-label');
|
|
125
|
+
if (label) label.textContent = meta.label;
|
|
126
|
+
});
|
|
127
|
+
if (mintStatusLabel) mintStatusLabel.textContent = meta.label;
|
|
128
|
+
|
|
129
|
+
if (window.api && window.api.setAiState) {
|
|
130
|
+
window.api.setAiState(normalizedState);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (normalizedState === 'error' || options.resetAfter) {
|
|
134
|
+
mintActivityResetTimer = setTimeout(() => {
|
|
135
|
+
setMintActivity('idle');
|
|
136
|
+
}, options.resetAfter || 3500);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
31
139
|
|
|
32
140
|
function detectConversationLanguage(text) {
|
|
33
141
|
const value = String(text || '');
|
|
@@ -56,8 +164,11 @@ function buildInteractionLanguageInstruction() {
|
|
|
56
164
|
// --- Theme Loading ---
|
|
57
165
|
function applyTheme(theme, accentColor, systemTextColor, config = {}) {
|
|
58
166
|
document.documentElement.setAttribute('data-theme', theme || 'dark');
|
|
59
|
-
const accent = accentColor || '#
|
|
60
|
-
const
|
|
167
|
+
const accent = accentColor || '#8f6cf5';
|
|
168
|
+
const defaultTextColor = theme === 'light' ? '#0f172a' : '#e8e8ea';
|
|
169
|
+
const textColor = (!systemTextColor || (theme === 'light' && systemTextColor === '#f8fafc'))
|
|
170
|
+
? defaultTextColor
|
|
171
|
+
: systemTextColor;
|
|
61
172
|
document.documentElement.style.setProperty('--accent', accent);
|
|
62
173
|
document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
|
|
63
174
|
document.documentElement.style.setProperty('--text-main', textColor);
|
|
@@ -65,19 +176,36 @@ function applyTheme(theme, accentColor, systemTextColor, config = {}) {
|
|
|
65
176
|
// Dynamic UI Customizations
|
|
66
177
|
document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
|
|
67
178
|
document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
|
|
179
|
+
document.documentElement.style.fontSize = config.fontSize || '15px';
|
|
68
180
|
|
|
69
181
|
if (theme === 'custom') {
|
|
70
182
|
if (config.customBgStart && config.customBgEnd) {
|
|
71
183
|
const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
|
|
184
|
+
document.documentElement.style.setProperty('--bg-color', config.customBgStart);
|
|
72
185
|
document.documentElement.style.setProperty('--bg-gradient', gradient);
|
|
73
186
|
}
|
|
74
187
|
if (config.customPanelBg) {
|
|
75
188
|
const rgb = hexToRgb(config.customPanelBg);
|
|
76
189
|
document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
|
|
190
|
+
document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`);
|
|
191
|
+
document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`);
|
|
192
|
+
document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`);
|
|
193
|
+
document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`);
|
|
194
|
+
document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`);
|
|
195
|
+
document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`);
|
|
77
196
|
}
|
|
78
197
|
} else {
|
|
79
|
-
|
|
80
|
-
|
|
198
|
+
[
|
|
199
|
+
'--bg-color',
|
|
200
|
+
'--bg-gradient',
|
|
201
|
+
'--panel-bg',
|
|
202
|
+
'--panel-raised',
|
|
203
|
+
'--panel-soft',
|
|
204
|
+
'--chrome-bg',
|
|
205
|
+
'--surface-bg',
|
|
206
|
+
'--surface-strong',
|
|
207
|
+
'--input-bg'
|
|
208
|
+
].forEach(name => document.documentElement.style.removeProperty(name));
|
|
81
209
|
}
|
|
82
210
|
}
|
|
83
211
|
|
|
@@ -93,14 +221,19 @@ function hexToRgb(hex) {
|
|
|
93
221
|
async function loadTheme() {
|
|
94
222
|
try {
|
|
95
223
|
const config = await window.api.getSettings();
|
|
224
|
+
currentSettings = config || {};
|
|
96
225
|
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
97
226
|
enableVoiceReply = config.enableVoiceReply !== false;
|
|
98
227
|
ttsProvider = config.ttsProvider || 'google';
|
|
99
228
|
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
100
229
|
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
101
230
|
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
231
|
+
buildProviderPicker(currentSettings);
|
|
232
|
+
syncAgentModeToggle(currentSettings);
|
|
102
233
|
} catch (e) {
|
|
103
234
|
applyTheme('dark', '#8b5cf6', '#f8fafc');
|
|
235
|
+
buildProviderPicker(currentSettings);
|
|
236
|
+
syncAgentModeToggle(currentSettings);
|
|
104
237
|
}
|
|
105
238
|
}
|
|
106
239
|
|
|
@@ -116,12 +249,41 @@ function lightenColor(hex, amount) {
|
|
|
116
249
|
|
|
117
250
|
// 🔔 Real-time theme sync from Settings window
|
|
118
251
|
window.api.onSettingsChanged((config) => {
|
|
252
|
+
currentSettings = config || currentSettings;
|
|
119
253
|
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
120
254
|
enableVoiceReply = config.enableVoiceReply !== false;
|
|
121
255
|
ttsProvider = config.ttsProvider || 'google';
|
|
122
256
|
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
123
257
|
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
124
258
|
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
259
|
+
buildProviderPicker(currentSettings);
|
|
260
|
+
syncAgentModeToggle(currentSettings);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
chatProviderSelect?.addEventListener('change', (event) => {
|
|
264
|
+
changeChatProvider(event.target.value);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
agentModeToggle?.addEventListener('change', async () => {
|
|
268
|
+
const nextSettings = {
|
|
269
|
+
...currentSettings,
|
|
270
|
+
assistantMode: agentModeToggle.checked ? 'agent' : 'chat'
|
|
271
|
+
};
|
|
272
|
+
agentModeToggle.disabled = true;
|
|
273
|
+
try {
|
|
274
|
+
const result = await window.api.saveSettings(nextSettings);
|
|
275
|
+
if (!result || result.success !== false) {
|
|
276
|
+
currentSettings = nextSettings;
|
|
277
|
+
} else {
|
|
278
|
+
throw new Error(result.message || 'Unable to save assistant mode');
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('Failed to change assistant mode:', error);
|
|
282
|
+
setMintActivity('error');
|
|
283
|
+
} finally {
|
|
284
|
+
syncAgentModeToggle(currentSettings);
|
|
285
|
+
agentModeToggle.disabled = false;
|
|
286
|
+
}
|
|
125
287
|
});
|
|
126
288
|
|
|
127
289
|
// --- Voice Input Setup ---
|
|
@@ -190,6 +352,7 @@ function setupSpeechRecognition() {
|
|
|
190
352
|
speechRecognition.onstart = () => {
|
|
191
353
|
micBtn.classList.add('listening');
|
|
192
354
|
chatInput.placeholder = "Listening... (Click to stop)";
|
|
355
|
+
setMintActivity('listening');
|
|
193
356
|
speechHadResult = false;
|
|
194
357
|
if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
|
|
195
358
|
speechFallbackTimer = setTimeout(() => {
|
|
@@ -228,6 +391,7 @@ function setupSpeechRecognition() {
|
|
|
228
391
|
|
|
229
392
|
speechRecognition.onerror = (err) => {
|
|
230
393
|
console.error("Speech recognition error:", err);
|
|
394
|
+
setMintActivity('error');
|
|
231
395
|
fallbackToMediaRecorder();
|
|
232
396
|
isSpeechStreaming = false;
|
|
233
397
|
resetMicUI();
|
|
@@ -300,10 +464,12 @@ async function setupMediaRecorder() {
|
|
|
300
464
|
mediaRecorder.onstart = () => {
|
|
301
465
|
micBtn.classList.add('listening');
|
|
302
466
|
chatInput.placeholder = "Listening... (Click to stop)";
|
|
467
|
+
setMintActivity('listening');
|
|
303
468
|
};
|
|
304
469
|
|
|
305
470
|
} catch (err) {
|
|
306
471
|
console.error("Microphone access error:", err);
|
|
472
|
+
setMintActivity('error');
|
|
307
473
|
micBtn.style.display = 'none';
|
|
308
474
|
appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
|
|
309
475
|
}
|
|
@@ -312,11 +478,15 @@ async function setupMediaRecorder() {
|
|
|
312
478
|
function resetMicUI() {
|
|
313
479
|
micBtn.classList.remove('listening');
|
|
314
480
|
chatInput.placeholder = DEFAULT_PLACEHOLDER;
|
|
481
|
+
if (voiceMode !== 'speech' && (!mediaRecorder || mediaRecorder.state === 'inactive')) {
|
|
482
|
+
setMintActivity('idle');
|
|
483
|
+
}
|
|
315
484
|
}
|
|
316
485
|
|
|
317
486
|
async function sendVoiceMessage(base64Audio) {
|
|
318
487
|
showTyping();
|
|
319
488
|
chatInput.placeholder = "Processing voice...";
|
|
489
|
+
setMintActivity('thinking');
|
|
320
490
|
try {
|
|
321
491
|
// Send empty text, but include the audio
|
|
322
492
|
const response = await window.api.sendMessage("", null, base64Audio);
|
|
@@ -330,11 +500,14 @@ async function sendVoiceMessage(base64Audio) {
|
|
|
330
500
|
await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
|
|
331
501
|
notifyAiIfNeeded();
|
|
332
502
|
|
|
333
|
-
if (response.
|
|
503
|
+
if (response.approval?.required) {
|
|
504
|
+
appendApprovalCard(msgDiv, response.approval);
|
|
505
|
+
} else if (response.action && response.action.type !== 'none') {
|
|
334
506
|
appendActionCard(msgDiv, response.action);
|
|
335
507
|
}
|
|
336
508
|
} catch (error) {
|
|
337
509
|
removeTyping();
|
|
510
|
+
setMintActivity('error');
|
|
338
511
|
appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
|
|
339
512
|
console.error(error);
|
|
340
513
|
resumeSpeechIfNeeded();
|
|
@@ -373,10 +546,10 @@ micBtn.addEventListener('click', (e) => {
|
|
|
373
546
|
if (mediaRecorder.state === 'inactive') {
|
|
374
547
|
audioChunks = [];
|
|
375
548
|
mediaRecorder.start();
|
|
376
|
-
|
|
549
|
+
setMintActivity('listening');
|
|
377
550
|
} else {
|
|
378
551
|
mediaRecorder.stop();
|
|
379
|
-
|
|
552
|
+
setMintActivity('thinking');
|
|
380
553
|
voiceMode = null;
|
|
381
554
|
}
|
|
382
555
|
return;
|
|
@@ -408,10 +581,10 @@ micBtn.addEventListener('click', (e) => {
|
|
|
408
581
|
if (mediaRecorder.state === 'inactive') {
|
|
409
582
|
audioChunks = [];
|
|
410
583
|
mediaRecorder.start();
|
|
411
|
-
|
|
584
|
+
setMintActivity('listening');
|
|
412
585
|
} else {
|
|
413
586
|
mediaRecorder.stop();
|
|
414
|
-
|
|
587
|
+
setMintActivity('thinking');
|
|
415
588
|
}
|
|
416
589
|
});
|
|
417
590
|
|
|
@@ -419,7 +592,7 @@ micBtn.addEventListener('click', (e) => {
|
|
|
419
592
|
let currentAudioPlayer = null;
|
|
420
593
|
|
|
421
594
|
function speakText(text, options = {}) {
|
|
422
|
-
|
|
595
|
+
setMintActivity('speaking');
|
|
423
596
|
const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
|
|
424
597
|
|
|
425
598
|
const wrappedOnEnd = () => {
|
|
@@ -429,7 +602,7 @@ function speakText(text, options = {}) {
|
|
|
429
602
|
|
|
430
603
|
return new Promise(async (resolve) => {
|
|
431
604
|
if (!enableVoiceReply) {
|
|
432
|
-
|
|
605
|
+
setMintActivity('idle');
|
|
433
606
|
wrappedOnEnd();
|
|
434
607
|
return resolve();
|
|
435
608
|
}
|
|
@@ -447,7 +620,7 @@ function speakText(text, options = {}) {
|
|
|
447
620
|
}
|
|
448
621
|
|
|
449
622
|
if (!text || !text.trim()) {
|
|
450
|
-
|
|
623
|
+
setMintActivity('idle');
|
|
451
624
|
wrappedOnEnd();
|
|
452
625
|
return resolve();
|
|
453
626
|
}
|
|
@@ -461,7 +634,7 @@ function speakText(text, options = {}) {
|
|
|
461
634
|
let i = 0;
|
|
462
635
|
const playNext = () => {
|
|
463
636
|
if (i >= urls.length) {
|
|
464
|
-
|
|
637
|
+
setMintActivity('idle');
|
|
465
638
|
wrappedOnEnd();
|
|
466
639
|
return resolve();
|
|
467
640
|
}
|
|
@@ -499,6 +672,7 @@ function speakText(text, options = {}) {
|
|
|
499
672
|
|
|
500
673
|
function fallbackSpeak(text, onEnd, resolve) {
|
|
501
674
|
if (!('speechSynthesis' in window)) {
|
|
675
|
+
setMintActivity('idle');
|
|
502
676
|
if (onEnd) onEnd();
|
|
503
677
|
resolve();
|
|
504
678
|
return;
|
|
@@ -515,7 +689,7 @@ function fallbackSpeak(text, onEnd, resolve) {
|
|
|
515
689
|
const done = () => {
|
|
516
690
|
if (finished) return;
|
|
517
691
|
finished = true;
|
|
518
|
-
|
|
692
|
+
setMintActivity('idle');
|
|
519
693
|
if (onEnd) onEnd();
|
|
520
694
|
resolve();
|
|
521
695
|
};
|
|
@@ -540,9 +714,82 @@ maximizeBtn.addEventListener('click', () => {
|
|
|
540
714
|
});
|
|
541
715
|
|
|
542
716
|
// Settings button
|
|
543
|
-
|
|
717
|
+
function openSettings() {
|
|
544
718
|
window.api.openSettings();
|
|
545
|
-
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
settingsBtn.addEventListener('click', openSettings);
|
|
722
|
+
sidebarSettingsBtn?.addEventListener('click', openSettings);
|
|
723
|
+
|
|
724
|
+
async function renderPicturesLibrary() {
|
|
725
|
+
if (!picturesGrid || !picturesEmpty) return;
|
|
726
|
+
picturesGrid.innerHTML = '';
|
|
727
|
+
|
|
728
|
+
const pictures = await window.api.listSavedPictures();
|
|
729
|
+
picturesEmpty.classList.toggle('is-hidden', pictures.length > 0);
|
|
730
|
+
|
|
731
|
+
for (const picture of pictures) {
|
|
732
|
+
const card = document.createElement('article');
|
|
733
|
+
card.className = 'picture-card';
|
|
734
|
+
|
|
735
|
+
const img = document.createElement('img');
|
|
736
|
+
img.src = picture.url;
|
|
737
|
+
img.alt = picture.filename || 'Saved picture';
|
|
738
|
+
img.loading = 'lazy';
|
|
739
|
+
|
|
740
|
+
const meta = document.createElement('div');
|
|
741
|
+
meta.className = 'picture-card-meta';
|
|
742
|
+
const date = picture.createdAt ? new Date(picture.createdAt).toLocaleString() : '';
|
|
743
|
+
meta.textContent = picture.message || date || picture.filename || 'Saved picture';
|
|
744
|
+
meta.title = [picture.filename, picture.message, date].filter(Boolean).join('\n');
|
|
745
|
+
|
|
746
|
+
card.appendChild(img);
|
|
747
|
+
card.appendChild(meta);
|
|
748
|
+
picturesGrid.appendChild(card);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function openPicturesLibrary() {
|
|
753
|
+
if (!appBody || !picturesLibrary) return;
|
|
754
|
+
picturesLibrary.hidden = false;
|
|
755
|
+
requestAnimationFrame(() => {
|
|
756
|
+
appBody.classList.add('pictures-open');
|
|
757
|
+
});
|
|
758
|
+
sidebarChatBtn?.classList.remove('is-active');
|
|
759
|
+
sidebarPicturesBtn?.classList.add('is-active');
|
|
760
|
+
await renderPicturesLibrary();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function closePicturesLibrary() {
|
|
764
|
+
if (!appBody || !picturesLibrary) return;
|
|
765
|
+
appBody.classList.remove('pictures-open');
|
|
766
|
+
setTimeout(() => {
|
|
767
|
+
if (!appBody.classList.contains('pictures-open')) {
|
|
768
|
+
picturesLibrary.hidden = true;
|
|
769
|
+
}
|
|
770
|
+
}, 240);
|
|
771
|
+
sidebarChatBtn?.classList.add('is-active');
|
|
772
|
+
sidebarPicturesBtn?.classList.remove('is-active');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
sidebarChatBtn?.addEventListener('click', closePicturesLibrary);
|
|
776
|
+
sidebarPicturesBtn?.addEventListener('click', openPicturesLibrary);
|
|
777
|
+
picturesCloseBtn?.addEventListener('click', closePicturesLibrary);
|
|
778
|
+
|
|
779
|
+
function setSidebarCollapsed(isCollapsed) {
|
|
780
|
+
if (!appBody || !sidebarToggleBtn) return;
|
|
781
|
+
appBody.classList.toggle('sidebar-collapsed', isCollapsed);
|
|
782
|
+
sidebarToggleBtn.setAttribute('aria-expanded', String(!isCollapsed));
|
|
783
|
+
sidebarToggleBtn.setAttribute('aria-label', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
784
|
+
sidebarToggleBtn.setAttribute('title', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (appBody && sidebarToggleBtn) {
|
|
788
|
+
setSidebarCollapsed(true);
|
|
789
|
+
sidebarToggleBtn.addEventListener('click', () => {
|
|
790
|
+
setSidebarCollapsed(!appBody.classList.contains('sidebar-collapsed'));
|
|
791
|
+
});
|
|
792
|
+
}
|
|
546
793
|
|
|
547
794
|
// Throttle utility to prevent UI spam
|
|
548
795
|
function throttle(func, limit) {
|
|
@@ -586,15 +833,141 @@ function formatTime(isoString) {
|
|
|
586
833
|
}
|
|
587
834
|
}
|
|
588
835
|
|
|
836
|
+
function compactSmartContext(context) {
|
|
837
|
+
if (!context || typeof context !== 'object') return null;
|
|
838
|
+
const activeWindow = context.activeWindow || {};
|
|
839
|
+
const currentApp = context.currentApp || {};
|
|
840
|
+
const browser = context.browser || null;
|
|
841
|
+
return {
|
|
842
|
+
capturedAt: context.capturedAt,
|
|
843
|
+
platform: context.platform,
|
|
844
|
+
currentApp: currentApp.name || activeWindow.appName || activeWindow.processName || '',
|
|
845
|
+
processName: currentApp.processName || activeWindow.processName || '',
|
|
846
|
+
pid: currentApp.pid || activeWindow.pid || null,
|
|
847
|
+
activeWindowTitle: activeWindow.title || '',
|
|
848
|
+
browser: browser ? {
|
|
849
|
+
title: browser.title || '',
|
|
850
|
+
url: browser.url || '',
|
|
851
|
+
urlUnavailableReason: browser.urlUnavailableReason || ''
|
|
852
|
+
} : null,
|
|
853
|
+
selectedText: context.selectedText || '',
|
|
854
|
+
clipboardText: context.clipboardText || ''
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function appendSmartContextToMessage(message, context) {
|
|
859
|
+
const compact = compactSmartContext(context);
|
|
860
|
+
if (!compact) return message;
|
|
861
|
+
return [
|
|
862
|
+
message,
|
|
863
|
+
'',
|
|
864
|
+
'[SMART_CONTEXT]',
|
|
865
|
+
'Use this structured desktop context together with the attached screenshot. Do not mention it unless it helps answer the user.',
|
|
866
|
+
JSON.stringify(compact, null, 2),
|
|
867
|
+
'[/SMART_CONTEXT]'
|
|
868
|
+
].join('\n');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function shouldShowAgentActivity(options = {}) {
|
|
872
|
+
return options.showAgentActivity !== false && currentSettings.assistantMode === 'agent';
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function createAgentActivityCard() {
|
|
876
|
+
const messageDiv = document.createElement('div');
|
|
877
|
+
messageDiv.classList.add('message', 'ai-message', 'agent-activity-message');
|
|
878
|
+
|
|
879
|
+
const bubble = document.createElement('div');
|
|
880
|
+
bubble.classList.add('message-bubble', 'agent-activity-card');
|
|
881
|
+
|
|
882
|
+
const header = document.createElement('div');
|
|
883
|
+
header.className = 'agent-activity-header';
|
|
884
|
+
const title = document.createElement('span');
|
|
885
|
+
title.textContent = 'Agent Activity';
|
|
886
|
+
const status = document.createElement('span');
|
|
887
|
+
status.className = 'agent-activity-status';
|
|
888
|
+
status.textContent = 'Running';
|
|
889
|
+
header.appendChild(title);
|
|
890
|
+
header.appendChild(status);
|
|
891
|
+
|
|
892
|
+
const list = document.createElement('div');
|
|
893
|
+
list.className = 'agent-activity-list';
|
|
894
|
+
|
|
895
|
+
bubble.appendChild(header);
|
|
896
|
+
bubble.appendChild(list);
|
|
897
|
+
messageDiv.appendChild(bubble);
|
|
898
|
+
chatContainer.appendChild(messageDiv);
|
|
899
|
+
scrollToBottom();
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
element: messageDiv,
|
|
903
|
+
list,
|
|
904
|
+
status,
|
|
905
|
+
add(label, state = 'running', detail = '') {
|
|
906
|
+
const item = document.createElement('div');
|
|
907
|
+
item.className = 'agent-activity-item';
|
|
908
|
+
item.dataset.state = state;
|
|
909
|
+
|
|
910
|
+
const dot = document.createElement('span');
|
|
911
|
+
dot.className = 'agent-activity-dot';
|
|
912
|
+
|
|
913
|
+
const content = document.createElement('span');
|
|
914
|
+
content.className = 'agent-activity-text';
|
|
915
|
+
content.textContent = detail ? `${label}: ${detail}` : label;
|
|
916
|
+
|
|
917
|
+
item.appendChild(dot);
|
|
918
|
+
item.appendChild(content);
|
|
919
|
+
list.appendChild(item);
|
|
920
|
+
scrollToBottom();
|
|
921
|
+
return item;
|
|
922
|
+
},
|
|
923
|
+
update(item, state, label, detail = '') {
|
|
924
|
+
if (!item) return;
|
|
925
|
+
item.dataset.state = state;
|
|
926
|
+
const content = item.querySelector('.agent-activity-text');
|
|
927
|
+
if (content && label) {
|
|
928
|
+
content.textContent = detail ? `${label}: ${detail}` : label;
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
finish(state = 'done', label = 'Done') {
|
|
932
|
+
status.textContent = label;
|
|
933
|
+
status.dataset.state = state;
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function describeSmartContextActivity(context, hasScreenshot) {
|
|
939
|
+
const compact = compactSmartContext(context) || {};
|
|
940
|
+
const parts = [];
|
|
941
|
+
if (hasScreenshot) parts.push('screen');
|
|
942
|
+
if (compact.currentApp) parts.push(compact.currentApp);
|
|
943
|
+
if (compact.activeWindowTitle) parts.push(compact.activeWindowTitle);
|
|
944
|
+
if (compact.selectedText) parts.push('selected text');
|
|
945
|
+
if (compact.clipboardText) parts.push('clipboard');
|
|
946
|
+
return parts.slice(0, 3).join(' · ') || 'desktop context';
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function describeActionActivity(action) {
|
|
950
|
+
if (!action || action.type === 'none') return 'No desktop action';
|
|
951
|
+
const meta = getActionCardMeta(action);
|
|
952
|
+
return meta.detail ? `${meta.title} · ${meta.detail}` : meta.title;
|
|
953
|
+
}
|
|
954
|
+
|
|
589
955
|
// Clear chat history
|
|
590
|
-
|
|
956
|
+
async function clearChatHistory(confirmMessage = 'Clear current chat history?') {
|
|
957
|
+
const shouldClear = window.confirm(confirmMessage);
|
|
958
|
+
if (!shouldClear) return;
|
|
959
|
+
|
|
960
|
+
closePicturesLibrary();
|
|
591
961
|
await window.api.resetChat();
|
|
592
962
|
// Remove all messages except the initial greeting
|
|
593
963
|
const messages = chatContainer.querySelectorAll('.message:not(.initial)');
|
|
594
964
|
messages.forEach(m => m.remove());
|
|
595
965
|
// Append a clear confirmation
|
|
596
966
|
appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
|
|
597
|
-
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
clearBtn.addEventListener('click', () => clearChatHistory('Clear current chat history?'));
|
|
970
|
+
sidebarNewChatBtn?.addEventListener('click', () => clearChatHistory('Start a new chat and clear current history?'));
|
|
598
971
|
|
|
599
972
|
function formatProviderInfo(providerInfo) {
|
|
600
973
|
if (!providerInfo || typeof providerInfo !== 'object') return '';
|
|
@@ -604,6 +977,85 @@ function formatProviderInfo(providerInfo) {
|
|
|
604
977
|
return model ? `${provider || 'AI'} • ${model}` : provider;
|
|
605
978
|
}
|
|
606
979
|
|
|
980
|
+
function formatNumber(value) {
|
|
981
|
+
const number = Number(value) || 0;
|
|
982
|
+
return number.toLocaleString('en-US');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function summarizeProviderUsage(providerInfo) {
|
|
986
|
+
const usage = Array.isArray(providerInfo?.usage) ? providerInfo.usage : [];
|
|
987
|
+
const selectedProvider = String(providerInfo?.provider || '').trim();
|
|
988
|
+
const selectedModel = String(providerInfo?.model || '').trim();
|
|
989
|
+
const row = usage.find(item =>
|
|
990
|
+
String(item.provider || '') === selectedProvider &&
|
|
991
|
+
String(item.model || '') === selectedModel
|
|
992
|
+
) || usage[0] || {};
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
requests: Number(row.requests) || 0,
|
|
996
|
+
inputTokens: Number(row.inputTokens) || 0,
|
|
997
|
+
outputTokens: Number(row.outputTokens) || 0,
|
|
998
|
+
reasoningTokens: Number(row.reasoningTokens) || 0,
|
|
999
|
+
cacheReads: Number(row.cacheReads) || 0,
|
|
1000
|
+
totalTokens: Number(row.totalTokens) || 0
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function closeProviderPopover() {
|
|
1005
|
+
document.querySelectorAll('.provider-popover').forEach(popover => popover.remove());
|
|
1006
|
+
document.querySelectorAll('.provider-badge.is-open').forEach(badge => badge.classList.remove('is-open'));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function createProviderRow(label, value) {
|
|
1010
|
+
const row = document.createElement('div');
|
|
1011
|
+
row.className = 'provider-popover-row';
|
|
1012
|
+
const labelEl = document.createElement('span');
|
|
1013
|
+
labelEl.textContent = label;
|
|
1014
|
+
const valueEl = document.createElement('strong');
|
|
1015
|
+
valueEl.textContent = value;
|
|
1016
|
+
row.appendChild(labelEl);
|
|
1017
|
+
row.appendChild(valueEl);
|
|
1018
|
+
return row;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function showProviderPopover(anchor, providerInfo) {
|
|
1022
|
+
closeProviderPopover();
|
|
1023
|
+
anchor.classList.add('is-open');
|
|
1024
|
+
|
|
1025
|
+
const provider = String(providerInfo?.provider || 'AI').trim();
|
|
1026
|
+
const model = String(providerInfo?.model || 'Unknown model').trim();
|
|
1027
|
+
const usage = summarizeProviderUsage(providerInfo);
|
|
1028
|
+
const popover = document.createElement('div');
|
|
1029
|
+
popover.className = 'provider-popover';
|
|
1030
|
+
|
|
1031
|
+
const title = document.createElement('div');
|
|
1032
|
+
title.className = 'provider-popover-title';
|
|
1033
|
+
title.textContent = 'Model details';
|
|
1034
|
+
popover.appendChild(title);
|
|
1035
|
+
|
|
1036
|
+
popover.appendChild(createProviderRow('Provider', provider));
|
|
1037
|
+
popover.appendChild(createProviderRow('Model', model));
|
|
1038
|
+
popover.appendChild(createProviderRow('Context tokens', formatNumber(usage.inputTokens)));
|
|
1039
|
+
popover.appendChild(createProviderRow('Output tokens', formatNumber(usage.outputTokens)));
|
|
1040
|
+
if (usage.reasoningTokens) {
|
|
1041
|
+
popover.appendChild(createProviderRow('Reasoning tokens', formatNumber(usage.reasoningTokens)));
|
|
1042
|
+
}
|
|
1043
|
+
popover.appendChild(createProviderRow('Total tokens', formatNumber(usage.totalTokens)));
|
|
1044
|
+
|
|
1045
|
+
const action = document.createElement('button');
|
|
1046
|
+
action.type = 'button';
|
|
1047
|
+
action.className = 'provider-popover-action';
|
|
1048
|
+
action.textContent = 'Change model in Settings';
|
|
1049
|
+
action.addEventListener('click', (event) => {
|
|
1050
|
+
event.stopPropagation();
|
|
1051
|
+
closeProviderPopover();
|
|
1052
|
+
if (window.api?.openSettings) window.api.openSettings();
|
|
1053
|
+
});
|
|
1054
|
+
popover.appendChild(action);
|
|
1055
|
+
|
|
1056
|
+
anchor.after(popover);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
607
1059
|
function splitListOutro(text) {
|
|
608
1060
|
const value = String(text || '').trim();
|
|
609
1061
|
const markers = [
|
|
@@ -726,10 +1178,20 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
|
|
|
726
1178
|
const timeDiv = document.createElement('div');
|
|
727
1179
|
timeDiv.classList.add('message-time');
|
|
728
1180
|
if (providerLabel) {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1181
|
+
const providerButton = document.createElement('button');
|
|
1182
|
+
providerButton.type = 'button';
|
|
1183
|
+
providerButton.classList.add('provider-badge');
|
|
1184
|
+
providerButton.textContent = providerLabel;
|
|
1185
|
+
providerButton.title = 'View model details';
|
|
1186
|
+
providerButton.addEventListener('click', (event) => {
|
|
1187
|
+
event.stopPropagation();
|
|
1188
|
+
if (providerButton.classList.contains('is-open')) {
|
|
1189
|
+
closeProviderPopover();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
showProviderPopover(providerButton, options.providerInfo);
|
|
1193
|
+
});
|
|
1194
|
+
timeDiv.appendChild(providerButton);
|
|
733
1195
|
}
|
|
734
1196
|
if (timestamp) {
|
|
735
1197
|
const timeSpan = document.createElement('span');
|
|
@@ -834,29 +1296,193 @@ function autoChunkAiText(text) {
|
|
|
834
1296
|
}
|
|
835
1297
|
|
|
836
1298
|
function appendActionCard(messageDiv, action) {
|
|
1299
|
+
if (!messageDiv || !action || action.type === 'none') return;
|
|
1300
|
+
|
|
1301
|
+
const meta = getActionCardMeta(action);
|
|
837
1302
|
const card = document.createElement('div');
|
|
838
1303
|
card.classList.add('action-card');
|
|
1304
|
+
card.dataset.actionType = action.type || 'unknown';
|
|
839
1305
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1306
|
+
const icon = document.createElement('span');
|
|
1307
|
+
icon.className = 'action-card-icon';
|
|
1308
|
+
icon.textContent = meta.icon;
|
|
1309
|
+
|
|
1310
|
+
const content = document.createElement('div');
|
|
1311
|
+
content.className = 'action-card-content';
|
|
1312
|
+
|
|
1313
|
+
const title = document.createElement('div');
|
|
1314
|
+
title.className = 'action-card-title';
|
|
1315
|
+
title.textContent = meta.title;
|
|
1316
|
+
content.appendChild(title);
|
|
1317
|
+
|
|
1318
|
+
if (meta.detail) {
|
|
1319
|
+
const detail = document.createElement('div');
|
|
1320
|
+
detail.className = 'action-card-detail';
|
|
1321
|
+
detail.textContent = meta.detail;
|
|
1322
|
+
content.appendChild(detail);
|
|
854
1323
|
}
|
|
855
1324
|
|
|
856
|
-
card.
|
|
1325
|
+
card.appendChild(icon);
|
|
1326
|
+
card.appendChild(content);
|
|
1327
|
+
messageDiv.querySelector('.message-bubble')?.appendChild(card);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function getActionCardMeta(action) {
|
|
1331
|
+
const target = formatActionTarget(action);
|
|
1332
|
+
const type = action?.type || 'unknown';
|
|
1333
|
+
const targetOrFallback = target || 'No target';
|
|
1334
|
+
|
|
1335
|
+
const map = {
|
|
1336
|
+
open_url: ['🌐', 'Opened URL', target],
|
|
1337
|
+
search: ['🔍', 'Searched the web', target],
|
|
1338
|
+
open_app: ['🚀', 'Launched app', target],
|
|
1339
|
+
web_automation: ['🧭', 'Ran browser automation', target],
|
|
1340
|
+
create_folder: ['📁', 'Created folder', target],
|
|
1341
|
+
open_file: ['📄', 'Opened file', target],
|
|
1342
|
+
open_folder: ['📂', 'Opened folder', target],
|
|
1343
|
+
delete_file: ['🗑️', 'Deleted file', target],
|
|
1344
|
+
find_path: ['🔎', action.openAfter ? 'Found and opened path' : 'Found path', buildFindPathDetail(action)],
|
|
1345
|
+
clipboard_write: ['📋', 'Updated clipboard', target],
|
|
1346
|
+
learn_file: ['📚', 'Indexed file', target],
|
|
1347
|
+
learn_folder: ['📚', 'Indexed folder', target],
|
|
1348
|
+
system_info: ['💻', target ? 'Checked weather' : 'Checked system info', target],
|
|
1349
|
+
plugin: ['🔌', 'Ran plugin', target],
|
|
1350
|
+
mcp_tool: ['🧩', 'Called MCP tool', target],
|
|
1351
|
+
mouse_move: ['↗', 'Moved pointer', target],
|
|
1352
|
+
mouse_click: ['☝', 'Clicked screen', buildMouseDetail(action)],
|
|
1353
|
+
type_text: ['⌨', 'Typed text', target],
|
|
1354
|
+
key_tap: ['⌨', 'Pressed key', target],
|
|
1355
|
+
system_automation: ['⚙', 'Changed system setting', target]
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
const [icon, title, detail] = map[type] || ['⚡', `Ran action: ${type}`, targetOrFallback];
|
|
1359
|
+
return { icon, title, detail };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function buildFindPathDetail(action) {
|
|
1363
|
+
const target = formatActionTarget(action);
|
|
1364
|
+
const typeLabel = action.pathType && action.pathType !== 'any' ? ` (${action.pathType})` : '';
|
|
1365
|
+
return target ? `${target}${typeLabel}` : typeLabel.trim();
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function buildMouseDetail(action) {
|
|
1369
|
+
const point = formatActionTarget(action);
|
|
1370
|
+
const button = action.button ? `button ${action.button}` : 'left button';
|
|
1371
|
+
return point ? `${point} · ${button}` : button;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function formatActionTarget(action) {
|
|
1375
|
+
if (!action || typeof action !== 'object') return '';
|
|
1376
|
+
if (action.server && action.target) return `${action.server}:${action.target}`;
|
|
1377
|
+
if (action.pluginName) return `${action.pluginName} ${action.target || ''}`.trim();
|
|
1378
|
+
if (action.target) return String(action.target);
|
|
1379
|
+
if (Number.isFinite(action.x) && Number.isFinite(action.y)) return `${action.x}, ${action.y}`;
|
|
1380
|
+
return '';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function getApprovalCopy(approval) {
|
|
1384
|
+
const action = approval?.action || {};
|
|
1385
|
+
const actionType = action.type || 'unknown';
|
|
1386
|
+
const target = formatActionTarget(action);
|
|
1387
|
+
const isDangerous = approval?.tier === 'dangerous';
|
|
1388
|
+
return {
|
|
1389
|
+
title: isDangerous ? 'Dangerous action requires approval' : 'Action requires approval',
|
|
1390
|
+
body: target ? `${actionType}: ${target}` : actionType,
|
|
1391
|
+
reason: approval?.reason || 'This action needs your permission before Mint can run it.',
|
|
1392
|
+
approveLabel: isDangerous ? 'Allow Dangerous Action' : 'Allow Action'
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function appendApprovalCard(messageDiv, approval, activity = null) {
|
|
1397
|
+
if (!messageDiv || !approval?.action || !window.api?.executeApprovedAction) return;
|
|
1398
|
+
|
|
1399
|
+
const copy = getApprovalCopy(approval);
|
|
1400
|
+
const card = document.createElement('div');
|
|
1401
|
+
card.classList.add('action-card', 'approval-card');
|
|
1402
|
+
card.dataset.tier = approval.tier || 'approval';
|
|
1403
|
+
|
|
1404
|
+
const content = document.createElement('div');
|
|
1405
|
+
content.className = 'approval-card-content';
|
|
1406
|
+
|
|
1407
|
+
const title = document.createElement('div');
|
|
1408
|
+
title.className = 'approval-card-title';
|
|
1409
|
+
title.textContent = copy.title;
|
|
1410
|
+
|
|
1411
|
+
const body = document.createElement('div');
|
|
1412
|
+
body.className = 'approval-card-body';
|
|
1413
|
+
body.textContent = copy.body;
|
|
1414
|
+
|
|
1415
|
+
const reason = document.createElement('div');
|
|
1416
|
+
reason.className = 'approval-card-reason';
|
|
1417
|
+
reason.textContent = copy.reason;
|
|
1418
|
+
|
|
1419
|
+
content.appendChild(title);
|
|
1420
|
+
content.appendChild(body);
|
|
1421
|
+
content.appendChild(reason);
|
|
1422
|
+
|
|
1423
|
+
const actions = document.createElement('div');
|
|
1424
|
+
actions.className = 'approval-card-actions';
|
|
1425
|
+
|
|
1426
|
+
const approveBtn = document.createElement('button');
|
|
1427
|
+
approveBtn.type = 'button';
|
|
1428
|
+
approveBtn.className = 'approval-btn approval-btn-approve';
|
|
1429
|
+
approveBtn.textContent = copy.approveLabel;
|
|
1430
|
+
|
|
1431
|
+
const cancelBtn = document.createElement('button');
|
|
1432
|
+
cancelBtn.type = 'button';
|
|
1433
|
+
cancelBtn.className = 'approval-btn approval-btn-cancel';
|
|
1434
|
+
cancelBtn.textContent = 'Cancel';
|
|
1435
|
+
|
|
1436
|
+
const setDone = (message, state) => {
|
|
1437
|
+
approveBtn.disabled = true;
|
|
1438
|
+
cancelBtn.disabled = true;
|
|
1439
|
+
card.dataset.state = state;
|
|
1440
|
+
reason.textContent = message;
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
approveBtn.addEventListener('click', async () => {
|
|
1444
|
+
approveBtn.disabled = true;
|
|
1445
|
+
cancelBtn.disabled = true;
|
|
1446
|
+
reason.textContent = 'Running approved action...';
|
|
1447
|
+
const runStep = activity?.add('Running approved action', 'running', describeActionActivity(approval.action));
|
|
1448
|
+
setMintActivity('thinking');
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
const result = await window.api.executeApprovedAction(approval.action);
|
|
1452
|
+
if (!result || result.success === false) {
|
|
1453
|
+
setDone(result?.message || 'Action failed.', 'error');
|
|
1454
|
+
activity?.update(runStep, 'error', 'Action failed', result?.message || '');
|
|
1455
|
+
activity?.finish('error', 'Failed');
|
|
1456
|
+
setMintActivity('error');
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
setDone(result.message || 'Action completed.', 'approved');
|
|
1461
|
+
activity?.update(runStep, 'done', 'Action completed', result.message || describeActionActivity(approval.action));
|
|
1462
|
+
activity?.finish('done', 'Completed');
|
|
1463
|
+
setMintActivity('idle');
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
console.error('[Approval] Failed to execute action:', error);
|
|
1466
|
+
setDone(error.message || 'Action failed.', 'error');
|
|
1467
|
+
activity?.update(runStep, 'error', 'Action failed', error.message || '');
|
|
1468
|
+
activity?.finish('error', 'Failed');
|
|
1469
|
+
setMintActivity('error');
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
cancelBtn.addEventListener('click', () => {
|
|
1474
|
+
setDone('Cancelled by user.', 'cancelled');
|
|
1475
|
+
activity?.add('Approval cancelled', 'cancelled');
|
|
1476
|
+
activity?.finish('cancelled', 'Cancelled');
|
|
1477
|
+
setMintActivity('idle');
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
actions.appendChild(approveBtn);
|
|
1481
|
+
actions.appendChild(cancelBtn);
|
|
1482
|
+
card.appendChild(content);
|
|
1483
|
+
card.appendChild(actions);
|
|
857
1484
|
|
|
858
|
-
|
|
859
|
-
messageDiv.querySelector('.message-bubble').appendChild(card);
|
|
1485
|
+
messageDiv.querySelector('.message-bubble')?.appendChild(card);
|
|
860
1486
|
}
|
|
861
1487
|
|
|
862
1488
|
function showTyping() {
|
|
@@ -898,8 +1524,18 @@ function loadScript(src) {
|
|
|
898
1524
|
});
|
|
899
1525
|
}
|
|
900
1526
|
|
|
1527
|
+
function hideStartupLoading() {
|
|
1528
|
+
appContainer?.classList.remove('is-loading');
|
|
1529
|
+
if (!startupLoading) return;
|
|
1530
|
+
startupLoading.classList.add('is-hidden');
|
|
1531
|
+
setTimeout(() => startupLoading.remove(), 400);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
901
1534
|
async function loadLive2DWhenIdle() {
|
|
902
|
-
if (!modelMount || window.Live2DManager)
|
|
1535
|
+
if (!modelMount || window.Live2DManager) {
|
|
1536
|
+
hideStartupLoading();
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
903
1539
|
try {
|
|
904
1540
|
await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
|
|
905
1541
|
await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
|
|
@@ -907,6 +1543,7 @@ async function loadLive2DWhenIdle() {
|
|
|
907
1543
|
await loadScript('live2d_manager.js');
|
|
908
1544
|
if (window.Live2DManager) {
|
|
909
1545
|
await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
|
|
1546
|
+
applyModelPanelControlState();
|
|
910
1547
|
}
|
|
911
1548
|
} catch (err) {
|
|
912
1549
|
console.error('[Live2D] Deferred load failed:', err);
|
|
@@ -914,6 +1551,8 @@ async function loadLive2DWhenIdle() {
|
|
|
914
1551
|
modelStatus.classList.add('is-error');
|
|
915
1552
|
modelStatus.textContent = 'Live2D model unavailable.';
|
|
916
1553
|
}
|
|
1554
|
+
} finally {
|
|
1555
|
+
hideStartupLoading();
|
|
917
1556
|
}
|
|
918
1557
|
}
|
|
919
1558
|
|
|
@@ -978,30 +1617,55 @@ async function sendTextMessage(text, options = {}) {
|
|
|
978
1617
|
rememberConversationLanguage(displayText || cleanText);
|
|
979
1618
|
}
|
|
980
1619
|
|
|
1620
|
+
const activity = shouldShowAgentActivity(options) ? createAgentActivityCard() : null;
|
|
1621
|
+
const contextStep = activity?.add('Preparing desktop context', 'running');
|
|
1622
|
+
|
|
981
1623
|
// Show typing early so user knows we are processing
|
|
982
1624
|
showTyping();
|
|
1625
|
+
setMintActivity('thinking');
|
|
1626
|
+
|
|
1627
|
+
let messageToSend = cleanText;
|
|
983
1628
|
|
|
984
1629
|
// Check Smart Context Toggle
|
|
985
1630
|
const smartToggle = document.getElementById('smart-context-toggle');
|
|
986
1631
|
if (allowSmartContext && smartToggle && smartToggle.checked && !imageToSend) {
|
|
987
1632
|
try {
|
|
988
|
-
const silentCapture = await
|
|
1633
|
+
const [silentCapture, smartContext] = await Promise.all([
|
|
1634
|
+
window.api.captureSilentScreen(),
|
|
1635
|
+
window.api.getSmartContext ? window.api.getSmartContext() : Promise.resolve(null)
|
|
1636
|
+
]);
|
|
989
1637
|
if (silentCapture) {
|
|
990
1638
|
// Set imageToSend so it gets sent to the API, but we already appended the chat bubble
|
|
991
1639
|
imageToSend = silentCapture;
|
|
992
1640
|
}
|
|
1641
|
+
if (smartContext) {
|
|
1642
|
+
messageToSend = appendSmartContextToMessage(cleanText, smartContext);
|
|
1643
|
+
}
|
|
1644
|
+
if (activity && contextStep) {
|
|
1645
|
+
activity.update(
|
|
1646
|
+
contextStep,
|
|
1647
|
+
'done',
|
|
1648
|
+
'Read Smart Context',
|
|
1649
|
+
describeSmartContextActivity(smartContext, Boolean(silentCapture))
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
993
1652
|
} catch (err) {
|
|
994
1653
|
console.error("Smart Context capture failed:", err);
|
|
1654
|
+
activity?.update(contextStep, 'error', 'Smart Context unavailable', err.message || '');
|
|
995
1655
|
}
|
|
1656
|
+
} else if (activity && contextStep) {
|
|
1657
|
+
activity.update(contextStep, 'skipped', 'Smart Context skipped', imageToSend ? 'image already attached' : 'toggle is off');
|
|
996
1658
|
}
|
|
997
1659
|
|
|
998
1660
|
// Hide proactive bar if user is actively typing a message
|
|
999
1661
|
hideProactiveBar();
|
|
1662
|
+
const modelStep = activity?.add('Waiting for model response', 'running');
|
|
1000
1663
|
|
|
1001
1664
|
try {
|
|
1002
1665
|
// Send to main process (text, image, audio=null)
|
|
1003
|
-
const response = await window.api.sendMessage(
|
|
1666
|
+
const response = await window.api.sendMessage(messageToSend, imageToSend, null);
|
|
1004
1667
|
removeTyping();
|
|
1668
|
+
activity?.update(modelStep, 'done', 'Model response received');
|
|
1005
1669
|
|
|
1006
1670
|
if (typeof response.response !== 'string') {
|
|
1007
1671
|
response.response = normalizeAiText(response.response);
|
|
@@ -1009,6 +1673,7 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1009
1673
|
|
|
1010
1674
|
// Handle system_info action: fetch data and append to AI message
|
|
1011
1675
|
if (response.action && response.action.type === 'system_info') {
|
|
1676
|
+
const infoStep = activity?.add('Running local info action', 'running', describeActionActivity(response.action));
|
|
1012
1677
|
const city = (response.action.target || '').trim();
|
|
1013
1678
|
// Only treat as weather if city looks like a real location name (not blank, not 'date', not 'time')
|
|
1014
1679
|
const weatherKeywords = ['date', 'time', 'วัน', 'เวลา', 'today', 'now'];
|
|
@@ -1018,12 +1683,14 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1018
1683
|
// Weather query
|
|
1019
1684
|
const weather = await window.api.getWeather(city);
|
|
1020
1685
|
response.response += `\n\n🌡️ ${weather.data}`;
|
|
1686
|
+
activity?.update(infoStep, 'done', 'Weather info added', city);
|
|
1021
1687
|
} else {
|
|
1022
1688
|
// General system info (date, time, RAM, CPU)
|
|
1023
1689
|
const info = await window.api.getSystemInfo();
|
|
1024
1690
|
const machine = info.machine && info.machine.display ? `\n🖥️ รุ่นเครื่อง: ${info.machine.display}` : '';
|
|
1025
1691
|
const distro = info.distro ? `\nระบบ: ${info.distro}` : '';
|
|
1026
1692
|
response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}${machine}${distro}\n💻 CPU: ${info.cpu.model} (${info.cpu.cores} คอร์)\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
|
|
1693
|
+
activity?.update(infoStep, 'done', 'System info added');
|
|
1027
1694
|
}
|
|
1028
1695
|
}
|
|
1029
1696
|
|
|
@@ -1039,11 +1706,27 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1039
1706
|
notifyAiIfNeeded();
|
|
1040
1707
|
|
|
1041
1708
|
// Append action card if applicable
|
|
1042
|
-
if (response.
|
|
1709
|
+
if (response.approval?.required) {
|
|
1710
|
+
activity?.add('Selected action', 'approval', describeActionActivity(response.approval.action));
|
|
1711
|
+
activity?.add('Waiting for approval', 'running', response.approval.reason || '');
|
|
1712
|
+
activity?.finish('waiting', 'Waiting');
|
|
1713
|
+
appendApprovalCard(msgDiv, response.approval, activity);
|
|
1714
|
+
} else if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
|
|
1715
|
+
activity?.add('Selected action', 'done', describeActionActivity(response.action));
|
|
1043
1716
|
appendActionCard(msgDiv, response.action);
|
|
1717
|
+
activity?.finish('done', 'Completed');
|
|
1718
|
+
} else if (response.action && response.action.type === 'system_info') {
|
|
1719
|
+
activity?.add('Selected action', 'done', describeActionActivity(response.action));
|
|
1720
|
+
activity?.finish('done', 'Completed');
|
|
1721
|
+
} else {
|
|
1722
|
+
activity?.add('No desktop action selected', 'done');
|
|
1723
|
+
activity?.finish('done', 'Completed');
|
|
1044
1724
|
}
|
|
1045
1725
|
} catch (error) {
|
|
1046
1726
|
removeTyping();
|
|
1727
|
+
setMintActivity('error');
|
|
1728
|
+
activity?.update(modelStep, 'error', 'Model request failed', error.message || '');
|
|
1729
|
+
activity?.finish('error', 'Failed');
|
|
1047
1730
|
appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
|
|
1048
1731
|
console.error(error);
|
|
1049
1732
|
resumeSpeechIfNeeded();
|
|
@@ -1052,7 +1735,6 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1052
1735
|
|
|
1053
1736
|
chatForm.addEventListener('submit', throttle(async (e) => {
|
|
1054
1737
|
e.preventDefault();
|
|
1055
|
-
if (window.api && window.api.setAiState) window.api.setAiState('thinking');
|
|
1056
1738
|
const text = chatInput.value.trim();
|
|
1057
1739
|
await sendTextMessage(text);
|
|
1058
1740
|
}, 500));
|
|
@@ -1060,7 +1742,7 @@ chatForm.addEventListener('submit', throttle(async (e) => {
|
|
|
1060
1742
|
window.addEventListener('live2d-model-interaction', async (event) => {
|
|
1061
1743
|
const prompt = event?.detail?.prompt;
|
|
1062
1744
|
if (!prompt) return;
|
|
1063
|
-
|
|
1745
|
+
setMintActivity('thinking');
|
|
1064
1746
|
const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
|
|
1065
1747
|
const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
|
|
1066
1748
|
await sendTextMessage(interactionPrompt, {
|
|
@@ -1126,9 +1808,9 @@ inputArea.addEventListener('drop', (e) => {
|
|
|
1126
1808
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
1127
1809
|
chatInput.focus();
|
|
1128
1810
|
await loadTheme();
|
|
1811
|
+
setMintActivity('idle');
|
|
1129
1812
|
await loadChatHistory();
|
|
1130
|
-
|
|
1131
|
-
scheduleLive2DLoad(() => loadLive2DWhenIdle());
|
|
1813
|
+
loadLive2DWhenIdle();
|
|
1132
1814
|
});
|
|
1133
1815
|
|
|
1134
1816
|
// Proactive OS Notifications (Battery, Network, etc.)
|
|
@@ -1143,6 +1825,11 @@ window.addEventListener('focus', () => {
|
|
|
1143
1825
|
if (window.api.clearAiNotifications) window.api.clearAiNotifications();
|
|
1144
1826
|
});
|
|
1145
1827
|
|
|
1828
|
+
document.addEventListener('click', closeProviderPopover);
|
|
1829
|
+
document.addEventListener('keydown', (event) => {
|
|
1830
|
+
if (event.key === 'Escape') closeProviderPopover();
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1146
1833
|
// =====================
|
|
1147
1834
|
// Proactive Smart Suggestion Engine
|
|
1148
1835
|
// =====================
|
|
@@ -1224,34 +1911,121 @@ if (smartContextToggle) {
|
|
|
1224
1911
|
// Toggle Live2D Model visibility
|
|
1225
1912
|
const toggleModelBtn = document.getElementById('toggle-model-btn');
|
|
1226
1913
|
const assistantWorkspace = document.querySelector('.assistant-workspace');
|
|
1914
|
+
const modelLockBtn = document.getElementById('model-lock-btn');
|
|
1915
|
+
const modelScaleSlider = document.getElementById('model-scale-slider');
|
|
1916
|
+
const modelScaleValue = document.getElementById('model-scale-value');
|
|
1917
|
+
const modelScaleResetBtn = document.getElementById('model-scale-reset-btn');
|
|
1918
|
+
const modelBgBtn = document.getElementById('model-bg-btn');
|
|
1919
|
+
const layoutPresetBtns = document.querySelectorAll('.layout-preset-btn');
|
|
1920
|
+
|
|
1921
|
+
const modelBgStorageKey = 'mint-model-background';
|
|
1922
|
+
const modelScaleStorageKey = 'mint-model-scale';
|
|
1923
|
+
const modelPositionLockStorageKey = 'mint-model-position-locked';
|
|
1924
|
+
const workspaceLayoutStorageKey = 'mint-workspace-layout';
|
|
1925
|
+
const modelBgClasses = ['model-bg-default', 'model-bg-clear', 'model-bg-grid', 'model-bg-stage'];
|
|
1926
|
+
const modelBgLabels = ['Default background', 'Clear background', 'Grid background', 'Stage background'];
|
|
1927
|
+
const workspaceLayoutClasses = ['layout-chat'];
|
|
1928
|
+
const workspaceLayoutPresets = ['companion', 'chat'];
|
|
1929
|
+
|
|
1930
|
+
function setModelHidden(isHidden) {
|
|
1931
|
+
if (!assistantWorkspace || !toggleModelBtn) return;
|
|
1932
|
+
assistantWorkspace.classList.toggle('model-hidden', Boolean(isHidden));
|
|
1933
|
+
toggleModelBtn.classList.toggle('active', Boolean(isHidden));
|
|
1934
|
+
toggleModelBtn.setAttribute('aria-pressed', String(Boolean(isHidden)));
|
|
1935
|
+
localStorage.setItem('mint-model-hidden', String(Boolean(isHidden)));
|
|
1936
|
+
|
|
1937
|
+
if (!isHidden && window.Live2DManager && Live2DManager.model) {
|
|
1938
|
+
setTimeout(() => {
|
|
1939
|
+
window.dispatchEvent(new Event('resize'));
|
|
1940
|
+
if (typeof Live2DManager.fitModelToMount === 'function') {
|
|
1941
|
+
Live2DManager.fitModelToMount();
|
|
1942
|
+
}
|
|
1943
|
+
}, 450);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function setModelPositionLocked(isLocked) {
|
|
1948
|
+
const locked = Boolean(isLocked);
|
|
1949
|
+
localStorage.setItem(modelPositionLockStorageKey, String(locked));
|
|
1950
|
+
modelLockBtn?.classList.toggle('is-active', locked);
|
|
1951
|
+
modelLockBtn?.setAttribute('aria-pressed', String(locked));
|
|
1952
|
+
modelLockBtn?.setAttribute('title', locked ? 'Unlock model position' : 'Lock model position');
|
|
1953
|
+
if (window.Live2DManager) {
|
|
1954
|
+
Live2DManager.setPointerTrackingEnabled(!locked);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function setModelBackground(index) {
|
|
1959
|
+
if (!modelShell) return;
|
|
1960
|
+
const normalized = ((Number(index) || 0) + modelBgClasses.length) % modelBgClasses.length;
|
|
1961
|
+
modelBgClasses.forEach(className => modelShell.classList.remove(className));
|
|
1962
|
+
if (normalized > 0) {
|
|
1963
|
+
modelShell.classList.add(modelBgClasses[normalized]);
|
|
1964
|
+
}
|
|
1965
|
+
localStorage.setItem(modelBgStorageKey, String(normalized));
|
|
1966
|
+
modelBgBtn?.setAttribute('title', modelBgLabels[normalized]);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function setModelScale(value) {
|
|
1970
|
+
const next = Math.max(78, Math.min(128, Number(value) || 100));
|
|
1971
|
+
localStorage.setItem(modelScaleStorageKey, String(next));
|
|
1972
|
+
if (modelScaleSlider) modelScaleSlider.value = String(next);
|
|
1973
|
+
if (modelScaleValue) modelScaleValue.textContent = `${(next / 100).toFixed(2)}x`;
|
|
1974
|
+
if (window.Live2DManager) {
|
|
1975
|
+
Live2DManager.setZoomMultiplier(next / 100);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function applyModelPanelControlState() {
|
|
1980
|
+
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) === 'true');
|
|
1981
|
+
setModelBackground(Number(localStorage.getItem(modelBgStorageKey) || 0));
|
|
1982
|
+
setModelScale(Number(localStorage.getItem(modelScaleStorageKey) || 100));
|
|
1983
|
+
setWorkspaceLayout(localStorage.getItem(workspaceLayoutStorageKey) || 'companion');
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function setWorkspaceLayout(layout) {
|
|
1987
|
+
if (!assistantWorkspace) return;
|
|
1988
|
+
const normalized = workspaceLayoutPresets.includes(layout) ? layout : 'companion';
|
|
1989
|
+
workspaceLayoutClasses.forEach(className => assistantWorkspace.classList.remove(className));
|
|
1990
|
+
if (normalized !== 'companion') {
|
|
1991
|
+
assistantWorkspace.classList.add(`layout-${normalized}`);
|
|
1992
|
+
}
|
|
1993
|
+
localStorage.setItem(workspaceLayoutStorageKey, normalized);
|
|
1994
|
+
layoutPresetBtns.forEach((button) => {
|
|
1995
|
+
const isActive = button.dataset.layoutPreset === normalized;
|
|
1996
|
+
button.classList.toggle('is-active', isActive);
|
|
1997
|
+
button.setAttribute('aria-pressed', String(isActive));
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
1227
2000
|
|
|
1228
2001
|
if (toggleModelBtn && assistantWorkspace) {
|
|
1229
2002
|
toggleModelBtn.addEventListener('click', () => {
|
|
1230
|
-
|
|
1231
|
-
toggleModelBtn.classList.toggle('active', isHidden);
|
|
1232
|
-
|
|
1233
|
-
// Save preference to local storage
|
|
1234
|
-
localStorage.setItem('mint-model-hidden', String(isHidden));
|
|
1235
|
-
|
|
1236
|
-
// Refit model if shown
|
|
1237
|
-
if (!isHidden && window.Live2DManager && Live2DManager.model) {
|
|
1238
|
-
// Wait for transition
|
|
1239
|
-
setTimeout(() => {
|
|
1240
|
-
// Trigger a resize event to refit
|
|
1241
|
-
window.dispatchEvent(new Event('resize'));
|
|
1242
|
-
}, 450);
|
|
1243
|
-
}
|
|
2003
|
+
setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
|
|
1244
2004
|
});
|
|
1245
2005
|
|
|
1246
2006
|
// Restore preference on load
|
|
1247
2007
|
const savedModelHidden = localStorage.getItem('mint-model-hidden');
|
|
1248
2008
|
const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
|
|
1249
2009
|
if (savedHidden) {
|
|
1250
|
-
|
|
1251
|
-
toggleModelBtn.classList.add('active');
|
|
2010
|
+
setModelHidden(true);
|
|
1252
2011
|
}
|
|
1253
2012
|
}
|
|
1254
2013
|
|
|
2014
|
+
modelLockBtn?.addEventListener('click', () => {
|
|
2015
|
+
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) !== 'true');
|
|
2016
|
+
});
|
|
2017
|
+
modelScaleSlider?.addEventListener('input', (event) => setModelScale(event.target.value));
|
|
2018
|
+
modelScaleResetBtn?.addEventListener('click', () => setModelScale(100));
|
|
2019
|
+
modelBgBtn?.addEventListener('click', () => {
|
|
2020
|
+
const current = Number(localStorage.getItem(modelBgStorageKey) || 0);
|
|
2021
|
+
setModelBackground(current + 1);
|
|
2022
|
+
});
|
|
2023
|
+
layoutPresetBtns.forEach((button) => {
|
|
2024
|
+
button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layoutPreset));
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
applyModelPanelControlState();
|
|
2028
|
+
|
|
1255
2029
|
// Cycle Shiroko's Expression
|
|
1256
2030
|
const changeExpressionBtn = document.getElementById('change-expression-btn');
|
|
1257
2031
|
if (changeExpressionBtn) {
|