@pheem49/mint 1.7.0 → 1.8.0

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