@pheem49/mint 1.7.1 → 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 (51) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/crates/mint-core/src/api_server.rs +62 -4
  5. package/crates/mint-core/src/chat.rs +79 -0
  6. package/crates/mint-core/src/lib.rs +1 -1
  7. package/crates/mint-core/src/workflows.rs +8 -0
  8. package/package.json +2 -1
  9. package/src/renderer/src/App.tsx +1 -1
  10. package/src/renderer/src/components/ChatPanel.tsx +27 -14
  11. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  12. package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
  13. package/src/renderer/src/components/MintDashboard.tsx +199 -5
  14. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  15. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  16. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  17. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  18. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  19. package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
  20. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  21. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  22. package/src/renderer/src/components/SettingsWindow.tsx +175 -962
  23. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  24. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  25. package/src/renderer/src/css/image-studio.css +9 -4
  26. package/src/renderer/src/css/settings.css +75 -6
  27. package/src/renderer/src/css/styles.css +355 -38
  28. package/src/renderer/src/css/workflows.css +542 -0
  29. package/src/renderer/src/env.d.ts +2 -0
  30. package/src/renderer/src/index.css +1 -0
  31. package/src/renderer/src/tauri.ts +97 -0
  32. package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
  33. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  34. package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
  35. package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
  36. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  37. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  38. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  39. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  40. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  41. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
  42. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  43. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  44. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
  45. package/src/renderer/src-web/css/image-studio.css +9 -4
  46. package/src/renderer/src-web/css/settings.css +75 -6
  47. package/src/renderer/src-web/css/styles.css +294 -38
  48. package/src/renderer/src-web/env.d.ts +2 -0
  49. package/src/renderer/src-web/tauri.ts +36 -0
  50. package/src-tauri/src/lib.rs +83 -90
  51. package/src-tauri/tauri.conf.json +1 -1
