@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.
Files changed (52) hide show
  1. package/GUIDE_TH.md +7 -7
  2. package/README.md +140 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/main.js +12 -0
  6. package/mint-cli.js +148 -921
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +31 -1
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +6 -1
  9. package/package.json +20 -21
  10. package/preload.js +2 -0
  11. package/scripts/install_linux_desktop_entry.js +48 -0
  12. package/src/AI_Brain/Gemini_API.js +194 -491
  13. package/src/AI_Brain/autonomous_brain.js +46 -19
  14. package/src/AI_Brain/headless_agent.js +21 -2
  15. package/src/AI_Brain/proactive_engine.js +12 -2
  16. package/src/AI_Brain/provider_adapter.js +358 -0
  17. package/src/Automation_Layer/browser_automation.js +26 -24
  18. package/src/CLI/approval_handler.js +47 -0
  19. package/src/CLI/chat_router.js +7 -0
  20. package/src/CLI/chat_ui.js +586 -80
  21. package/src/CLI/cli_colors.js +115 -0
  22. package/src/CLI/cli_formatters.js +94 -0
  23. package/src/CLI/code_agent.js +825 -283
  24. package/src/CLI/intent_detectors.js +181 -0
  25. package/src/CLI/interactive_chat.js +641 -0
  26. package/src/CLI/list_features.js +3 -0
  27. package/src/CLI/repo_summarizer.js +282 -0
  28. package/src/CLI/semantic_code_search.js +312 -0
  29. package/src/CLI/skill_manager.js +41 -0
  30. package/src/CLI/slash_command_handler.js +418 -0
  31. package/src/CLI/symbol_indexer.js +231 -0
  32. package/src/CLI/updater.js +21 -1
  33. package/src/Channels/discord_bridge.js +11 -13
  34. package/src/Channels/line_bridge.js +10 -10
  35. package/src/Channels/slack_bridge.js +7 -12
  36. package/src/Channels/telegram_bridge.js +6 -14
  37. package/src/Channels/whatsapp_bridge.js +11 -9
  38. package/src/System/chat_history_manager.js +20 -12
  39. package/src/System/config_manager.js +4 -1
  40. package/src/System/ipc_handlers.js +10 -0
  41. package/src/System/optional_require.js +23 -0
  42. package/src/System/picture_store.js +109 -0
  43. package/src/System/task_manager.js +127 -0
  44. package/src/System/tool_registry.js +13 -0
  45. package/src/System/window_manager.js +16 -8
  46. package/src/UI/live2d_manager.js +246 -14
  47. package/src/UI/renderer.js +620 -45
  48. package/src/UI/settings.css +738 -439
  49. package/src/UI/settings.html +487 -432
  50. package/src/UI/settings.js +44 -10
  51. package/src/UI/styles.css +1403 -106
  52. package/privacy.txt +0 -1
@@ -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 || '#8b5cf6';
60
- const textColor = systemTextColor || '#f8fafc';
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
- document.documentElement.style.removeProperty('--bg-gradient');
80
- document.documentElement.style.removeProperty('--panel-bg');
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
- if (window.api && window.api.setAiState) window.api.setAiState('listening');
515
+ setMintActivity('listening');
377
516
  } else {
378
517
  mediaRecorder.stop();
379
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
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
- if (window.api && window.api.setAiState) window.api.setAiState('listening');
550
+ setMintActivity('listening');
412
551
  } else {
413
552
  mediaRecorder.stop();
414
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
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
- if (window.api && window.api.setAiState) window.api.setAiState('speaking');
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
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
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
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
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
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
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
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
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
- settingsBtn.addEventListener('click', () => {
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
- clearBtn.addEventListener('click', async () => {
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
- const textSpan = document.createElement('span');
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 providerSpan = document.createElement('span');
643
- providerSpan.classList.add('provider-badge');
644
- providerSpan.textContent = providerLabel;
645
- timeDiv.appendChild(providerSpan);
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) return;
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
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
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
- const scheduleLive2DLoad = window.requestIdleCallback || ((callback) => setTimeout(callback, 750));
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
- const isHidden = assistantWorkspace.classList.toggle('model-hidden');
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
- assistantWorkspace.classList.add('model-hidden');
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) {