@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
@@ -0,0 +1,1467 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { getProfileValue, setProfileValue } from '../tauri'
3
+
4
+ const DEFAULT_CONFIG = {
5
+ theme: 'dark',
6
+ accentColor: '#4f83e6',
7
+ systemTextColor: '#f8fafc',
8
+ customBgStart: '#0f172a',
9
+ customBgEnd: '#1e1b4b',
10
+ customPanelBg: '#1e293b',
11
+ glassBlur: 'blur(16px)',
12
+ fontFamily: "'Outfit', sans-serif",
13
+ fontSize: '15px',
14
+ apiKey: '',
15
+ aiProvider: 'gemini',
16
+ geminiModel: 'gemini-2.5-flash',
17
+ openaiModel: 'gpt-4o',
18
+ openrouterModel: 'openai/gpt-4o-mini',
19
+ deepseekModel: 'deepseek-v4-flash',
20
+ anthropicModel: 'claude-3-5-sonnet-latest',
21
+ ollamaModel: 'llama3:latest',
22
+ language: 'th-TH',
23
+ proactiveInterval: 60,
24
+ proactiveCooldown: 120,
25
+ enableVoiceReply: true,
26
+ enableCustomWorkflows: true,
27
+ enableAgentCollaboration: true,
28
+ ttsProvider: 'google',
29
+ ttsVolume: 1.0,
30
+ ttsSpeed: 1.0,
31
+ ttsPitch: 1.0,
32
+ pluginSpotifyEnabled: true,
33
+ pluginCalendarEnabled: false,
34
+ pluginGmailEnabled: false,
35
+ pluginNotionEnabled: false,
36
+ pluginDiscordEnabled: false,
37
+ showDesktopWidget: true,
38
+ mcpServers: {} as Record<string, any>,
39
+ hfModel: 'meta-llama/Meta-Llama-3-8B-Instruct',
40
+ localApiBaseUrl: '',
41
+ localModelName: 'local-model',
42
+ ollamaHost: '',
43
+ anthropicApiKey: '',
44
+ openaiApiKey: '',
45
+ openrouterApiKey: '',
46
+ deepseekApiKey: '',
47
+ hfApiKey: '',
48
+ automationBrowser: 'chromium',
49
+ browserDebugUrl: 'http://127.0.0.1:9222/json/list',
50
+ browserExtensionContextUrl: 'http://127.0.0.1:3212/context',
51
+ enableHeadlessTaskQueue: false,
52
+ enableAutoUpdate: false,
53
+ updaterEndpoint: '',
54
+ updaterPublicKey: '',
55
+ telegramBotToken: '',
56
+ enableTelegramBridge: false,
57
+ discordBotToken: '',
58
+ discordApplicationId: '',
59
+ enableDiscordBridge: false,
60
+ slackBotToken: '',
61
+ slackAppToken: '',
62
+ enableSlackBridge: false,
63
+ lineChannelAccessToken: '',
64
+ lineChannelSecret: '',
65
+ enableLineBridge: false,
66
+ whatsappCloudAccessToken: '',
67
+ whatsappPhoneNumberId: '',
68
+ whatsappVerifyToken: '',
69
+ whatsappAppSecret: '',
70
+ enableWhatsappBridge: false
71
+ }
72
+
73
+ type TabType = 'sect-general' | 'sect-audio' | 'sect-automation' | 'sect-theme' | 'sect-plugins' | 'sect-shortcuts' | 'sect-memory'
74
+
75
+ const GEMINI_MODELS = [
76
+ 'gemini-2.5-flash',
77
+ 'gemini-2.5-pro',
78
+ 'gemini-2.0-flash',
79
+ 'gemini-1.5-flash',
80
+ 'gemini-1.5-pro',
81
+ 'gemini-3.1-flash-lite',
82
+ 'gemini-3.1-flash-lite-preview'
83
+ ]
84
+
85
+ const OPENAI_MODELS = [
86
+ 'gpt-4o',
87
+ 'gpt-4o-mini',
88
+ 'o1',
89
+ 'o3-mini',
90
+ 'o1-preview',
91
+ 'o1-mini',
92
+ 'gpt-4-turbo'
93
+ ]
94
+
95
+ const OPENROUTER_MODELS = [
96
+ 'openai/gpt-4o-mini',
97
+ 'openai/gpt-4o',
98
+ 'anthropic/claude-3.5-sonnet',
99
+ 'anthropic/claude-3.5-haiku',
100
+ 'google/gemini-2.5-flash',
101
+ 'meta-llama/llama-3.3-70b-instruct',
102
+ 'mistralai/mistral-large'
103
+ ]
104
+
105
+ const DEEPSEEK_MODELS = [
106
+ 'deepseek-v4-flash',
107
+ 'deepseek-v4-pro',
108
+ 'deepseek-chat',
109
+ 'deepseek-reasoner'
110
+ ]
111
+
112
+ const ANTHROPIC_MODELS = [
113
+ 'claude-3-7-sonnet-latest',
114
+ 'claude-3-5-sonnet-latest',
115
+ 'claude-3-5-haiku-latest',
116
+ 'claude-3-opus-latest'
117
+ ]
118
+
119
+ const HF_MODELS = [
120
+ 'meta-llama/Llama-3.3-70B-Instruct',
121
+ 'meta-llama/Meta-Llama-3-8B-Instruct',
122
+ 'meta-llama/Llama-3.2-3B-Instruct',
123
+ 'Qwen/Qwen2.5-72B-Instruct',
124
+ 'Qwen/Qwen2.5-Coder-32B-Instruct',
125
+ 'mistralai/Mistral-7B-Instruct-v0.3',
126
+ 'google/gemma-2-9b-it'
127
+ ]
128
+
129
+ const LOCAL_MODELS = [
130
+ 'local-model',
131
+ 'Qwen/Qwen2.5-7B-Instruct-GGUF',
132
+ 'meta-llama/Llama-3.2-3B-Instruct-GGUF',
133
+ 'lmstudio-community/gemma-2-9b-it-GGUF'
134
+ ]
135
+
136
+ const OLLAMA_MODELS = [
137
+ 'llama3:latest',
138
+ 'llama3.1:latest',
139
+ 'llama3.2:latest',
140
+ 'gemma2:latest',
141
+ 'mistral:latest',
142
+ 'phi3:latest',
143
+ 'qwen2.5:latest'
144
+ ]
145
+
146
+ export default function SettingsWindow() {
147
+ const [activeTab, setActiveTab] = useState<TabType>('sect-general')
148
+ const [config, setConfig] = useState(DEFAULT_CONFIG)
149
+
150
+ // Custom user profile / memory state
151
+ const [userName, setUserName] = useState('')
152
+ const [userPreferences, setUserPreferences] = useState('')
153
+
154
+ // Custom model helpers for all providers
155
+ const [customGemini, setCustomGemini] = useState('')
156
+ const [customOpenAI, setCustomOpenAI] = useState('')
157
+ const [customOpenRouter, setCustomOpenRouter] = useState('')
158
+ const [customDeepSeek, setCustomDeepSeek] = useState('')
159
+ const [customAnthropic, setCustomAnthropic] = useState('')
160
+ const [customHF, setCustomHF] = useState('')
161
+ const [customLocal, setCustomLocal] = useState('')
162
+ const [customOllama, setCustomOllama] = useState('')
163
+
164
+ // New MCP Server Form state
165
+ const [mcpName, setMcpName] = useState('')
166
+ const [mcpCmd, setMcpCmd] = useState('')
167
+ const [mcpArgs, setMcpArgs] = useState('')
168
+ const [mcpEnv, setMcpEnv] = useState('')
169
+ const [updateMessage, setUpdateMessage] = useState('')
170
+ const [updateAvailable, setUpdateAvailable] = useState(false)
171
+
172
+ // Load settings on mount
173
+ useEffect(() => {
174
+ async function loadSettings() {
175
+ try {
176
+ const nameVal = await getProfileValue('name')
177
+ const prefVal = await getProfileValue('preferences')
178
+ setUserName(nameVal || '')
179
+ setUserPreferences(prefVal || '')
180
+ } catch (e) {
181
+ console.error("Failed to load user profile memory:", e)
182
+ }
183
+
184
+ if (window.settingsApi) {
185
+ const loaded = await window.settingsApi.getSettings()
186
+ const merged = { ...DEFAULT_CONFIG, ...loaded }
187
+ setConfig(merged)
188
+
189
+ // Set custom text helpers
190
+ if (!GEMINI_MODELS.includes(merged.geminiModel)) {
191
+ setCustomGemini(merged.geminiModel)
192
+ }
193
+ if (!OPENAI_MODELS.includes(merged.openaiModel)) {
194
+ setCustomOpenAI(merged.openaiModel)
195
+ }
196
+ if (!OPENROUTER_MODELS.includes(merged.openrouterModel)) {
197
+ setCustomOpenRouter(merged.openrouterModel)
198
+ }
199
+ if (!DEEPSEEK_MODELS.includes(merged.deepseekModel)) {
200
+ setCustomDeepSeek(merged.deepseekModel)
201
+ }
202
+ if (!ANTHROPIC_MODELS.includes(merged.anthropicModel)) {
203
+ setCustomAnthropic(merged.anthropicModel)
204
+ }
205
+ if (!HF_MODELS.includes(merged.hfModel)) {
206
+ setCustomHF(merged.hfModel)
207
+ }
208
+ if (!LOCAL_MODELS.includes(merged.localModelName)) {
209
+ setCustomLocal(merged.localModelName)
210
+ }
211
+ if (!OLLAMA_MODELS.includes(merged.ollamaModel)) {
212
+ setCustomOllama(merged.ollamaModel)
213
+ }
214
+
215
+ applyThemeStyles(merged)
216
+ }
217
+ }
218
+ loadSettings()
219
+ }, [])
220
+
221
+ const applyThemeStyles = (cfg: typeof DEFAULT_CONFIG) => {
222
+ document.documentElement.setAttribute('data-theme', cfg.theme)
223
+ document.documentElement.style.setProperty('--accent', cfg.accentColor)
224
+ document.documentElement.style.setProperty('--accent-hover', lightenColor(cfg.accentColor, 20))
225
+ document.documentElement.style.setProperty('--text-main', cfg.systemTextColor)
226
+ document.documentElement.style.setProperty('--glass-blur', cfg.glassBlur)
227
+ document.body.style.fontFamily = cfg.fontFamily
228
+ document.documentElement.style.fontSize = cfg.fontSize
229
+
230
+ if (cfg.theme === 'custom') {
231
+ if (cfg.customBgStart && cfg.customBgEnd) {
232
+ const gradient = `linear-gradient(135deg, ${cfg.customBgStart} 0%, ${cfg.customBgEnd} 100%)`
233
+ document.documentElement.style.setProperty('--bg-color', cfg.customBgStart)
234
+ document.documentElement.style.setProperty('--bg-gradient', gradient)
235
+ }
236
+ if (cfg.customPanelBg) {
237
+ const rgb = hexToRgb(cfg.customPanelBg)
238
+ document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`)
239
+ document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`)
240
+ document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`)
241
+ document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`)
242
+ document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`)
243
+ document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`)
244
+ document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`)
245
+ }
246
+ } else {
247
+ [
248
+ '--bg-color',
249
+ '--bg-gradient',
250
+ '--panel-bg',
251
+ '--panel-raised',
252
+ '--panel-soft',
253
+ '--chrome-bg',
254
+ '--surface-bg',
255
+ '--surface-strong',
256
+ '--input-bg'
257
+ ].forEach(name => document.documentElement.style.removeProperty(name))
258
+ }
259
+ }
260
+
261
+ const lightenColor = (hex: string, amount: number) => {
262
+ const clean = hex.replace('#', '')
263
+ if (clean.length !== 6) return hex
264
+ const num = parseInt(clean, 16)
265
+ const r = Math.min(255, (num >> 16) + amount)
266
+ const g = Math.min(255, ((num >> 8) & 0x00FF) + amount)
267
+ const b = Math.min(255, (num & 0x0000FF) + amount)
268
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
269
+ }
270
+
271
+ const hexToRgb = (hex: string) => {
272
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
273
+ return result ? {
274
+ r: parseInt(result[1], 16),
275
+ g: parseInt(result[2], 16),
276
+ b: parseInt(result[3], 16)
277
+ } : { r: 15, g: 23, b: 42 }
278
+ }
279
+
280
+ const handleSave = async () => {
281
+ const finalConfig = { ...config }
282
+
283
+ // Process custom models
284
+ if (config.geminiModel === 'custom') {
285
+ finalConfig.geminiModel = customGemini || 'gemini-2.5-flash'
286
+ }
287
+ if (config.openaiModel === 'custom') {
288
+ finalConfig.openaiModel = customOpenAI || 'gpt-4o'
289
+ }
290
+ if (config.openrouterModel === 'custom') {
291
+ finalConfig.openrouterModel = customOpenRouter || 'openai/gpt-4o-mini'
292
+ }
293
+ if (config.deepseekModel === 'custom') {
294
+ finalConfig.deepseekModel = customDeepSeek || 'deepseek-v4-flash'
295
+ }
296
+ if (config.anthropicModel === 'custom') {
297
+ finalConfig.anthropicModel = customAnthropic || 'claude-3-5-sonnet-latest'
298
+ }
299
+ if (config.hfModel === 'custom') {
300
+ finalConfig.hfModel = customHF || 'meta-llama/Meta-Llama-3-8B-Instruct'
301
+ }
302
+ if (config.localModelName === 'custom') {
303
+ finalConfig.localModelName = customLocal || 'local-model'
304
+ }
305
+ if (config.ollamaModel === 'custom') {
306
+ finalConfig.ollamaModel = customOllama || 'llama3:latest'
307
+ }
308
+
309
+ try {
310
+ await setProfileValue('name', userName)
311
+ await setProfileValue('preferences', userPreferences)
312
+ } catch (e) {
313
+ console.error("Failed to save user profile memory:", e)
314
+ }
315
+
316
+ if (window.settingsApi) {
317
+ await window.settingsApi.saveSettings(finalConfig)
318
+ applyThemeStyles(finalConfig)
319
+ window.settingsApi.closeSettings()
320
+ }
321
+ }
322
+
323
+ const handleReset = async () => {
324
+ if (confirm('Reset all settings to default?')) {
325
+ setConfig(DEFAULT_CONFIG)
326
+ setCustomGemini('')
327
+ setCustomOpenAI('')
328
+ setCustomAnthropic('')
329
+ setCustomHF('')
330
+ setCustomLocal('')
331
+ setCustomOllama('')
332
+ setUserName('')
333
+ setUserPreferences('')
334
+ try {
335
+ await setProfileValue('name', '')
336
+ await setProfileValue('preferences', '')
337
+ } catch (e) {
338
+ console.error("Failed to reset profile memory:", e)
339
+ }
340
+ applyThemeStyles(DEFAULT_CONFIG)
341
+ }
342
+ }
343
+
344
+ const handleClose = () => {
345
+ window.settingsApi?.closeSettings()
346
+ }
347
+
348
+ const handleQuit = () => {
349
+ if (confirm('Are you sure you want to exit Mint?')) {
350
+ window.settingsApi?.quitApp()
351
+ }
352
+ }
353
+
354
+ const handleOpenWorkflows = () => {
355
+ window.settingsApi?.openCustomWorkflows()
356
+ }
357
+
358
+ const handleReloadWorkflows = async () => {
359
+ if (window.settingsApi) {
360
+ const res = await window.settingsApi.reloadCustomWorkflows()
361
+ alert(res?.success ? 'Workflows reloaded successfully!' : 'Workflow reload failed.')
362
+ }
363
+ }
364
+
365
+ const handleAddMcpServer = () => {
366
+ if (!mcpName.trim() || !mcpCmd.trim()) {
367
+ alert('Please provide at least a server name and command.')
368
+ return
369
+ }
370
+
371
+ let parsedEnv = {}
372
+ if (mcpEnv.trim()) {
373
+ try {
374
+ parsedEnv = JSON.parse(mcpEnv)
375
+ } catch (e) {
376
+ alert('Invalid JSON in Environment variable field.')
377
+ return
378
+ }
379
+ }
380
+
381
+ // Split args nicely or pass as string array
382
+ const argList = mcpArgs.split(/\s+/).filter(Boolean)
383
+
384
+ const updatedMcp = {
385
+ ...config.mcpServers,
386
+ [mcpName.trim()]: {
387
+ command: mcpCmd.trim(),
388
+ args: argList,
389
+ env: parsedEnv
390
+ }
391
+ }
392
+
393
+ setConfig({
394
+ ...config,
395
+ mcpServers: updatedMcp
396
+ })
397
+
398
+ // Reset fields
399
+ setMcpName('')
400
+ setMcpCmd('')
401
+ setMcpArgs('')
402
+ setMcpEnv('')
403
+ }
404
+
405
+ const handleRemoveMcpServer = (name: string) => {
406
+ const updated = { ...config.mcpServers }
407
+ delete updated[name]
408
+ setConfig({
409
+ ...config,
410
+ mcpServers: updated
411
+ })
412
+ }
413
+
414
+ const handleConnectPlugin = async (plugin: string) => {
415
+ if (plugin === 'discord') {
416
+ try {
417
+ await window.api.executeProactiveAction({ type: 'plugin', pluginName: 'discord', target: '' })
418
+ alert('Discord Rich Presence updated.')
419
+ } catch (reason) {
420
+ alert(String(reason))
421
+ }
422
+ return
423
+ }
424
+ alert(`Configure ${plugin} credentials, then invoke the plugin from Mint chat.`)
425
+ }
426
+
427
+ const handleCheckUpdates = async () => {
428
+ try {
429
+ const update = await window.settingsApi.checkForUpdates()
430
+ setUpdateAvailable(Boolean(update.available))
431
+ setUpdateMessage(update.available ? `Mint ${update.version} is available.` : 'Mint is up to date.')
432
+ } catch (reason) {
433
+ setUpdateMessage(String(reason))
434
+ }
435
+ }
436
+
437
+ const handleInstallUpdate = async () => {
438
+ if (!confirm('Install the signed Mint update now?')) return
439
+ try {
440
+ setUpdateMessage(await window.settingsApi.installAvailableUpdate())
441
+ setUpdateAvailable(false)
442
+ } catch (reason) {
443
+ setUpdateMessage(String(reason))
444
+ }
445
+ }
446
+
447
+ const updateField = (field: keyof typeof DEFAULT_CONFIG, value: any) => {
448
+ const updated = { ...config, [field]: value }
449
+ setConfig(updated)
450
+ applyThemeStyles(updated)
451
+ }
452
+
453
+ return (
454
+ <div className="settings-container">
455
+ <header className="settings-header drag-region">
456
+ <div className="header-left">
457
+ <span className="settings-icon" style={{ display: 'inline-flex', alignItems: 'center' }}>
458
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
459
+ <circle cx="12" cy="12" r="3"></circle>
460
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
461
+ </svg>
462
+ </span>
463
+ <div>
464
+ <h1>Settings</h1>
465
+ <p>Configure Mint assistant behavior and integrations.</p>
466
+ </div>
467
+ </div>
468
+ <button className="close-btn" onClick={handleClose} aria-label="Close">
469
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
470
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
471
+ <line x1="18" y1="6" x2="6" y2="18"></line>
472
+ <line x1="6" y1="6" x2="18" y2="18"></line>
473
+ </svg>
474
+ </button>
475
+ </header>
476
+
477
+ <main className="settings-body">
478
+ <nav className="settings-sidebar" aria-label="Settings sections">
479
+ <button className={`tab-btn ${activeTab === 'sect-general' ? 'active' : ''}`} onClick={() => setActiveTab('sect-general')}>
480
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
481
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
482
+ <circle cx="12" cy="12" r="3"></circle>
483
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
484
+ </svg>
485
+ </span>
486
+ <strong>General</strong>
487
+ </button>
488
+ <button className={`tab-btn ${activeTab === 'sect-memory' ? 'active' : ''}`} onClick={() => setActiveTab('sect-memory')}>
489
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
490
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
491
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
492
+ <path d="M12 6a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"></path>
493
+ <path d="M6 18a6 6 0 0 1 12 0"></path>
494
+ </svg>
495
+ </span>
496
+ <strong>Memory & Profile</strong>
497
+ </button>
498
+ <button className={`tab-btn ${activeTab === 'sect-audio' ? 'active' : ''}`} onClick={() => setActiveTab('sect-audio')}>
499
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
500
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
501
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
502
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
503
+ </svg>
504
+ </span>
505
+ <strong>Audio & Voice</strong>
506
+ </button>
507
+ <button className={`tab-btn ${activeTab === 'sect-automation' ? 'active' : ''}`} onClick={() => setActiveTab('sect-automation')}>
508
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
509
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
510
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
511
+ </svg>
512
+ </span>
513
+ <strong>Automation</strong>
514
+ </button>
515
+ <button className={`tab-btn ${activeTab === 'sect-theme' ? 'active' : ''}`} onClick={() => setActiveTab('sect-theme')}>
516
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
517
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
518
+ <circle cx="12" cy="12" r="5"></circle>
519
+ <line x1="12" y1="1" x2="12" y2="3"></line>
520
+ <line x1="12" y1="21" x2="12" y2="23"></line>
521
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
522
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
523
+ <line x1="1" y1="12" x2="3" y2="12"></line>
524
+ <line x1="21" y1="12" x2="23" y2="12"></line>
525
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
526
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
527
+ </svg>
528
+ </span>
529
+ <strong>Theme & UI</strong>
530
+ </button>
531
+ <button className={`tab-btn ${activeTab === 'sect-plugins' ? 'active' : ''}`} onClick={() => setActiveTab('sect-plugins')}>
532
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
533
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
534
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
535
+ </svg>
536
+ </span>
537
+ <strong>Plugins</strong>
538
+ </button>
539
+ <button className={`tab-btn ${activeTab === 'sect-shortcuts' ? 'active' : ''}`} onClick={() => setActiveTab('sect-shortcuts')}>
540
+ <span style={{ display: 'inline-flex', alignItems: 'center' }}>
541
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
542
+ <rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
543
+ <line x1="6" y1="8" x2="6" y2="8"></line>
544
+ <line x1="10" y1="8" x2="10" y2="8"></line>
545
+ <line x1="14" y1="8" x2="14" y2="8"></line>
546
+ <line x1="18" y1="8" x2="18" y2="8"></line>
547
+ <line x1="6" y1="12" x2="6" y2="12"></line>
548
+ <line x1="10" y1="12" x2="10" y2="12"></line>
549
+ <line x1="14" y1="12" x2="14" y2="12"></line>
550
+ <line x1="18" y1="12" x2="18" y2="12"></line>
551
+ <line x1="7" y1="16" x2="17" y2="16"></line>
552
+ </svg>
553
+ </span>
554
+ <strong>Shortcuts</strong>
555
+ </button>
556
+ </nav>
557
+
558
+ <div className="settings-content">
559
+ {/* GENERAL SECTION */}
560
+ {activeTab === 'sect-general' && (
561
+ <div className="tab-pane active">
562
+ <section className="setting-section">
563
+ <div className="section-heading">
564
+ <div>
565
+ <p className="section-kicker">AI routing</p>
566
+ <h2 class="section-title">Provider & Model</h2>
567
+ </div>
568
+ <p className="section-description">Choose which AI backend Mint uses, then set the default model.</p>
569
+ </div>
570
+
571
+ <div className="form-grid compact">
572
+ <div className="setting-row wide">
573
+ <label>Active Provider</label>
574
+ <select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
575
+ <option value="gemini">Google Gemini (Cloud)</option>
576
+ <option value="anthropic">Anthropic Claude</option>
577
+ <option value="openai">OpenAI</option>
578
+ <option value="openrouter">OpenRouter</option>
579
+ <option value="deepseek">DeepSeek</option>
580
+ <option value="ollama">Ollama (Local / Private)</option>
581
+ <option value="huggingface">Hugging Face (Inference API)</option>
582
+ <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
583
+ </select>
584
+ </div>
585
+
586
+ <div className="setting-row">
587
+ <label>Gemini Model</label>
588
+ <select
589
+ value={GEMINI_MODELS.includes(config.geminiModel) ? config.geminiModel : 'custom'}
590
+ onChange={(e) => updateField('geminiModel', e.target.value)}
591
+ >
592
+ {GEMINI_MODELS.map(model => (
593
+ <option key={model} value={model}>{model}</option>
594
+ ))}
595
+ <option value="custom">Custom...</option>
596
+ </select>
597
+ </div>
598
+ {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
599
+ <div className="setting-row">
600
+ <label>Custom Gemini Model</label>
601
+ <input
602
+ type="text"
603
+ value={customGemini}
604
+ onChange={(e) => { setCustomGemini(e.target.value); updateField('geminiModel', 'custom') }}
605
+ placeholder="e.g. gemini-3.1-flash-lite-preview"
606
+ />
607
+ </div>
608
+ )}
609
+
610
+ <div className="setting-row">
611
+ <label>OpenAI Model</label>
612
+ <select
613
+ value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
614
+ onChange={(e) => updateField('openaiModel', e.target.value)}
615
+ >
616
+ {OPENAI_MODELS.map(model => (
617
+ <option key={model} value={model}>{model}</option>
618
+ ))}
619
+ <option value="custom">Custom...</option>
620
+ </select>
621
+ </div>
622
+ {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
623
+ <div className="setting-row">
624
+ <label>Custom OpenAI Model</label>
625
+ <input
626
+ type="text"
627
+ value={customOpenAI}
628
+ onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
629
+ placeholder="e.g. gpt-4o"
630
+ />
631
+ </div>
632
+ )}
633
+
634
+ <div className="setting-row">
635
+ <label>OpenRouter Model</label>
636
+ <select
637
+ value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
638
+ onChange={(e) => updateField('openrouterModel', e.target.value)}
639
+ >
640
+ {OPENROUTER_MODELS.map(model => (
641
+ <option key={model} value={model}>{model}</option>
642
+ ))}
643
+ <option value="custom">Custom...</option>
644
+ </select>
645
+ </div>
646
+ {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
647
+ <div className="setting-row">
648
+ <label>Custom OpenRouter Model</label>
649
+ <input
650
+ type="text"
651
+ value={customOpenRouter}
652
+ onChange={(e) => { setCustomOpenRouter(e.target.value); updateField('openrouterModel', 'custom') }}
653
+ placeholder="e.g. anthropic/claude-3.5-sonnet"
654
+ />
655
+ </div>
656
+ )}
657
+
658
+ <div className="setting-row">
659
+ <label>DeepSeek Model</label>
660
+ <select
661
+ value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
662
+ onChange={(e) => updateField('deepseekModel', e.target.value)}
663
+ >
664
+ {DEEPSEEK_MODELS.map(model => (
665
+ <option key={model} value={model}>{model}</option>
666
+ ))}
667
+ <option value="custom">Custom...</option>
668
+ </select>
669
+ </div>
670
+ {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
671
+ <div className="setting-row">
672
+ <label>Custom DeepSeek Model</label>
673
+ <input
674
+ type="text"
675
+ value={customDeepSeek}
676
+ onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
677
+ placeholder="e.g. deepseek-v4-pro"
678
+ />
679
+ </div>
680
+ )}
681
+
682
+ <div className="setting-row">
683
+ <label>Anthropic Model</label>
684
+ <select
685
+ value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
686
+ onChange={(e) => updateField('anthropicModel', e.target.value)}
687
+ >
688
+ {ANTHROPIC_MODELS.map(model => (
689
+ <option key={model} value={model}>{model}</option>
690
+ ))}
691
+ <option value="custom">Custom...</option>
692
+ </select>
693
+ </div>
694
+ {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
695
+ <div className="setting-row">
696
+ <label>Custom Anthropic Model</label>
697
+ <input
698
+ type="text"
699
+ value={customAnthropic}
700
+ onChange={(e) => { setCustomAnthropic(e.target.value); updateField('anthropicModel', 'custom') }}
701
+ placeholder="e.g. claude-3-5-sonnet-latest"
702
+ />
703
+ </div>
704
+ )}
705
+
706
+ <div className="setting-row">
707
+ <label>Hugging Face Model</label>
708
+ <select
709
+ value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
710
+ onChange={(e) => updateField('hfModel', e.target.value)}
711
+ >
712
+ {HF_MODELS.map(model => (
713
+ <option key={model} value={model}>{model}</option>
714
+ ))}
715
+ <option value="custom">Custom...</option>
716
+ </select>
717
+ </div>
718
+ {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
719
+ <div className="setting-row">
720
+ <label>Custom Hugging Face Model</label>
721
+ <input
722
+ type="text"
723
+ value={customHF}
724
+ onChange={(e) => { setCustomHF(e.target.value); updateField('hfModel', 'custom') }}
725
+ placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct"
726
+ />
727
+ </div>
728
+ )}
729
+
730
+ <div className="setting-row">
731
+ <label>LM Studio Model</label>
732
+ <select
733
+ value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
734
+ onChange={(e) => updateField('localModelName', e.target.value)}
735
+ >
736
+ {LOCAL_MODELS.map(model => (
737
+ <option key={model} value={model}>{model}</option>
738
+ ))}
739
+ <option value="custom">Custom...</option>
740
+ </select>
741
+ </div>
742
+ {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
743
+ <div className="setting-row">
744
+ <label>Custom LM Studio Model</label>
745
+ <input
746
+ type="text"
747
+ value={customLocal}
748
+ onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
749
+ placeholder="e.g. local-model"
750
+ />
751
+ </div>
752
+ )}
753
+
754
+ <div className="setting-row">
755
+ <label>Ollama Model</label>
756
+ <select
757
+ value={OLLAMA_MODELS.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
758
+ onChange={(e) => updateField('ollamaModel', e.target.value)}
759
+ >
760
+ {OLLAMA_MODELS.map(model => (
761
+ <option key={model} value={model}>{model}</option>
762
+ ))}
763
+ <option value="custom">Custom...</option>
764
+ </select>
765
+ </div>
766
+ {(!OLLAMA_MODELS.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
767
+ <div className="setting-row">
768
+ <label>Custom Ollama Model</label>
769
+ <input
770
+ type="text"
771
+ value={customOllama}
772
+ onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
773
+ placeholder="e.g. llama3:latest"
774
+ />
775
+ </div>
776
+ )}
777
+ </div>
778
+ </section>
779
+
780
+ <section className="setting-section">
781
+ <div className="section-heading">
782
+ <div>
783
+ <p className="section-kicker">Desktop updates</p>
784
+ <h2 className="section-title">Signed Tauri Channel</h2>
785
+ </div>
786
+ <p className="section-description">Check and explicitly install signed Tauri releases from your configured update channel.</p>
787
+ </div>
788
+ <div className="toggle-row">
789
+ <div>
790
+ <label>Check signed update channel</label>
791
+ <p className="hint">This flag does not install updates automatically.</p>
792
+ </div>
793
+ <label className="toggle-switch">
794
+ <input
795
+ type="checkbox"
796
+ checked={config.enableAutoUpdate}
797
+ onChange={(e) => updateField('enableAutoUpdate', e.target.checked)}
798
+ />
799
+ <span className="toggle-slider"></span>
800
+ </label>
801
+ </div>
802
+ <div className="form-grid single">
803
+ <div className="setting-row">
804
+ <label>Updater Endpoint</label>
805
+ <input type="text" value={config.updaterEndpoint} onChange={(e) => updateField('updaterEndpoint', e.target.value)} placeholder="https://updates.example.com/latest.json" />
806
+ </div>
807
+ <div className="setting-row">
808
+ <label>Updater Public Key</label>
809
+ <textarea value={config.updaterPublicKey} onChange={(e) => updateField('updaterPublicKey', e.target.value)} placeholder="Minisign public key" />
810
+ </div>
811
+ </div>
812
+ <div className="setting-actions">
813
+ <button type="button" className="btn-connect" onClick={handleCheckUpdates}>Check for updates</button>
814
+ {updateAvailable && <button type="button" className="btn-primary" onClick={handleInstallUpdate}>Install signed update</button>}
815
+ </div>
816
+ {updateMessage && <p className="hint">{updateMessage}</p>}
817
+ </section>
818
+
819
+ <section className="setting-section">
820
+ <div className="section-heading">
821
+ <div>
822
+ <p className="section-kicker">Credentials</p>
823
+ <h2 class="section-title">API Keys & Hosts</h2>
824
+ </div>
825
+ </div>
826
+
827
+ <div className="form-grid">
828
+ <div className="setting-row">
829
+ <label>Gemini API Key</label>
830
+ <input
831
+ type="password"
832
+ value={config.apiKey}
833
+ onChange={(e) => updateField('apiKey', e.target.value)}
834
+ placeholder="Enter Gemini API Key..."
835
+ />
836
+ </div>
837
+ <div className="setting-row">
838
+ <label>OpenAI API Key</label>
839
+ <input
840
+ type="password"
841
+ value={config.openaiApiKey}
842
+ onChange={(e) => updateField('openaiApiKey', e.target.value)}
843
+ placeholder="Enter OpenAI API Key..."
844
+ />
845
+ </div>
846
+ <div className="setting-row">
847
+ <label>OpenRouter API Key</label>
848
+ <input
849
+ type="password"
850
+ value={config.openrouterApiKey}
851
+ onChange={(e) => updateField('openrouterApiKey', e.target.value)}
852
+ placeholder="Enter OpenRouter API Key..."
853
+ />
854
+ </div>
855
+ <div className="setting-row">
856
+ <label>DeepSeek API Key</label>
857
+ <input
858
+ type="password"
859
+ value={config.deepseekApiKey}
860
+ onChange={(e) => updateField('deepseekApiKey', e.target.value)}
861
+ placeholder="Enter DeepSeek API Key..."
862
+ />
863
+ </div>
864
+ <div className="setting-row">
865
+ <label>Anthropic API Key</label>
866
+ <input
867
+ type="password"
868
+ value={config.anthropicApiKey}
869
+ onChange={(e) => updateField('anthropicApiKey', e.target.value)}
870
+ placeholder="Enter Anthropic API Key..."
871
+ />
872
+ </div>
873
+ <div className="setting-row">
874
+ <label>Hugging Face API Key</label>
875
+ <input
876
+ type="password"
877
+ value={config.hfApiKey}
878
+ onChange={(e) => updateField('hfApiKey', e.target.value)}
879
+ placeholder="Enter Hugging Face API Key..."
880
+ />
881
+ </div>
882
+ <div className="setting-row">
883
+ <label>LM Studio Base URL</label>
884
+ <input
885
+ type="text"
886
+ value={config.localApiBaseUrl}
887
+ onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
888
+ placeholder="e.g. http://localhost:1234/v1"
889
+ />
890
+ </div>
891
+ <div className="setting-row">
892
+ <label>Ollama Host</label>
893
+ <input
894
+ type="text"
895
+ value={config.ollamaHost}
896
+ onChange={(e) => updateField('ollamaHost', e.target.value)}
897
+ placeholder="e.g. http://localhost:11434"
898
+ />
899
+ </div>
900
+ </div>
901
+ </section>
902
+
903
+ <section className="setting-section">
904
+ <div className="section-heading">
905
+ <div>
906
+ <p className="section-kicker">Desktop</p>
907
+ <h2 class="section-title">Assistant Presence</h2>
908
+ </div>
909
+ </div>
910
+ <div className="toggle-row">
911
+ <div>
912
+ <label>Show Desktop AI Candidate</label>
913
+ <p className="hint">Show the mini AI character on your desktop.</p>
914
+ </div>
915
+ <label className="toggle-switch">
916
+ <input
917
+ type="checkbox"
918
+ checked={config.showDesktopWidget}
919
+ onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
920
+ />
921
+ <span className="toggle-slider"></span>
922
+ </label>
923
+ </div>
924
+ </section>
925
+ </div>
926
+ )}
927
+
928
+ {/* MEMORY SECTION */}
929
+ {activeTab === 'sect-memory' && (
930
+ <div className="tab-pane active animate-fade-in">
931
+ <section className="setting-section">
932
+ <div className="section-heading">
933
+ <div>
934
+ <p className="section-kicker">Cross-session Memory</p>
935
+ <h2 className="section-title">User Profile & Preferences</h2>
936
+ </div>
937
+ <p className="section-description">
938
+ Information and preferences stored here will be remembered across all conversations. Mint automatically learns and updates your profile details from your chats in the background, but you can also edit them manually.
939
+ </p>
940
+ </div>
941
+
942
+ <div className="form-grid single">
943
+ <div className="setting-row">
944
+ <label>Your Name / Nickname</label>
945
+ <input
946
+ type="text"
947
+ value={userName}
948
+ onChange={(e) => setUserName(e.target.value)}
949
+ placeholder="e.g. Pheem, Jane"
950
+ />
951
+ <p className="hint">Used by the assistant to address you in conversation.</p>
952
+ </div>
953
+
954
+ <div className="setting-row">
955
+ <label>Custom Instructions & Preferences</label>
956
+ <textarea
957
+ value={userPreferences}
958
+ onChange={(e) => setUserPreferences(e.target.value)}
959
+ placeholder="e.g. Explain coding concepts step-by-step. Prefer TypeScript. Talk in Thai. Keep explanations concise."
960
+ style={{ minHeight: '150px', resize: 'vertical' }}
961
+ />
962
+ <p className="hint">Preferences, guidelines, or persona instructions for the assistant.</p>
963
+ </div>
964
+ </div>
965
+ </section>
966
+ </div>
967
+ )}
968
+
969
+ {/* AUDIO SECTION */}
970
+ {activeTab === 'sect-audio' && (
971
+ <div className="tab-pane active">
972
+ <section className="setting-section">
973
+ <div className="section-heading">
974
+ <div>
975
+ <p className="section-kicker">Speech</p>
976
+ <h2 class="section-title">Voice Reply</h2>
977
+ </div>
978
+ <p className="section-description">Control spoken responses and TTS behavior.</p>
979
+ </div>
980
+
981
+ <div className="toggle-row">
982
+ <div>
983
+ <label>Enable Voice Reply</label>
984
+ <p className="hint">Mint will speak responses out loud when this is enabled.</p>
985
+ </div>
986
+ <label className="toggle-switch">
987
+ <input
988
+ type="checkbox"
989
+ checked={config.enableVoiceReply}
990
+ onChange={(e) => updateField('enableVoiceReply', e.target.checked)}
991
+ />
992
+ <span className="toggle-slider"></span>
993
+ </label>
994
+ </div>
995
+
996
+ <div className="form-grid single">
997
+ <div className="setting-row">
998
+ <label>Voice Engine</label>
999
+ <select value={config.ttsProvider} onChange={(e) => updateField('ttsProvider', e.target.value)}>
1000
+ <option value="google">Google Cloud (Natural, Auto Lang)</option>
1001
+ <option value="native">OS Native (Supports Pitch)</option>
1002
+ </select>
1003
+ </div>
1004
+ </div>
1005
+
1006
+ <div className="slider-stack">
1007
+ <div className="setting-row">
1008
+ <label>Volume</label>
1009
+ <div className="slider-group">
1010
+ <input
1011
+ type="range"
1012
+ min="0"
1013
+ max="1"
1014
+ step="0.1"
1015
+ value={config.ttsVolume}
1016
+ onChange={(e) => updateField('ttsVolume', parseFloat(e.target.value))}
1017
+ className="range-slider"
1018
+ />
1019
+ <span className="range-value">{Math.round(config.ttsVolume * 100)}%</span>
1020
+ </div>
1021
+ </div>
1022
+ <div className="setting-row">
1023
+ <label>Speed</label>
1024
+ <div className="slider-group">
1025
+ <input
1026
+ type="range"
1027
+ min="0.5"
1028
+ max="2"
1029
+ step="0.1"
1030
+ value={config.ttsSpeed}
1031
+ onChange={(e) => updateField('ttsSpeed', parseFloat(e.target.value))}
1032
+ className="range-slider"
1033
+ />
1034
+ <span className="range-value">{parseFloat(String(config.ttsSpeed)).toFixed(1)}x</span>
1035
+ </div>
1036
+ </div>
1037
+ <div className="setting-row">
1038
+ <label>Pitch</label>
1039
+ <div className="slider-group">
1040
+ <input
1041
+ type="range"
1042
+ min="0"
1043
+ max="2"
1044
+ step="0.1"
1045
+ value={config.ttsPitch}
1046
+ onChange={(e) => updateField('ttsPitch', parseFloat(e.target.value))}
1047
+ className="range-slider"
1048
+ />
1049
+ <span className="range-value">{parseFloat(String(config.ttsPitch)).toFixed(1)}</span>
1050
+ </div>
1051
+ <p className="hint">Pitch applies to OS native voice only.</p>
1052
+ </div>
1053
+ </div>
1054
+ </section>
1055
+ </div>
1056
+ )}
1057
+
1058
+ {/* AUTOMATION SECTION */}
1059
+ {activeTab === 'sect-automation' && (
1060
+ <div className="tab-pane active">
1061
+ <section className="setting-section">
1062
+ <div className="section-heading">
1063
+ <div>
1064
+ <p className="section-kicker">Browser</p>
1065
+ <h2 class="section-title">Automation Engine</h2>
1066
+ </div>
1067
+ </div>
1068
+ <div className="form-grid single">
1069
+ <div className="setting-row">
1070
+ <label>Browser Engine</label>
1071
+ <select value={config.automationBrowser} onChange={(e) => updateField('automationBrowser', e.target.value)}>
1072
+ <option value="chromium">Chromium (Bundled)</option>
1073
+ <option value="/usr/bin/firefox">Firefox (System - Linux)</option>
1074
+ </select>
1075
+ </div>
1076
+ <div className="setting-row">
1077
+ <label>Chromium DevTools Endpoint</label>
1078
+ <input type="text" value={config.browserDebugUrl} onChange={(e) => updateField('browserDebugUrl', e.target.value)} />
1079
+ <p className="hint">Required for native tab reading and selector clicks.</p>
1080
+ </div>
1081
+ <div className="setting-row">
1082
+ <label>Browser Extension Context Endpoint</label>
1083
+ <input type="text" value={config.browserExtensionContextUrl} onChange={(e) => updateField('browserExtensionContextUrl', e.target.value)} />
1084
+ <p className="hint">Fallback endpoint used when Chromium remote debugging is unavailable.</p>
1085
+ </div>
1086
+ </div>
1087
+ </section>
1088
+
1089
+ <section className="setting-section">
1090
+ <div className="section-heading">
1091
+ <div>
1092
+ <p className="section-kicker">Background tasks</p>
1093
+ <h2 className="section-title">Native Headless Queue</h2>
1094
+ </div>
1095
+ </div>
1096
+ <div className="toggle-row">
1097
+ <div>
1098
+ <label>Process queued tasks automatically</label>
1099
+ <p className="hint">Allow the bounded Rust worker to process pending tasks every 15 seconds.</p>
1100
+ </div>
1101
+ <label className="toggle-switch">
1102
+ <input
1103
+ type="checkbox"
1104
+ checked={config.enableHeadlessTaskQueue}
1105
+ onChange={(e) => updateField('enableHeadlessTaskQueue', e.target.checked)}
1106
+ />
1107
+ <span className="toggle-slider"></span>
1108
+ </label>
1109
+ </div>
1110
+ </section>
1111
+
1112
+ <section className="setting-section">
1113
+ <div className="section-heading">
1114
+ <div>
1115
+ <p className="section-kicker">Awareness</p>
1116
+ <h2 class="section-title">Proactive Assistant</h2>
1117
+ </div>
1118
+ <p className="section-description">Tune screen analysis frequency and suggestion timing.</p>
1119
+ </div>
1120
+ <div className="slider-stack">
1121
+ <div className="setting-row">
1122
+ <label>Screen Capture Frequency</label>
1123
+ <div className="slider-group">
1124
+ <input
1125
+ type="range"
1126
+ min="30"
1127
+ max="300"
1128
+ step="30"
1129
+ value={config.proactiveInterval}
1130
+ onChange={(e) => updateField('proactiveInterval', parseInt(e.target.value))}
1131
+ className="range-slider"
1132
+ />
1133
+ <span className="range-value">{config.proactiveInterval} sec</span>
1134
+ </div>
1135
+ <p className="hint">Lower values respond faster but use more API calls.</p>
1136
+ </div>
1137
+ <div className="setting-row">
1138
+ <label>Suggestion Cooldown</label>
1139
+ <div className="slider-group">
1140
+ <input
1141
+ type="range"
1142
+ min="60"
1143
+ max="600"
1144
+ step="60"
1145
+ value={config.proactiveCooldown}
1146
+ onChange={(e) => updateField('proactiveCooldown', parseInt(e.target.value))}
1147
+ className="range-slider"
1148
+ />
1149
+ <span className="range-value">{Math.round(config.proactiveCooldown / 60)} min</span>
1150
+ </div>
1151
+ <p className="hint">Minimum time between repeat suggestions.</p>
1152
+ </div>
1153
+ </div>
1154
+ </section>
1155
+
1156
+ <section className="setting-section">
1157
+ <div className="section-heading">
1158
+ <div>
1159
+ <p className="section-kicker">Messaging</p>
1160
+ <h2 className="section-title">Native Channel Bridges</h2>
1161
+ </div>
1162
+ <p className="section-description">Configure credentials used directly by the Rust channel workers.</p>
1163
+ </div>
1164
+ <div className="form-grid">
1165
+ <label className="toggle-row"><span>Telegram Bot API</span><input type="checkbox" checked={config.enableTelegramBridge} onChange={(e) => updateField('enableTelegramBridge', e.target.checked)} /></label>
1166
+ <input type="password" placeholder="Telegram bot token" value={config.telegramBotToken} onChange={(e) => updateField('telegramBotToken', e.target.value)} />
1167
+ <label className="toggle-row"><span>Discord Gateway</span><input type="checkbox" checked={config.enableDiscordBridge} onChange={(e) => updateField('enableDiscordBridge', e.target.checked)} /></label>
1168
+ <input type="password" placeholder="Discord bot token" value={config.discordBotToken} onChange={(e) => updateField('discordBotToken', e.target.value)} />
1169
+ <input type="text" placeholder="Discord application ID for Rich Presence" value={config.discordApplicationId} onChange={(e) => updateField('discordApplicationId', e.target.value)} />
1170
+ <label className="toggle-row"><span>Slack Socket Mode</span><input type="checkbox" checked={config.enableSlackBridge} onChange={(e) => updateField('enableSlackBridge', e.target.checked)} /></label>
1171
+ <input type="password" placeholder="Slack bot token (xoxb-...)" value={config.slackBotToken} onChange={(e) => updateField('slackBotToken', e.target.value)} />
1172
+ <input type="password" placeholder="Slack app token (xapp-...)" value={config.slackAppToken} onChange={(e) => updateField('slackAppToken', e.target.value)} />
1173
+ <label className="toggle-row"><span>LINE Webhook</span><input type="checkbox" checked={config.enableLineBridge} onChange={(e) => updateField('enableLineBridge', e.target.checked)} /></label>
1174
+ <input type="password" placeholder="LINE channel access token" value={config.lineChannelAccessToken} onChange={(e) => updateField('lineChannelAccessToken', e.target.value)} />
1175
+ <input type="password" placeholder="LINE channel secret" value={config.lineChannelSecret} onChange={(e) => updateField('lineChannelSecret', e.target.value)} />
1176
+ <label className="toggle-row"><span>WhatsApp Cloud API</span><input type="checkbox" checked={config.enableWhatsappBridge} onChange={(e) => updateField('enableWhatsappBridge', e.target.checked)} /></label>
1177
+ <input type="password" placeholder="WhatsApp Cloud access token" value={config.whatsappCloudAccessToken} onChange={(e) => updateField('whatsappCloudAccessToken', e.target.value)} />
1178
+ <input type="text" placeholder="WhatsApp phone number ID" value={config.whatsappPhoneNumberId} onChange={(e) => updateField('whatsappPhoneNumberId', e.target.value)} />
1179
+ <input type="password" placeholder="WhatsApp verify token" value={config.whatsappVerifyToken} onChange={(e) => updateField('whatsappVerifyToken', e.target.value)} />
1180
+ <input type="password" placeholder="WhatsApp app secret (optional HMAC validation)" value={config.whatsappAppSecret} onChange={(e) => updateField('whatsappAppSecret', e.target.value)} />
1181
+ </div>
1182
+ </section>
1183
+
1184
+ <section className="setting-section">
1185
+ <div className="section-heading">
1186
+ <div>
1187
+ <p className="section-kicker">Rules</p>
1188
+ <h2 class="section-title">Custom Workflows</h2>
1189
+ </div>
1190
+ </div>
1191
+ <div className="toggle-row">
1192
+ <div>
1193
+ <label>Enable Custom Workflows</label>
1194
+ <p className="hint">Run "If This Then Mint" rules from the workflow JSON file.</p>
1195
+ </div>
1196
+ <label className="toggle-switch">
1197
+ <input
1198
+ type="checkbox"
1199
+ checked={config.enableCustomWorkflows}
1200
+ onChange={(e) => updateField('enableCustomWorkflows', e.target.checked)}
1201
+ />
1202
+ <span className="toggle-slider"></span>
1203
+ </label>
1204
+ </div>
1205
+ <div className="button-row" style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
1206
+ <button className="btn btn-secondary" onClick={handleOpenWorkflows}>Open workflows.json</button>
1207
+ <button className="btn btn-primary" onClick={handleReloadWorkflows}>Reload Rules</button>
1208
+ </div>
1209
+ </section>
1210
+
1211
+ <section className="setting-section">
1212
+ <div className="section-heading">
1213
+ <div>
1214
+ <p className="section-kicker">Code mode</p>
1215
+ <h2 class="section-title">Agent Collaboration</h2>
1216
+ </div>
1217
+ </div>
1218
+ <div className="toggle-row">
1219
+ <div>
1220
+ <label>Enable Multi-Agent Review</label>
1221
+ <p className="hint">Allow a secondary model to review code written by the primary model.</p>
1222
+ </div>
1223
+ <label className="toggle-switch">
1224
+ <input
1225
+ type="checkbox"
1226
+ checked={config.enableAgentCollaboration}
1227
+ onChange={(e) => updateField('enableAgentCollaboration', e.target.checked)}
1228
+ />
1229
+ <span className="toggle-slider"></span>
1230
+ </label>
1231
+ </div>
1232
+ </section>
1233
+ </div>
1234
+ )}
1235
+
1236
+ {/* THEME & UI SECTION */}
1237
+ {activeTab === 'sect-theme' && (
1238
+ <div className="tab-pane active">
1239
+ <section className="setting-section">
1240
+ <div className="section-heading">
1241
+ <div>
1242
+ <p className="section-kicker">Appearance</p>
1243
+ <h2 class="section-title">Theme</h2>
1244
+ </div>
1245
+ </div>
1246
+ <div className="theme-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
1247
+ {(['dark', 'light', 'midnight', 'custom'] as const).map(t => (
1248
+ <button
1249
+ key={t}
1250
+ className={`theme-card ${config.theme === t ? 'active' : ''}`}
1251
+ onClick={() => updateField('theme', t)}
1252
+ >
1253
+ <div className={`theme-preview ${t}-preview`}></div>
1254
+ <span style={{ textTransform: 'capitalize' }}>{t}</span>
1255
+ </button>
1256
+ ))}
1257
+ </div>
1258
+
1259
+ {config.theme === 'custom' && (
1260
+ <div className="custom-theme-panel" style={{ marginTop: '15px' }}>
1261
+ <div className="setting-row">
1262
+ <label>Background Gradient</label>
1263
+ <div className="color-range" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
1264
+ <input type="color" value={config.customBgStart} onChange={(e) => updateField('customBgStart', e.target.value)} />
1265
+ <span style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
1266
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1267
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1268
+ <polyline points="12 5 19 12 12 19"></polyline>
1269
+ </svg>
1270
+ </span>
1271
+ <input type="color" value={config.customBgEnd} onChange={(e) => updateField('customBgEnd', e.target.value)} />
1272
+ </div>
1273
+ </div>
1274
+ <div className="setting-row">
1275
+ <label>Panel Background</label>
1276
+ <input type="color" value={config.customPanelBg} onChange={(e) => updateField('customPanelBg', e.target.value)} />
1277
+ </div>
1278
+ </div>
1279
+ )}
1280
+ </section>
1281
+
1282
+ <section className="setting-section">
1283
+ <div className="section-heading">
1284
+ <div>
1285
+ <p className="section-kicker">Color</p>
1286
+ <h2 class="section-title">Accent & Text</h2>
1287
+ </div>
1288
+ </div>
1289
+ <div className="color-section">
1290
+ <div>
1291
+ <label>Accent Color</label>
1292
+ <div className="color-presets">
1293
+ {['#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899'].map(c => (
1294
+ <button
1295
+ key={c}
1296
+ className="color-dot"
1297
+ style={{ backgroundColor: c, border: config.accentColor === c ? '2px solid white' : 'none' }}
1298
+ onClick={() => updateField('accentColor', c)}
1299
+ />
1300
+ ))}
1301
+ </div>
1302
+ </div>
1303
+ <div className="color-inputs" style={{ display: 'flex', gap: '15px', marginTop: '12px' }}>
1304
+ <div>
1305
+ <label>Custom Accent</label>
1306
+ <input type="color" value={config.accentColor} onChange={(e) => updateField('accentColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
1307
+ </div>
1308
+ <div>
1309
+ <label>System Text</label>
1310
+ <input type="color" value={config.systemTextColor} onChange={(e) => updateField('systemTextColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
1311
+ </div>
1312
+ </div>
1313
+ </div>
1314
+ </section>
1315
+
1316
+ <section className="setting-section">
1317
+ <div className="section-heading">
1318
+ <div>
1319
+ <p className="section-kicker">Surface</p>
1320
+ <h2 class="section-title">Interface Style</h2>
1321
+ </div>
1322
+ </div>
1323
+ <div className="form-grid">
1324
+ <div className="setting-row">
1325
+ <label>Glass Blur</label>
1326
+ <select value={config.glassBlur} onChange={(e) => updateField('glassBlur', e.target.value)}>
1327
+ <option value="blur(4px)">Low (4px)</option>
1328
+ <option value="blur(16px)">Medium (16px) - Default</option>
1329
+ <option value="blur(32px)">High (32px)</option>
1330
+ <option value="none">Off (Solid)</option>
1331
+ </select>
1332
+ </div>
1333
+ <div className="setting-row">
1334
+ <label>Font Family</label>
1335
+ <select value={config.fontFamily} onChange={(e) => updateField('fontFamily', e.target.value)}>
1336
+ <option value="'Outfit', sans-serif">Outfit (Default)</option>
1337
+ <option value="'Mali', cursive">Mali (Cute Thai Font)</option>
1338
+ <option value="'Prompt', sans-serif">Prompt (Modern Thai)</option>
1339
+ <option value="'Sarabun', sans-serif">Sarabun (Formal Thai)</option>
1340
+ </select>
1341
+ </div>
1342
+ <div className="setting-row">
1343
+ <label>Font Size</label>
1344
+ <select value={config.fontSize} onChange={(e) => updateField('fontSize', e.target.value)}>
1345
+ <option value="14px">Small</option>
1346
+ <option value="15px">Medium (Default)</option>
1347
+ <option value="16px">Large</option>
1348
+ <option value="17px">Extra Large</option>
1349
+ </select>
1350
+ </div>
1351
+ </div>
1352
+ </section>
1353
+ </div>
1354
+ )}
1355
+
1356
+ {/* PLUGINS SECTION */}
1357
+ {activeTab === 'sect-plugins' && (
1358
+ <div className="tab-pane active">
1359
+ <section className="setting-section">
1360
+ <div className="section-heading">
1361
+ <div>
1362
+ <p className="section-kicker">Integrations</p>
1363
+ <h2 class="section-title">Built-in Plugins</h2>
1364
+ </div>
1365
+ </div>
1366
+ <div className="plugin-list">
1367
+ {[
1368
+ { key: 'spotify', name: 'Spotify', desc: 'Control playback with AI. Requires playerctl.', icon: '🎵' },
1369
+ { key: 'calendar', name: 'Google Calendar', desc: 'Read and schedule events.', icon: '📅' },
1370
+ { key: 'gmail', name: 'Gmail', desc: 'Read email and create drafts safely.', icon: '✉' },
1371
+ { key: 'notion', name: 'Notion', desc: 'Create notes, pages, and read databases.', icon: '📝' },
1372
+ { key: 'discord', name: 'Discord RPC', desc: 'Show "Using Mint Assistant" on Discord status.', icon: '💬' },
1373
+ ].map(p => (
1374
+ <div className="plugin-card" key={p.key}>
1375
+ <div className="plugin-icon">{p.icon}</div>
1376
+ <div className="plugin-info">
1377
+ <div className="plugin-name">{p.name}</div>
1378
+ <div className="plugin-desc">{p.desc}</div>
1379
+ </div>
1380
+ <div className="plugin-actions">
1381
+ <button className="btn-connect" onClick={() => handleConnectPlugin(p.key)}>Connect</button>
1382
+ </div>
1383
+ </div>
1384
+ ))}
1385
+ </div>
1386
+ </section>
1387
+
1388
+ <section className="setting-section">
1389
+ <div className="section-heading">
1390
+ <div>
1391
+ <p className="section-kicker">External tools</p>
1392
+ <h2 class="section-title">MCP Servers</h2>
1393
+ </div>
1394
+ <p className="section-description">Connect Mint to tools like search, GitHub, or filesystem servers.</p>
1395
+ </div>
1396
+
1397
+ <div className="mcp-list">
1398
+ {Object.entries(config.mcpServers || {}).map(([name, srv]: [string, any]) => (
1399
+ <div className="plugin-card" key={name} style={{ marginBottom: '10px' }}>
1400
+ <div className="plugin-icon">⚙</div>
1401
+ <div className="plugin-info">
1402
+ <div className="plugin-name">{name}</div>
1403
+ <div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
1404
+ Command: {srv.command} {srv.args?.join(' ')}
1405
+ </div>
1406
+ </div>
1407
+ <div className="plugin-actions">
1408
+ <button className="btn btn-danger" onClick={() => handleRemoveMcpServer(name)}>Remove</button>
1409
+ </div>
1410
+ </div>
1411
+ ))}
1412
+ </div>
1413
+
1414
+ <div className="add-mcp-box">
1415
+ <h3>Add MCP Server</h3>
1416
+ <div className="form-grid">
1417
+ <input type="text" placeholder="Server Name (e.g. google-search)" value={mcpName} onChange={(e) => setMcpName(e.target.value)} />
1418
+ <input type="text" placeholder="Command (e.g. npx)" value={mcpCmd} onChange={(e) => setMcpCmd(e.target.value)} />
1419
+ </div>
1420
+ <input type="text" placeholder="Arguments (e.g. -y @modelcontextprotocol/server-brave-search)" value={mcpArgs} onChange={(e) => setMcpArgs(e.target.value)} style={{ width: '100%', marginTop: '8px' }} />
1421
+ <textarea placeholder='Env JSON, e.g. {"BRAVE_API_KEY": "..."}' value={mcpEnv} onChange={(e) => setMcpEnv(e.target.value)} style={{ width: '100%', marginTop: '8px', height: '60px' }}></textarea>
1422
+ <button className="btn-primary full-width" onClick={handleAddMcpServer} style={{ marginTop: '10px', width: '100%' }}>Add MCP Server</button>
1423
+ </div>
1424
+ </section>
1425
+ </div>
1426
+ )}
1427
+
1428
+ {/* KEYBOARD SHORTCUTS SECTION */}
1429
+ {activeTab === 'sect-shortcuts' && (
1430
+ <div className="tab-pane active">
1431
+ <section className="setting-section">
1432
+ <div className="section-heading">
1433
+ <div>
1434
+ <p className="section-kicker">Keyboard</p>
1435
+ <h2 class="section-title">Shortcuts</h2>
1436
+ </div>
1437
+ </div>
1438
+ <div className="shortcut-list">
1439
+ <div className="shortcut-item">
1440
+ <span>Show / Hide Mint Window</span>
1441
+ <div className="keys"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>Space</kbd></div>
1442
+ </div>
1443
+ <div className="shortcut-item">
1444
+ <span>Open Spotlight</span>
1445
+ <div className="keys"><kbd>Alt</kbd><kbd>Space</kbd></div>
1446
+ </div>
1447
+ <div className="shortcut-item">
1448
+ <span>Close / Dismiss</span>
1449
+ <div className="keys"><kbd>Esc</kbd></div>
1450
+ </div>
1451
+ </div>
1452
+ </section>
1453
+ </div>
1454
+ )}
1455
+ </div>
1456
+ </main>
1457
+
1458
+ <footer className="settings-footer">
1459
+ <button className="btn-danger" onClick={handleQuit}>Quit Application</button>
1460
+ <div className="footer-actions">
1461
+ <button className="btn-secondary" onClick={handleReset}>Reset to Default</button>
1462
+ <button className="btn-primary" onClick={handleSave}>Save Settings</button>
1463
+ </div>
1464
+ </footer>
1465
+ </div>
1466
+ )
1467
+ }