@@ -0,0 +1,948 @@
1
+ import React from 'react'
2
+ import {
3
+ DEFAULT_CONFIG,
4
+ GEMINI_MODELS,
5
+ OPENAI_MODELS,
6
+ OPENROUTER_MODELS,
7
+ DEEPSEEK_MODELS,
8
+ ANTHROPIC_MODELS,
9
+ HF_MODELS,
10
+ LOCAL_MODELS
11
+ } from '../SettingsWindow'
12
+
13
+ interface GeneralTabProps {
14
+ config: typeof DEFAULT_CONFIG
15
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
16
+ customGemini: string
17
+ setCustomGemini: (val: string) => void
18
+ customOpenAI: string
19
+ setCustomOpenAI: (val: string) => void
20
+ customOpenRouter: string
21
+ setCustomOpenRouter: (val: string) => void
22
+ customDeepSeek: string
23
+ setCustomDeepSeek: (val: string) => void
24
+ customAnthropic: string
25
+ setCustomAnthropic: (val: string) => void
26
+ customHF: string
27
+ setCustomHF: (val: string) => void
28
+ customLocal: string
29
+ setCustomLocal: (val: string) => void
30
+ customOllama: string
31
+ setCustomOllama: (val: string) => void
32
+ dynamicOllamaModels: string[]
33
+ updateAvailable: boolean
34
+ updateMessage: string
35
+ handleCheckUpdates: () => void
36
+ handleInstallUpdate: () => void
37
+ isDesktopApp?: boolean
38
+ }
39
+
40
+ export default function GeneralTab({
41
+ config,
42
+ updateField,
43
+ customGemini,
44
+ setCustomGemini,
45
+ customOpenAI,
46
+ setCustomOpenAI,
47
+ customOpenRouter,
48
+ setCustomOpenRouter,
49
+ customDeepSeek,
50
+ setCustomDeepSeek,
51
+ customAnthropic,
52
+ setCustomAnthropic,
53
+ customHF,
54
+ setCustomHF,
55
+ customLocal,
56
+ setCustomLocal,
57
+ customOllama,
58
+ setCustomOllama,
59
+ dynamicOllamaModels,
60
+ updateAvailable,
61
+ updateMessage,
62
+ handleCheckUpdates,
63
+ handleInstallUpdate,
64
+ isDesktopApp = false
65
+ }: GeneralTabProps) {
66
+ return (
67
+ <div className="tab-pane active">
68
+ <section className="setting-section">
69
+ <div className="section-heading">
70
+ <div>
71
+ <p className="section-kicker">AI routing</p>
72
+ <h2 className="section-title">Provider & Model</h2>
73
+ </div>
74
+ <p className="section-description">Choose which AI backend Mint uses, then set the default model.</p>
75
+ </div>
76
+
77
+ <div className="form-grid compact">
78
+ <div className="setting-row wide">
79
+ <label>Active Provider</label>
80
+ <select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
81
+ <option value="gemini">Google Gemini (Cloud)</option>
82
+ <option value="anthropic">Anthropic Claude</option>
83
+ <option value="openai">OpenAI</option>
84
+ <option value="openrouter">OpenRouter</option>
85
+ <option value="deepseek">DeepSeek</option>
86
+ <option value="ollama">Ollama (Local / Private)</option>
87
+ <option value="huggingface">Hugging Face (Inference API)</option>
88
+ <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
89
+ </select>
90
+ </div>
91
+ </div>
92
+
93
+ <div className="provider-cards-container">
94
+ {/* Google Gemini Card */}
95
+ <div className={`provider-card ${config.aiProvider === 'gemini' ? 'active-provider' : ''}`}>
96
+ <div className="provider-card-header">
97
+ <div className="provider-card-title">
98
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
99
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
100
+ </svg>
101
+ Google Gemini (Cloud)
102
+ </div>
103
+ {config.aiProvider === 'gemini' && <span className="provider-active-badge">Active</span>}
104
+ </div>
105
+ <div className="provider-card-body">
106
+ <div className="setting-row">
107
+ <label>Gemini Model</label>
108
+ <select
109
+ value={GEMINI_MODELS.includes(config.geminiModel) ? config.geminiModel : 'custom'}
110
+ onChange={(e) => updateField('geminiModel', e.target.value)}
111
+ >
112
+ {GEMINI_MODELS.map(model => (
113
+ <option key={model} value={model}>{model}</option>
114
+ ))}
115
+ <option value="custom">Custom...</option>
116
+ </select>
117
+ </div>
118
+ {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
119
+ <div className="setting-row">
120
+ <label>Custom Gemini Model</label>
121
+ <input
122
+ type="text"
123
+ value={customGemini}
124
+ onChange={(e) => { setCustomGemini(e.target.value); updateField('geminiModel', 'custom') }}
125
+ placeholder="e.g. gemini-3.1-flash-lite-preview"
126
+ />
127
+ </div>
128
+ )}
129
+ <div className="setting-row">
130
+ <label>Gemini API Key</label>
131
+ <input
132
+ type="password"
133
+ value={config.apiKey}
134
+ onChange={(e) => updateField('apiKey', e.target.value)}
135
+ placeholder="Enter Gemini API Key..."
136
+ />
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Anthropic Claude Card */}
142
+ <div className={`provider-card ${config.aiProvider === 'anthropic' ? 'active-provider' : ''}`}>
143
+ <div className="provider-card-header">
144
+ <div className="provider-card-title">
145
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
146
+ <path d="M4.5 16.5c-1.5 1.26-2.5 3.19-2.5 5.5h20c0-2.31-1-4.24-2.5-5.5"></path>
147
+ <path d="M12 2L2 22h20L12 2z"></path>
148
+ </svg>
149
+ Anthropic Claude
150
+ </div>
151
+ {config.aiProvider === 'anthropic' && <span className="provider-active-badge">Active</span>}
152
+ </div>
153
+ <div className="provider-card-body">
154
+ <div className="setting-row">
155
+ <label>Anthropic Model</label>
156
+ <select
157
+ value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
158
+ onChange={(e) => updateField('anthropicModel', e.target.value)}
159
+ >
160
+ {ANTHROPIC_MODELS.map(model => (
161
+ <option key={model} value={model}>{model}</option>
162
+ ))}
163
+ <option value="custom">Custom...</option>
164
+ </select>
165
+ </div>
166
+ {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
167
+ <div className="setting-row">
168
+ <label>Custom Anthropic Model</label>
169
+ <input
170
+ type="text"
171
+ value={customAnthropic}
172
+ onChange={(e) => { setCustomAnthropic(e.target.value); updateField('anthropicModel', 'custom') }}
173
+ placeholder="e.g. claude-3-5-sonnet-latest"
174
+ />
175
+ </div>
176
+ )}
177
+ <div className="setting-row">
178
+ <label>Anthropic API Key</label>
179
+ <input
180
+ type="password"
181
+ value={config.anthropicApiKey}
182
+ onChange={(e) => updateField('anthropicApiKey', e.target.value)}
183
+ placeholder="Enter Anthropic API Key..."
184
+ />
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ {/* OpenAI Card */}
190
+ <div className={`provider-card ${config.aiProvider === 'openai' ? 'active-provider' : ''}`}>
191
+ <div className="provider-card-header">
192
+ <div className="provider-card-title">
193
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
194
+ <circle cx="12" cy="12" r="10"></circle>
195
+ <line x1="12" y1="2" x2="12" y2="22"></line>
196
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
197
+ </svg>
198
+ OpenAI
199
+ </div>
200
+ {config.aiProvider === 'openai' && <span className="provider-active-badge">Active</span>}
201
+ </div>
202
+ <div className="provider-card-body">
203
+ <div className="setting-row">
204
+ <label>OpenAI Model</label>
205
+ <select
206
+ value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
207
+ onChange={(e) => updateField('openaiModel', e.target.value)}
208
+ >
209
+ {OPENAI_MODELS.map(model => (
210
+ <option key={model} value={model}>{model}</option>
211
+ ))}
212
+ <option value="custom">Custom...</option>
213
+ </select>
214
+ </div>
215
+ {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
216
+ <div className="setting-row">
217
+ <label>Custom OpenAI Model</label>
218
+ <input
219
+ type="text"
220
+ value={customOpenAI}
221
+ onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
222
+ placeholder="e.g. gpt-4o"
223
+ />
224
+ </div>
225
+ )}
226
+ <div className="setting-row">
227
+ <label>OpenAI API Key</label>
228
+ <input
229
+ type="password"
230
+ value={config.openaiApiKey}
231
+ onChange={(e) => updateField('openaiApiKey', e.target.value)}
232
+ placeholder="Enter OpenAI API Key..."
233
+ />
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ {/* OpenRouter Card */}
239
+ <div className={`provider-card ${config.aiProvider === 'openrouter' ? 'active-provider' : ''}`}>
240
+ <div className="provider-card-header">
241
+ <div className="provider-card-title">
242
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
243
+ <polyline points="16 3 21 3 21 8"></polyline>
244
+ <line x1="4" y1="20" x2="21" y2="3"></line>
245
+ <polyline points="21 16 21 21 16 21"></polyline>
246
+ <line x1="15" y1="15" x2="21" y2="21"></line>
247
+ <line x1="4" y1="4" x2="9" y2="9"></line>
248
+ </svg>
249
+ OpenRouter
250
+ </div>
251
+ {config.aiProvider === 'openrouter' && <span className="provider-active-badge">Active</span>}
252
+ </div>
253
+ <div className="provider-card-body">
254
+ <div className="setting-row">
255
+ <label>OpenRouter Model</label>
256
+ <select
257
+ value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
258
+ onChange={(e) => updateField('openrouterModel', e.target.value)}
259
+ >
260
+ {OPENROUTER_MODELS.map(model => (
261
+ <option key={model} value={model}>{model}</option>
262
+ ))}
263
+ <option value="custom">Custom...</option>
264
+ </select>
265
+ </div>
266
+ {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
267
+ <div className="setting-row">
268
+ <label>Custom OpenRouter Model</label>
269
+ <input
270
+ type="text"
271
+ value={customOpenRouter}
272
+ onChange={(e) => { setCustomOpenRouter(e.target.value); updateField('openrouterModel', 'custom') }}
273
+ placeholder="e.g. anthropic/claude-3.5-sonnet"
274
+ />
275
+ </div>
276
+ )}
277
+ <div className="setting-row">
278
+ <label>OpenRouter API Key</label>
279
+ <input
280
+ type="password"
281
+ value={config.openrouterApiKey}
282
+ onChange={(e) => updateField('openrouterApiKey', e.target.value)}
283
+ placeholder="Enter OpenRouter API Key..."
284
+ />
285
+ </div>
286
+ </div>
287
+ </div>
288
+
289
+ {/* DeepSeek Card */}
290
+ <div className={`provider-card ${config.aiProvider === 'deepseek' ? 'active-provider' : ''}`}>
291
+ <div className="provider-card-header">
292
+ <div className="provider-card-title">
293
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
294
+ <circle cx="11" cy="11" r="8"></circle>
295
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
296
+ </svg>
297
+ DeepSeek
298
+ </div>
299
+ {config.aiProvider === 'deepseek' && <span className="provider-active-badge">Active</span>}
300
+ </div>
301
+ <div className="provider-card-body">
302
+ <div className="setting-row">
303
+ <label>DeepSeek Model</label>
304
+ <select
305
+ value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
306
+ onChange={(e) => updateField('deepseekModel', e.target.value)}
307
+ >
308
+ {DEEPSEEK_MODELS.map(model => (
309
+ <option key={model} value={model}>{model}</option>
310
+ ))}
311
+ <option value="custom">Custom...</option>
312
+ </select>
313
+ </div>
314
+ {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
315
+ <div className="setting-row">
316
+ <label>Custom DeepSeek Model</label>
317
+ <input
318
+ type="text"
319
+ value={customDeepSeek}
320
+ onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
321
+ placeholder="e.g. deepseek-v4-pro"
322
+ />
323
+ </div>
324
+ )}
325
+ <div className="setting-row">
326
+ <label>DeepSeek API Key</label>
327
+ <input
328
+ type="password"
329
+ value={config.deepseekApiKey}
330
+ onChange={(e) => updateField('deepseekApiKey', e.target.value)}
331
+ placeholder="Enter DeepSeek API Key..."
332
+ />
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ {/* Hugging Face Card */}
338
+ <div className={`provider-card ${config.aiProvider === 'huggingface' ? 'active-provider' : ''}`}>
339
+ <div className="provider-card-header">
340
+ <div className="provider-card-title">
341
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
342
+ <circle cx="12" cy="12" r="10"></circle>
343
+ <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
344
+ <line x1="9" y1="9" x2="9.01" y2="9"></line>
345
+ <line x1="15" y1="9" x2="15.01" y2="9"></line>
346
+ </svg>
347
+ Hugging Face (Inference API)
348
+ </div>
349
+ {config.aiProvider === 'huggingface' && <span className="provider-active-badge">Active</span>}
350
+ </div>
351
+ <div className="provider-card-body">
352
+ <div className="setting-row">
353
+ <label>Hugging Face Model</label>
354
+ <select
355
+ value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
356
+ onChange={(e) => updateField('hfModel', e.target.value)}
357
+ >
358
+ {HF_MODELS.map(model => (
359
+ <option key={model} value={model}>{model}</option>
360
+ ))}
361
+ <option value="custom">Custom...</option>
362
+ </select>
363
+ </div>
364
+ {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
365
+ <div className="setting-row">
366
+ <label>Custom Hugging Face Model</label>
367
+ <input
368
+ type="text"
369
+ value={customHF}
370
+ onChange={(e) => { setCustomHF(e.target.value); updateField('hfModel', 'custom') }}
371
+ placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct"
372
+ />
373
+ </div>
374
+ )}
375
+ <div className="setting-row">
376
+ <label>Hugging Face API Key</label>
377
+ <input
378
+ type="password"
379
+ value={config.hfApiKey}
380
+ onChange={(e) => updateField('hfApiKey', e.target.value)}
381
+ placeholder="Enter Hugging Face API Key..."
382
+ />
383
+ </div>
384
+ </div>
385
+ </div>
386
+
387
+ {/* LM Studio Card */}
388
+ <div className={`provider-card ${config.aiProvider === 'local_openai' ? 'active-provider' : ''}`}>
389
+ <div className="provider-card-header">
390
+ <div className="provider-card-title">
391
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
392
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
393
+ <line x1="8" y1="21" x2="16" y2="21"></line>
394
+ <line x1="12" y1="17" x2="12" y2="21"></line>
395
+ </svg>
396
+ LM Studio / Local OpenAI
397
+ </div>
398
+ {config.aiProvider === 'local_openai' && <span className="provider-active-badge">Active</span>}
399
+ </div>
400
+ <div className="provider-card-body">
401
+ <div className="setting-row">
402
+ <label>LM Studio Model</label>
403
+ <select
404
+ value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
405
+ onChange={(e) => updateField('localModelName', e.target.value)}
406
+ >
407
+ {LOCAL_MODELS.map(model => (
408
+ <option key={model} value={model}>{model}</option>
409
+ ))}
410
+ <option value="custom">Custom...</option>
411
+ </select>
412
+ </div>
413
+ {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
414
+ <div className="setting-row">
415
+ <label>Custom LM Studio Model</label>
416
+ <input
417
+ type="text"
418
+ value={customLocal}
419
+ onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
420
+ placeholder="e.g. local-model"
421
+ />
422
+ </div>
423
+ )}
424
+ <div className="setting-row">
425
+ <label>LM Studio Base URL</label>
426
+ <input
427
+ type="text"
428
+ value={config.localApiBaseUrl}
429
+ onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
430
+ placeholder="e.g. http://localhost:1234/v1"
431
+ />
432
+ </div>
433
+ </div>
434
+ </div>
435
+
436
+ {/* Ollama Card */}
437
+ <div className={`provider-card ${config.aiProvider === 'ollama' ? 'active-provider' : ''}`}>
438
+ <div className="provider-card-header">
439
+ <div className="provider-card-title">
440
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
441
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
442
+ </svg>
443
+ Ollama (Local)
444
+ </div>
445
+ {config.aiProvider === 'ollama' && <span className="provider-active-badge">Active</span>}
446
+ </div>
447
+ <div className="provider-card-body">
448
+ <div className="setting-row">
449
+ <label>Ollama Model</label>
450
+ <select
451
+ value={dynamicOllamaModels.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
452
+ onChange={(e) => updateField('ollamaModel', e.target.value)}
453
+ >
454
+ {dynamicOllamaModels.map(model => (
455
+ <option key={model} value={model}>{model}</option>
456
+ ))}
457
+ {!dynamicOllamaModels.includes(config.ollamaModel) && config.ollamaModel && config.ollamaModel !== 'custom' && (
458
+ <option value={config.ollamaModel}>{config.ollamaModel}</option>
459
+ )}
460
+ <option value="custom">Custom...</option>
461
+ </select>
462
+ </div>
463
+ {(!dynamicOllamaModels.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
464
+ <div className="setting-row">
465
+ <label>Custom Ollama Model</label>
466
+ <input
467
+ type="text"
468
+ value={customOllama}
469
+ onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
470
+ placeholder="e.g. llama3:latest"
471
+ />
472
+ </div>
473
+ )}
474
+ <div className="setting-row">
475
+ <label>Ollama Host</label>
476
+ <input
477
+ type="text"
478
+ value={config.ollamaHost}
479
+ onChange={(e) => updateField('ollamaHost', e.target.value)}
480
+ placeholder="e.g. http://localhost:11434"
481
+ />
482
+ </div>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </section>
487
+
488
+ {/* ── Search ── */}
489
+ <section className="setting-section">
490
+ <div className="section-heading">
491
+ <div>
492
+ <p className="section-kicker">Web search</p>
493
+ <h2 className="section-title">Search</h2>
494
+ </div>
495
+ <p className="section-description">Choose which search API Mint uses when performing web lookups.</p>
496
+ </div>
497
+ <div className="provider-cards-container">
498
+ {/* Brave Search */}
499
+ <div className={`provider-card ${config.searchProvider === 'brave' ? 'active-provider' : ''}`}>
500
+ <div className="provider-card-header">
501
+ <div className="provider-card-title">
502
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
503
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
504
+ </svg>
505
+ Brave Search API
506
+ </div>
507
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
508
+ {config.searchProvider === 'brave' && <span className="provider-active-badge">Active</span>}
509
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
510
+ <input type="radio" name="searchProvider" style={{ display: 'none' }} checked={config.searchProvider === 'brave'} onChange={() => updateField('searchProvider', 'brave')} />
511
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('searchProvider', 'brave')} />
512
+ </label>
513
+ </div>
514
+ </div>
515
+ <div className="provider-card-body">
516
+ <div className="setting-row">
517
+ <label>Brave Search API Key</label>
518
+ <input
519
+ type="password"
520
+ value={config.braveSearchApiKey}
521
+ onChange={(e) => updateField('braveSearchApiKey', e.target.value)}
522
+ placeholder="Enter Brave Search API Key..."
523
+ />
524
+ </div>
525
+ </div>
526
+ </div>
527
+
528
+ {/* Google Search */}
529
+ <div className={`provider-card ${config.searchProvider === 'google' ? 'active-provider' : ''}`}>
530
+ <div className="provider-card-header">
531
+ <div className="provider-card-title">
532
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
533
+ <circle cx="11" cy="11" r="8"/>
534
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
535
+ </svg>
536
+ Google Search API
537
+ </div>
538
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
539
+ {config.searchProvider === 'google' && <span className="provider-active-badge">Active</span>}
540
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
541
+ <input type="radio" name="searchProvider" style={{ display: 'none' }} checked={config.searchProvider === 'google'} onChange={() => updateField('searchProvider', 'google')} />
542
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('searchProvider', 'google')} />
543
+ </label>
544
+ </div>
545
+ </div>
546
+ <div className="provider-card-body">
547
+ <div className="setting-row">
548
+ <label>Google Search API Key</label>
549
+ <input
550
+ type="password"
551
+ value={config.googleSearchApiKey}
552
+ onChange={(e) => updateField('googleSearchApiKey', e.target.value)}
553
+ placeholder="Enter Google Search API Key..."
554
+ />
555
+ </div>
556
+ <div className="setting-row">
557
+ <label>Google Search Engine ID (CX)</label>
558
+ <input
559
+ type="text"
560
+ value={config.googleSearchCx}
561
+ onChange={(e) => updateField('googleSearchCx', e.target.value)}
562
+ placeholder="Enter Google Custom Search Engine ID (CX)..."
563
+ />
564
+ </div>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ </section>
569
+
570
+ {/* ── Productivity ── */}
571
+ <section className="setting-section">
572
+ <div className="section-heading">
573
+ <div>
574
+ <p className="section-kicker">Productivity integrations</p>
575
+ <h2 className="section-title">Productivity</h2>
576
+ </div>
577
+ <p className="section-description">Enable and configure Gmail, Google Calendar, and Notion plugins directly.</p>
578
+ </div>
579
+ <div className="provider-cards-container">
580
+ {/* Gmail */}
581
+ <div className={`provider-card ${config.pluginGmailEnabled ? 'active-provider' : ''}`}>
582
+ <div className="provider-card-header">
583
+ <div className="provider-card-title">
584
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
585
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
586
+ <polyline points="22,6 12,13 2,6"/>
587
+ </svg>
588
+ Gmail Plugin
589
+ </div>
590
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
591
+ {config.pluginGmailEnabled && <span className="provider-active-badge">Enabled</span>}
592
+ <label className="settings-toggle-switch">
593
+ <input type="checkbox" checked={config.pluginGmailEnabled} onChange={(e) => updateField('pluginGmailEnabled', e.target.checked)} />
594
+ <span className="settings-toggle-slider" />
595
+ </label>
596
+ </div>
597
+ </div>
598
+ <div className="provider-card-body">
599
+ <div className="setting-row">
600
+ <label>Gmail Client ID</label>
601
+ <input
602
+ type="text"
603
+ placeholder="Enter Gmail Client ID..."
604
+ value={config.gmailClientId || ''}
605
+ onChange={(e) => updateField('gmailClientId', e.target.value)}
606
+ />
607
+ </div>
608
+ <div className="setting-row">
609
+ <label>Gmail Client Secret</label>
610
+ <input
611
+ type="password"
612
+ placeholder="Enter Gmail Client Secret..."
613
+ value={config.gmailClientSecret || ''}
614
+ onChange={(e) => updateField('gmailClientSecret', e.target.value)}
615
+ />
616
+ </div>
617
+ <div className="setting-row">
618
+ <label>Gmail Refresh Token</label>
619
+ <input
620
+ type="password"
621
+ placeholder="Enter Gmail Refresh Token..."
622
+ value={config.gmailRefreshToken || ''}
623
+ onChange={(e) => updateField('gmailRefreshToken', e.target.value)}
624
+ />
625
+ </div>
626
+ </div>
627
+ </div>
628
+
629
+ {/* Google Calendar */}
630
+ <div className={`provider-card ${config.pluginCalendarEnabled ? 'active-provider' : ''}`}>
631
+ <div className="provider-card-header">
632
+ <div className="provider-card-title">
633
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
634
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
635
+ <line x1="16" y1="2" x2="16" y2="6"/>
636
+ <line x1="8" y1="2" x2="8" y2="6"/>
637
+ <line x1="3" y1="10" x2="21" y2="10"/>
638
+ </svg>
639
+ Google Calendar Plugin
640
+ </div>
641
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
642
+ {config.pluginCalendarEnabled && <span className="provider-active-badge">Enabled</span>}
643
+ <label className="settings-toggle-switch">
644
+ <input type="checkbox" checked={config.pluginCalendarEnabled} onChange={(e) => updateField('pluginCalendarEnabled', e.target.checked)} />
645
+ <span className="settings-toggle-slider" />
646
+ </label>
647
+ </div>
648
+ </div>
649
+ <div className="provider-card-body">
650
+ <div className="setting-row">
651
+ <label>Google Client ID</label>
652
+ <input
653
+ type="text"
654
+ placeholder="Enter Google Client ID..."
655
+ value={config.googleCalendarClientId || ''}
656
+ onChange={(e) => updateField('googleCalendarClientId', e.target.value)}
657
+ />
658
+ </div>
659
+ <div className="setting-row">
660
+ <label>Google Client Secret</label>
661
+ <input
662
+ type="password"
663
+ placeholder="Enter Google Client Secret..."
664
+ value={config.googleCalendarClientSecret || ''}
665
+ onChange={(e) => updateField('googleCalendarClientSecret', e.target.value)}
666
+ />
667
+ </div>
668
+ <div className="setting-row">
669
+ <label>Google Refresh Token</label>
670
+ <input
671
+ type="password"
672
+ placeholder="Enter Google Refresh Token..."
673
+ value={config.googleCalendarRefreshToken || ''}
674
+ onChange={(e) => updateField('googleCalendarRefreshToken', e.target.value)}
675
+ />
676
+ </div>
677
+ </div>
678
+ </div>
679
+
680
+ {/* Notion */}
681
+ <div className={`provider-card ${config.pluginNotionEnabled ? 'active-provider' : ''}`}>
682
+ <div className="provider-card-header">
683
+ <div className="provider-card-title">
684
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
685
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
686
+ <polyline points="14 2 14 8 20 8"/>
687
+ <line x1="16" y1="13" x2="8" y2="13"/>
688
+ <line x1="16" y1="17" x2="8" y2="17"/>
689
+ <polyline points="10 9 9 9 8 9"/>
690
+ </svg>
691
+ Notion Plugin
692
+ </div>
693
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
694
+ {config.pluginNotionEnabled && <span className="provider-active-badge">Enabled</span>}
695
+ <label className="settings-toggle-switch">
696
+ <input type="checkbox" checked={config.pluginNotionEnabled} onChange={(e) => updateField('pluginNotionEnabled', e.target.checked)} />
697
+ <span className="settings-toggle-slider" />
698
+ </label>
699
+ </div>
700
+ </div>
701
+ <div className="provider-card-body">
702
+ <div className="setting-row">
703
+ <label>Notion API Key</label>
704
+ <input
705
+ type="password"
706
+ placeholder="secret_..."
707
+ value={config.notionApiKey || ''}
708
+ onChange={(e) => updateField('notionApiKey', e.target.value)}
709
+ />
710
+ </div>
711
+ <div className="setting-row">
712
+ <label>Notion Database ID</label>
713
+ <input
714
+ type="text"
715
+ placeholder="Enter Notion Database ID..."
716
+ value={config.notionDatabaseId || ''}
717
+ onChange={(e) => updateField('notionDatabaseId', e.target.value)}
718
+ />
719
+ </div>
720
+ </div>
721
+ </div>
722
+ </div>
723
+ </section>
724
+
725
+ {/* ── Image Generation ── */}
726
+ <section className="setting-section">
727
+ <div className="section-heading">
728
+ <div>
729
+ <p className="section-kicker">AI image creation</p>
730
+ <h2 className="section-title">Image Generation</h2>
731
+ </div>
732
+ <p className="section-description">Choose which image generation provider Mint uses.</p>
733
+ </div>
734
+ <div className="provider-cards-container">
735
+ {/* Gemini Images */}
736
+ <div className={`provider-card ${config.imageGenProvider === 'gemini' ? 'active-provider' : ''}`}>
737
+ <div className="provider-card-header">
738
+ <div className="provider-card-title">
739
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
740
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
741
+ </svg>
742
+ Google NanoBanana (Gemini Images)
743
+ </div>
744
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
745
+ {config.imageGenProvider === 'gemini' && <span className="provider-active-badge">Active</span>}
746
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
747
+ <input type="radio" name="imageGenProvider" style={{ display: 'none' }} checked={config.imageGenProvider === 'gemini'} onChange={() => updateField('imageGenProvider', 'gemini')} />
748
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('imageGenProvider', 'gemini')} />
749
+ </label>
750
+ </div>
751
+ </div>
752
+ <div className="provider-card-body">
753
+ <p className="hint" style={{ margin: 0 }}>Uses your Gemini API key — no extra key needed.</p>
754
+ </div>
755
+ </div>
756
+
757
+ {/* DALL·E */}
758
+ <div className={`provider-card ${config.imageGenProvider === 'dalle' ? 'active-provider' : ''}`}>
759
+ <div className="provider-card-header">
760
+ <div className="provider-card-title">
761
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
762
+ <circle cx="12" cy="12" r="10"/>
763
+ <line x1="12" y1="2" x2="12" y2="22"/>
764
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
765
+ </svg>
766
+ OpenAI DALL·E
767
+ </div>
768
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
769
+ {config.imageGenProvider === 'dalle' && <span className="provider-active-badge">Active</span>}
770
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
771
+ <input type="radio" name="imageGenProvider" style={{ display: 'none' }} checked={config.imageGenProvider === 'dalle'} onChange={() => updateField('imageGenProvider', 'dalle')} />
772
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('imageGenProvider', 'dalle')} />
773
+ </label>
774
+ </div>
775
+ </div>
776
+ <div className="provider-card-body">
777
+ <p className="hint" style={{ margin: 0 }}>Uses your OpenAI API key — no extra key needed.</p>
778
+ </div>
779
+ </div>
780
+
781
+ {/* Stability AI */}
782
+ <div className={`provider-card ${config.imageGenProvider === 'stability' ? 'active-provider' : ''}`}>
783
+ <div className="provider-card-header">
784
+ <div className="provider-card-title">
785
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
786
+ <path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
787
+ <path d="M8 12l2 2 4-4"/>
788
+ </svg>
789
+ Stability AI (Stable Diffusion)
790
+ </div>
791
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
792
+ {config.imageGenProvider === 'stability' && <span className="provider-active-badge">Active</span>}
793
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
794
+ <input type="radio" name="imageGenProvider" style={{ display: 'none' }} checked={config.imageGenProvider === 'stability'} onChange={() => updateField('imageGenProvider', 'stability')} />
795
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('imageGenProvider', 'stability')} />
796
+ </label>
797
+ </div>
798
+ </div>
799
+ <div className="provider-card-body">
800
+ <div className="setting-row">
801
+ <label>Stability AI API Key</label>
802
+ <input
803
+ type="password"
804
+ value={config.stabilityApiKey}
805
+ onChange={(e) => updateField('stabilityApiKey', e.target.value)}
806
+ placeholder="Enter Stability AI API Key..."
807
+ />
808
+ </div>
809
+ </div>
810
+ </div>
811
+
812
+ {/* Ideogram */}
813
+ <div className={`provider-card ${config.imageGenProvider === 'ideogram' ? 'active-provider' : ''}`}>
814
+ <div className="provider-card-header">
815
+ <div className="provider-card-title">
816
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
817
+ <rect x="3" y="3" width="18" height="18" rx="2"/>
818
+ <circle cx="8.5" cy="8.5" r="1.5"/>
819
+ <polyline points="21 15 16 10 5 21"/>
820
+ </svg>
821
+ Ideogram v3
822
+ </div>
823
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
824
+ {config.imageGenProvider === 'ideogram' && <span className="provider-active-badge">Active</span>}
825
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
826
+ <input type="radio" name="imageGenProvider" style={{ display: 'none' }} checked={config.imageGenProvider === 'ideogram'} onChange={() => updateField('imageGenProvider', 'ideogram')} />
827
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('imageGenProvider', 'ideogram')} />
828
+ </label>
829
+ </div>
830
+ </div>
831
+ <div className="provider-card-body">
832
+ <div className="setting-row">
833
+ <label>Ideogram API Key</label>
834
+ <input
835
+ type="password"
836
+ value={config.ideogramApiKey}
837
+ onChange={(e) => updateField('ideogramApiKey', e.target.value)}
838
+ placeholder="Enter Ideogram API Key..."
839
+ />
840
+ </div>
841
+ </div>
842
+ </div>
843
+
844
+ {/* Replicate */}
845
+ <div className={`provider-card ${config.imageGenProvider === 'replicate' ? 'active-provider' : ''}`}>
846
+ <div className="provider-card-header">
847
+ <div className="provider-card-title">
848
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
849
+ <polyline points="16 3 21 3 21 8"/>
850
+ <line x1="4" y1="20" x2="21" y2="3"/>
851
+ <polyline points="21 16 21 21 16 21"/>
852
+ <line x1="15" y1="15" x2="21" y2="21"/>
853
+ <line x1="4" y1="4" x2="9" y2="9"/>
854
+ </svg>
855
+ Replicate (FLUX / SDXL / custom)
856
+ </div>
857
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
858
+ {config.imageGenProvider === 'replicate' && <span className="provider-active-badge">Active</span>}
859
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
860
+ <input type="radio" name="imageGenProvider" style={{ display: 'none' }} checked={config.imageGenProvider === 'replicate'} onChange={() => updateField('imageGenProvider', 'replicate')} />
861
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('imageGenProvider', 'replicate')} />
862
+ </label>
863
+ </div>
864
+ </div>
865
+ <div className="provider-card-body">
866
+ <div className="setting-row">
867
+ <label>Replicate API Key</label>
868
+ <input
869
+ type="password"
870
+ value={config.replicateApiKey}
871
+ onChange={(e) => updateField('replicateApiKey', e.target.value)}
872
+ placeholder="Enter Replicate API Key..."
873
+ />
874
+ </div>
875
+ </div>
876
+ </div>
877
+ </div>
878
+ </section>
879
+
880
+ {isDesktopApp && (
881
+ <section className="setting-section">
882
+ <div className="section-heading">
883
+ <div>
884
+ <p className="section-kicker">Desktop updates</p>
885
+
886
+ <h2 className="section-title">Signed Tauri Channel</h2>
887
+ </div>
888
+ <p className="section-description">Check and explicitly install signed Tauri releases from your configured update channel.</p>
889
+ </div>
890
+ <div className="toggle-row">
891
+ <div>
892
+ <label>Check signed update channel</label>
893
+ <p className="hint">This flag does not install updates automatically.</p>
894
+ </div>
895
+ <label className="settings-toggle-switch">
896
+ <input
897
+ type="checkbox"
898
+ checked={config.enableAutoUpdate}
899
+ onChange={(e) => updateField('enableAutoUpdate', e.target.checked)}
900
+ />
901
+ <span className="settings-toggle-slider"></span>
902
+ </label>
903
+ </div>
904
+ <div className="form-grid single">
905
+ <div className="setting-row">
906
+ <label>Updater Endpoint</label>
907
+ <input type="text" value={config.updaterEndpoint} onChange={(e) => updateField('updaterEndpoint', e.target.value)} placeholder="https://updates.example.com/latest.json" />
908
+ </div>
909
+ <div className="setting-row">
910
+ <label>Updater Public Key</label>
911
+ <textarea value={config.updaterPublicKey} onChange={(e) => updateField('updaterPublicKey', e.target.value)} placeholder="Minisign public key" />
912
+ </div>
913
+ </div>
914
+ <div className="setting-actions">
915
+ <button type="button" className="btn-connect" onClick={handleCheckUpdates}>Check for updates</button>
916
+ {updateAvailable && <button type="button" className="btn-primary" onClick={handleInstallUpdate}>Install signed update</button>}
917
+ </div>
918
+ {updateMessage && <p className="hint">{updateMessage}</p>}
919
+ </section>
920
+ )}
921
+
922
+ {isDesktopApp && (
923
+ <section className="setting-section">
924
+ <div className="section-heading">
925
+ <div>
926
+ <p className="section-kicker">Desktop</p>
927
+ <h2 className="section-title">Assistant Presence</h2>
928
+ </div>
929
+ </div>
930
+ <div className="toggle-row">
931
+ <div>
932
+ <label>Show Desktop AI Candidate</label>
933
+ <p className="hint">Show the mini AI character on your desktop.</p>
934
+ </div>
935
+ <label className="settings-toggle-switch">
936
+ <input
937
+ type="checkbox"
938
+ checked={config.showDesktopWidget}
939
+ onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
940
+ />
941
+ <span className="settings-toggle-slider"></span>
942
+ </label>
943
+ </div>
944
+ </section>
945
+ )}
946
+ </div>
947
+ )
948
+ }