@pheem49/mint 1.5.5 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.codex +0 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/.github/workflows/ci.yml +45 -0
  4. package/.github/workflows/release.yml +79 -0
  5. package/Cargo.lock +5792 -0
  6. package/Cargo.toml +32 -0
  7. package/README.md +387 -353
  8. package/assets/icon.png +0 -0
  9. package/bin/mint +0 -0
  10. package/crates/mint-cli/Cargo.toml +23 -0
  11. package/crates/mint-cli/src/agent.rs +851 -0
  12. package/crates/mint-cli/src/gmail.rs +216 -0
  13. package/crates/mint-cli/src/image.rs +142 -0
  14. package/crates/mint-cli/src/main.rs +2837 -0
  15. package/crates/mint-cli/src/mcp.rs +63 -0
  16. package/crates/mint-cli/src/onboard.rs +1149 -0
  17. package/crates/mint-cli/src/setup.rs +390 -0
  18. package/crates/mint-cli/src/skills.rs +8 -0
  19. package/crates/mint-cli/src/updater.rs +279 -0
  20. package/crates/mint-core/Cargo.toml +22 -0
  21. package/crates/mint-core/src/agent_loop.rs +94 -0
  22. package/crates/mint-core/src/api_server.rs +991 -0
  23. package/crates/mint-core/src/channels.rs +248 -0
  24. package/crates/mint-core/src/chat.rs +895 -0
  25. package/crates/mint-core/src/code_tools.rs +729 -0
  26. package/crates/mint-core/src/config.rs +368 -0
  27. package/crates/mint-core/src/files.rs +159 -0
  28. package/crates/mint-core/src/knowledge.rs +541 -0
  29. package/crates/mint-core/src/lib.rs +84 -0
  30. package/crates/mint-core/src/mcp.rs +273 -0
  31. package/crates/mint-core/src/memory.rs +673 -0
  32. package/crates/mint-core/src/orchestration.rs +2157 -0
  33. package/crates/mint-core/src/pictures.rs +314 -0
  34. package/crates/mint-core/src/plugins.rs +727 -0
  35. package/crates/mint-core/src/safety.rs +416 -0
  36. package/crates/mint-core/src/semantic.rs +254 -0
  37. package/crates/mint-core/src/shell.rs +317 -0
  38. package/crates/mint-core/src/skills.rs +71 -0
  39. package/crates/mint-core/src/symbols.rs +157 -0
  40. package/crates/mint-core/src/tasks.rs +308 -0
  41. package/crates/mint-core/src/tts.rs +92 -0
  42. package/crates/mint-core/src/weather.rs +93 -0
  43. package/crates/mint-core/src/web_search.rs +200 -0
  44. package/crates/mint-core/src/workflows.rs +81 -0
  45. package/crates/mint-core/tests/mcp_stdio.rs +45 -0
  46. package/crates/mint-core/tests/memory_persistence.rs +172 -0
  47. package/crates/mint-core/tests/pictures_storage.rs +14 -0
  48. package/crates/mint-core/tests/task_lifecycle.rs +87 -0
  49. package/package.json +35 -99
  50. package/src/bin/index.js +16 -0
  51. package/src/renderer/index-web.html +17 -0
  52. package/src/renderer/index.html +17 -0
  53. package/src/renderer/public/Live2DCubismCore.js +9 -0
  54. package/src/renderer/public/assets/icon.png +0 -0
  55. package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
  56. package/src/renderer/src/App.tsx +33 -0
  57. package/src/renderer/src/calculator.ts +47 -0
  58. package/src/renderer/src/components/ChatPanel.tsx +1598 -0
  59. package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
  60. package/src/renderer/src/components/Live2DStage.tsx +374 -0
  61. package/src/renderer/src/components/MintDashboard.tsx +950 -0
  62. package/src/renderer/src/components/ModelPanel.tsx +154 -0
  63. package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
  64. package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
  65. package/src/renderer/src/components/ScreenPicker.tsx +579 -0
  66. package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
  67. package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
  68. package/src/renderer/src/components/WidgetWindow.tsx +36 -0
  69. package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
  70. package/src/{UI → renderer/src/css}/settings.css +69 -16
  71. package/src/renderer/src/css/spotlight.css +113 -0
  72. package/src/renderer/src/css/styles.css +3722 -0
  73. package/src/renderer/src/css/widget.css +185 -0
  74. package/src/renderer/src/env.d.ts +116 -0
  75. package/src/renderer/src/index.css +379 -0
  76. package/src/renderer/src/main.tsx +13 -0
  77. package/src/renderer/src/tauri.ts +996 -0
  78. package/src/renderer/src-web/App.tsx +25 -0
  79. package/src/renderer/src-web/calculator.ts +47 -0
  80. package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
  81. package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
  82. package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
  83. package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
  84. package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
  85. package/src/renderer/src-web/css/settings.css +1100 -0
  86. package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
  87. package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
  88. package/src/{UI → renderer/src-web/css}/widget.css +2 -2
  89. package/src/renderer/src-web/env.d.ts +107 -0
  90. package/src/renderer/src-web/index.css +379 -0
  91. package/src/renderer/src-web/main.tsx +13 -0
  92. package/src/renderer/src-web/tauri.ts +983 -0
  93. package/tsconfig.json +30 -0
  94. package/vite.config.ts +33 -0
  95. package/vite.config.web.ts +51 -0
  96. package/GUIDE_TH.md +0 -125
  97. package/assets/Agent_Mint.png +0 -0
  98. package/assets/CLI_Screen.png +0 -0
  99. package/assets/Settings.png +0 -0
  100. package/benchmark_ai.js +0 -71
  101. package/install.ps1 +0 -64
  102. package/install.sh +0 -54
  103. package/main.js +0 -139
  104. package/mint-cli-logic.js +0 -3
  105. package/mint-cli.js +0 -410
  106. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
  107. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
  108. package/preload-picker.js +0 -11
  109. package/preload-settings.js +0 -11
  110. package/preload.js +0 -41
  111. package/scripts/install_linux_desktop_entry.js +0 -48
  112. package/src/AI_Brain/Gemini_API.js +0 -813
  113. package/src/AI_Brain/agent_orchestrator.js +0 -73
  114. package/src/AI_Brain/autonomous_brain.js +0 -179
  115. package/src/AI_Brain/behavior_memory.js +0 -135
  116. package/src/AI_Brain/headless_agent.js +0 -143
  117. package/src/AI_Brain/knowledge_base.js +0 -349
  118. package/src/AI_Brain/memory_store.js +0 -662
  119. package/src/AI_Brain/proactive_engine.js +0 -172
  120. package/src/AI_Brain/provider_adapter.js +0 -365
  121. package/src/Automation_Layer/browser_automation.js +0 -149
  122. package/src/Automation_Layer/file_operations.js +0 -286
  123. package/src/Automation_Layer/open_app.js +0 -85
  124. package/src/Automation_Layer/open_website.js +0 -38
  125. package/src/CLI/approval_handler.js +0 -47
  126. package/src/CLI/chat_router.js +0 -247
  127. package/src/CLI/chat_ui.js +0 -1159
  128. package/src/CLI/cli_colors.js +0 -115
  129. package/src/CLI/cli_formatters.js +0 -94
  130. package/src/CLI/code_agent.js +0 -1667
  131. package/src/CLI/code_session_memory.js +0 -62
  132. package/src/CLI/gmail_auth.js +0 -210
  133. package/src/CLI/image_input.js +0 -90
  134. package/src/CLI/intent_detectors.js +0 -181
  135. package/src/CLI/interactive_chat.js +0 -658
  136. package/src/CLI/list_features.js +0 -64
  137. package/src/CLI/onboarding.js +0 -416
  138. package/src/CLI/repo_summarizer.js +0 -282
  139. package/src/CLI/semantic_code_search.js +0 -312
  140. package/src/CLI/skill_manager.js +0 -41
  141. package/src/CLI/slash_command_handler.js +0 -418
  142. package/src/CLI/symbol_indexer.js +0 -231
  143. package/src/CLI/updater.js +0 -230
  144. package/src/CLI/workspace_manager.js +0 -90
  145. package/src/Channels/brave_search_bridge.js +0 -35
  146. package/src/Channels/discord_bridge.js +0 -66
  147. package/src/Channels/google_search_bridge.js +0 -38
  148. package/src/Channels/line_bridge.js +0 -60
  149. package/src/Channels/slack_bridge.js +0 -48
  150. package/src/Channels/telegram_bridge.js +0 -41
  151. package/src/Channels/whatsapp_bridge.js +0 -57
  152. package/src/Command_Parser/parser.js +0 -45
  153. package/src/Plugins/dev_tools.js +0 -41
  154. package/src/Plugins/discord.js +0 -20
  155. package/src/Plugins/docker.js +0 -47
  156. package/src/Plugins/gmail.js +0 -251
  157. package/src/Plugins/google_calendar.js +0 -252
  158. package/src/Plugins/mcp_manager.js +0 -95
  159. package/src/Plugins/notion.js +0 -256
  160. package/src/Plugins/obsidian.js +0 -54
  161. package/src/Plugins/plugin_manager.js +0 -81
  162. package/src/Plugins/spotify.js +0 -173
  163. package/src/Plugins/system_metrics.js +0 -31
  164. package/src/Plugins/system_monitor.js +0 -72
  165. package/src/System/action_executor.js +0 -178
  166. package/src/System/bridge_manager.js +0 -76
  167. package/src/System/chat_history_manager.js +0 -83
  168. package/src/System/config_manager.js +0 -194
  169. package/src/System/custom_workflows.js +0 -163
  170. package/src/System/daemon_manager.js +0 -67
  171. package/src/System/google_tts_urls.js +0 -51
  172. package/src/System/granular_automation.js +0 -157
  173. package/src/System/ipc_handlers.js +0 -332
  174. package/src/System/notifications.js +0 -23
  175. package/src/System/optional_require.js +0 -23
  176. package/src/System/picture_store.js +0 -109
  177. package/src/System/proactive_loop.js +0 -153
  178. package/src/System/safety_manager.js +0 -273
  179. package/src/System/sandbox_runner.js +0 -182
  180. package/src/System/screen_capture.js +0 -175
  181. package/src/System/smart_context.js +0 -227
  182. package/src/System/system_automation.js +0 -162
  183. package/src/System/system_events.js +0 -79
  184. package/src/System/system_info.js +0 -125
  185. package/src/System/task_manager.js +0 -222
  186. package/src/System/tool_registry.js +0 -293
  187. package/src/System/window_manager.js +0 -220
  188. package/src/UI/floating.css +0 -80
  189. package/src/UI/floating.html +0 -17
  190. package/src/UI/floating.js +0 -67
  191. package/src/UI/live2d_manager.js +0 -600
  192. package/src/UI/preload-floating.js +0 -7
  193. package/src/UI/preload-spotlight.js +0 -11
  194. package/src/UI/preload-widget.js +0 -5
  195. package/src/UI/proactive-glow.html +0 -42
  196. package/src/UI/renderer.js +0 -2127
  197. package/src/UI/screenPicker.html +0 -214
  198. package/src/UI/screenPicker.js +0 -262
  199. package/src/UI/settings.html +0 -577
  200. package/src/UI/settings.js +0 -770
  201. package/src/UI/spotlight.html +0 -23
  202. package/src/UI/spotlight.js +0 -185
  203. package/src/UI/widget.html +0 -29
  204. package/src/UI/widget.js +0 -10
  205. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  206. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
  207. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
  208. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
  209. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
  210. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
  211. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
  212. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
  213. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
  214. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
  215. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
  216. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
  217. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
  218. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
  219. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
  220. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
  221. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
  222. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
