@pheem49/mint 1.5.2 → 1.5.3
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 +7 -7
- package/README.md +132 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/main.js +12 -0
- package/package.json +3 -2
- package/preload.js +2 -0
- package/scripts/install_linux_desktop_entry.js +48 -0
- package/src/AI_Brain/Gemini_API.js +194 -491
- 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/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 +4 -1
- package/src/System/ipc_handlers.js +10 -0
- package/src/System/picture_store.js +109 -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/renderer.js +457 -42
- package/src/UI/settings.css +160 -96
- package/src/UI/settings.html +9 -0
- package/src/UI/settings.js +34 -2
- package/src/UI/styles.css +1350 -117
- package/privacy.txt +0 -1
package/src/UI/renderer.js
CHANGED
|
@@ -6,14 +6,35 @@ 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');
|
|
14
25
|
const modelMount = document.getElementById('model-mount');
|
|
15
26
|
const modelShell = document.getElementById('model-shell');
|
|
16
27
|
const modelStatus = document.getElementById('model-status');
|
|
28
|
+
const mintStatus = document.getElementById('mint-status');
|
|
29
|
+
const mintStatusLabel = document.getElementById('mint-status-label');
|
|
30
|
+
const modelActivityBadge = document.getElementById('model-activity-badge');
|
|
31
|
+
const startupLoading = document.getElementById('startup-loading');
|
|
32
|
+
const appContainer = document.querySelector('.app-container');
|
|
33
|
+
|
|
34
|
+
if (startupLoading) {
|
|
35
|
+
startupLoading.style.background = 'var(--bg-gradient)';
|
|
36
|
+
startupLoading.style.color = 'var(--text-muted)';
|
|
37
|
+
}
|
|
17
38
|
|
|
18
39
|
// Proactive Assistant elements
|
|
19
40
|
const proactiveBar = document.getElementById('proactive-bar');
|
|
@@ -28,6 +49,86 @@ let ttsVolume = 1.0;
|
|
|
28
49
|
let ttsSpeed = 1.0;
|
|
29
50
|
let ttsPitch = 1.0;
|
|
30
51
|
let lastConversationLanguage = 'auto';
|
|
52
|
+
let mintActivityResetTimer = null;
|
|
53
|
+
let currentSettings = {};
|
|
54
|
+
|
|
55
|
+
const PROVIDER_PICKER_OPTIONS = [
|
|
56
|
+
['gemini', 'Gemini'],
|
|
57
|
+
['anthropic', 'Claude'],
|
|
58
|
+
['openai', 'OpenAI'],
|
|
59
|
+
['ollama', 'Ollama'],
|
|
60
|
+
['huggingface', 'Hugging Face'],
|
|
61
|
+
['local_openai', 'Local']
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function buildProviderPicker(settings = currentSettings) {
|
|
65
|
+
if (!chatProviderSelect) return;
|
|
66
|
+
chatProviderSelect.textContent = '';
|
|
67
|
+
PROVIDER_PICKER_OPTIONS.forEach(([value, label]) => {
|
|
68
|
+
const option = document.createElement('option');
|
|
69
|
+
option.value = value;
|
|
70
|
+
option.textContent = label;
|
|
71
|
+
chatProviderSelect.appendChild(option);
|
|
72
|
+
});
|
|
73
|
+
chatProviderSelect.value = settings.aiProvider || 'gemini';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function changeChatProvider(provider) {
|
|
77
|
+
if (!PROVIDER_PICKER_OPTIONS.some(([value]) => value === provider)) return;
|
|
78
|
+
const nextSettings = { ...currentSettings, aiProvider: provider };
|
|
79
|
+
chatProviderSelect.disabled = true;
|
|
80
|
+
try {
|
|
81
|
+
const result = await window.api.saveSettings(nextSettings);
|
|
82
|
+
if (!result || result.success !== false) {
|
|
83
|
+
currentSettings = nextSettings;
|
|
84
|
+
buildProviderPicker(currentSettings);
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error(result.message || 'Unable to save provider setting');
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to change provider:', error);
|
|
90
|
+
buildProviderPicker(currentSettings);
|
|
91
|
+
setMintActivity('error');
|
|
92
|
+
} finally {
|
|
93
|
+
chatProviderSelect.disabled = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const MINT_ACTIVITY_STATES = {
|
|
98
|
+
idle: { label: 'Idle', title: 'Mint is idle' },
|
|
99
|
+
listening: { label: 'Listening', title: 'Mint is listening' },
|
|
100
|
+
thinking: { label: 'Thinking', title: 'Mint is thinking' },
|
|
101
|
+
speaking: { label: 'Speaking', title: 'Mint is speaking' },
|
|
102
|
+
error: { label: 'Error', title: 'Mint needs attention' }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function setMintActivity(state, options = {}) {
|
|
106
|
+
const normalizedState = MINT_ACTIVITY_STATES[state] ? state : 'idle';
|
|
107
|
+
const meta = MINT_ACTIVITY_STATES[normalizedState];
|
|
108
|
+
if (mintActivityResetTimer) {
|
|
109
|
+
clearTimeout(mintActivityResetTimer);
|
|
110
|
+
mintActivityResetTimer = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[mintStatus, modelActivityBadge].forEach((element) => {
|
|
114
|
+
if (!element) return;
|
|
115
|
+
element.dataset.state = normalizedState;
|
|
116
|
+
element.title = meta.title;
|
|
117
|
+
const label = element.querySelector('.mint-status-label');
|
|
118
|
+
if (label) label.textContent = meta.label;
|
|
119
|
+
});
|
|
120
|
+
if (mintStatusLabel) mintStatusLabel.textContent = meta.label;
|
|
121
|
+
|
|
122
|
+
if (window.api && window.api.setAiState) {
|
|
123
|
+
window.api.setAiState(normalizedState);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (normalizedState === 'error' || options.resetAfter) {
|
|
127
|
+
mintActivityResetTimer = setTimeout(() => {
|
|
128
|
+
setMintActivity('idle');
|
|
129
|
+
}, options.resetAfter || 3500);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
31
132
|
|
|
32
133
|
function detectConversationLanguage(text) {
|
|
33
134
|
const value = String(text || '');
|
|
@@ -56,8 +157,11 @@ function buildInteractionLanguageInstruction() {
|
|
|
56
157
|
// --- Theme Loading ---
|
|
57
158
|
function applyTheme(theme, accentColor, systemTextColor, config = {}) {
|
|
58
159
|
document.documentElement.setAttribute('data-theme', theme || 'dark');
|
|
59
|
-
const accent = accentColor || '#
|
|
60
|
-
const
|
|
160
|
+
const accent = accentColor || '#8f6cf5';
|
|
161
|
+
const defaultTextColor = theme === 'light' ? '#0f172a' : '#e8e8ea';
|
|
162
|
+
const textColor = (!systemTextColor || (theme === 'light' && systemTextColor === '#f8fafc'))
|
|
163
|
+
? defaultTextColor
|
|
164
|
+
: systemTextColor;
|
|
61
165
|
document.documentElement.style.setProperty('--accent', accent);
|
|
62
166
|
document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
|
|
63
167
|
document.documentElement.style.setProperty('--text-main', textColor);
|
|
@@ -65,19 +169,36 @@ function applyTheme(theme, accentColor, systemTextColor, config = {}) {
|
|
|
65
169
|
// Dynamic UI Customizations
|
|
66
170
|
document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
|
|
67
171
|
document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
|
|
172
|
+
document.documentElement.style.fontSize = config.fontSize || '15px';
|
|
68
173
|
|
|
69
174
|
if (theme === 'custom') {
|
|
70
175
|
if (config.customBgStart && config.customBgEnd) {
|
|
71
176
|
const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
|
|
177
|
+
document.documentElement.style.setProperty('--bg-color', config.customBgStart);
|
|
72
178
|
document.documentElement.style.setProperty('--bg-gradient', gradient);
|
|
73
179
|
}
|
|
74
180
|
if (config.customPanelBg) {
|
|
75
181
|
const rgb = hexToRgb(config.customPanelBg);
|
|
76
182
|
document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
|
|
183
|
+
document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`);
|
|
184
|
+
document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`);
|
|
185
|
+
document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`);
|
|
186
|
+
document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`);
|
|
187
|
+
document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`);
|
|
188
|
+
document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`);
|
|
77
189
|
}
|
|
78
190
|
} else {
|
|
79
|
-
|
|
80
|
-
|
|
191
|
+
[
|
|
192
|
+
'--bg-color',
|
|
193
|
+
'--bg-gradient',
|
|
194
|
+
'--panel-bg',
|
|
195
|
+
'--panel-raised',
|
|
196
|
+
'--panel-soft',
|
|
197
|
+
'--chrome-bg',
|
|
198
|
+
'--surface-bg',
|
|
199
|
+
'--surface-strong',
|
|
200
|
+
'--input-bg'
|
|
201
|
+
].forEach(name => document.documentElement.style.removeProperty(name));
|
|
81
202
|
}
|
|
82
203
|
}
|
|
83
204
|
|
|
@@ -93,14 +214,17 @@ function hexToRgb(hex) {
|
|
|
93
214
|
async function loadTheme() {
|
|
94
215
|
try {
|
|
95
216
|
const config = await window.api.getSettings();
|
|
217
|
+
currentSettings = config || {};
|
|
96
218
|
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
97
219
|
enableVoiceReply = config.enableVoiceReply !== false;
|
|
98
220
|
ttsProvider = config.ttsProvider || 'google';
|
|
99
221
|
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
100
222
|
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
101
223
|
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
224
|
+
buildProviderPicker(currentSettings);
|
|
102
225
|
} catch (e) {
|
|
103
226
|
applyTheme('dark', '#8b5cf6', '#f8fafc');
|
|
227
|
+
buildProviderPicker(currentSettings);
|
|
104
228
|
}
|
|
105
229
|
}
|
|
106
230
|
|
|
@@ -116,12 +240,18 @@ function lightenColor(hex, amount) {
|
|
|
116
240
|
|
|
117
241
|
// 🔔 Real-time theme sync from Settings window
|
|
118
242
|
window.api.onSettingsChanged((config) => {
|
|
243
|
+
currentSettings = config || currentSettings;
|
|
119
244
|
applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
|
|
120
245
|
enableVoiceReply = config.enableVoiceReply !== false;
|
|
121
246
|
ttsProvider = config.ttsProvider || 'google';
|
|
122
247
|
ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
|
|
123
248
|
ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
|
|
124
249
|
ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
|
|
250
|
+
buildProviderPicker(currentSettings);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
chatProviderSelect?.addEventListener('change', (event) => {
|
|
254
|
+
changeChatProvider(event.target.value);
|
|
125
255
|
});
|
|
126
256
|
|
|
127
257
|
// --- Voice Input Setup ---
|
|
@@ -190,6 +320,7 @@ function setupSpeechRecognition() {
|
|
|
190
320
|
speechRecognition.onstart = () => {
|
|
191
321
|
micBtn.classList.add('listening');
|
|
192
322
|
chatInput.placeholder = "Listening... (Click to stop)";
|
|
323
|
+
setMintActivity('listening');
|
|
193
324
|
speechHadResult = false;
|
|
194
325
|
if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
|
|
195
326
|
speechFallbackTimer = setTimeout(() => {
|
|
@@ -228,6 +359,7 @@ function setupSpeechRecognition() {
|
|
|
228
359
|
|
|
229
360
|
speechRecognition.onerror = (err) => {
|
|
230
361
|
console.error("Speech recognition error:", err);
|
|
362
|
+
setMintActivity('error');
|
|
231
363
|
fallbackToMediaRecorder();
|
|
232
364
|
isSpeechStreaming = false;
|
|
233
365
|
resetMicUI();
|
|
@@ -300,10 +432,12 @@ async function setupMediaRecorder() {
|
|
|
300
432
|
mediaRecorder.onstart = () => {
|
|
301
433
|
micBtn.classList.add('listening');
|
|
302
434
|
chatInput.placeholder = "Listening... (Click to stop)";
|
|
435
|
+
setMintActivity('listening');
|
|
303
436
|
};
|
|
304
437
|
|
|
305
438
|
} catch (err) {
|
|
306
439
|
console.error("Microphone access error:", err);
|
|
440
|
+
setMintActivity('error');
|
|
307
441
|
micBtn.style.display = 'none';
|
|
308
442
|
appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
|
|
309
443
|
}
|
|
@@ -312,11 +446,15 @@ async function setupMediaRecorder() {
|
|
|
312
446
|
function resetMicUI() {
|
|
313
447
|
micBtn.classList.remove('listening');
|
|
314
448
|
chatInput.placeholder = DEFAULT_PLACEHOLDER;
|
|
449
|
+
if (voiceMode !== 'speech' && (!mediaRecorder || mediaRecorder.state === 'inactive')) {
|
|
450
|
+
setMintActivity('idle');
|
|
451
|
+
}
|
|
315
452
|
}
|
|
316
453
|
|
|
317
454
|
async function sendVoiceMessage(base64Audio) {
|
|
318
455
|
showTyping();
|
|
319
456
|
chatInput.placeholder = "Processing voice...";
|
|
457
|
+
setMintActivity('thinking');
|
|
320
458
|
try {
|
|
321
459
|
// Send empty text, but include the audio
|
|
322
460
|
const response = await window.api.sendMessage("", null, base64Audio);
|
|
@@ -335,6 +473,7 @@ async function sendVoiceMessage(base64Audio) {
|
|
|
335
473
|
}
|
|
336
474
|
} catch (error) {
|
|
337
475
|
removeTyping();
|
|
476
|
+
setMintActivity('error');
|
|
338
477
|
appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
|
|
339
478
|
console.error(error);
|
|
340
479
|
resumeSpeechIfNeeded();
|
|
@@ -373,10 +512,10 @@ micBtn.addEventListener('click', (e) => {
|
|
|
373
512
|
if (mediaRecorder.state === 'inactive') {
|
|
374
513
|
audioChunks = [];
|
|
375
514
|
mediaRecorder.start();
|
|
376
|
-
|
|
515
|
+
setMintActivity('listening');
|
|
377
516
|
} else {
|
|
378
517
|
mediaRecorder.stop();
|
|
379
|
-
|
|
518
|
+
setMintActivity('thinking');
|
|
380
519
|
voiceMode = null;
|
|
381
520
|
}
|
|
382
521
|
return;
|
|
@@ -408,10 +547,10 @@ micBtn.addEventListener('click', (e) => {
|
|
|
408
547
|
if (mediaRecorder.state === 'inactive') {
|
|
409
548
|
audioChunks = [];
|
|
410
549
|
mediaRecorder.start();
|
|
411
|
-
|
|
550
|
+
setMintActivity('listening');
|
|
412
551
|
} else {
|
|
413
552
|
mediaRecorder.stop();
|
|
414
|
-
|
|
553
|
+
setMintActivity('thinking');
|
|
415
554
|
}
|
|
416
555
|
});
|
|
417
556
|
|
|
@@ -419,7 +558,7 @@ micBtn.addEventListener('click', (e) => {
|
|
|
419
558
|
let currentAudioPlayer = null;
|
|
420
559
|
|
|
421
560
|
function speakText(text, options = {}) {
|
|
422
|
-
|
|
561
|
+
setMintActivity('speaking');
|
|
423
562
|
const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
|
|
424
563
|
|
|
425
564
|
const wrappedOnEnd = () => {
|
|
@@ -429,7 +568,7 @@ function speakText(text, options = {}) {
|
|
|
429
568
|
|
|
430
569
|
return new Promise(async (resolve) => {
|
|
431
570
|
if (!enableVoiceReply) {
|
|
432
|
-
|
|
571
|
+
setMintActivity('idle');
|
|
433
572
|
wrappedOnEnd();
|
|
434
573
|
return resolve();
|
|
435
574
|
}
|
|
@@ -447,7 +586,7 @@ function speakText(text, options = {}) {
|
|
|
447
586
|
}
|
|
448
587
|
|
|
449
588
|
if (!text || !text.trim()) {
|
|
450
|
-
|
|
589
|
+
setMintActivity('idle');
|
|
451
590
|
wrappedOnEnd();
|
|
452
591
|
return resolve();
|
|
453
592
|
}
|
|
@@ -461,7 +600,7 @@ function speakText(text, options = {}) {
|
|
|
461
600
|
let i = 0;
|
|
462
601
|
const playNext = () => {
|
|
463
602
|
if (i >= urls.length) {
|
|
464
|
-
|
|
603
|
+
setMintActivity('idle');
|
|
465
604
|
wrappedOnEnd();
|
|
466
605
|
return resolve();
|
|
467
606
|
}
|
|
@@ -499,6 +638,7 @@ function speakText(text, options = {}) {
|
|
|
499
638
|
|
|
500
639
|
function fallbackSpeak(text, onEnd, resolve) {
|
|
501
640
|
if (!('speechSynthesis' in window)) {
|
|
641
|
+
setMintActivity('idle');
|
|
502
642
|
if (onEnd) onEnd();
|
|
503
643
|
resolve();
|
|
504
644
|
return;
|
|
@@ -515,7 +655,7 @@ function fallbackSpeak(text, onEnd, resolve) {
|
|
|
515
655
|
const done = () => {
|
|
516
656
|
if (finished) return;
|
|
517
657
|
finished = true;
|
|
518
|
-
|
|
658
|
+
setMintActivity('idle');
|
|
519
659
|
if (onEnd) onEnd();
|
|
520
660
|
resolve();
|
|
521
661
|
};
|
|
@@ -540,9 +680,82 @@ maximizeBtn.addEventListener('click', () => {
|
|
|
540
680
|
});
|
|
541
681
|
|
|
542
682
|
// Settings button
|
|
543
|
-
|
|
683
|
+
function openSettings() {
|
|
544
684
|
window.api.openSettings();
|
|
545
|
-
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
settingsBtn.addEventListener('click', openSettings);
|
|
688
|
+
sidebarSettingsBtn?.addEventListener('click', openSettings);
|
|
689
|
+
|
|
690
|
+
async function renderPicturesLibrary() {
|
|
691
|
+
if (!picturesGrid || !picturesEmpty) return;
|
|
692
|
+
picturesGrid.innerHTML = '';
|
|
693
|
+
|
|
694
|
+
const pictures = await window.api.listSavedPictures();
|
|
695
|
+
picturesEmpty.classList.toggle('is-hidden', pictures.length > 0);
|
|
696
|
+
|
|
697
|
+
for (const picture of pictures) {
|
|
698
|
+
const card = document.createElement('article');
|
|
699
|
+
card.className = 'picture-card';
|
|
700
|
+
|
|
701
|
+
const img = document.createElement('img');
|
|
702
|
+
img.src = picture.url;
|
|
703
|
+
img.alt = picture.filename || 'Saved picture';
|
|
704
|
+
img.loading = 'lazy';
|
|
705
|
+
|
|
706
|
+
const meta = document.createElement('div');
|
|
707
|
+
meta.className = 'picture-card-meta';
|
|
708
|
+
const date = picture.createdAt ? new Date(picture.createdAt).toLocaleString() : '';
|
|
709
|
+
meta.textContent = picture.message || date || picture.filename || 'Saved picture';
|
|
710
|
+
meta.title = [picture.filename, picture.message, date].filter(Boolean).join('\n');
|
|
711
|
+
|
|
712
|
+
card.appendChild(img);
|
|
713
|
+
card.appendChild(meta);
|
|
714
|
+
picturesGrid.appendChild(card);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function openPicturesLibrary() {
|
|
719
|
+
if (!appBody || !picturesLibrary) return;
|
|
720
|
+
picturesLibrary.hidden = false;
|
|
721
|
+
requestAnimationFrame(() => {
|
|
722
|
+
appBody.classList.add('pictures-open');
|
|
723
|
+
});
|
|
724
|
+
sidebarChatBtn?.classList.remove('is-active');
|
|
725
|
+
sidebarPicturesBtn?.classList.add('is-active');
|
|
726
|
+
await renderPicturesLibrary();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function closePicturesLibrary() {
|
|
730
|
+
if (!appBody || !picturesLibrary) return;
|
|
731
|
+
appBody.classList.remove('pictures-open');
|
|
732
|
+
setTimeout(() => {
|
|
733
|
+
if (!appBody.classList.contains('pictures-open')) {
|
|
734
|
+
picturesLibrary.hidden = true;
|
|
735
|
+
}
|
|
736
|
+
}, 240);
|
|
737
|
+
sidebarChatBtn?.classList.add('is-active');
|
|
738
|
+
sidebarPicturesBtn?.classList.remove('is-active');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
sidebarChatBtn?.addEventListener('click', closePicturesLibrary);
|
|
742
|
+
sidebarPicturesBtn?.addEventListener('click', openPicturesLibrary);
|
|
743
|
+
picturesCloseBtn?.addEventListener('click', closePicturesLibrary);
|
|
744
|
+
|
|
745
|
+
function setSidebarCollapsed(isCollapsed) {
|
|
746
|
+
if (!appBody || !sidebarToggleBtn) return;
|
|
747
|
+
appBody.classList.toggle('sidebar-collapsed', isCollapsed);
|
|
748
|
+
sidebarToggleBtn.setAttribute('aria-expanded', String(!isCollapsed));
|
|
749
|
+
sidebarToggleBtn.setAttribute('aria-label', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
750
|
+
sidebarToggleBtn.setAttribute('title', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (appBody && sidebarToggleBtn) {
|
|
754
|
+
setSidebarCollapsed(true);
|
|
755
|
+
sidebarToggleBtn.addEventListener('click', () => {
|
|
756
|
+
setSidebarCollapsed(!appBody.classList.contains('sidebar-collapsed'));
|
|
757
|
+
});
|
|
758
|
+
}
|
|
546
759
|
|
|
547
760
|
// Throttle utility to prevent UI spam
|
|
548
761
|
function throttle(func, limit) {
|
|
@@ -587,14 +800,21 @@ function formatTime(isoString) {
|
|
|
587
800
|
}
|
|
588
801
|
|
|
589
802
|
// Clear chat history
|
|
590
|
-
|
|
803
|
+
async function clearChatHistory(confirmMessage = 'Clear current chat history?') {
|
|
804
|
+
const shouldClear = window.confirm(confirmMessage);
|
|
805
|
+
if (!shouldClear) return;
|
|
806
|
+
|
|
807
|
+
closePicturesLibrary();
|
|
591
808
|
await window.api.resetChat();
|
|
592
809
|
// Remove all messages except the initial greeting
|
|
593
810
|
const messages = chatContainer.querySelectorAll('.message:not(.initial)');
|
|
594
811
|
messages.forEach(m => m.remove());
|
|
595
812
|
// Append a clear confirmation
|
|
596
813
|
appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
|
|
597
|
-
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
clearBtn.addEventListener('click', () => clearChatHistory('Clear current chat history?'));
|
|
817
|
+
sidebarNewChatBtn?.addEventListener('click', () => clearChatHistory('Start a new chat and clear current history?'));
|
|
598
818
|
|
|
599
819
|
function formatProviderInfo(providerInfo) {
|
|
600
820
|
if (!providerInfo || typeof providerInfo !== 'object') return '';
|
|
@@ -604,6 +824,85 @@ function formatProviderInfo(providerInfo) {
|
|
|
604
824
|
return model ? `${provider || 'AI'} • ${model}` : provider;
|
|
605
825
|
}
|
|
606
826
|
|
|
827
|
+
function formatNumber(value) {
|
|
828
|
+
const number = Number(value) || 0;
|
|
829
|
+
return number.toLocaleString('en-US');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function summarizeProviderUsage(providerInfo) {
|
|
833
|
+
const usage = Array.isArray(providerInfo?.usage) ? providerInfo.usage : [];
|
|
834
|
+
const selectedProvider = String(providerInfo?.provider || '').trim();
|
|
835
|
+
const selectedModel = String(providerInfo?.model || '').trim();
|
|
836
|
+
const row = usage.find(item =>
|
|
837
|
+
String(item.provider || '') === selectedProvider &&
|
|
838
|
+
String(item.model || '') === selectedModel
|
|
839
|
+
) || usage[0] || {};
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
requests: Number(row.requests) || 0,
|
|
843
|
+
inputTokens: Number(row.inputTokens) || 0,
|
|
844
|
+
outputTokens: Number(row.outputTokens) || 0,
|
|
845
|
+
reasoningTokens: Number(row.reasoningTokens) || 0,
|
|
846
|
+
cacheReads: Number(row.cacheReads) || 0,
|
|
847
|
+
totalTokens: Number(row.totalTokens) || 0
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function closeProviderPopover() {
|
|
852
|
+
document.querySelectorAll('.provider-popover').forEach(popover => popover.remove());
|
|
853
|
+
document.querySelectorAll('.provider-badge.is-open').forEach(badge => badge.classList.remove('is-open'));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function createProviderRow(label, value) {
|
|
857
|
+
const row = document.createElement('div');
|
|
858
|
+
row.className = 'provider-popover-row';
|
|
859
|
+
const labelEl = document.createElement('span');
|
|
860
|
+
labelEl.textContent = label;
|
|
861
|
+
const valueEl = document.createElement('strong');
|
|
862
|
+
valueEl.textContent = value;
|
|
863
|
+
row.appendChild(labelEl);
|
|
864
|
+
row.appendChild(valueEl);
|
|
865
|
+
return row;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function showProviderPopover(anchor, providerInfo) {
|
|
869
|
+
closeProviderPopover();
|
|
870
|
+
anchor.classList.add('is-open');
|
|
871
|
+
|
|
872
|
+
const provider = String(providerInfo?.provider || 'AI').trim();
|
|
873
|
+
const model = String(providerInfo?.model || 'Unknown model').trim();
|
|
874
|
+
const usage = summarizeProviderUsage(providerInfo);
|
|
875
|
+
const popover = document.createElement('div');
|
|
876
|
+
popover.className = 'provider-popover';
|
|
877
|
+
|
|
878
|
+
const title = document.createElement('div');
|
|
879
|
+
title.className = 'provider-popover-title';
|
|
880
|
+
title.textContent = 'Model details';
|
|
881
|
+
popover.appendChild(title);
|
|
882
|
+
|
|
883
|
+
popover.appendChild(createProviderRow('Provider', provider));
|
|
884
|
+
popover.appendChild(createProviderRow('Model', model));
|
|
885
|
+
popover.appendChild(createProviderRow('Context tokens', formatNumber(usage.inputTokens)));
|
|
886
|
+
popover.appendChild(createProviderRow('Output tokens', formatNumber(usage.outputTokens)));
|
|
887
|
+
if (usage.reasoningTokens) {
|
|
888
|
+
popover.appendChild(createProviderRow('Reasoning tokens', formatNumber(usage.reasoningTokens)));
|
|
889
|
+
}
|
|
890
|
+
popover.appendChild(createProviderRow('Total tokens', formatNumber(usage.totalTokens)));
|
|
891
|
+
|
|
892
|
+
const action = document.createElement('button');
|
|
893
|
+
action.type = 'button';
|
|
894
|
+
action.className = 'provider-popover-action';
|
|
895
|
+
action.textContent = 'Change model in Settings';
|
|
896
|
+
action.addEventListener('click', (event) => {
|
|
897
|
+
event.stopPropagation();
|
|
898
|
+
closeProviderPopover();
|
|
899
|
+
if (window.api?.openSettings) window.api.openSettings();
|
|
900
|
+
});
|
|
901
|
+
popover.appendChild(action);
|
|
902
|
+
|
|
903
|
+
anchor.after(popover);
|
|
904
|
+
}
|
|
905
|
+
|
|
607
906
|
function splitListOutro(text) {
|
|
608
907
|
const value = String(text || '').trim();
|
|
609
908
|
const markers = [
|
|
@@ -726,10 +1025,20 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
|
|
|
726
1025
|
const timeDiv = document.createElement('div');
|
|
727
1026
|
timeDiv.classList.add('message-time');
|
|
728
1027
|
if (providerLabel) {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1028
|
+
const providerButton = document.createElement('button');
|
|
1029
|
+
providerButton.type = 'button';
|
|
1030
|
+
providerButton.classList.add('provider-badge');
|
|
1031
|
+
providerButton.textContent = providerLabel;
|
|
1032
|
+
providerButton.title = 'View model details';
|
|
1033
|
+
providerButton.addEventListener('click', (event) => {
|
|
1034
|
+
event.stopPropagation();
|
|
1035
|
+
if (providerButton.classList.contains('is-open')) {
|
|
1036
|
+
closeProviderPopover();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
showProviderPopover(providerButton, options.providerInfo);
|
|
1040
|
+
});
|
|
1041
|
+
timeDiv.appendChild(providerButton);
|
|
733
1042
|
}
|
|
734
1043
|
if (timestamp) {
|
|
735
1044
|
const timeSpan = document.createElement('span');
|
|
@@ -898,8 +1207,18 @@ function loadScript(src) {
|
|
|
898
1207
|
});
|
|
899
1208
|
}
|
|
900
1209
|
|
|
1210
|
+
function hideStartupLoading() {
|
|
1211
|
+
appContainer?.classList.remove('is-loading');
|
|
1212
|
+
if (!startupLoading) return;
|
|
1213
|
+
startupLoading.classList.add('is-hidden');
|
|
1214
|
+
setTimeout(() => startupLoading.remove(), 400);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
901
1217
|
async function loadLive2DWhenIdle() {
|
|
902
|
-
if (!modelMount || window.Live2DManager)
|
|
1218
|
+
if (!modelMount || window.Live2DManager) {
|
|
1219
|
+
hideStartupLoading();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
903
1222
|
try {
|
|
904
1223
|
await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
|
|
905
1224
|
await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
|
|
@@ -907,6 +1226,7 @@ async function loadLive2DWhenIdle() {
|
|
|
907
1226
|
await loadScript('live2d_manager.js');
|
|
908
1227
|
if (window.Live2DManager) {
|
|
909
1228
|
await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
|
|
1229
|
+
applyModelPanelControlState();
|
|
910
1230
|
}
|
|
911
1231
|
} catch (err) {
|
|
912
1232
|
console.error('[Live2D] Deferred load failed:', err);
|
|
@@ -914,6 +1234,8 @@ async function loadLive2DWhenIdle() {
|
|
|
914
1234
|
modelStatus.classList.add('is-error');
|
|
915
1235
|
modelStatus.textContent = 'Live2D model unavailable.';
|
|
916
1236
|
}
|
|
1237
|
+
} finally {
|
|
1238
|
+
hideStartupLoading();
|
|
917
1239
|
}
|
|
918
1240
|
}
|
|
919
1241
|
|
|
@@ -980,6 +1302,7 @@ async function sendTextMessage(text, options = {}) {
|
|
|
980
1302
|
|
|
981
1303
|
// Show typing early so user knows we are processing
|
|
982
1304
|
showTyping();
|
|
1305
|
+
setMintActivity('thinking');
|
|
983
1306
|
|
|
984
1307
|
// Check Smart Context Toggle
|
|
985
1308
|
const smartToggle = document.getElementById('smart-context-toggle');
|
|
@@ -1044,6 +1367,7 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1044
1367
|
}
|
|
1045
1368
|
} catch (error) {
|
|
1046
1369
|
removeTyping();
|
|
1370
|
+
setMintActivity('error');
|
|
1047
1371
|
appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
|
|
1048
1372
|
console.error(error);
|
|
1049
1373
|
resumeSpeechIfNeeded();
|
|
@@ -1052,7 +1376,6 @@ async function sendTextMessage(text, options = {}) {
|
|
|
1052
1376
|
|
|
1053
1377
|
chatForm.addEventListener('submit', throttle(async (e) => {
|
|
1054
1378
|
e.preventDefault();
|
|
1055
|
-
if (window.api && window.api.setAiState) window.api.setAiState('thinking');
|
|
1056
1379
|
const text = chatInput.value.trim();
|
|
1057
1380
|
await sendTextMessage(text);
|
|
1058
1381
|
}, 500));
|
|
@@ -1060,7 +1383,7 @@ chatForm.addEventListener('submit', throttle(async (e) => {
|
|
|
1060
1383
|
window.addEventListener('live2d-model-interaction', async (event) => {
|
|
1061
1384
|
const prompt = event?.detail?.prompt;
|
|
1062
1385
|
if (!prompt) return;
|
|
1063
|
-
|
|
1386
|
+
setMintActivity('thinking');
|
|
1064
1387
|
const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
|
|
1065
1388
|
const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
|
|
1066
1389
|
await sendTextMessage(interactionPrompt, {
|
|
@@ -1126,9 +1449,9 @@ inputArea.addEventListener('drop', (e) => {
|
|
|
1126
1449
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
1127
1450
|
chatInput.focus();
|
|
1128
1451
|
await loadTheme();
|
|
1452
|
+
setMintActivity('idle');
|
|
1129
1453
|
await loadChatHistory();
|
|
1130
|
-
|
|
1131
|
-
scheduleLive2DLoad(() => loadLive2DWhenIdle());
|
|
1454
|
+
loadLive2DWhenIdle();
|
|
1132
1455
|
});
|
|
1133
1456
|
|
|
1134
1457
|
// Proactive OS Notifications (Battery, Network, etc.)
|
|
@@ -1143,6 +1466,11 @@ window.addEventListener('focus', () => {
|
|
|
1143
1466
|
if (window.api.clearAiNotifications) window.api.clearAiNotifications();
|
|
1144
1467
|
});
|
|
1145
1468
|
|
|
1469
|
+
document.addEventListener('click', closeProviderPopover);
|
|
1470
|
+
document.addEventListener('keydown', (event) => {
|
|
1471
|
+
if (event.key === 'Escape') closeProviderPopover();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1146
1474
|
// =====================
|
|
1147
1475
|
// Proactive Smart Suggestion Engine
|
|
1148
1476
|
// =====================
|
|
@@ -1224,34 +1552,121 @@ if (smartContextToggle) {
|
|
|
1224
1552
|
// Toggle Live2D Model visibility
|
|
1225
1553
|
const toggleModelBtn = document.getElementById('toggle-model-btn');
|
|
1226
1554
|
const assistantWorkspace = document.querySelector('.assistant-workspace');
|
|
1555
|
+
const modelLockBtn = document.getElementById('model-lock-btn');
|
|
1556
|
+
const modelScaleSlider = document.getElementById('model-scale-slider');
|
|
1557
|
+
const modelScaleValue = document.getElementById('model-scale-value');
|
|
1558
|
+
const modelScaleResetBtn = document.getElementById('model-scale-reset-btn');
|
|
1559
|
+
const modelBgBtn = document.getElementById('model-bg-btn');
|
|
1560
|
+
const layoutPresetBtns = document.querySelectorAll('.layout-preset-btn');
|
|
1561
|
+
|
|
1562
|
+
const modelBgStorageKey = 'mint-model-background';
|
|
1563
|
+
const modelScaleStorageKey = 'mint-model-scale';
|
|
1564
|
+
const modelPositionLockStorageKey = 'mint-model-position-locked';
|
|
1565
|
+
const workspaceLayoutStorageKey = 'mint-workspace-layout';
|
|
1566
|
+
const modelBgClasses = ['model-bg-default', 'model-bg-clear', 'model-bg-grid', 'model-bg-stage'];
|
|
1567
|
+
const modelBgLabels = ['Default background', 'Clear background', 'Grid background', 'Stage background'];
|
|
1568
|
+
const workspaceLayoutClasses = ['layout-chat'];
|
|
1569
|
+
const workspaceLayoutPresets = ['companion', 'chat'];
|
|
1570
|
+
|
|
1571
|
+
function setModelHidden(isHidden) {
|
|
1572
|
+
if (!assistantWorkspace || !toggleModelBtn) return;
|
|
1573
|
+
assistantWorkspace.classList.toggle('model-hidden', Boolean(isHidden));
|
|
1574
|
+
toggleModelBtn.classList.toggle('active', Boolean(isHidden));
|
|
1575
|
+
toggleModelBtn.setAttribute('aria-pressed', String(Boolean(isHidden)));
|
|
1576
|
+
localStorage.setItem('mint-model-hidden', String(Boolean(isHidden)));
|
|
1577
|
+
|
|
1578
|
+
if (!isHidden && window.Live2DManager && Live2DManager.model) {
|
|
1579
|
+
setTimeout(() => {
|
|
1580
|
+
window.dispatchEvent(new Event('resize'));
|
|
1581
|
+
if (typeof Live2DManager.fitModelToMount === 'function') {
|
|
1582
|
+
Live2DManager.fitModelToMount();
|
|
1583
|
+
}
|
|
1584
|
+
}, 450);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function setModelPositionLocked(isLocked) {
|
|
1589
|
+
const locked = Boolean(isLocked);
|
|
1590
|
+
localStorage.setItem(modelPositionLockStorageKey, String(locked));
|
|
1591
|
+
modelLockBtn?.classList.toggle('is-active', locked);
|
|
1592
|
+
modelLockBtn?.setAttribute('aria-pressed', String(locked));
|
|
1593
|
+
modelLockBtn?.setAttribute('title', locked ? 'Unlock model position' : 'Lock model position');
|
|
1594
|
+
if (window.Live2DManager) {
|
|
1595
|
+
Live2DManager.setPointerTrackingEnabled(!locked);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function setModelBackground(index) {
|
|
1600
|
+
if (!modelShell) return;
|
|
1601
|
+
const normalized = ((Number(index) || 0) + modelBgClasses.length) % modelBgClasses.length;
|
|
1602
|
+
modelBgClasses.forEach(className => modelShell.classList.remove(className));
|
|
1603
|
+
if (normalized > 0) {
|
|
1604
|
+
modelShell.classList.add(modelBgClasses[normalized]);
|
|
1605
|
+
}
|
|
1606
|
+
localStorage.setItem(modelBgStorageKey, String(normalized));
|
|
1607
|
+
modelBgBtn?.setAttribute('title', modelBgLabels[normalized]);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function setModelScale(value) {
|
|
1611
|
+
const next = Math.max(78, Math.min(128, Number(value) || 100));
|
|
1612
|
+
localStorage.setItem(modelScaleStorageKey, String(next));
|
|
1613
|
+
if (modelScaleSlider) modelScaleSlider.value = String(next);
|
|
1614
|
+
if (modelScaleValue) modelScaleValue.textContent = `${(next / 100).toFixed(2)}x`;
|
|
1615
|
+
if (window.Live2DManager) {
|
|
1616
|
+
Live2DManager.setZoomMultiplier(next / 100);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function applyModelPanelControlState() {
|
|
1621
|
+
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) === 'true');
|
|
1622
|
+
setModelBackground(Number(localStorage.getItem(modelBgStorageKey) || 0));
|
|
1623
|
+
setModelScale(Number(localStorage.getItem(modelScaleStorageKey) || 100));
|
|
1624
|
+
setWorkspaceLayout(localStorage.getItem(workspaceLayoutStorageKey) || 'companion');
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function setWorkspaceLayout(layout) {
|
|
1628
|
+
if (!assistantWorkspace) return;
|
|
1629
|
+
const normalized = workspaceLayoutPresets.includes(layout) ? layout : 'companion';
|
|
1630
|
+
workspaceLayoutClasses.forEach(className => assistantWorkspace.classList.remove(className));
|
|
1631
|
+
if (normalized !== 'companion') {
|
|
1632
|
+
assistantWorkspace.classList.add(`layout-${normalized}`);
|
|
1633
|
+
}
|
|
1634
|
+
localStorage.setItem(workspaceLayoutStorageKey, normalized);
|
|
1635
|
+
layoutPresetBtns.forEach((button) => {
|
|
1636
|
+
const isActive = button.dataset.layoutPreset === normalized;
|
|
1637
|
+
button.classList.toggle('is-active', isActive);
|
|
1638
|
+
button.setAttribute('aria-pressed', String(isActive));
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1227
1641
|
|
|
1228
1642
|
if (toggleModelBtn && assistantWorkspace) {
|
|
1229
1643
|
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
|
-
}
|
|
1644
|
+
setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
|
|
1244
1645
|
});
|
|
1245
1646
|
|
|
1246
1647
|
// Restore preference on load
|
|
1247
1648
|
const savedModelHidden = localStorage.getItem('mint-model-hidden');
|
|
1248
1649
|
const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
|
|
1249
1650
|
if (savedHidden) {
|
|
1250
|
-
|
|
1251
|
-
toggleModelBtn.classList.add('active');
|
|
1651
|
+
setModelHidden(true);
|
|
1252
1652
|
}
|
|
1253
1653
|
}
|
|
1254
1654
|
|
|
1655
|
+
modelLockBtn?.addEventListener('click', () => {
|
|
1656
|
+
setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) !== 'true');
|
|
1657
|
+
});
|
|
1658
|
+
modelScaleSlider?.addEventListener('input', (event) => setModelScale(event.target.value));
|
|
1659
|
+
modelScaleResetBtn?.addEventListener('click', () => setModelScale(100));
|
|
1660
|
+
modelBgBtn?.addEventListener('click', () => {
|
|
1661
|
+
const current = Number(localStorage.getItem(modelBgStorageKey) || 0);
|
|
1662
|
+
setModelBackground(current + 1);
|
|
1663
|
+
});
|
|
1664
|
+
layoutPresetBtns.forEach((button) => {
|
|
1665
|
+
button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layoutPreset));
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
applyModelPanelControlState();
|
|
1669
|
+
|
|
1255
1670
|
// Cycle Shiroko's Expression
|
|
1256
1671
|
const changeExpressionBtn = document.getElementById('change-expression-btn');
|
|
1257
1672
|
if (changeExpressionBtn) {
|