@pheem49/mint 1.5.1 → 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 +140 -66
- package/assets/Agent_Mint.png +0 -0
- package/assets/Settings.png +0 -0
- package/main.js +12 -0
- package/mint-cli.js +148 -921
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
- package/package.json +20 -21
- 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/proactive_engine.js +12 -2
- package/src/AI_Brain/provider_adapter.js +358 -0
- package/src/Automation_Layer/browser_automation.js +26 -24
- package/src/CLI/approval_handler.js +47 -0
- package/src/CLI/chat_router.js +7 -0
- package/src/CLI/chat_ui.js +586 -80
- package/src/CLI/cli_colors.js +115 -0
- package/src/CLI/cli_formatters.js +94 -0
- package/src/CLI/code_agent.js +825 -283
- package/src/CLI/intent_detectors.js +181 -0
- package/src/CLI/interactive_chat.js +641 -0
- package/src/CLI/list_features.js +3 -0
- package/src/CLI/repo_summarizer.js +282 -0
- package/src/CLI/semantic_code_search.js +312 -0
- package/src/CLI/skill_manager.js +41 -0
- package/src/CLI/slash_command_handler.js +418 -0
- package/src/CLI/symbol_indexer.js +231 -0
- package/src/CLI/updater.js +21 -1
- package/src/Channels/discord_bridge.js +11 -13
- package/src/Channels/line_bridge.js +10 -10
- package/src/Channels/slack_bridge.js +7 -12
- package/src/Channels/telegram_bridge.js +6 -14
- package/src/Channels/whatsapp_bridge.js +11 -9
- package/src/System/chat_history_manager.js +20 -12
- package/src/System/config_manager.js +4 -1
- package/src/System/ipc_handlers.js +10 -0
- package/src/System/optional_require.js +23 -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 +246 -14
- package/src/UI/renderer.js +620 -45
- package/src/UI/settings.css +738 -439
- package/src/UI/settings.html +487 -432
- package/src/UI/settings.js +44 -10
- package/src/UI/styles.css +1403 -106
- 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,174 @@ 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
|
+
|
|
906
|
+
function splitListOutro(text) {
|
|
907
|
+
const value = String(text || '').trim();
|
|
908
|
+
const markers = [
|
|
909
|
+
' คุณภีมอยาก',
|
|
910
|
+
' อยากให้',
|
|
911
|
+
' อยากดู',
|
|
912
|
+
' บอกมิ้นท์',
|
|
913
|
+
' Would you',
|
|
914
|
+
' Do you want',
|
|
915
|
+
' Tell me'
|
|
916
|
+
];
|
|
917
|
+
|
|
918
|
+
for (const marker of markers) {
|
|
919
|
+
const index = value.indexOf(marker);
|
|
920
|
+
if (index > 60) {
|
|
921
|
+
return {
|
|
922
|
+
main: value.slice(0, index).trim(),
|
|
923
|
+
outro: value.slice(index).trim()
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return { main: value, outro: '' };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function buildAiTextBlocks(text) {
|
|
932
|
+
const normalized = normalizeAiText(text).replace(/\r\n/g, '\n').trim();
|
|
933
|
+
if (!normalized) return [];
|
|
934
|
+
|
|
935
|
+
const readable = normalized
|
|
936
|
+
.replace(/\s+(\d+)[.)]\s+/g, '\n$1. ')
|
|
937
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
938
|
+
|
|
939
|
+
const blocks = [];
|
|
940
|
+
const lines = readable.split(/\n+/).map(line => line.trim()).filter(Boolean);
|
|
941
|
+
|
|
942
|
+
for (const line of lines) {
|
|
943
|
+
const numbered = line.match(/^\d+[.)]\s+(.+)$/);
|
|
944
|
+
const bullet = line.match(/^[-*•]\s+(.+)$/);
|
|
945
|
+
|
|
946
|
+
if (numbered || bullet) {
|
|
947
|
+
const content = numbered ? numbered[1] : bullet[1];
|
|
948
|
+
const { main, outro } = splitListOutro(content);
|
|
949
|
+
blocks.push({ type: 'bullet', text: main });
|
|
950
|
+
if (outro) blocks.push({ type: 'paragraph', text: outro });
|
|
951
|
+
} else {
|
|
952
|
+
blocks.push({ type: 'paragraph', text: line });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return blocks;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function appendFormattedMessageText(bubble, text, sender) {
|
|
960
|
+
if (sender !== 'ai') {
|
|
961
|
+
const textSpan = document.createElement('span');
|
|
962
|
+
textSpan.textContent = text;
|
|
963
|
+
bubble.appendChild(textSpan);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const blocks = buildAiTextBlocks(text);
|
|
968
|
+
if (blocks.length === 0) return;
|
|
969
|
+
|
|
970
|
+
const wrapper = document.createElement('div');
|
|
971
|
+
wrapper.classList.add('formatted-ai-text');
|
|
972
|
+
|
|
973
|
+
for (const block of blocks) {
|
|
974
|
+
const item = document.createElement(block.type === 'bullet' ? 'div' : 'p');
|
|
975
|
+
item.classList.add(block.type === 'bullet' ? 'ai-list-item' : 'ai-paragraph');
|
|
976
|
+
|
|
977
|
+
if (block.type === 'bullet') {
|
|
978
|
+
const bullet = document.createElement('span');
|
|
979
|
+
bullet.classList.add('ai-list-bullet');
|
|
980
|
+
bullet.textContent = '•';
|
|
981
|
+
const content = document.createElement('span');
|
|
982
|
+
content.textContent = block.text;
|
|
983
|
+
item.appendChild(bullet);
|
|
984
|
+
item.appendChild(content);
|
|
985
|
+
} else {
|
|
986
|
+
item.textContent = block.text;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
wrapper.appendChild(item);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
bubble.appendChild(wrapper);
|
|
993
|
+
}
|
|
994
|
+
|
|
607
995
|
function appendMessage(text, sender, base64Image = null, timestamp = null, options = {}) {
|
|
608
996
|
const messageDiv = document.createElement('div');
|
|
609
997
|
messageDiv.classList.add('message', `${sender}-message`);
|
|
@@ -625,9 +1013,7 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
|
|
|
625
1013
|
}
|
|
626
1014
|
|
|
627
1015
|
if (text) {
|
|
628
|
-
|
|
629
|
-
textSpan.textContent = text;
|
|
630
|
-
bubble.appendChild(textSpan);
|
|
1016
|
+
appendFormattedMessageText(bubble, text, sender);
|
|
631
1017
|
}
|
|
632
1018
|
|
|
633
1019
|
bubbleWrapper.appendChild(bubble);
|
|
@@ -639,10 +1025,20 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
|
|
|
639
1025
|
const timeDiv = document.createElement('div');
|
|
640
1026
|
timeDiv.classList.add('message-time');
|
|
641
1027
|
if (providerLabel) {
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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);
|
|
646
1042
|
}
|
|
647
1043
|
if (timestamp) {
|
|
648
1044
|
const timeSpan = document.createElement('span');
|
|
@@ -673,6 +1069,9 @@ function normalizeAiText(input) {
|
|
|
673
1069
|
function splitAiMessages(text) {
|
|
674
1070
|
const normalized = normalizeAiText(text).trim();
|
|
675
1071
|
if (!normalized) return [];
|
|
1072
|
+
if (/(^|\s)\d+[.)]\s+/.test(normalized) || /(^|\n)\s*[-*•]\s+/.test(normalized)) {
|
|
1073
|
+
return [normalized];
|
|
1074
|
+
}
|
|
676
1075
|
const byBlankLine = normalized
|
|
677
1076
|
.split(/\n\s*\n/)
|
|
678
1077
|
.map((part) => part.trim())
|
|
@@ -808,8 +1207,18 @@ function loadScript(src) {
|
|
|
808
1207
|
});
|
|
809
1208
|
}
|
|
810
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
|
+
|
|
811
1217
|
async function loadLive2DWhenIdle() {
|
|
812
|
-
if (!modelMount || window.Live2DManager)
|
|
1218
|
+
if (!modelMount || window.Live2DManager) {
|
|
1219
|
+
hideStartupLoading();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
813
1222
|
try {
|
|
814
1223
|
await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
|
|
815
1224
|
await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
|
|
@@ -817,6 +1226,7 @@ async function loadLive2DWhenIdle() {
|
|
|
817
1226
|
await loadScript('live2d_manager.js');
|
|
818
1227
|
if (window.Live2DManager) {
|
|
819
1228
|
await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
|
|
1229
|
+
applyModelPanelControlState();
|
|
820
1230
|
}
|
|
821
1231
|
} catch (err) {
|
|
822
1232
|
console.error('[Live2D] Deferred load failed:', err);
|
|
@@ -824,6 +1234,8 @@ async function loadLive2DWhenIdle() {
|
|
|
824
1234
|
modelStatus.classList.add('is-error');
|
|
825
1235
|
modelStatus.textContent = 'Live2D model unavailable.';
|
|
826
1236
|
}
|
|
1237
|
+
} finally {
|
|
1238
|
+
hideStartupLoading();
|
|
827
1239
|
}
|
|
828
1240
|
}
|
|
829
1241
|
|
|
@@ -890,6 +1302,7 @@ async function sendTextMessage(text, options = {}) {
|
|
|
890
1302
|
|
|
891
1303
|
// Show typing early so user knows we are processing
|
|
892
1304
|
showTyping();
|
|
1305
|
+
setMintActivity('thinking');
|
|
893
1306
|
|
|
894
1307
|
// Check Smart Context Toggle
|
|
895
1308
|
const smartToggle = document.getElementById('smart-context-toggle');
|
|
@@ -954,6 +1367,7 @@ async function sendTextMessage(text, options = {}) {
|
|
|
954
1367
|
}
|
|
955
1368
|
} catch (error) {
|
|
956
1369
|
removeTyping();
|
|
1370
|
+
setMintActivity('error');
|
|
957
1371
|
appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
|
|
958
1372
|
console.error(error);
|
|
959
1373
|
resumeSpeechIfNeeded();
|
|
@@ -962,7 +1376,6 @@ async function sendTextMessage(text, options = {}) {
|
|
|
962
1376
|
|
|
963
1377
|
chatForm.addEventListener('submit', throttle(async (e) => {
|
|
964
1378
|
e.preventDefault();
|
|
965
|
-
if (window.api && window.api.setAiState) window.api.setAiState('thinking');
|
|
966
1379
|
const text = chatInput.value.trim();
|
|
967
1380
|
await sendTextMessage(text);
|
|
968
1381
|
}, 500));
|
|
@@ -970,7 +1383,7 @@ chatForm.addEventListener('submit', throttle(async (e) => {
|
|
|
970
1383
|
window.addEventListener('live2d-model-interaction', async (event) => {
|
|
971
1384
|
const prompt = event?.detail?.prompt;
|
|
972
1385
|
if (!prompt) return;
|
|
973
|
-
|
|
1386
|
+
setMintActivity('thinking');
|
|
974
1387
|
const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
|
|
975
1388
|
const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
|
|
976
1389
|
await sendTextMessage(interactionPrompt, {
|
|
@@ -1036,9 +1449,9 @@ inputArea.addEventListener('drop', (e) => {
|
|
|
1036
1449
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
1037
1450
|
chatInput.focus();
|
|
1038
1451
|
await loadTheme();
|
|
1452
|
+
setMintActivity('idle');
|
|
1039
1453
|
await loadChatHistory();
|
|
1040
|
-
|
|
1041
|
-
scheduleLive2DLoad(() => loadLive2DWhenIdle());
|
|
1454
|
+
loadLive2DWhenIdle();
|
|
1042
1455
|
});
|
|
1043
1456
|
|
|
1044
1457
|
// Proactive OS Notifications (Battery, Network, etc.)
|
|
@@ -1053,6 +1466,11 @@ window.addEventListener('focus', () => {
|
|
|
1053
1466
|
if (window.api.clearAiNotifications) window.api.clearAiNotifications();
|
|
1054
1467
|
});
|
|
1055
1468
|
|
|
1469
|
+
document.addEventListener('click', closeProviderPopover);
|
|
1470
|
+
document.addEventListener('keydown', (event) => {
|
|
1471
|
+
if (event.key === 'Escape') closeProviderPopover();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1056
1474
|
// =====================
|
|
1057
1475
|
// Proactive Smart Suggestion Engine
|
|
1058
1476
|
// =====================
|
|
@@ -1134,34 +1552,121 @@ if (smartContextToggle) {
|
|
|
1134
1552
|
// Toggle Live2D Model visibility
|
|
1135
1553
|
const toggleModelBtn = document.getElementById('toggle-model-btn');
|
|
1136
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
|
+
}
|
|
1137
1641
|
|
|
1138
1642
|
if (toggleModelBtn && assistantWorkspace) {
|
|
1139
1643
|
toggleModelBtn.addEventListener('click', () => {
|
|
1140
|
-
|
|
1141
|
-
toggleModelBtn.classList.toggle('active', isHidden);
|
|
1142
|
-
|
|
1143
|
-
// Save preference to local storage
|
|
1144
|
-
localStorage.setItem('mint-model-hidden', String(isHidden));
|
|
1145
|
-
|
|
1146
|
-
// Refit model if shown
|
|
1147
|
-
if (!isHidden && window.Live2DManager && Live2DManager.model) {
|
|
1148
|
-
// Wait for transition
|
|
1149
|
-
setTimeout(() => {
|
|
1150
|
-
// Trigger a resize event to refit
|
|
1151
|
-
window.dispatchEvent(new Event('resize'));
|
|
1152
|
-
}, 450);
|
|
1153
|
-
}
|
|
1644
|
+
setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
|
|
1154
1645
|
});
|
|
1155
1646
|
|
|
1156
1647
|
// Restore preference on load
|
|
1157
1648
|
const savedModelHidden = localStorage.getItem('mint-model-hidden');
|
|
1158
1649
|
const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
|
|
1159
1650
|
if (savedHidden) {
|
|
1160
|
-
|
|
1161
|
-
toggleModelBtn.classList.add('active');
|
|
1651
|
+
setModelHidden(true);
|
|
1162
1652
|
}
|
|
1163
1653
|
}
|
|
1164
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
|
+
|
|
1165
1670
|
// Cycle Shiroko's Expression
|
|
1166
1671
|
const changeExpressionBtn = document.getElementById('change-expression-btn');
|
|
1167
1672
|
if (changeExpressionBtn) {
|
|
@@ -1172,6 +1677,76 @@ if (changeExpressionBtn) {
|
|
|
1172
1677
|
});
|
|
1173
1678
|
}
|
|
1174
1679
|
|
|
1680
|
+
// Cycle Live2D accessories
|
|
1681
|
+
const accessoryStorageKey = 'mint-live2d-accessories';
|
|
1682
|
+
const accessoryCycleBtn = document.getElementById('accessory-cycle-btn');
|
|
1683
|
+
const accessoryCycleLabel = document.getElementById('accessory-cycle-label');
|
|
1684
|
+
const accessoryCycleOrder = [null, 'glasses', 'pen', 'cat'];
|
|
1685
|
+
const accessoryLabels = {
|
|
1686
|
+
glasses: 'Glasses',
|
|
1687
|
+
pen: 'Pen',
|
|
1688
|
+
cat: 'Cat'
|
|
1689
|
+
};
|
|
1690
|
+
let savedAccessories = {};
|
|
1691
|
+
try {
|
|
1692
|
+
savedAccessories = JSON.parse(localStorage.getItem(accessoryStorageKey) || '{}') || {};
|
|
1693
|
+
} catch (_) {
|
|
1694
|
+
savedAccessories = {};
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const getSavedAccessoryId = () => accessoryCycleOrder.find(id => id && savedAccessories[id] === true) || null;
|
|
1698
|
+
|
|
1699
|
+
function updateAccessoryCycleButton(accessoryId) {
|
|
1700
|
+
if (!accessoryCycleBtn) return;
|
|
1701
|
+
const isActive = Boolean(accessoryId);
|
|
1702
|
+
const label = accessoryId ? accessoryLabels[accessoryId] : 'Accessory';
|
|
1703
|
+
accessoryCycleBtn.classList.toggle('active', isActive);
|
|
1704
|
+
accessoryCycleBtn.setAttribute('aria-pressed', String(isActive));
|
|
1705
|
+
accessoryCycleBtn.title = `Accessory: ${label}`;
|
|
1706
|
+
if (accessoryCycleLabel) accessoryCycleLabel.textContent = label;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
let currentAccessoryId = getSavedAccessoryId();
|
|
1710
|
+
updateAccessoryCycleButton(currentAccessoryId);
|
|
1711
|
+
|
|
1712
|
+
if (accessoryCycleBtn) {
|
|
1713
|
+
accessoryCycleBtn.addEventListener('click', () => {
|
|
1714
|
+
const currentIndex = accessoryCycleOrder.indexOf(currentAccessoryId);
|
|
1715
|
+
currentAccessoryId = accessoryCycleOrder[(currentIndex + 1) % accessoryCycleOrder.length];
|
|
1716
|
+
updateAccessoryCycleButton(currentAccessoryId);
|
|
1717
|
+
|
|
1718
|
+
if (window.Live2DManager) {
|
|
1719
|
+
Live2DManager.setExclusiveAccessory(currentAccessoryId, true);
|
|
1720
|
+
} else {
|
|
1721
|
+
savedAccessories = {};
|
|
1722
|
+
if (currentAccessoryId) savedAccessories[currentAccessoryId] = true;
|
|
1723
|
+
localStorage.setItem(accessoryStorageKey, JSON.stringify(savedAccessories));
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Toggle Live2D model interaction
|
|
1729
|
+
const toggleInteractionBtn = document.getElementById('toggle-interaction-btn');
|
|
1730
|
+
if (toggleInteractionBtn) {
|
|
1731
|
+
const savedInteractionEnabled = localStorage.getItem('mint-model-interaction-enabled') !== 'false';
|
|
1732
|
+
toggleInteractionBtn.classList.toggle('active', savedInteractionEnabled);
|
|
1733
|
+
toggleInteractionBtn.setAttribute('aria-pressed', String(savedInteractionEnabled));
|
|
1734
|
+
if (window.Live2DManager) {
|
|
1735
|
+
Live2DManager.setInteractionEnabled(savedInteractionEnabled);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
toggleInteractionBtn.addEventListener('click', () => {
|
|
1739
|
+
const isEnabled = !toggleInteractionBtn.classList.contains('active');
|
|
1740
|
+
toggleInteractionBtn.classList.toggle('active', isEnabled);
|
|
1741
|
+
toggleInteractionBtn.setAttribute('aria-pressed', String(isEnabled));
|
|
1742
|
+
if (window.Live2DManager) {
|
|
1743
|
+
Live2DManager.setInteractionEnabled(isEnabled, true);
|
|
1744
|
+
} else {
|
|
1745
|
+
localStorage.setItem('mint-model-interaction-enabled', String(isEnabled));
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1175
1750
|
// Toggle Live2D interaction area guide
|
|
1176
1751
|
const interactionGuideBtn = document.getElementById('interaction-guide-btn');
|
|
1177
1752
|
if (interactionGuideBtn && modelShell) {
|