@@ -1,2127 +0,0 @@
1
- const chatContainer = document.getElementById('chat-container');
2
- const chatForm = document.getElementById('chat-form');
3
- const chatInput = document.getElementById('chat-input');
4
- const closeBtn = document.getElementById('close-btn');
5
- const maximizeBtn = document.getElementById('maximize-btn');
6
- const minimizeBtn = document.getElementById('minimize-btn');
7
- const clearBtn = document.getElementById('clear-btn');
8
- const settingsBtn = document.getElementById('settings-btn');
9
- const 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');
19
- const micBtn = document.getElementById('mic-btn');
20
- const visionBtn = document.getElementById('vision-btn');
21
- const chatProviderSelect = document.getElementById('chat-provider-select');
22
- const imagePreviewContainer = document.getElementById('image-preview-container');
23
- const imagePreview = document.getElementById('image-preview');
24
- const removeImageBtn = document.getElementById('remove-image-btn');
25
- const agentModeToggle = document.getElementById('agent-mode-toggle');
26
- const modelMount = document.getElementById('model-mount');
27
- const modelShell = document.getElementById('model-shell');
28
- const modelStatus = document.getElementById('model-status');
29
- const mintStatus = document.getElementById('mint-status');
30
- const mintStatusLabel = document.getElementById('mint-status-label');
31
- const modelActivityBadge = document.getElementById('model-activity-badge');
32
- const startupLoading = document.getElementById('startup-loading');
33
- const appContainer = document.querySelector('.app-container');
34
-
35
- if (startupLoading) {
36
- startupLoading.style.background = 'var(--bg-gradient)';
37
- startupLoading.style.color = 'var(--text-muted)';
38
- }
39
-
40
- // Proactive Assistant elements
41
- const proactiveBar = document.getElementById('proactive-bar');
42
- const proactiveMessage = document.getElementById('proactive-message');
43
- const proactiveChips = document.getElementById('proactive-chips');
44
- const proactiveDismissBtn = document.getElementById('proactive-dismiss-btn');
45
-
46
- let currentBase64Image = null;
47
- let enableVoiceReply = true;
48
- let ttsProvider = 'google';
49
- let ttsVolume = 1.0;
50
- let ttsSpeed = 1.0;
51
- let ttsPitch = 1.0;
52
- let lastConversationLanguage = 'auto';
53
- let mintActivityResetTimer = null;
54
- let currentSettings = {};
55
-
56
- const PROVIDER_PICKER_OPTIONS = [
57
- ['gemini', 'Gemini'],
58
- ['anthropic', 'Claude'],
59
- ['openai', 'OpenAI'],
60
- ['ollama', 'Ollama'],
61
- ['huggingface', 'Hugging Face'],
62
- ['local_openai', 'Local']
63
- ];
64
-
65
- function buildProviderPicker(settings = currentSettings) {
66
- if (!chatProviderSelect) return;
67
- chatProviderSelect.textContent = '';
68
- PROVIDER_PICKER_OPTIONS.forEach(([value, label]) => {
69
- const option = document.createElement('option');
70
- option.value = value;
71
- option.textContent = label;
72
- chatProviderSelect.appendChild(option);
73
- });
74
- chatProviderSelect.value = settings.aiProvider || 'gemini';
75
- }
76
-
77
- function syncAgentModeToggle(settings = currentSettings) {
78
- if (!agentModeToggle) return;
79
- agentModeToggle.checked = settings.assistantMode === 'agent';
80
- agentModeToggle.closest('.smart-context-control')?.classList.toggle('is-active', agentModeToggle.checked);
81
- }
82
-
83
- async function changeChatProvider(provider) {
84
- if (!PROVIDER_PICKER_OPTIONS.some(([value]) => value === provider)) return;
85
- const nextSettings = { ...currentSettings, aiProvider: provider };
86
- chatProviderSelect.disabled = true;
87
- try {
88
- const result = await window.api.saveSettings(nextSettings);
89
- if (!result || result.success !== false) {
90
- currentSettings = nextSettings;
91
- buildProviderPicker(currentSettings);
92
- } else {
93
- throw new Error(result.message || 'Unable to save provider setting');
94
- }
95
- } catch (error) {
96
- console.error('Failed to change provider:', error);
97
- buildProviderPicker(currentSettings);
98
- setMintActivity('error');
99
- } finally {
100
- chatProviderSelect.disabled = false;
101
- }
102
- }
103
-
104
- const MINT_ACTIVITY_STATES = {
105
- idle: { label: 'Idle', title: 'Mint is idle' },
106
- listening: { label: 'Listening', title: 'Mint is listening' },
107
- thinking: { label: 'Thinking', title: 'Mint is thinking' },
108
- speaking: { label: 'Speaking', title: 'Mint is speaking' },
109
- error: { label: 'Error', title: 'Mint needs attention' }
110
- };
111
-
112
- function setMintActivity(state, options = {}) {
113
- const normalizedState = MINT_ACTIVITY_STATES[state] ? state : 'idle';
114
- const meta = MINT_ACTIVITY_STATES[normalizedState];
115
- if (mintActivityResetTimer) {
116
- clearTimeout(mintActivityResetTimer);
117
- mintActivityResetTimer = null;
118
- }
119
-
120
- [mintStatus, modelActivityBadge].forEach((element) => {
121
- if (!element) return;
122
- element.dataset.state = normalizedState;
123
- element.title = meta.title;
124
- const label = element.querySelector('.mint-status-label');
125
- if (label) label.textContent = meta.label;
126
- });
127
- if (mintStatusLabel) mintStatusLabel.textContent = meta.label;
128
-
129
- if (window.api && window.api.setAiState) {
130
- window.api.setAiState(normalizedState);
131
- }
132
-
133
- if (normalizedState === 'error' || options.resetAfter) {
134
- mintActivityResetTimer = setTimeout(() => {
135
- setMintActivity('idle');
136
- }, options.resetAfter || 3500);
137
- }
138
- }
139
-
140
- function detectConversationLanguage(text) {
141
- const value = String(text || '');
142
- if (/[\u0E00-\u0E7F]/.test(value)) return 'thai';
143
- if (/[A-Za-z]/.test(value)) return 'english';
144
- return 'auto';
145
- }
146
-
147
- function rememberConversationLanguage(text) {
148
- const detected = detectConversationLanguage(text);
149
- if (detected !== 'auto') {
150
- lastConversationLanguage = detected;
151
- }
152
- }
153
-
154
- function buildInteractionLanguageInstruction() {
155
- if (lastConversationLanguage === 'thai') {
156
- return 'Current conversation language: Thai. Reply in Thai. Do not reply in English just because this interaction instruction is written in English.';
157
- }
158
- if (lastConversationLanguage === 'english') {
159
- return 'Current conversation language: English. Reply in English. Do not switch to Thai.';
160
- }
161
- return 'Infer the reply language from the recent conversation before this interaction instruction, not from the language of this instruction.';
162
- }
163
-
164
- // --- Theme Loading ---
165
- function applyTheme(theme, accentColor, systemTextColor, config = {}) {
166
- document.documentElement.setAttribute('data-theme', theme || 'dark');
167
- const accent = accentColor || '#8f6cf5';
168
- const defaultTextColor = theme === 'light' ? '#0f172a' : '#e8e8ea';
169
- const textColor = (!systemTextColor || (theme === 'light' && systemTextColor === '#f8fafc'))
170
- ? defaultTextColor
171
- : systemTextColor;
172
- document.documentElement.style.setProperty('--accent', accent);
173
- document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
174
- document.documentElement.style.setProperty('--text-main', textColor);
175
-
176
- // Dynamic UI Customizations
177
- document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
178
- document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
179
- document.documentElement.style.fontSize = config.fontSize || '15px';
180
-
181
- if (theme === 'custom') {
182
- if (config.customBgStart && config.customBgEnd) {
183
- const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
184
- document.documentElement.style.setProperty('--bg-color', config.customBgStart);
185
- document.documentElement.style.setProperty('--bg-gradient', gradient);
186
- }
187
- if (config.customPanelBg) {
188
- const rgb = hexToRgb(config.customPanelBg);
189
- document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
190
- document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`);
191
- document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`);
192
- document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`);
193
- document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`);
194
- document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`);
195
- document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`);
196
- }
197
- } else {
198
- [
199
- '--bg-color',
200
- '--bg-gradient',
201
- '--panel-bg',
202
- '--panel-raised',
203
- '--panel-soft',
204
- '--chrome-bg',
205
- '--surface-bg',
206
- '--surface-strong',
207
- '--input-bg'
208
- ].forEach(name => document.documentElement.style.removeProperty(name));
209
- }
210
- }
211
-
212
- function hexToRgb(hex) {
213
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
214
- return result ? {
215
- r: parseInt(result[1], 16),
216
- g: parseInt(result[2], 16),
217
- b: parseInt(result[3], 16)
218
- } : { r: 15, g: 23, b: 42 };
219
- }
220
-
221
- async function loadTheme() {
222
- try {
223
- const config = await window.api.getSettings();
224
- currentSettings = config || {};
225
- applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
226
- enableVoiceReply = config.enableVoiceReply !== false;
227
- ttsProvider = config.ttsProvider || 'google';
228
- ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
229
- ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
230
- ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
231
- buildProviderPicker(currentSettings);
232
- syncAgentModeToggle(currentSettings);
233
- } catch (e) {
234
- applyTheme('dark', '#8b5cf6', '#f8fafc');
235
- buildProviderPicker(currentSettings);
236
- syncAgentModeToggle(currentSettings);
237
- }
238
- }
239
-
240
- function lightenColor(hex, amount) {
241
- const clean = hex.replace('#', '');
242
- if (clean.length !== 6) return hex;
243
- const num = parseInt(clean, 16);
244
- const r = Math.min(255, (num >> 16) + amount);
245
- const g = Math.min(255, ((num >> 8) & 0x00FF) + amount);
246
- const b = Math.min(255, (num & 0x0000FF) + amount);
247
- return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
248
- }
249
-
250
- // 🔔 Real-time theme sync from Settings window
251
- window.api.onSettingsChanged((config) => {
252
- currentSettings = config || currentSettings;
253
- applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
254
- enableVoiceReply = config.enableVoiceReply !== false;
255
- ttsProvider = config.ttsProvider || 'google';
256
- ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
257
- ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
258
- ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
259
- buildProviderPicker(currentSettings);
260
- syncAgentModeToggle(currentSettings);
261
- });
262
-
263
- chatProviderSelect?.addEventListener('change', (event) => {
264
- changeChatProvider(event.target.value);
265
- });
266
-
267
- agentModeToggle?.addEventListener('change', async () => {
268
- const nextSettings = {
269
- ...currentSettings,
270
- assistantMode: agentModeToggle.checked ? 'agent' : 'chat'
271
- };
272
- agentModeToggle.disabled = true;
273
- try {
274
- const result = await window.api.saveSettings(nextSettings);
275
- if (!result || result.success !== false) {
276
- currentSettings = nextSettings;
277
- } else {
278
- throw new Error(result.message || 'Unable to save assistant mode');
279
- }
280
- } catch (error) {
281
- console.error('Failed to change assistant mode:', error);
282
- setMintActivity('error');
283
- } finally {
284
- syncAgentModeToggle(currentSettings);
285
- agentModeToggle.disabled = false;
286
- }
287
- });
288
-
289
- // --- Voice Input Setup ---
290
- let mediaRecorder = null;
291
- let audioChunks = [];
292
- let speechRecognition = null;
293
- let isSpeechStreaming = false;
294
- let speechInterim = '';
295
- let speechHadResult = false;
296
- let speechFallbackTimer = null;
297
- let voiceMode = null; // 'speech' | 'recorder' | null
298
- let voiceSendQueue = Promise.resolve();
299
- let speechPausedForReply = false;
300
- let resumeSpeechAfterResponse = false;
301
- const DEFAULT_PLACEHOLDER = "Type or speak a command...";
302
- const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
303
-
304
- function notifyAiIfNeeded() {
305
- if (!window.api.notifyAiResponse) return;
306
- if (!document.hasFocus() || document.hidden) {
307
- window.api.notifyAiResponse();
308
- } else if (window.api.clearAiNotifications) {
309
- window.api.clearAiNotifications();
310
- }
311
- }
312
-
313
- function queueVoiceTextSend(text) {
314
- const clean = (text || '').trim();
315
- if (!clean) return;
316
- voiceSendQueue = voiceSendQueue.then(() => sendTextMessage(clean, { allowSmartContext: false }));
317
- }
318
-
319
- function pauseSpeechForReply() {
320
- if (!speechRecognition || !isSpeechStreaming) return;
321
- resumeSpeechAfterResponse = true;
322
- speechPausedForReply = true;
323
- try {
324
- speechRecognition.stop();
325
- } catch (_) {}
326
- }
327
-
328
- function resumeSpeechIfNeeded() {
329
- if (!speechRecognition || !isSpeechStreaming) {
330
- resumeSpeechAfterResponse = false;
331
- speechPausedForReply = false;
332
- return;
333
- }
334
- if (!resumeSpeechAfterResponse) return;
335
- resumeSpeechAfterResponse = false;
336
- speechPausedForReply = false;
337
- try {
338
- speechRecognition.start();
339
- } catch (e) {
340
- console.error("Speech recognition resume error:", e);
341
- }
342
- }
343
-
344
- function setupSpeechRecognition() {
345
- if (!SpeechRecognitionCtor) return;
346
- speechRecognition = new SpeechRecognitionCtor();
347
- speechRecognition.lang = 'th-TH';
348
- speechRecognition.interimResults = true;
349
- // Let the engine auto-stop on silence, then we restart if streaming is enabled.
350
- speechRecognition.continuous = false;
351
-
352
- speechRecognition.onstart = () => {
353
- micBtn.classList.add('listening');
354
- chatInput.placeholder = "Listening... (Click to stop)";
355
- setMintActivity('listening');
356
- speechHadResult = false;
357
- if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
358
- speechFallbackTimer = setTimeout(() => {
359
- if (isSpeechStreaming && !speechHadResult) {
360
- fallbackToMediaRecorder();
361
- }
362
- }, 1500);
363
- };
364
-
365
- speechRecognition.onresult = (event) => {
366
- speechHadResult = true;
367
- let interimTranscript = '';
368
- let finalTranscript = '';
369
-
370
- for (let i = event.resultIndex; i < event.results.length; i++) {
371
- const result = event.results[i];
372
- const transcript = result[0]?.transcript || '';
373
- if (result.isFinal) {
374
- finalTranscript += transcript;
375
- } else {
376
- interimTranscript += transcript;
377
- }
378
- }
379
-
380
- if (finalTranscript.trim()) {
381
- const textToSend = finalTranscript.trim();
382
- speechInterim = '';
383
- chatInput.value = '';
384
- pauseSpeechForReply();
385
- queueVoiceTextSend(textToSend);
386
- } else {
387
- speechInterim = interimTranscript;
388
- chatInput.value = speechInterim.trimStart();
389
- }
390
- };
391
-
392
- speechRecognition.onerror = (err) => {
393
- console.error("Speech recognition error:", err);
394
- setMintActivity('error');
395
- fallbackToMediaRecorder();
396
- isSpeechStreaming = false;
397
- resetMicUI();
398
- };
399
-
400
- speechRecognition.onend = () => {
401
- if (speechFallbackTimer) {
402
- clearTimeout(speechFallbackTimer);
403
- speechFallbackTimer = null;
404
- }
405
- if (speechPausedForReply) {
406
- return;
407
- }
408
- if (isSpeechStreaming && !speechHadResult) {
409
- fallbackToMediaRecorder();
410
- return;
411
- }
412
- if (isSpeechStreaming) {
413
- try {
414
- speechRecognition.start();
415
- } catch (e) {
416
- console.error("Speech recognition restart error:", e);
417
- isSpeechStreaming = false;
418
- resetMicUI();
419
- }
420
- } else {
421
- resetMicUI();
422
- }
423
- };
424
- }
425
-
426
- async function setupMediaRecorder() {
427
- try {
428
- // Improved audio constraints for better quality and noise reduction
429
- const stream = await navigator.mediaDevices.getUserMedia({
430
- audio: {
431
- echoCancellation: true,
432
- noiseSuppression: true,
433
- autoGainControl: true
434
- }
435
- });
436
-
437
- // Check for supported MIME types
438
- const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
439
- mediaRecorder = new MediaRecorder(stream, { mimeType });
440
-
441
- mediaRecorder.ondataavailable = (event) => {
442
- if (event.data.size > 0) audioChunks.push(event.data);
443
- };
444
-
445
- mediaRecorder.onstop = async () => {
446
- if (audioChunks.length === 0) {
447
- resetMicUI();
448
- return;
449
- }
450
-
451
- const audioBlob = new Blob(audioChunks, { type: mimeType });
452
- audioChunks = [];
453
-
454
- // Convert Blob to Base64
455
- const reader = new FileReader();
456
- reader.readAsDataURL(audioBlob);
457
- reader.onloadend = async () => {
458
- const base64Audio = reader.result;
459
- // Send to Gemini
460
- await sendVoiceMessage(base64Audio);
461
- };
462
- };
463
-
464
- mediaRecorder.onstart = () => {
465
- micBtn.classList.add('listening');
466
- chatInput.placeholder = "Listening... (Click to stop)";
467
- setMintActivity('listening');
468
- };
469
-
470
- } catch (err) {
471
- console.error("Microphone access error:", err);
472
- setMintActivity('error');
473
- micBtn.style.display = 'none';
474
- appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
475
- }
476
- }
477
-
478
- function resetMicUI() {
479
- micBtn.classList.remove('listening');
480
- chatInput.placeholder = DEFAULT_PLACEHOLDER;
481
- if (voiceMode !== 'speech' && (!mediaRecorder || mediaRecorder.state === 'inactive')) {
482
- setMintActivity('idle');
483
- }
484
- }
485
-
486
- async function sendVoiceMessage(base64Audio) {
487
- showTyping();
488
- chatInput.placeholder = "Processing voice...";
489
- setMintActivity('thinking');
490
- try {
491
- // Send empty text, but include the audio
492
- const response = await window.api.sendMessage("", null, base64Audio);
493
- removeTyping();
494
-
495
- // Show AI response
496
- const msgDiv = await appendAiMessages(response.response, {
497
- allowDelay: true,
498
- timestamp: new Date().toISOString()
499
- });
500
- await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
501
- notifyAiIfNeeded();
502
-
503
- if (response.approval?.required) {
504
- appendApprovalCard(msgDiv, response.approval);
505
- } else if (response.action && response.action.type !== 'none') {
506
- appendActionCard(msgDiv, response.action);
507
- }
508
- } catch (error) {
509
- removeTyping();
510
- setMintActivity('error');
511
- appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
512
- console.error(error);
513
- resumeSpeechIfNeeded();
514
- } finally {
515
- resetMicUI();
516
- }
517
- }
518
-
519
- function fallbackToMediaRecorder() {
520
- if (voiceMode === 'recorder') return;
521
- isSpeechStreaming = false;
522
- speechPausedForReply = false;
523
- resumeSpeechAfterResponse = false;
524
- voiceMode = 'recorder';
525
- try {
526
- if (speechRecognition) {
527
- speechRecognition.stop();
528
- }
529
- } catch (_) {}
530
- if (mediaRecorder && mediaRecorder.state === 'inactive') {
531
- audioChunks = [];
532
- mediaRecorder.start();
533
- }
534
- }
535
-
536
- // Initialize voice input
537
- setupMediaRecorder();
538
- if (SpeechRecognitionCtor) {
539
- setupSpeechRecognition();
540
- }
541
-
542
- micBtn.addEventListener('click', (e) => {
543
- e.preventDefault();
544
- if (voiceMode === 'recorder') {
545
- if (!mediaRecorder) return;
546
- if (mediaRecorder.state === 'inactive') {
547
- audioChunks = [];
548
- mediaRecorder.start();
549
- setMintActivity('listening');
550
- } else {
551
- mediaRecorder.stop();
552
- setMintActivity('thinking');
553
- voiceMode = null;
554
- }
555
- return;
556
- }
557
-
558
- if (speechRecognition) {
559
- if (!isSpeechStreaming) {
560
- isSpeechStreaming = true;
561
- voiceMode = 'speech';
562
- speechInterim = '';
563
- chatInput.value = '';
564
- try {
565
- speechRecognition.start();
566
- } catch (err) {
567
- console.error("Speech recognition start error:", err);
568
- isSpeechStreaming = false;
569
- resetMicUI();
570
- }
571
- } else {
572
- isSpeechStreaming = false;
573
- speechRecognition.stop();
574
- voiceMode = null;
575
- }
576
- return;
577
- }
578
-
579
- if (!mediaRecorder) return;
580
-
581
- if (mediaRecorder.state === 'inactive') {
582
- audioChunks = [];
583
- mediaRecorder.start();
584
- setMintActivity('listening');
585
- } else {
586
- mediaRecorder.stop();
587
- setMintActivity('thinking');
588
- }
589
- });
590
-
591
- // --- Speech Synthesis Setup ---
592
- let currentAudioPlayer = null;
593
-
594
- function speakText(text, options = {}) {
595
- setMintActivity('speaking');
596
- const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
597
-
598
- const wrappedOnEnd = () => {
599
- if (window.Live2DManager) Live2DManager.stopLipSync();
600
- onEnd();
601
- };
602
-
603
- return new Promise(async (resolve) => {
604
- if (!enableVoiceReply) {
605
- setMintActivity('idle');
606
- wrappedOnEnd();
607
- return resolve();
608
- }
609
-
610
- // Stop any currently playing audio
611
- if (currentAudioPlayer) {
612
- currentAudioPlayer.pause();
613
- currentAudioPlayer.currentTime = 0;
614
- currentAudioPlayer = null;
615
- }
616
- if (window.Live2DManager) Live2DManager.stopLipSync();
617
-
618
- if ('speechSynthesis' in window) {
619
- window.speechSynthesis.cancel();
620
- }
621
-
622
- if (!text || !text.trim()) {
623
- setMintActivity('idle');
624
- wrappedOnEnd();
625
- return resolve();
626
- }
627
-
628
- if (window.Live2DManager) Live2DManager.startLipSync();
629
-
630
- try {
631
- if (ttsProvider !== 'native') {
632
- const urls = await window.api.getTtsUrls(text);
633
- if (urls && urls.length > 0) {
634
- let i = 0;
635
- const playNext = () => {
636
- if (i >= urls.length) {
637
- setMintActivity('idle');
638
- wrappedOnEnd();
639
- return resolve();
640
- }
641
- const audio = new Audio(urls[i].url);
642
- audio.volume = ttsVolume;
643
- audio.playbackRate = ttsSpeed;
644
-
645
- currentAudioPlayer = audio;
646
- audio.onended = () => {
647
- i++;
648
- playNext();
649
- };
650
- audio.onerror = () => {
651
- console.error("TTS Audio error", urls[i]);
652
- i++;
653
- playNext();
654
- };
655
- audio.play().catch(e => {
656
- console.error("Audio playback prevented:", e);
657
- fallbackSpeak(text, wrappedOnEnd, resolve);
658
- });
659
- };
660
- playNext();
661
- return;
662
- }
663
- }
664
- } catch (err) {
665
- console.error("Cloud TTS Error, falling back to local:", err);
666
- }
667
-
668
- // Fallback
669
- fallbackSpeak(text, wrappedOnEnd, resolve);
670
- });
671
- }
672
-
673
- function fallbackSpeak(text, onEnd, resolve) {
674
- if (!('speechSynthesis' in window)) {
675
- setMintActivity('idle');
676
- if (onEnd) onEnd();
677
- resolve();
678
- return;
679
- }
680
-
681
- window.speechSynthesis.cancel();
682
- const utterance = new SpeechSynthesisUtterance(text);
683
- utterance.lang = 'th-TH';
684
- utterance.volume = ttsVolume;
685
- utterance.rate = ttsSpeed;
686
- utterance.pitch = ttsPitch;
687
-
688
- let finished = false;
689
- const done = () => {
690
- if (finished) return;
691
- finished = true;
692
- setMintActivity('idle');
693
- if (onEnd) onEnd();
694
- resolve();
695
- };
696
-
697
- utterance.onend = done;
698
- utterance.onerror = done;
699
- window.speechSynthesis.speak(utterance);
700
- }
701
-
702
- // Minimize window handler (hides to tray)
703
- minimizeBtn.addEventListener('click', () => {
704
- window.api.minimizeWindow();
705
- });
706
-
707
- // Close window handler (quits app)
708
- closeBtn.addEventListener('click', () => {
709
- window.api.quitApp();
710
- });
711
-
712
- maximizeBtn.addEventListener('click', () => {
713
- window.api.maximizeWindow();
714
- });
715
-
716
- // Settings button
717
- function openSettings() {
718
- window.api.openSettings();
719
- }
720
-
721
- settingsBtn.addEventListener('click', openSettings);
722
- sidebarSettingsBtn?.addEventListener('click', openSettings);
723
-
724
- async function renderPicturesLibrary() {
725
- if (!picturesGrid || !picturesEmpty) return;
726
- picturesGrid.innerHTML = '';
727
-
728
- const pictures = await window.api.listSavedPictures();
729
- picturesEmpty.classList.toggle('is-hidden', pictures.length > 0);
730
-
731
- for (const picture of pictures) {
732
- const card = document.createElement('article');
733
- card.className = 'picture-card';
734
-
735
- const img = document.createElement('img');
736
- img.src = picture.url;
737
- img.alt = picture.filename || 'Saved picture';
738
- img.loading = 'lazy';
739
-
740
- const meta = document.createElement('div');
741
- meta.className = 'picture-card-meta';
742
- const date = picture.createdAt ? new Date(picture.createdAt).toLocaleString() : '';
743
- meta.textContent = picture.message || date || picture.filename || 'Saved picture';
744
- meta.title = [picture.filename, picture.message, date].filter(Boolean).join('\n');
745
-
746
- card.appendChild(img);
747
- card.appendChild(meta);
748
- picturesGrid.appendChild(card);
749
- }
750
- }
751
-
752
- async function openPicturesLibrary() {
753
- if (!appBody || !picturesLibrary) return;
754
- picturesLibrary.hidden = false;
755
- requestAnimationFrame(() => {
756
- appBody.classList.add('pictures-open');
757
- });
758
- sidebarChatBtn?.classList.remove('is-active');
759
- sidebarPicturesBtn?.classList.add('is-active');
760
- await renderPicturesLibrary();
761
- }
762
-
763
- function closePicturesLibrary() {
764
- if (!appBody || !picturesLibrary) return;
765
- appBody.classList.remove('pictures-open');
766
- setTimeout(() => {
767
- if (!appBody.classList.contains('pictures-open')) {
768
- picturesLibrary.hidden = true;
769
- }
770
- }, 240);
771
- sidebarChatBtn?.classList.add('is-active');
772
- sidebarPicturesBtn?.classList.remove('is-active');
773
- }
774
-
775
- sidebarChatBtn?.addEventListener('click', closePicturesLibrary);
776
- sidebarPicturesBtn?.addEventListener('click', openPicturesLibrary);
777
- picturesCloseBtn?.addEventListener('click', closePicturesLibrary);
778
-
779
- function setSidebarCollapsed(isCollapsed) {
780
- if (!appBody || !sidebarToggleBtn) return;
781
- appBody.classList.toggle('sidebar-collapsed', isCollapsed);
782
- sidebarToggleBtn.setAttribute('aria-expanded', String(!isCollapsed));
783
- sidebarToggleBtn.setAttribute('aria-label', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
784
- sidebarToggleBtn.setAttribute('title', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
785
- }
786
-
787
- if (appBody && sidebarToggleBtn) {
788
- setSidebarCollapsed(true);
789
- sidebarToggleBtn.addEventListener('click', () => {
790
- setSidebarCollapsed(!appBody.classList.contains('sidebar-collapsed'));
791
- });
792
- }
793
-
794
- // Throttle utility to prevent UI spam
795
- function throttle(func, limit) {
796
- let inThrottle;
797
- return function() {
798
- const args = arguments;
799
- const context = this;
800
- if (!inThrottle) {
801
- func.apply(context, args);
802
- inThrottle = true;
803
- setTimeout(() => inThrottle = false, limit);
804
- }
805
- }
806
- }
807
-
808
- // Vision system
809
- visionBtn.addEventListener('click', throttle(async () => {
810
- await window.api.startVision();
811
- }, 1000));
812
-
813
- window.api.onVisionReady((base64Image) => {
814
- currentBase64Image = base64Image;
815
- imagePreview.src = base64Image;
816
- imagePreviewContainer.style.display = 'block';
817
- chatInput.focus();
818
- });
819
-
820
- removeImageBtn.addEventListener('click', () => {
821
- currentBase64Image = null;
822
- imagePreview.src = '';
823
- imagePreviewContainer.style.display = 'none';
824
- });
825
-
826
- function formatTime(isoString) {
827
- if (!isoString) return '';
828
- try {
829
- const date = new Date(isoString);
830
- return date.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
831
- } catch (e) {
832
- return '';
833
- }
834
- }
835
-
836
- function compactSmartContext(context) {
837
- if (!context || typeof context !== 'object') return null;
838
- const activeWindow = context.activeWindow || {};
839
- const currentApp = context.currentApp || {};
840
- const browser = context.browser || null;
841
- return {
842
- capturedAt: context.capturedAt,
843
- platform: context.platform,
844
- currentApp: currentApp.name || activeWindow.appName || activeWindow.processName || '',
845
- processName: currentApp.processName || activeWindow.processName || '',
846
- pid: currentApp.pid || activeWindow.pid || null,
847
- activeWindowTitle: activeWindow.title || '',
848
- browser: browser ? {
849
- title: browser.title || '',
850
- url: browser.url || '',
851
- urlUnavailableReason: browser.urlUnavailableReason || ''
852
- } : null,
853
- selectedText: context.selectedText || '',
854
- clipboardText: context.clipboardText || ''
855
- };
856
- }
857
-
858
- function appendSmartContextToMessage(message, context) {
859
- const compact = compactSmartContext(context);
860
- if (!compact) return message;
861
- return [
862
- message,
863
- '',
864
- '[SMART_CONTEXT]',
865
- 'Use this structured desktop context together with the attached screenshot. Do not mention it unless it helps answer the user.',
866
- JSON.stringify(compact, null, 2),
867
- '[/SMART_CONTEXT]'
868
- ].join('\n');
869
- }
870
-
871
- function shouldShowAgentActivity(options = {}) {
872
- return options.showAgentActivity !== false && currentSettings.assistantMode === 'agent';
873
- }
874
-
875
- function createAgentActivityCard() {
876
- const messageDiv = document.createElement('div');
877
- messageDiv.classList.add('message', 'ai-message', 'agent-activity-message');
878
-
879
- const bubble = document.createElement('div');
880
- bubble.classList.add('message-bubble', 'agent-activity-card');
881
-
882
- const header = document.createElement('div');
883
- header.className = 'agent-activity-header';
884
- const title = document.createElement('span');
885
- title.textContent = 'Agent Activity';
886
- const status = document.createElement('span');
887
- status.className = 'agent-activity-status';
888
- status.textContent = 'Running';
889
- header.appendChild(title);
890
- header.appendChild(status);
891
-
892
- const list = document.createElement('div');
893
- list.className = 'agent-activity-list';
894
-
895
- bubble.appendChild(header);
896
- bubble.appendChild(list);
897
- messageDiv.appendChild(bubble);
898
- chatContainer.appendChild(messageDiv);
899
- scrollToBottom();
900
-
901
- return {
902
- element: messageDiv,
903
- list,
904
- status,
905
- add(label, state = 'running', detail = '') {
906
- const item = document.createElement('div');
907
- item.className = 'agent-activity-item';
908
- item.dataset.state = state;
909
-
910
- const dot = document.createElement('span');
911
- dot.className = 'agent-activity-dot';
912
-
913
- const content = document.createElement('span');
914
- content.className = 'agent-activity-text';
915
- content.textContent = detail ? `${label}: ${detail}` : label;
916
-
917
- item.appendChild(dot);
918
- item.appendChild(content);
919
- list.appendChild(item);
920
- scrollToBottom();
921
- return item;
922
- },
923
- update(item, state, label, detail = '') {
924
- if (!item) return;
925
- item.dataset.state = state;
926
- const content = item.querySelector('.agent-activity-text');
927
- if (content && label) {
928
- content.textContent = detail ? `${label}: ${detail}` : label;
929
- }
930
- },
931
- finish(state = 'done', label = 'Done') {
932
- status.textContent = label;
933
- status.dataset.state = state;
934
- }
935
- };
936
- }
937
-
938
- function describeSmartContextActivity(context, hasScreenshot) {
939
- const compact = compactSmartContext(context) || {};
940
- const parts = [];
941
- if (hasScreenshot) parts.push('screen');
942
- if (compact.currentApp) parts.push(compact.currentApp);
943
- if (compact.activeWindowTitle) parts.push(compact.activeWindowTitle);
944
- if (compact.selectedText) parts.push('selected text');
945
- if (compact.clipboardText) parts.push('clipboard');
946
- return parts.slice(0, 3).join(' · ') || 'desktop context';
947
- }
948
-
949
- function describeActionActivity(action) {
950
- if (!action || action.type === 'none') return 'No desktop action';
951
- const meta = getActionCardMeta(action);
952
- return meta.detail ? `${meta.title} · ${meta.detail}` : meta.title;
953
- }
954
-
955
- // Clear chat history
956
- async function clearChatHistory(confirmMessage = 'Clear current chat history?') {
957
- const shouldClear = window.confirm(confirmMessage);
958
- if (!shouldClear) return;
959
-
960
- closePicturesLibrary();
961
- await window.api.resetChat();
962
- // Remove all messages except the initial greeting
963
- const messages = chatContainer.querySelectorAll('.message:not(.initial)');
964
- messages.forEach(m => m.remove());
965
- // Append a clear confirmation
966
- appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
967
- }
968
-
969
- clearBtn.addEventListener('click', () => clearChatHistory('Clear current chat history?'));
970
- sidebarNewChatBtn?.addEventListener('click', () => clearChatHistory('Start a new chat and clear current history?'));
971
-
972
- function formatProviderInfo(providerInfo) {
973
- if (!providerInfo || typeof providerInfo !== 'object') return '';
974
- const provider = String(providerInfo.provider || '').trim();
975
- const model = String(providerInfo.model || '').trim();
976
- if (!provider && !model) return '';
977
- return model ? `${provider || 'AI'} • ${model}` : provider;
978
- }
979
-
980
- function formatNumber(value) {
981
- const number = Number(value) || 0;
982
- return number.toLocaleString('en-US');
983
- }
984
-
985
- function summarizeProviderUsage(providerInfo) {
986
- const usage = Array.isArray(providerInfo?.usage) ? providerInfo.usage : [];
987
- const selectedProvider = String(providerInfo?.provider || '').trim();
988
- const selectedModel = String(providerInfo?.model || '').trim();
989
- const row = usage.find(item =>
990
- String(item.provider || '') === selectedProvider &&
991
- String(item.model || '') === selectedModel
992
- ) || usage[0] || {};
993
-
994
- return {
995
- requests: Number(row.requests) || 0,
996
- inputTokens: Number(row.inputTokens) || 0,
997
- outputTokens: Number(row.outputTokens) || 0,
998
- reasoningTokens: Number(row.reasoningTokens) || 0,
999
- cacheReads: Number(row.cacheReads) || 0,
1000
- totalTokens: Number(row.totalTokens) || 0
1001
- };
1002
- }
1003
-
1004
- function closeProviderPopover() {
1005
- document.querySelectorAll('.provider-popover').forEach(popover => popover.remove());
1006
- document.querySelectorAll('.provider-badge.is-open').forEach(badge => badge.classList.remove('is-open'));
1007
- }
1008
-
1009
- function createProviderRow(label, value) {
1010
- const row = document.createElement('div');
1011
- row.className = 'provider-popover-row';
1012
- const labelEl = document.createElement('span');
1013
- labelEl.textContent = label;
1014
- const valueEl = document.createElement('strong');
1015
- valueEl.textContent = value;
1016
- row.appendChild(labelEl);
1017
- row.appendChild(valueEl);
1018
- return row;
1019
- }
1020
-
1021
- function showProviderPopover(anchor, providerInfo) {
1022
- closeProviderPopover();
1023
- anchor.classList.add('is-open');
1024
-
1025
- const provider = String(providerInfo?.provider || 'AI').trim();
1026
- const model = String(providerInfo?.model || 'Unknown model').trim();
1027
- const usage = summarizeProviderUsage(providerInfo);
1028
- const popover = document.createElement('div');
1029
- popover.className = 'provider-popover';
1030
-
1031
- const title = document.createElement('div');
1032
- title.className = 'provider-popover-title';
1033
- title.textContent = 'Model details';
1034
- popover.appendChild(title);
1035
-
1036
- popover.appendChild(createProviderRow('Provider', provider));
1037
- popover.appendChild(createProviderRow('Model', model));
1038
- popover.appendChild(createProviderRow('Context tokens', formatNumber(usage.inputTokens)));
1039
- popover.appendChild(createProviderRow('Output tokens', formatNumber(usage.outputTokens)));
1040
- if (usage.reasoningTokens) {
1041
- popover.appendChild(createProviderRow('Reasoning tokens', formatNumber(usage.reasoningTokens)));
1042
- }
1043
- popover.appendChild(createProviderRow('Total tokens', formatNumber(usage.totalTokens)));
1044
-
1045
- const action = document.createElement('button');
1046
- action.type = 'button';
1047
- action.className = 'provider-popover-action';
1048
- action.textContent = 'Change model in Settings';
1049
- action.addEventListener('click', (event) => {
1050
- event.stopPropagation();
1051
- closeProviderPopover();
1052
- if (window.api?.openSettings) window.api.openSettings();
1053
- });
1054
- popover.appendChild(action);
1055
-
1056
- anchor.after(popover);
1057
- }
1058
-
1059
- function splitListOutro(text) {
1060
- const value = String(text || '').trim();
1061
- const markers = [
1062
- ' คุณภีมอยาก',
1063
- ' อยากให้',
1064
- ' อยากดู',
1065
- ' บอกมิ้นท์',
1066
- ' Would you',
1067
- ' Do you want',
1068
- ' Tell me'
1069
- ];
1070
-
1071
- for (const marker of markers) {
1072
- const index = value.indexOf(marker);
1073
- if (index > 60) {
1074
- return {
1075
- main: value.slice(0, index).trim(),
1076
- outro: value.slice(index).trim()
1077
- };
1078
- }
1079
- }
1080
-
1081
- return { main: value, outro: '' };
1082
- }
1083
-
1084
- function buildAiTextBlocks(text) {
1085
- const normalized = normalizeAiText(text).replace(/\r\n/g, '\n').trim();
1086
- if (!normalized) return [];
1087
-
1088
- const readable = normalized
1089
- .replace(/\s+(\d+)[.)]\s+/g, '\n$1. ')
1090
- .replace(/\n{3,}/g, '\n\n');
1091
-
1092
- const blocks = [];
1093
- const lines = readable.split(/\n+/).map(line => line.trim()).filter(Boolean);
1094
-
1095
- for (const line of lines) {
1096
- const numbered = line.match(/^\d+[.)]\s+(.+)$/);
1097
- const bullet = line.match(/^[-*•]\s+(.+)$/);
1098
-
1099
- if (numbered || bullet) {
1100
- const content = numbered ? numbered[1] : bullet[1];
1101
- const { main, outro } = splitListOutro(content);
1102
- blocks.push({ type: 'bullet', text: main });
1103
- if (outro) blocks.push({ type: 'paragraph', text: outro });
1104
- } else {
1105
- blocks.push({ type: 'paragraph', text: line });
1106
- }
1107
- }
1108
-
1109
- return blocks;
1110
- }
1111
-
1112
- function appendFormattedMessageText(bubble, text, sender) {
1113
- if (sender !== 'ai') {
1114
- const textSpan = document.createElement('span');
1115
- textSpan.textContent = text;
1116
- bubble.appendChild(textSpan);
1117
- return;
1118
- }
1119
-
1120
- const blocks = buildAiTextBlocks(text);
1121
- if (blocks.length === 0) return;
1122
-
1123
- const wrapper = document.createElement('div');
1124
- wrapper.classList.add('formatted-ai-text');
1125
-
1126
- for (const block of blocks) {
1127
- const item = document.createElement(block.type === 'bullet' ? 'div' : 'p');
1128
- item.classList.add(block.type === 'bullet' ? 'ai-list-item' : 'ai-paragraph');
1129
-
1130
- if (block.type === 'bullet') {
1131
- const bullet = document.createElement('span');
1132
- bullet.classList.add('ai-list-bullet');
1133
- bullet.textContent = '•';
1134
- const content = document.createElement('span');
1135
- content.textContent = block.text;
1136
- item.appendChild(bullet);
1137
- item.appendChild(content);
1138
- } else {
1139
- item.textContent = block.text;
1140
- }
1141
-
1142
- wrapper.appendChild(item);
1143
- }
1144
-
1145
- bubble.appendChild(wrapper);
1146
- }
1147
-
1148
- function appendMessage(text, sender, base64Image = null, timestamp = null, options = {}) {
1149
- const messageDiv = document.createElement('div');
1150
- messageDiv.classList.add('message', `${sender}-message`);
1151
-
1152
- const bubbleWrapper = document.createElement('div');
1153
- bubbleWrapper.classList.add('bubble-wrapper');
1154
-
1155
- const bubble = document.createElement('div');
1156
- bubble.classList.add('message-bubble');
1157
-
1158
- if (base64Image && sender === 'user') {
1159
- const img = document.createElement('img');
1160
- img.src = base64Image;
1161
- img.style.maxWidth = '100%';
1162
- img.style.borderRadius = '4px';
1163
- img.style.marginBottom = '8px';
1164
- img.style.display = 'block';
1165
- bubble.appendChild(img);
1166
- }
1167
-
1168
- if (text) {
1169
- appendFormattedMessageText(bubble, text, sender);
1170
- }
1171
-
1172
- bubbleWrapper.appendChild(bubble);
1173
-
1174
- const providerLabel = sender === 'ai' ? formatProviderInfo(options.providerInfo) : '';
1175
-
1176
- // Add metadata
1177
- if (timestamp || providerLabel) {
1178
- const timeDiv = document.createElement('div');
1179
- timeDiv.classList.add('message-time');
1180
- if (providerLabel) {
1181
- const providerButton = document.createElement('button');
1182
- providerButton.type = 'button';
1183
- providerButton.classList.add('provider-badge');
1184
- providerButton.textContent = providerLabel;
1185
- providerButton.title = 'View model details';
1186
- providerButton.addEventListener('click', (event) => {
1187
- event.stopPropagation();
1188
- if (providerButton.classList.contains('is-open')) {
1189
- closeProviderPopover();
1190
- return;
1191
- }
1192
- showProviderPopover(providerButton, options.providerInfo);
1193
- });
1194
- timeDiv.appendChild(providerButton);
1195
- }
1196
- if (timestamp) {
1197
- const timeSpan = document.createElement('span');
1198
- timeSpan.textContent = formatTime(timestamp);
1199
- timeDiv.appendChild(timeSpan);
1200
- }
1201
- bubbleWrapper.appendChild(timeDiv);
1202
- }
1203
-
1204
- messageDiv.appendChild(bubbleWrapper);
1205
- chatContainer.appendChild(messageDiv);
1206
- scrollToBottom();
1207
-
1208
- return messageDiv; // Return it so we can append action cards if needed
1209
- }
1210
-
1211
- function normalizeAiText(input) {
1212
- if (Array.isArray(input)) {
1213
- return input
1214
- .map((item) => (item == null ? '' : String(item).trim()))
1215
- .filter(Boolean)
1216
- .join('\n\n');
1217
- }
1218
- if (input == null) return '';
1219
- return String(input);
1220
- }
1221
-
1222
- function splitAiMessages(text) {
1223
- const normalized = normalizeAiText(text).trim();
1224
- if (!normalized) return [];
1225
- if (/(^|\s)\d+[.)]\s+/.test(normalized) || /(^|\n)\s*[-*•]\s+/.test(normalized)) {
1226
- return [normalized];
1227
- }
1228
- const byBlankLine = normalized
1229
- .split(/\n\s*\n/)
1230
- .map((part) => part.trim())
1231
- .filter(Boolean);
1232
- if (byBlankLine.length > 1) return byBlankLine;
1233
- return autoChunkAiText(normalized);
1234
- }
1235
-
1236
- function sleep(ms) {
1237
- return new Promise((resolve) => setTimeout(resolve, ms));
1238
- }
1239
-
1240
- function estimateMessageDelay(text) {
1241
- const base = 260;
1242
- const perChar = 12;
1243
- const jitter = Math.floor(Math.random() * 120);
1244
- const scaled = base + Math.min(1200, text.length * perChar) + jitter;
1245
- return Math.min(1600, scaled);
1246
- }
1247
-
1248
- async function appendAiMessages(text, options = {}) {
1249
- const allowDelay = options.allowDelay !== false;
1250
- const timestamp = options.timestamp || new Date().toISOString();
1251
- const providerInfo = options.providerInfo || null;
1252
- const parts = splitAiMessages(text);
1253
- let lastDiv = null;
1254
-
1255
- for (let index = 0; index < parts.length; index += 1) {
1256
- if (allowDelay && index > 0) {
1257
- showTyping();
1258
- await sleep(estimateMessageDelay(parts[index]));
1259
- removeTyping();
1260
- }
1261
- // Only show timestamp for the last bubble in a group if multiple
1262
- const partTimestamp = (index === parts.length - 1) ? timestamp : null;
1263
- const partProviderInfo = (index === parts.length - 1) ? providerInfo : null;
1264
- lastDiv = appendMessage(parts[index], 'ai', null, partTimestamp, { providerInfo: partProviderInfo });
1265
- }
1266
-
1267
- return lastDiv;
1268
- }
1269
-
1270
- function autoChunkAiText(text) {
1271
- const trimmed = text.trim();
1272
- if (trimmed.length <= 120) return [trimmed];
1273
-
1274
- const sentenceMatches = trimmed.match(/[^.!?…\n]+[.!?…]+|[^.!?…\n]+$/g);
1275
- if (!sentenceMatches || sentenceMatches.length <= 1) return [trimmed];
1276
-
1277
- const bubbles = [];
1278
- let current = '';
1279
- for (const sentence of sentenceMatches) {
1280
- const next = current ? `${current} ${sentence}` : sentence;
1281
- if (next.length > 180 && current) {
1282
- bubbles.push(current.trim());
1283
- current = sentence;
1284
- } else {
1285
- current = next;
1286
- }
1287
- }
1288
- if (current.trim()) bubbles.push(current.trim());
1289
-
1290
- if (bubbles.length > 3) {
1291
- const merged = [bubbles[0], bubbles[1], bubbles.slice(2).join(' ').trim()];
1292
- return merged.filter(Boolean);
1293
- }
1294
-
1295
- return bubbles.length > 0 ? bubbles : [trimmed];
1296
- }
1297
-
1298
- function appendActionCard(messageDiv, action) {
1299
- if (!messageDiv || !action || action.type === 'none') return;
1300
-
1301
- const meta = getActionCardMeta(action);
1302
- const card = document.createElement('div');
1303
- card.classList.add('action-card');
1304
- card.dataset.actionType = action.type || 'unknown';
1305
-
1306
- const icon = document.createElement('span');
1307
- icon.className = 'action-card-icon';
1308
- icon.textContent = meta.icon;
1309
-
1310
- const content = document.createElement('div');
1311
- content.className = 'action-card-content';
1312
-
1313
- const title = document.createElement('div');
1314
- title.className = 'action-card-title';
1315
- title.textContent = meta.title;
1316
- content.appendChild(title);
1317
-
1318
- if (meta.detail) {
1319
- const detail = document.createElement('div');
1320
- detail.className = 'action-card-detail';
1321
- detail.textContent = meta.detail;
1322
- content.appendChild(detail);
1323
- }
1324
-
1325
- card.appendChild(icon);
1326
- card.appendChild(content);
1327
- messageDiv.querySelector('.message-bubble')?.appendChild(card);
1328
- }
1329
-
1330
- function getActionCardMeta(action) {
1331
- const target = formatActionTarget(action);
1332
- const type = action?.type || 'unknown';
1333
- const targetOrFallback = target || 'No target';
1334
-
1335
- const map = {
1336
- open_url: ['🌐', 'Opened URL', target],
1337
- search: ['🔍', 'Searched the web', target],
1338
- open_app: ['🚀', 'Launched app', target],
1339
- web_automation: ['🧭', 'Ran browser automation', target],
1340
- create_folder: ['📁', 'Created folder', target],
1341
- open_file: ['📄', 'Opened file', target],
1342
- open_folder: ['📂', 'Opened folder', target],
1343
- delete_file: ['🗑️', 'Deleted file', target],
1344
- find_path: ['🔎', action.openAfter ? 'Found and opened path' : 'Found path', buildFindPathDetail(action)],
1345
- clipboard_write: ['📋', 'Updated clipboard', target],
1346
- learn_file: ['📚', 'Indexed file', target],
1347
- learn_folder: ['📚', 'Indexed folder', target],
1348
- system_info: ['💻', target ? 'Checked weather' : 'Checked system info', target],
1349
- plugin: ['🔌', 'Ran plugin', target],
1350
- mcp_tool: ['🧩', 'Called MCP tool', target],
1351
- mouse_move: ['↗', 'Moved pointer', target],
1352
- mouse_click: ['☝', 'Clicked screen', buildMouseDetail(action)],
1353
- type_text: ['⌨', 'Typed text', target],
1354
- key_tap: ['⌨', 'Pressed key', target],
1355
- system_automation: ['⚙', 'Changed system setting', target]
1356
- };
1357
-
1358
- const [icon, title, detail] = map[type] || ['⚡', `Ran action: ${type}`, targetOrFallback];
1359
- return { icon, title, detail };
1360
- }
1361
-
1362
- function buildFindPathDetail(action) {
1363
- const target = formatActionTarget(action);
1364
- const typeLabel = action.pathType && action.pathType !== 'any' ? ` (${action.pathType})` : '';
1365
- return target ? `${target}${typeLabel}` : typeLabel.trim();
1366
- }
1367
-
1368
- function buildMouseDetail(action) {
1369
- const point = formatActionTarget(action);
1370
- const button = action.button ? `button ${action.button}` : 'left button';
1371
- return point ? `${point} · ${button}` : button;
1372
- }
1373
-
1374
- function formatActionTarget(action) {
1375
- if (!action || typeof action !== 'object') return '';
1376
- if (action.server && action.target) return `${action.server}:${action.target}`;
1377
- if (action.pluginName) return `${action.pluginName} ${action.target || ''}`.trim();
1378
- if (action.target) return String(action.target);
1379
- if (Number.isFinite(action.x) && Number.isFinite(action.y)) return `${action.x}, ${action.y}`;
1380
- return '';
1381
- }
1382
-
1383
- function getApprovalCopy(approval) {
1384
- const action = approval?.action || {};
1385
- const actionType = action.type || 'unknown';
1386
- const target = formatActionTarget(action);
1387
- const isDangerous = approval?.tier === 'dangerous';
1388
- return {
1389
- title: isDangerous ? 'Dangerous action requires approval' : 'Action requires approval',
1390
- body: target ? `${actionType}: ${target}` : actionType,
1391
- reason: approval?.reason || 'This action needs your permission before Mint can run it.',
1392
- approveLabel: isDangerous ? 'Allow Dangerous Action' : 'Allow Action'
1393
- };
1394
- }
1395
-
1396
- function appendApprovalCard(messageDiv, approval, activity = null) {
1397
- if (!messageDiv || !approval?.action || !window.api?.executeApprovedAction) return;
1398
-
1399
- const copy = getApprovalCopy(approval);
1400
- const card = document.createElement('div');
1401
- card.classList.add('action-card', 'approval-card');
1402
- card.dataset.tier = approval.tier || 'approval';
1403
-
1404
- const content = document.createElement('div');
1405
- content.className = 'approval-card-content';
1406
-
1407
- const title = document.createElement('div');
1408
- title.className = 'approval-card-title';
1409
- title.textContent = copy.title;
1410
-
1411
- const body = document.createElement('div');
1412
- body.className = 'approval-card-body';
1413
- body.textContent = copy.body;
1414
-
1415
- const reason = document.createElement('div');
1416
- reason.className = 'approval-card-reason';
1417
- reason.textContent = copy.reason;
1418
-
1419
- content.appendChild(title);
1420
- content.appendChild(body);
1421
- content.appendChild(reason);
1422
-
1423
- const actions = document.createElement('div');
1424
- actions.className = 'approval-card-actions';
1425
-
1426
- const approveBtn = document.createElement('button');
1427
- approveBtn.type = 'button';
1428
- approveBtn.className = 'approval-btn approval-btn-approve';
1429
- approveBtn.textContent = copy.approveLabel;
1430
-
1431
- const cancelBtn = document.createElement('button');
1432
- cancelBtn.type = 'button';
1433
- cancelBtn.className = 'approval-btn approval-btn-cancel';
1434
- cancelBtn.textContent = 'Cancel';
1435
-
1436
- const setDone = (message, state) => {
1437
- approveBtn.disabled = true;
1438
- cancelBtn.disabled = true;
1439
- card.dataset.state = state;
1440
- reason.textContent = message;
1441
- };
1442
-
1443
- approveBtn.addEventListener('click', async () => {
1444
- approveBtn.disabled = true;
1445
- cancelBtn.disabled = true;
1446
- reason.textContent = 'Running approved action...';
1447
- const runStep = activity?.add('Running approved action', 'running', describeActionActivity(approval.action));
1448
- setMintActivity('thinking');
1449
-
1450
- try {
1451
- const result = await window.api.executeApprovedAction(approval.action);
1452
- if (!result || result.success === false) {
1453
- setDone(result?.message || 'Action failed.', 'error');
1454
- activity?.update(runStep, 'error', 'Action failed', result?.message || '');
1455
- activity?.finish('error', 'Failed');
1456
- setMintActivity('error');
1457
- return;
1458
- }
1459
-
1460
- setDone(result.message || 'Action completed.', 'approved');
1461
- activity?.update(runStep, 'done', 'Action completed', result.message || describeActionActivity(approval.action));
1462
- activity?.finish('done', 'Completed');
1463
- setMintActivity('idle');
1464
- } catch (error) {
1465
- console.error('[Approval] Failed to execute action:', error);
1466
- setDone(error.message || 'Action failed.', 'error');
1467
- activity?.update(runStep, 'error', 'Action failed', error.message || '');
1468
- activity?.finish('error', 'Failed');
1469
- setMintActivity('error');
1470
- }
1471
- });
1472
-
1473
- cancelBtn.addEventListener('click', () => {
1474
- setDone('Cancelled by user.', 'cancelled');
1475
- activity?.add('Approval cancelled', 'cancelled');
1476
- activity?.finish('cancelled', 'Cancelled');
1477
- setMintActivity('idle');
1478
- });
1479
-
1480
- actions.appendChild(approveBtn);
1481
- actions.appendChild(cancelBtn);
1482
- card.appendChild(content);
1483
- card.appendChild(actions);
1484
-
1485
- messageDiv.querySelector('.message-bubble')?.appendChild(card);
1486
- }
1487
-
1488
- function showTyping() {
1489
- const typingDiv = document.createElement('div');
1490
- typingDiv.classList.add('message', 'ai-message', 'typing-message');
1491
- typingDiv.id = 'typing-indicator';
1492
-
1493
- const indicator = document.createElement('div');
1494
- indicator.classList.add('typing-indicator');
1495
- indicator.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
1496
-
1497
- typingDiv.appendChild(indicator);
1498
- chatContainer.appendChild(typingDiv);
1499
- scrollToBottom();
1500
- }
1501
-
1502
- function removeTyping() {
1503
- const typingDiv = document.getElementById('typing-indicator');
1504
- if (typingDiv) {
1505
- typingDiv.remove();
1506
- }
1507
- }
1508
-
1509
- function scrollToBottom() {
1510
- chatContainer.scrollTop = chatContainer.scrollHeight;
1511
- }
1512
-
1513
- function loadScript(src) {
1514
- return new Promise((resolve, reject) => {
1515
- if (document.querySelector(`script[src="${src}"]`)) {
1516
- resolve();
1517
- return;
1518
- }
1519
- const script = document.createElement('script');
1520
- script.src = src;
1521
- script.onload = resolve;
1522
- script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
1523
- document.body.appendChild(script);
1524
- });
1525
- }
1526
-
1527
- function hideStartupLoading() {
1528
- appContainer?.classList.remove('is-loading');
1529
- if (!startupLoading) return;
1530
- startupLoading.classList.add('is-hidden');
1531
- setTimeout(() => startupLoading.remove(), 400);
1532
- }
1533
-
1534
- async function loadLive2DWhenIdle() {
1535
- if (!modelMount || window.Live2DManager) {
1536
- hideStartupLoading();
1537
- return;
1538
- }
1539
- try {
1540
- await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
1541
- await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
1542
- await loadScript('../../node_modules/pixi-live2d-display/dist/cubism4.min.js');
1543
- await loadScript('live2d_manager.js');
1544
- if (window.Live2DManager) {
1545
- await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
1546
- applyModelPanelControlState();
1547
- }
1548
- } catch (err) {
1549
- console.error('[Live2D] Deferred load failed:', err);
1550
- if (modelStatus) {
1551
- modelStatus.classList.add('is-error');
1552
- modelStatus.textContent = 'Live2D model unavailable.';
1553
- }
1554
- } finally {
1555
- hideStartupLoading();
1556
- }
1557
- }
1558
-
1559
- async function loadChatHistory() {
1560
- try {
1561
- const history = await window.api.getChatHistory();
1562
- const initial = chatContainer.querySelector('.message.initial');
1563
-
1564
- if (!Array.isArray(history) || history.length === 0) {
1565
- if (initial) {
1566
- initial.style.display = 'flex';
1567
- initial.style.opacity = '1';
1568
- }
1569
- return;
1570
- }
1571
-
1572
- if (initial) {
1573
- initial.remove();
1574
- }
1575
-
1576
- for (const item of history) {
1577
- if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
1578
- const sender = item.sender === 'user' ? 'user' : 'ai';
1579
- if (sender === 'user' && !String(item.text).startsWith('Model interaction:')) {
1580
- rememberConversationLanguage(item.text);
1581
- }
1582
- appendMessage(item.text, sender, null, item.timestamp, {
1583
- providerInfo: sender === 'ai' ? item.providerInfo : null
1584
- });
1585
- }
1586
- } catch (error) {
1587
- console.error('Failed to load chat history:', error);
1588
- }
1589
- }
1590
-
1591
- async function sendTextMessage(text, options = {}) {
1592
- const cleanText = (text || '').trim();
1593
- const allowSmartContext = options.allowSmartContext !== false;
1594
- const includePendingImage = options.includePendingImage !== false;
1595
- const displayText = options.displayText !== undefined ? options.displayText : cleanText;
1596
- const trackLanguage = options.trackLanguage !== false;
1597
-
1598
- // We can send either a text message, an image, or both.
1599
- if (!cleanText && (!includePendingImage || !currentBase64Image)) return;
1600
-
1601
- // Cache the image for sending and UI, then clear
1602
- let imageToSend = includePendingImage ? currentBase64Image : null;
1603
-
1604
- // Clear input & UI for explicit images
1605
- chatInput.value = '';
1606
- if (includePendingImage) {
1607
- currentBase64Image = null;
1608
- imagePreviewContainer.style.display = 'none';
1609
- imagePreview.src = '';
1610
- }
1611
-
1612
- const now = new Date().toISOString();
1613
-
1614
- // Show user message (with explicit image if available)
1615
- appendMessage(displayText, 'user', imageToSend, now);
1616
- if (trackLanguage) {
1617
- rememberConversationLanguage(displayText || cleanText);
1618
- }
1619
-
1620
- const activity = shouldShowAgentActivity(options) ? createAgentActivityCard() : null;
1621
- const contextStep = activity?.add('Preparing desktop context', 'running');
1622
-
1623
- // Show typing early so user knows we are processing
1624
- showTyping();
1625
- setMintActivity('thinking');
1626
-
1627
- let messageToSend = cleanText;
1628
-
1629
- // Check Smart Context Toggle
1630
- const smartToggle = document.getElementById('smart-context-toggle');
1631
- if (allowSmartContext && smartToggle && smartToggle.checked && !imageToSend) {
1632
- try {
1633
- const [silentCapture, smartContext] = await Promise.all([
1634
- window.api.captureSilentScreen(),
1635
- window.api.getSmartContext ? window.api.getSmartContext() : Promise.resolve(null)
1636
- ]);
1637
- if (silentCapture) {
1638
- // Set imageToSend so it gets sent to the API, but we already appended the chat bubble
1639
- imageToSend = silentCapture;
1640
- }
1641
- if (smartContext) {
1642
- messageToSend = appendSmartContextToMessage(cleanText, smartContext);
1643
- }
1644
- if (activity && contextStep) {
1645
- activity.update(
1646
- contextStep,
1647
- 'done',
1648
- 'Read Smart Context',
1649
- describeSmartContextActivity(smartContext, Boolean(silentCapture))
1650
- );
1651
- }
1652
- } catch (err) {
1653
- console.error("Smart Context capture failed:", err);
1654
- activity?.update(contextStep, 'error', 'Smart Context unavailable', err.message || '');
1655
- }
1656
- } else if (activity && contextStep) {
1657
- activity.update(contextStep, 'skipped', 'Smart Context skipped', imageToSend ? 'image already attached' : 'toggle is off');
1658
- }
1659
-
1660
- // Hide proactive bar if user is actively typing a message
1661
- hideProactiveBar();
1662
- const modelStep = activity?.add('Waiting for model response', 'running');
1663
-
1664
- try {
1665
- // Send to main process (text, image, audio=null)
1666
- const response = await window.api.sendMessage(messageToSend, imageToSend, null);
1667
- removeTyping();
1668
- activity?.update(modelStep, 'done', 'Model response received');
1669
-
1670
- if (typeof response.response !== 'string') {
1671
- response.response = normalizeAiText(response.response);
1672
- }
1673
-
1674
- // Handle system_info action: fetch data and append to AI message
1675
- if (response.action && response.action.type === 'system_info') {
1676
- const infoStep = activity?.add('Running local info action', 'running', describeActionActivity(response.action));
1677
- const city = (response.action.target || '').trim();
1678
- // Only treat as weather if city looks like a real location name (not blank, not 'date', not 'time')
1679
- const weatherKeywords = ['date', 'time', 'วัน', 'เวลา', 'today', 'now'];
1680
- const isWeather = city && !weatherKeywords.some(k => city.toLowerCase().includes(k));
1681
-
1682
- if (isWeather) {
1683
- // Weather query
1684
- const weather = await window.api.getWeather(city);
1685
- response.response += `\n\n🌡️ ${weather.data}`;
1686
- activity?.update(infoStep, 'done', 'Weather info added', city);
1687
- } else {
1688
- // General system info (date, time, RAM, CPU)
1689
- const info = await window.api.getSystemInfo();
1690
- const machine = info.machine && info.machine.display ? `\n🖥️ รุ่นเครื่อง: ${info.machine.display}` : '';
1691
- const distro = info.distro ? `\nระบบ: ${info.distro}` : '';
1692
- response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}${machine}${distro}\n💻 CPU: ${info.cpu.model} (${info.cpu.cores} คอร์)\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
1693
- activity?.update(infoStep, 'done', 'System info added');
1694
- }
1695
- }
1696
-
1697
- // Show AI response
1698
- const msgDiv = await appendAiMessages(response.response, {
1699
- allowDelay: true,
1700
- timestamp: response.timestamp,
1701
- providerInfo: response.providerInfo
1702
- });
1703
-
1704
- // Speak AI response
1705
- await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
1706
- notifyAiIfNeeded();
1707
-
1708
- // Append action card if applicable
1709
- if (response.approval?.required) {
1710
- activity?.add('Selected action', 'approval', describeActionActivity(response.approval.action));
1711
- activity?.add('Waiting for approval', 'running', response.approval.reason || '');
1712
- activity?.finish('waiting', 'Waiting');
1713
- appendApprovalCard(msgDiv, response.approval, activity);
1714
- } else if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
1715
- activity?.add('Selected action', 'done', describeActionActivity(response.action));
1716
- appendActionCard(msgDiv, response.action);
1717
- activity?.finish('done', 'Completed');
1718
- } else if (response.action && response.action.type === 'system_info') {
1719
- activity?.add('Selected action', 'done', describeActionActivity(response.action));
1720
- activity?.finish('done', 'Completed');
1721
- } else {
1722
- activity?.add('No desktop action selected', 'done');
1723
- activity?.finish('done', 'Completed');
1724
- }
1725
- } catch (error) {
1726
- removeTyping();
1727
- setMintActivity('error');
1728
- activity?.update(modelStep, 'error', 'Model request failed', error.message || '');
1729
- activity?.finish('error', 'Failed');
1730
- appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
1731
- console.error(error);
1732
- resumeSpeechIfNeeded();
1733
- }
1734
- }
1735
-
1736
- chatForm.addEventListener('submit', throttle(async (e) => {
1737
- e.preventDefault();
1738
- const text = chatInput.value.trim();
1739
- await sendTextMessage(text);
1740
- }, 500));
1741
-
1742
- window.addEventListener('live2d-model-interaction', async (event) => {
1743
- const prompt = event?.detail?.prompt;
1744
- if (!prompt) return;
1745
- setMintActivity('thinking');
1746
- const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
1747
- const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
1748
- await sendTextMessage(interactionPrompt, {
1749
- allowSmartContext: false,
1750
- includePendingImage: false,
1751
- trackLanguage: false,
1752
- displayText: `${displayPrefix}: ${event.detail.label || event.detail.region || 'Interaction'}`
1753
- });
1754
- });
1755
-
1756
- // --- Image Paste and Drag-n-Drop Support ---
1757
- function handleImageFile(file) {
1758
- if (!file || !file.type.startsWith('image/')) return;
1759
- const reader = new FileReader();
1760
- reader.onload = (e) => {
1761
- currentBase64Image = e.target.result;
1762
- imagePreview.src = currentBase64Image;
1763
- imagePreviewContainer.style.display = 'block';
1764
- chatInput.focus();
1765
- };
1766
- reader.readAsDataURL(file);
1767
- }
1768
-
1769
- // Paste Event
1770
- chatInput.addEventListener('paste', (e) => {
1771
- const items = (e.clipboardData || e.originalEvent.clipboardData).items;
1772
- for (let index in items) {
1773
- const item = items[index];
1774
- if (item.kind === 'file' && item.type.startsWith('image/')) {
1775
- const blob = item.getAsFile();
1776
- handleImageFile(blob);
1777
- break; // Handle only the first image
1778
- }
1779
- }
1780
- });
1781
-
1782
- // Drag and Drop Events (on the whole chat form/input area)
1783
- const inputArea = document.querySelector('.input-area');
1784
-
1785
- inputArea.addEventListener('dragover', (e) => {
1786
- e.preventDefault();
1787
- e.stopPropagation();
1788
- inputArea.style.opacity = '0.7'; // Visual feedback
1789
- });
1790
-
1791
- inputArea.addEventListener('dragleave', (e) => {
1792
- e.preventDefault();
1793
- e.stopPropagation();
1794
- inputArea.style.opacity = '1';
1795
- });
1796
-
1797
- inputArea.addEventListener('drop', (e) => {
1798
- e.preventDefault();
1799
- e.stopPropagation();
1800
- inputArea.style.opacity = '1';
1801
-
1802
- if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
1803
- handleImageFile(e.dataTransfer.files[0]);
1804
- }
1805
- });
1806
-
1807
- // Focus input on load + init theme
1808
- window.addEventListener('DOMContentLoaded', async () => {
1809
- chatInput.focus();
1810
- await loadTheme();
1811
- setMintActivity('idle');
1812
- await loadChatHistory();
1813
- loadLive2DWhenIdle();
1814
- });
1815
-
1816
- // Proactive OS Notifications (Battery, Network, etc.)
1817
- window.api.onProactiveNotification((data) => {
1818
- if (!data || !data.message) return;
1819
- appendMessage(data.message, 'ai');
1820
- // Also speak the notification automatically
1821
- speakText(data.message);
1822
- });
1823
-
1824
- window.addEventListener('focus', () => {
1825
- if (window.api.clearAiNotifications) window.api.clearAiNotifications();
1826
- });
1827
-
1828
- document.addEventListener('click', closeProviderPopover);
1829
- document.addEventListener('keydown', (event) => {
1830
- if (event.key === 'Escape') closeProviderPopover();
1831
- });
1832
-
1833
- // =====================
1834
- // Proactive Smart Suggestion Engine
1835
- // =====================
1836
-
1837
- function showProactiveBar(data) {
1838
- // Clear old chips
1839
- proactiveChips.innerHTML = '';
1840
-
1841
- // Set message
1842
- proactiveMessage.textContent = data.message || '';
1843
-
1844
- // Render each suggestion as a chip
1845
- data.suggestions.forEach((item, index) => {
1846
- const chip = document.createElement('button');
1847
- chip.className = 'suggestion-chip';
1848
- chip.textContent = item.label;
1849
- chip.style.animationDelay = `${index * 60}ms`;
1850
-
1851
- chip.addEventListener('click', async () => {
1852
- hideProactiveBar();
1853
-
1854
- if (window.api.recordBehavior) {
1855
- window.api.recordBehavior(`User picked: ${item.label}`);
1856
- }
1857
-
1858
- showTyping();
1859
- try {
1860
- const result = await window.api.executeProactiveAction(item.action);
1861
- removeTyping();
1862
- const confirmText = result?.message || `เปิด ${item.label} แล้วค่ะ ✅`;
1863
- const msgDiv = appendMessage(confirmText, 'ai');
1864
- speakText(confirmText);
1865
- if (item.action && item.action.type !== 'none') {
1866
- appendActionCard(msgDiv, item.action);
1867
- }
1868
- } catch (err) {
1869
- removeTyping();
1870
- appendMessage('ขออภัยค่ะ เกิดข้อผิดพลาด', 'ai');
1871
- console.error('[Chip] Error:', err);
1872
- }
1873
- });
1874
-
1875
- proactiveChips.appendChild(chip);
1876
- });
1877
-
1878
- // Show bar with animation reset
1879
- proactiveBar.style.display = 'none';
1880
- requestAnimationFrame(() => {
1881
- proactiveBar.style.display = 'block';
1882
- });
1883
- }
1884
-
1885
- function hideProactiveBar() {
1886
- proactiveBar.style.display = 'none';
1887
- proactiveChips.innerHTML = '';
1888
- }
1889
-
1890
- // Receive multi-suggestion data from main process
1891
- window.api.onProactiveSuggestion((data) => {
1892
- if (data && data.message && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
1893
- showProactiveBar(data);
1894
- notifyAiIfNeeded();
1895
- }
1896
- });
1897
-
1898
- // Dismiss button
1899
- proactiveDismissBtn.addEventListener('click', () => {
1900
- hideProactiveBar();
1901
- });
1902
-
1903
- // Sync Smart Context toggle → start/stop proactive loop
1904
- const smartContextToggle = document.getElementById('smart-context-toggle');
1905
- if (smartContextToggle) {
1906
- smartContextToggle.addEventListener('change', () => {
1907
- window.api.toggleProactive(smartContextToggle.checked);
1908
- });
1909
- }
1910
-
1911
- // Toggle Live2D Model visibility
1912
- const toggleModelBtn = document.getElementById('toggle-model-btn');
1913
- const assistantWorkspace = document.querySelector('.assistant-workspace');
1914
- const modelLockBtn = document.getElementById('model-lock-btn');
1915
- const modelScaleSlider = document.getElementById('model-scale-slider');
1916
- const modelScaleValue = document.getElementById('model-scale-value');
1917
- const modelScaleResetBtn = document.getElementById('model-scale-reset-btn');
1918
- const modelBgBtn = document.getElementById('model-bg-btn');
1919
- const layoutPresetBtns = document.querySelectorAll('.layout-preset-btn');
1920
-
1921
- const modelBgStorageKey = 'mint-model-background';
1922
- const modelScaleStorageKey = 'mint-model-scale';
1923
- const modelPositionLockStorageKey = 'mint-model-position-locked';
1924
- const workspaceLayoutStorageKey = 'mint-workspace-layout';
1925
- const modelBgClasses = ['model-bg-default', 'model-bg-clear', 'model-bg-grid', 'model-bg-stage'];
1926
- const modelBgLabels = ['Default background', 'Clear background', 'Grid background', 'Stage background'];
1927
- const workspaceLayoutClasses = ['layout-chat'];
1928
- const workspaceLayoutPresets = ['companion', 'chat'];
1929
-
1930
- function setModelHidden(isHidden) {
1931
- if (!assistantWorkspace || !toggleModelBtn) return;
1932
- assistantWorkspace.classList.toggle('model-hidden', Boolean(isHidden));
1933
- toggleModelBtn.classList.toggle('active', Boolean(isHidden));
1934
- toggleModelBtn.setAttribute('aria-pressed', String(Boolean(isHidden)));
1935
- localStorage.setItem('mint-model-hidden', String(Boolean(isHidden)));
1936
-
1937
- if (!isHidden && window.Live2DManager && Live2DManager.model) {
1938
- setTimeout(() => {
1939
- window.dispatchEvent(new Event('resize'));
1940
- if (typeof Live2DManager.fitModelToMount === 'function') {
1941
- Live2DManager.fitModelToMount();
1942
- }
1943
- }, 450);
1944
- }
1945
- }
1946
-
1947
- function setModelPositionLocked(isLocked) {
1948
- const locked = Boolean(isLocked);
1949
- localStorage.setItem(modelPositionLockStorageKey, String(locked));
1950
- modelLockBtn?.classList.toggle('is-active', locked);
1951
- modelLockBtn?.setAttribute('aria-pressed', String(locked));
1952
- modelLockBtn?.setAttribute('title', locked ? 'Unlock model position' : 'Lock model position');
1953
- if (window.Live2DManager) {
1954
- Live2DManager.setPointerTrackingEnabled(!locked);
1955
- }
1956
- }
1957
-
1958
- function setModelBackground(index) {
1959
- if (!modelShell) return;
1960
- const normalized = ((Number(index) || 0) + modelBgClasses.length) % modelBgClasses.length;
1961
- modelBgClasses.forEach(className => modelShell.classList.remove(className));
1962
- if (normalized > 0) {
1963
- modelShell.classList.add(modelBgClasses[normalized]);
1964
- }
1965
- localStorage.setItem(modelBgStorageKey, String(normalized));
1966
- modelBgBtn?.setAttribute('title', modelBgLabels[normalized]);
1967
- }
1968
-
1969
- function setModelScale(value) {
1970
- const next = Math.max(78, Math.min(128, Number(value) || 100));
1971
- localStorage.setItem(modelScaleStorageKey, String(next));
1972
- if (modelScaleSlider) modelScaleSlider.value = String(next);
1973
- if (modelScaleValue) modelScaleValue.textContent = `${(next / 100).toFixed(2)}x`;
1974
- if (window.Live2DManager) {
1975
- Live2DManager.setZoomMultiplier(next / 100);
1976
- }
1977
- }
1978
-
1979
- function applyModelPanelControlState() {
1980
- setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) === 'true');
1981
- setModelBackground(Number(localStorage.getItem(modelBgStorageKey) || 0));
1982
- setModelScale(Number(localStorage.getItem(modelScaleStorageKey) || 100));
1983
- setWorkspaceLayout(localStorage.getItem(workspaceLayoutStorageKey) || 'companion');
1984
- }
1985
-
1986
- function setWorkspaceLayout(layout) {
1987
- if (!assistantWorkspace) return;
1988
- const normalized = workspaceLayoutPresets.includes(layout) ? layout : 'companion';
1989
- workspaceLayoutClasses.forEach(className => assistantWorkspace.classList.remove(className));
1990
- if (normalized !== 'companion') {
1991
- assistantWorkspace.classList.add(`layout-${normalized}`);
1992
- }
1993
- localStorage.setItem(workspaceLayoutStorageKey, normalized);
1994
- layoutPresetBtns.forEach((button) => {
1995
- const isActive = button.dataset.layoutPreset === normalized;
1996
- button.classList.toggle('is-active', isActive);
1997
- button.setAttribute('aria-pressed', String(isActive));
1998
- });
1999
- }
2000
-
2001
- if (toggleModelBtn && assistantWorkspace) {
2002
- toggleModelBtn.addEventListener('click', () => {
2003
- setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
2004
- });
2005
-
2006
- // Restore preference on load
2007
- const savedModelHidden = localStorage.getItem('mint-model-hidden');
2008
- const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
2009
- if (savedHidden) {
2010
- setModelHidden(true);
2011
- }
2012
- }
2013
-
2014
- modelLockBtn?.addEventListener('click', () => {
2015
- setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) !== 'true');
2016
- });
2017
- modelScaleSlider?.addEventListener('input', (event) => setModelScale(event.target.value));
2018
- modelScaleResetBtn?.addEventListener('click', () => setModelScale(100));
2019
- modelBgBtn?.addEventListener('click', () => {
2020
- const current = Number(localStorage.getItem(modelBgStorageKey) || 0);
2021
- setModelBackground(current + 1);
2022
- });
2023
- layoutPresetBtns.forEach((button) => {
2024
- button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layoutPreset));
2025
- });
2026
-
2027
- applyModelPanelControlState();
2028
-
2029
- // Cycle Shiroko's Expression
2030
- const changeExpressionBtn = document.getElementById('change-expression-btn');
2031
- if (changeExpressionBtn) {
2032
- changeExpressionBtn.addEventListener('click', () => {
2033
- if (window.Live2DManager) {
2034
- Live2DManager.cycleExpression();
2035
- }
2036
- });
2037
- }
2038
-
2039
- // Cycle Live2D accessories
2040
- const accessoryStorageKey = 'mint-live2d-accessories';
2041
- const accessoryCycleBtn = document.getElementById('accessory-cycle-btn');
2042
- const accessoryCycleLabel = document.getElementById('accessory-cycle-label');
2043
- const accessoryCycleOrder = [null, 'glasses', 'pen', 'cat'];
2044
- const accessoryLabels = {
2045
- glasses: 'Glasses',
2046
- pen: 'Pen',
2047
- cat: 'Cat'
2048
- };
2049
- let savedAccessories = {};
2050
- try {
2051
- savedAccessories = JSON.parse(localStorage.getItem(accessoryStorageKey) || '{}') || {};
2052
- } catch (_) {
2053
- savedAccessories = {};
2054
- }
2055
-
2056
- const getSavedAccessoryId = () => accessoryCycleOrder.find(id => id && savedAccessories[id] === true) || null;
2057
-
2058
- function updateAccessoryCycleButton(accessoryId) {
2059
- if (!accessoryCycleBtn) return;
2060
- const isActive = Boolean(accessoryId);
2061
- const label = accessoryId ? accessoryLabels[accessoryId] : 'Accessory';
2062
- accessoryCycleBtn.classList.toggle('active', isActive);
2063
- accessoryCycleBtn.setAttribute('aria-pressed', String(isActive));
2064
- accessoryCycleBtn.title = `Accessory: ${label}`;
2065
- if (accessoryCycleLabel) accessoryCycleLabel.textContent = label;
2066
- }
2067
-
2068
- let currentAccessoryId = getSavedAccessoryId();
2069
- updateAccessoryCycleButton(currentAccessoryId);
2070
-
2071
- if (accessoryCycleBtn) {
2072
- accessoryCycleBtn.addEventListener('click', () => {
2073
- const currentIndex = accessoryCycleOrder.indexOf(currentAccessoryId);
2074
- currentAccessoryId = accessoryCycleOrder[(currentIndex + 1) % accessoryCycleOrder.length];
2075
- updateAccessoryCycleButton(currentAccessoryId);
2076
-
2077
- if (window.Live2DManager) {
2078
- Live2DManager.setExclusiveAccessory(currentAccessoryId, true);
2079
- } else {
2080
- savedAccessories = {};
2081
- if (currentAccessoryId) savedAccessories[currentAccessoryId] = true;
2082
- localStorage.setItem(accessoryStorageKey, JSON.stringify(savedAccessories));
2083
- }
2084
- });
2085
- }
2086
-
2087
- // Toggle Live2D model interaction
2088
- const toggleInteractionBtn = document.getElementById('toggle-interaction-btn');
2089
- if (toggleInteractionBtn) {
2090
- const savedInteractionEnabled = localStorage.getItem('mint-model-interaction-enabled') !== 'false';
2091
- toggleInteractionBtn.classList.toggle('active', savedInteractionEnabled);
2092
- toggleInteractionBtn.setAttribute('aria-pressed', String(savedInteractionEnabled));
2093
- if (window.Live2DManager) {
2094
- Live2DManager.setInteractionEnabled(savedInteractionEnabled);
2095
- }
2096
-
2097
- toggleInteractionBtn.addEventListener('click', () => {
2098
- const isEnabled = !toggleInteractionBtn.classList.contains('active');
2099
- toggleInteractionBtn.classList.toggle('active', isEnabled);
2100
- toggleInteractionBtn.setAttribute('aria-pressed', String(isEnabled));
2101
- if (window.Live2DManager) {
2102
- Live2DManager.setInteractionEnabled(isEnabled, true);
2103
- } else {
2104
- localStorage.setItem('mint-model-interaction-enabled', String(isEnabled));
2105
- }
2106
- });
2107
- }
2108
-
2109
- // Toggle Live2D interaction area guide
2110
- const interactionGuideBtn = document.getElementById('interaction-guide-btn');
2111
- if (interactionGuideBtn && modelShell) {
2112
- const savedGuideVisible = localStorage.getItem('mint-interaction-guide-visible') === 'true';
2113
- modelShell.classList.toggle('show-interaction-guide', savedGuideVisible);
2114
- interactionGuideBtn.classList.toggle('active', savedGuideVisible);
2115
-
2116
- interactionGuideBtn.addEventListener('click', () => {
2117
- const isVisible = modelShell.classList.toggle('show-interaction-guide');
2118
- interactionGuideBtn.classList.toggle('active', isVisible);
2119
- localStorage.setItem('mint-interaction-guide-visible', String(isVisible));
2120
- });
2121
- }
2122
-
2123
- // Spotlight integration
2124
- window.api.onSpotlightToChat((query) => {
2125
- chatInput.value = query;
2126
- chatForm.dispatchEvent(new Event('submit'));
2127
- });