@pheem49/mint 1.8.2 → 1.10.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 (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -7,7 +7,10 @@ import {
7
7
  DEEPSEEK_MODELS,
8
8
  ANTHROPIC_MODELS,
9
9
  HF_MODELS,
10
- LOCAL_MODELS
10
+ LOCAL_MODELS,
11
+ CustomProviderConfig,
12
+ CustomProviderModel,
13
+ CustomProviderHeader,
11
14
  } from '../SettingsWindow'
12
15
 
13
16
  interface GeneralTabProps {
@@ -35,6 +38,7 @@ interface GeneralTabProps {
35
38
  handleCheckUpdates: () => void
36
39
  handleInstallUpdate: () => void
37
40
  isDesktopApp?: boolean
41
+ onSaveWithoutClosing?: () => Promise<void>
38
42
  }
39
43
 
40
44
  export default function GeneralTab({
@@ -61,439 +65,586 @@ export default function GeneralTab({
61
65
  updateMessage,
62
66
  handleCheckUpdates,
63
67
  handleInstallUpdate,
64
- isDesktopApp = true
68
+ isDesktopApp = true,
69
+ onSaveWithoutClosing
65
70
  }: GeneralTabProps) {
66
- return (
67
- <div className="tab-pane active">
68
- <section className="setting-section">
69
- <div className="section-heading">
71
+ const [savedProviderId, setSavedProviderId] = React.useState<string | null>(null)
72
+ const [openSections, setOpenSections] = React.useState<Record<string, boolean>>({
73
+ ai_routing: true,
74
+ search: false,
75
+ productivity: false,
76
+ image_gen: false,
77
+ video_gen: false,
78
+ custom_providers: false,
79
+ desktop_updates: false,
80
+ assistant_presence: false,
81
+ })
82
+
83
+ const toggleSection = (key: string) => {
84
+ setOpenSections(prev => ({ ...prev, [key]: !prev[key] }))
85
+ }
86
+
87
+ const setAllSections = (isOpen: boolean) => {
88
+ setOpenSections({
89
+ ai_routing: isOpen,
90
+ search: isOpen,
91
+ productivity: isOpen,
92
+ image_gen: isOpen,
93
+ video_gen: isOpen,
94
+ custom_providers: isOpen,
95
+ desktop_updates: isOpen,
96
+ assistant_presence: isOpen,
97
+ })
98
+ }
99
+
100
+ const handleSaveProvider = async (providerId: string) => {
101
+ if (onSaveWithoutClosing) {
102
+ await onSaveWithoutClosing()
103
+ setSavedProviderId(providerId)
104
+ setTimeout(() => setSavedProviderId(null), 2000)
105
+ }
106
+ }
107
+
108
+ const renderCollapsibleSection = (
109
+ key: string,
110
+ kicker: string,
111
+ title: string,
112
+ description: string,
113
+ children: React.ReactNode,
114
+ badgeContent?: React.ReactNode
115
+ ) => {
116
+ const isOpen = openSections[key] ?? false
117
+
118
+ return (
119
+ <section
120
+ className={`setting-section collapsible-section ${isOpen ? 'is-open' : 'is-collapsed'}`}
121
+ style={{
122
+ border: '1px solid var(--border)',
123
+ borderRadius: '12px',
124
+ padding: '16px 20px',
125
+ background: 'var(--surface-bg)',
126
+ marginBottom: '14px',
127
+ transition: 'all 0.25s ease',
128
+ overflow: 'hidden'
129
+ }}
130
+ >
131
+ <div
132
+ className="section-heading-collapsible"
133
+ onClick={() => toggleSection(key)}
134
+ style={{
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'space-between',
138
+ cursor: 'pointer',
139
+ userSelect: 'none'
140
+ }}
141
+ >
70
142
  <div>
71
- <p className="section-kicker">AI routing</p>
72
- <h2 className="section-title">Provider & Model</h2>
143
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
144
+ <p className="section-kicker" style={{ margin: 0 }}>{kicker}</p>
145
+ {badgeContent}
146
+ </div>
147
+ <h2 className="section-title" style={{ margin: '2px 0 0 0', fontSize: '1.15rem' }}>{title}</h2>
148
+ {description && <p className="section-description" style={{ margin: '4px 0 0 0', fontSize: '0.82rem' }}>{description}</p>}
149
+ </div>
150
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-muted)' }}>
151
+ <span style={{ fontSize: '0.75rem', fontWeight: '500' }}>{isOpen ? 'Collapse' : 'Expand'}</span>
152
+ <svg
153
+ width="18" height="18" viewBox="0 0 24 24" fill="none"
154
+ stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
155
+ style={{
156
+ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
157
+ transition: 'transform 0.25s ease'
158
+ }}
159
+ >
160
+ <polyline points="6 9 12 15 18 9"></polyline>
161
+ </svg>
73
162
  </div>
74
- <p className="section-description">Choose which AI backend Mint uses, then set the default model.</p>
75
163
  </div>
76
164
 
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>
165
+ {isOpen && (
166
+ <div className="section-collapsible-body" style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border)' }}>
167
+ {children}
90
168
  </div>
91
- </div>
169
+ )}
170
+ </section>
171
+ )
172
+ }
92
173
 
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>}
174
+ return (
175
+ <div className="tab-pane active">
176
+ {/* Accordion Quick Control Bar */}
177
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginBottom: '14px' }}>
178
+ <button
179
+ className="btn-secondary"
180
+ onClick={() => setAllSections(true)}
181
+ style={{ fontSize: '0.78rem', padding: '4px 10px', borderRadius: '6px', cursor: 'pointer', border: '1px solid var(--border)', background: 'var(--surface-strong)', color: 'var(--text-main)' }}
182
+ >
183
+ Expand All
184
+ </button>
185
+ <button
186
+ className="btn-secondary"
187
+ onClick={() => setAllSections(false)}
188
+ style={{ fontSize: '0.78rem', padding: '4px 10px', borderRadius: '6px', cursor: 'pointer', border: '1px solid var(--border)', background: 'var(--surface-strong)', color: 'var(--text-main)' }}
189
+ >
190
+ Collapse All
191
+ </button>
192
+ </div>
193
+
194
+ {/* ── Section 1: AI Routing ── */}
195
+ {renderCollapsibleSection(
196
+ 'ai_routing',
197
+ 'AI routing',
198
+ 'Provider & Model',
199
+ 'Choose which AI backend Mint uses, then set the default model.',
200
+ <>
201
+ <div className="form-grid compact">
202
+ <div className="setting-row wide">
203
+ <label>Active Provider</label>
204
+ <select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
205
+ <option value="gemini">Google Gemini (Cloud)</option>
206
+ <option value="anthropic">Anthropic Claude</option>
207
+ <option value="openai">OpenAI</option>
208
+ <option value="openrouter">OpenRouter</option>
209
+ <option value="deepseek">DeepSeek</option>
210
+ <option value="ollama">Ollama (Local / Private)</option>
211
+ <option value="huggingface">Hugging Face (Inference API)</option>
212
+ <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
213
+ {(config.customProviders ?? []).map(cp => (
214
+ <option key={`custom:${cp.id}`} value={`custom:${cp.id}`}>
215
+ {cp.displayName || cp.id} (Custom)
216
+ </option>
217
+ ))}
218
+ </select>
104
219
  </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>
220
+ {config.aiProvider.startsWith('custom:') && (() => {
221
+ const activeId = config.aiProvider.replace(/^custom:/, '')
222
+ const cp = (config.customProviders ?? []).find(p => p.id === activeId)
223
+ if (!cp || cp.models.length === 0) return null
224
+ const currentModel = (config.customModelSelections ?? {})[activeId] ?? cp.models[0]?.modelId ?? ''
225
+ return (
226
+ <div className="setting-row wide">
227
+ <label>Active Model</label>
228
+ <select
229
+ value={currentModel}
230
+ onChange={(e) => updateField('customModelSelections', {
231
+ ...(config.customModelSelections ?? {}),
232
+ [activeId]: e.target.value
233
+ })}
234
+ >
235
+ {cp.models.map(m => (
236
+ <option key={m.modelId} value={m.modelId}>
237
+ {m.displayName || m.modelId}
238
+ </option>
239
+ ))}
240
+ </select>
241
+ </div>
242
+ )
243
+ })()}
244
+ </div>
245
+
246
+ <div className="provider-cards-container">
247
+ {/* Google Gemini Card */}
248
+ <div className={`provider-card ${config.aiProvider === 'gemini' ? 'active-provider' : ''}`}>
249
+ <div className="provider-card-header">
250
+ <div className="provider-card-title">
251
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
252
+ <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>
253
+ </svg>
254
+ Google Gemini (Cloud)
255
+ </div>
256
+ {config.aiProvider === 'gemini' && <span className="provider-active-badge">Active</span>}
117
257
  </div>
118
- {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
258
+ <div className="provider-card-body">
119
259
  <div className="setting-row">
120
- <label>Custom Gemini Model</label>
260
+ <label>Gemini Model</label>
261
+ <select
262
+ value={GEMINI_MODELS.includes(config.geminiModel) ? config.geminiModel : 'custom'}
263
+ onChange={(e) => updateField('geminiModel', e.target.value)}
264
+ >
265
+ {GEMINI_MODELS.map(model => (
266
+ <option key={model} value={model}>{model}</option>
267
+ ))}
268
+ <option value="custom">Custom...</option>
269
+ </select>
270
+ </div>
271
+ {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
272
+ <div className="setting-row">
273
+ <label>Custom Gemini Model</label>
274
+ <input
275
+ type="text"
276
+ value={customGemini}
277
+ onChange={(e) => { setCustomGemini(e.target.value); updateField('geminiModel', 'custom') }}
278
+ placeholder="e.g. gemini-3.1-flash-lite-preview"
279
+ />
280
+ </div>
281
+ )}
282
+ <div className="setting-row">
283
+ <label>Gemini API Key</label>
121
284
  <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"
285
+ type="password"
286
+ value={config.apiKey}
287
+ onChange={(e) => updateField('apiKey', e.target.value)}
288
+ placeholder="Enter Gemini API Key..."
126
289
  />
127
290
  </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
291
  </div>
138
292
  </div>
139
- </div>
140
293
 
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>
294
+ {/* Anthropic Claude Card */}
295
+ <div className={`provider-card ${config.aiProvider === 'anthropic' ? 'active-provider' : ''}`}>
296
+ <div className="provider-card-header">
297
+ <div className="provider-card-title">
298
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
299
+ <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>
300
+ <path d="M12 2L2 22h20L12 2z"></path>
301
+ </svg>
302
+ Anthropic Claude
303
+ </div>
304
+ {config.aiProvider === 'anthropic' && <span className="provider-active-badge">Active</span>}
165
305
  </div>
166
- {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
306
+ <div className="provider-card-body">
167
307
  <div className="setting-row">
168
- <label>Custom Anthropic Model</label>
308
+ <label>Anthropic Model</label>
309
+ <select
310
+ value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
311
+ onChange={(e) => updateField('anthropicModel', e.target.value)}
312
+ >
313
+ {ANTHROPIC_MODELS.map(model => (
314
+ <option key={model} value={model}>{model}</option>
315
+ ))}
316
+ <option value="custom">Custom...</option>
317
+ </select>
318
+ </div>
319
+ {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
320
+ <div className="setting-row">
321
+ <label>Custom Anthropic Model</label>
322
+ <input
323
+ type="text"
324
+ value={customAnthropic}
325
+ onChange={(e) => { setCustomAnthropic(e.target.value); updateField('anthropicModel', 'custom') }}
326
+ placeholder="e.g. claude-3-5-sonnet-latest"
327
+ />
328
+ </div>
329
+ )}
330
+ <div className="setting-row">
331
+ <label>Anthropic API Key</label>
169
332
  <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"
333
+ type="password"
334
+ value={config.anthropicApiKey}
335
+ onChange={(e) => updateField('anthropicApiKey', e.target.value)}
336
+ placeholder="Enter Anthropic API Key..."
174
337
  />
175
338
  </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
339
  </div>
186
340
  </div>
187
- </div>
188
341
 
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>
342
+ {/* OpenAI Card */}
343
+ <div className={`provider-card ${config.aiProvider === 'openai' ? 'active-provider' : ''}`}>
344
+ <div className="provider-card-header">
345
+ <div className="provider-card-title">
346
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
347
+ <circle cx="12" cy="12" r="10"></circle>
348
+ <line x1="12" y1="2" x2="12" y2="22"></line>
349
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
350
+ </svg>
351
+ OpenAI
352
+ </div>
353
+ {config.aiProvider === 'openai' && <span className="provider-active-badge">Active</span>}
214
354
  </div>
215
- {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
355
+ <div className="provider-card-body">
356
+ <div className="setting-row">
357
+ <label>OpenAI Model</label>
358
+ <select
359
+ value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
360
+ onChange={(e) => updateField('openaiModel', e.target.value)}
361
+ >
362
+ {OPENAI_MODELS.map(model => (
363
+ <option key={model} value={model}>{model}</option>
364
+ ))}
365
+ <option value="custom">Custom...</option>
366
+ </select>
367
+ </div>
368
+ {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
369
+ <div className="setting-row">
370
+ <label>Custom OpenAI Model</label>
371
+ <input
372
+ type="text"
373
+ value={customOpenAI}
374
+ onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
375
+ placeholder="e.g. gpt-4o"
376
+ />
377
+ </div>
378
+ )}
216
379
  <div className="setting-row">
217
- <label>Custom OpenAI Model</label>
380
+ <label>OpenAI API Key</label>
218
381
  <input
219
- type="text"
220
- value={customOpenAI}
221
- onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
222
- placeholder="e.g. gpt-4o"
382
+ type="password"
383
+ value={config.openaiApiKey}
384
+ onChange={(e) => updateField('openaiApiKey', e.target.value)}
385
+ placeholder="Enter OpenAI API Key..."
223
386
  />
224
387
  </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
388
  </div>
235
389
  </div>
236
- </div>
237
390
 
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>
391
+ {/* OpenRouter Card */}
392
+ <div className={`provider-card ${config.aiProvider === 'openrouter' ? 'active-provider' : ''}`}>
393
+ <div className="provider-card-header">
394
+ <div className="provider-card-title">
395
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
396
+ <polyline points="16 3 21 3 21 8"></polyline>
397
+ <line x1="4" y1="20" x2="21" y2="3"></line>
398
+ <polyline points="21 16 21 21 16 21"></polyline>
399
+ <line x1="15" y1="15" x2="21" y2="21"></line>
400
+ <line x1="4" y1="4" x2="9" y2="9"></line>
401
+ </svg>
402
+ OpenRouter
403
+ </div>
404
+ {config.aiProvider === 'openrouter' && <span className="provider-active-badge">Active</span>}
265
405
  </div>
266
- {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
406
+ <div className="provider-card-body">
267
407
  <div className="setting-row">
268
- <label>Custom OpenRouter Model</label>
408
+ <label>OpenRouter Model</label>
409
+ <select
410
+ value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
411
+ onChange={(e) => updateField('openrouterModel', e.target.value)}
412
+ >
413
+ {OPENROUTER_MODELS.map(model => (
414
+ <option key={model} value={model}>{model}</option>
415
+ ))}
416
+ <option value="custom">Custom...</option>
417
+ </select>
418
+ </div>
419
+ {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
420
+ <div className="setting-row">
421
+ <label>Custom OpenRouter Model</label>
422
+ <input
423
+ type="text"
424
+ value={customOpenRouter}
425
+ onChange={(e) => { setCustomOpenRouter(e.target.value); updateField('openrouterModel', 'custom') }}
426
+ placeholder="e.g. anthropic/claude-3.5-sonnet"
427
+ />
428
+ </div>
429
+ )}
430
+ <div className="setting-row">
431
+ <label>OpenRouter API Key</label>
269
432
  <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"
433
+ type="password"
434
+ value={config.openrouterApiKey}
435
+ onChange={(e) => updateField('openrouterApiKey', e.target.value)}
436
+ placeholder="Enter OpenRouter API Key..."
274
437
  />
275
438
  </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
439
  </div>
286
440
  </div>
287
- </div>
288
441
 
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>
442
+ {/* DeepSeek Card */}
443
+ <div className={`provider-card ${config.aiProvider === 'deepseek' ? 'active-provider' : ''}`}>
444
+ <div className="provider-card-header">
445
+ <div className="provider-card-title">
446
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
447
+ <circle cx="11" cy="11" r="8"></circle>
448
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
449
+ </svg>
450
+ DeepSeek
451
+ </div>
452
+ {config.aiProvider === 'deepseek' && <span className="provider-active-badge">Active</span>}
313
453
  </div>
314
- {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
454
+ <div className="provider-card-body">
455
+ <div className="setting-row">
456
+ <label>DeepSeek Model</label>
457
+ <select
458
+ value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
459
+ onChange={(e) => updateField('deepseekModel', e.target.value)}
460
+ >
461
+ {DEEPSEEK_MODELS.map(model => (
462
+ <option key={model} value={model}>{model}</option>
463
+ ))}
464
+ <option value="custom">Custom...</option>
465
+ </select>
466
+ </div>
467
+ {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
468
+ <div className="setting-row">
469
+ <label>Custom DeepSeek Model</label>
470
+ <input
471
+ type="text"
472
+ value={customDeepSeek}
473
+ onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
474
+ placeholder="e.g. deepseek-v4-pro"
475
+ />
476
+ </div>
477
+ )}
315
478
  <div className="setting-row">
316
- <label>Custom DeepSeek Model</label>
479
+ <label>DeepSeek API Key</label>
317
480
  <input
318
- type="text"
319
- value={customDeepSeek}
320
- onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
321
- placeholder="e.g. deepseek-v4-pro"
481
+ type="password"
482
+ value={config.deepseekApiKey}
483
+ onChange={(e) => updateField('deepseekApiKey', e.target.value)}
484
+ placeholder="Enter DeepSeek API Key..."
322
485
  />
323
486
  </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
487
  </div>
334
488
  </div>
335
- </div>
336
489
 
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>
490
+ {/* Hugging Face Card */}
491
+ <div className={`provider-card ${config.aiProvider === 'huggingface' ? 'active-provider' : ''}`}>
492
+ <div className="provider-card-header">
493
+ <div className="provider-card-title">
494
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
495
+ <circle cx="12" cy="12" r="10"></circle>
496
+ <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
497
+ <line x1="9" y1="9" x2="9.01" y2="9"></line>
498
+ <line x1="15" y1="9" x2="15.01" y2="9"></line>
499
+ </svg>
500
+ Hugging Face (Inference API)
501
+ </div>
502
+ {config.aiProvider === 'huggingface' && <span className="provider-active-badge">Active</span>}
363
503
  </div>
364
- {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
504
+ <div className="provider-card-body">
505
+ <div className="setting-row">
506
+ <label>Hugging Face Model</label>
507
+ <select
508
+ value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
509
+ onChange={(e) => updateField('hfModel', e.target.value)}
510
+ >
511
+ {HF_MODELS.map(model => (
512
+ <option key={model} value={model}>{model}</option>
513
+ ))}
514
+ <option value="custom">Custom...</option>
515
+ </select>
516
+ </div>
517
+ {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
518
+ <div className="setting-row">
519
+ <label>Custom Hugging Face Model</label>
520
+ <input
521
+ type="text"
522
+ value={customHF}
523
+ onChange={(e) => { setCustomHF(e.target.value); updateField('hfModel', 'custom') }}
524
+ placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct"
525
+ />
526
+ </div>
527
+ )}
365
528
  <div className="setting-row">
366
- <label>Custom Hugging Face Model</label>
529
+ <label>Hugging Face API Key</label>
367
530
  <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"
531
+ type="password"
532
+ value={config.hfApiKey}
533
+ onChange={(e) => updateField('hfApiKey', e.target.value)}
534
+ placeholder="Enter Hugging Face API Key..."
372
535
  />
373
536
  </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
537
  </div>
384
538
  </div>
385
- </div>
386
539
 
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>
540
+ {/* LM Studio Card */}
541
+ <div className={`provider-card ${config.aiProvider === 'local_openai' ? 'active-provider' : ''}`}>
542
+ <div className="provider-card-header">
543
+ <div className="provider-card-title">
544
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
545
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
546
+ <line x1="8" y1="21" x2="16" y2="21"></line>
547
+ <line x1="12" y1="17" x2="12" y2="21"></line>
548
+ </svg>
549
+ LM Studio / Local OpenAI
550
+ </div>
551
+ {config.aiProvider === 'local_openai' && <span className="provider-active-badge">Active</span>}
412
552
  </div>
413
- {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
553
+ <div className="provider-card-body">
554
+ <div className="setting-row">
555
+ <label>LM Studio Model</label>
556
+ <select
557
+ value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
558
+ onChange={(e) => updateField('localModelName', e.target.value)}
559
+ >
560
+ {LOCAL_MODELS.map(model => (
561
+ <option key={model} value={model}>{model}</option>
562
+ ))}
563
+ <option value="custom">Custom...</option>
564
+ </select>
565
+ </div>
566
+ {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
567
+ <div className="setting-row">
568
+ <label>Custom LM Studio Model</label>
569
+ <input
570
+ type="text"
571
+ value={customLocal}
572
+ onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
573
+ placeholder="e.g. local-model"
574
+ />
575
+ </div>
576
+ )}
414
577
  <div className="setting-row">
415
- <label>Custom LM Studio Model</label>
578
+ <label>LM Studio Base URL</label>
416
579
  <input
417
580
  type="text"
418
- value={customLocal}
419
- onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
420
- placeholder="e.g. local-model"
581
+ value={config.localApiBaseUrl}
582
+ onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
583
+ placeholder="e.g. http://localhost:1234/v1"
421
584
  />
422
585
  </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
586
  </div>
433
587
  </div>
434
- </div>
435
588
 
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>
589
+ {/* Ollama Card */}
590
+ <div className={`provider-card ${config.aiProvider === 'ollama' ? 'active-provider' : ''}`}>
591
+ <div className="provider-card-header">
592
+ <div className="provider-card-title">
593
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
594
+ <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>
595
+ </svg>
596
+ Ollama (Local)
597
+ </div>
598
+ {config.aiProvider === 'ollama' && <span className="provider-active-badge">Active</span>}
462
599
  </div>
463
- {(!dynamicOllamaModels.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
600
+ <div className="provider-card-body">
464
601
  <div className="setting-row">
465
- <label>Custom Ollama Model</label>
602
+ <label>Ollama Model</label>
603
+ <select
604
+ value={dynamicOllamaModels.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
605
+ onChange={(e) => updateField('ollamaModel', e.target.value)}
606
+ >
607
+ {dynamicOllamaModels.map(model => (
608
+ <option key={model} value={model}>{model}</option>
609
+ ))}
610
+ {!dynamicOllamaModels.includes(config.ollamaModel) && config.ollamaModel && config.ollamaModel !== 'custom' && (
611
+ <option value={config.ollamaModel}>{config.ollamaModel}</option>
612
+ )}
613
+ <option value="custom">Custom...</option>
614
+ </select>
615
+ </div>
616
+ {(!dynamicOllamaModels.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
617
+ <div className="setting-row">
618
+ <label>Custom Ollama Model</label>
619
+ <input
620
+ type="text"
621
+ value={customOllama}
622
+ onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
623
+ placeholder="e.g. llama3:latest"
624
+ />
625
+ </div>
626
+ )}
627
+ <div className="setting-row">
628
+ <label>Ollama Host</label>
466
629
  <input
467
630
  type="text"
468
- value={customOllama}
469
- onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
470
- placeholder="e.g. llama3:latest"
631
+ value={config.ollamaHost}
632
+ onChange={(e) => updateField('ollamaHost', e.target.value)}
633
+ placeholder="e.g. http://localhost:11434"
471
634
  />
472
635
  </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
636
  </div>
483
637
  </div>
484
638
  </div>
485
- </div>
486
- </section>
639
+ </>
640
+ )}
487
641
 
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>
642
+ {/* ── Section 2: Web Search ── */}
643
+ {renderCollapsibleSection(
644
+ 'search',
645
+ 'Web search',
646
+ 'Search Provider',
647
+ 'Choose which search API Mint uses when performing web lookups.',
497
648
  <div className="provider-cards-container">
498
649
  {/* Brave Search */}
499
650
  <div className={`provider-card ${config.searchProvider === 'brave' ? 'active-provider' : ''}`}>
@@ -565,17 +716,14 @@ export default function GeneralTab({
565
716
  </div>
566
717
  </div>
567
718
  </div>
568
- </section>
719
+ )}
569
720
 
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>
721
+ {/* ── Section 3: Productivity Integrations ── */}
722
+ {renderCollapsibleSection(
723
+ 'productivity',
724
+ 'Productivity integrations',
725
+ 'Productivity',
726
+ 'Enable and configure Gmail, Google Calendar, and Notion plugins directly.',
579
727
  <div className="provider-cards-container">
580
728
  {/* Gmail */}
581
729
  <div className={`provider-card ${config.pluginGmailEnabled ? 'active-provider' : ''}`}>
@@ -720,17 +868,14 @@ export default function GeneralTab({
720
868
  </div>
721
869
  </div>
722
870
  </div>
723
- </section>
871
+ )}
724
872
 
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>
873
+ {/* ── Section 4: Image Generation ── */}
874
+ {renderCollapsibleSection(
875
+ 'image_gen',
876
+ 'AI image creation',
877
+ 'Image Generation',
878
+ 'Choose which image generation provider Mint uses.',
734
879
  <div className="provider-cards-container">
735
880
  {/* Gemini Images */}
736
881
  <div className={`provider-card ${config.imageGenProvider === 'gemini' ? 'active-provider' : ''}`}>
@@ -875,18 +1020,260 @@ export default function GeneralTab({
875
1020
  </div>
876
1021
  </div>
877
1022
  </div>
878
- </section>
879
-
1023
+ )}
880
1024
 
881
- {isDesktopApp && (
882
- <section className="setting-section">
883
- <div className="section-heading">
884
- <div>
885
- <p className="section-kicker">Desktop updates</p>
886
- <h2 className="section-title">Signed Tauri Channel</h2>
1025
+ {/* ── Section 5: Video Generation ── */}
1026
+ {renderCollapsibleSection(
1027
+ 'video_gen',
1028
+ 'AI video creation',
1029
+ 'Video Generation',
1030
+ 'Choose which video generation provider Mint uses.',
1031
+ <div className="provider-cards-container">
1032
+ {/* Veo */}
1033
+ <div className={`provider-card ${config.videoGenProvider === 'veo' ? 'active-provider' : ''}`}>
1034
+ <div className="provider-card-header">
1035
+ <div className="provider-card-title">
1036
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1037
+ <polygon points="23 7 16 12 23 17 23 7" />
1038
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
1039
+ </svg>
1040
+ Google Veo (Gemini Videos)
1041
+ </div>
1042
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1043
+ {config.videoGenProvider === 'veo' && <span className="provider-active-badge">Active</span>}
1044
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
1045
+ <input type="radio" name="videoGenProvider" style={{ display: 'none' }} checked={config.videoGenProvider === 'veo'} onChange={() => updateField('videoGenProvider', 'veo')} />
1046
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('videoGenProvider', 'veo')} />
1047
+ </label>
1048
+ </div>
1049
+ </div>
1050
+ <div className="provider-card-body">
1051
+ <p className="hint" style={{ margin: 0 }}>Uses your Gemini API key — no extra key needed.</p>
1052
+ <div className="setting-row" style={{ marginTop: '12px' }}>
1053
+ <label>Default Veo Model</label>
1054
+ <select
1055
+ value={config.veoModel || 'veo-2.0-generate-001'}
1056
+ onChange={(e) => updateField('veoModel', e.target.value)}
1057
+ style={{ width: '100%', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '6px', borderRadius: '4px' }}
1058
+ >
1059
+ <option value="veo-2.0-generate-001">veo-2.0-generate-001 (Default)</option>
1060
+ <option value="veo-2.0-flash-001">veo-2.0-flash-001</option>
1061
+ </select>
1062
+ </div>
887
1063
  </div>
888
- <p className="section-description">Check and explicitly install signed Tauri releases from your configured update channel.</p>
889
1064
  </div>
1065
+ </div>
1066
+ )}
1067
+
1068
+ {/* ── Section 6: Custom Providers ── */}
1069
+ {renderCollapsibleSection(
1070
+ 'custom_providers',
1071
+ 'Custom AI Routing',
1072
+ 'Custom Providers',
1073
+ 'Add any OpenAI-compatible endpoint as a custom provider. Configure its models and optional headers.',
1074
+ <>
1075
+ <div className="provider-cards-container">
1076
+ {(config.customProviders ?? []).map((cp, cpIdx) => {
1077
+ const isActive = config.aiProvider === `custom:${cp.id}`
1078
+
1079
+ const updateCp = (patch: Partial<CustomProviderConfig>) => {
1080
+ const updated = (config.customProviders ?? []).map((p, i) =>
1081
+ i === cpIdx ? { ...p, ...patch } : p
1082
+ )
1083
+ updateField('customProviders', updated)
1084
+ }
1085
+
1086
+ const deleteCp = () => {
1087
+ const updated = (config.customProviders ?? []).filter((_, i) => i !== cpIdx)
1088
+ updateField('customProviders', updated)
1089
+ if (config.aiProvider === `custom:${cp.id}`) {
1090
+ updateField('aiProvider', 'gemini')
1091
+ }
1092
+ }
1093
+
1094
+ const updateModel = (mIdx: number, patch: Partial<CustomProviderModel>) => {
1095
+ const updatedModels = cp.models.map((m, i) => i === mIdx ? { ...m, ...patch } : m)
1096
+ updateCp({ models: updatedModels })
1097
+ }
1098
+ const deleteModel = (mIdx: number) => {
1099
+ updateCp({ models: cp.models.filter((_, i) => i !== mIdx) })
1100
+ }
1101
+ const addModel = () => {
1102
+ updateCp({ models: [...cp.models, { modelId: '', displayName: '' }] })
1103
+ }
1104
+
1105
+ const updateHeader = (hIdx: number, patch: Partial<CustomProviderHeader>) => {
1106
+ const updatedHeaders = cp.headers.map((h, i) => i === hIdx ? { ...h, ...patch } : h)
1107
+ updateCp({ headers: updatedHeaders })
1108
+ }
1109
+ const deleteHeader = (hIdx: number) => {
1110
+ updateCp({ headers: cp.headers.filter((_, i) => i !== hIdx) })
1111
+ }
1112
+ const addHeader = () => {
1113
+ updateCp({ headers: [...cp.headers, { name: '', value: '' }] })
1114
+ }
1115
+
1116
+ return (
1117
+ <div key={cpIdx} className={`provider-card ${isActive ? 'active-provider' : ''}`}>
1118
+ <div className="provider-card-header">
1119
+ <div className="provider-card-title">
1120
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1121
+ <circle cx="12" cy="12" r="3" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
1122
+ </svg>
1123
+ {cp.displayName || cp.id || 'Unnamed Provider'}
1124
+ </div>
1125
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1126
+ {isActive && <span className="provider-active-badge">Active</span>}
1127
+ <button
1128
+ className="btn btn-primary"
1129
+ style={{ padding: '3px 8px', fontSize: '12px', background: savedProviderId === cp.id ? '#10b981' : '' }}
1130
+ onClick={() => handleSaveProvider(cp.id)}
1131
+ title="Save this provider settings"
1132
+ >
1133
+ {savedProviderId === cp.id ? 'Saved! ✓' : 'Save'}
1134
+ </button>
1135
+ <button
1136
+ className="btn btn-danger"
1137
+ style={{ padding: '3px 8px', fontSize: '12px' }}
1138
+ onClick={deleteCp}
1139
+ title="Delete this provider"
1140
+ >
1141
+ Delete
1142
+ </button>
1143
+ </div>
1144
+ </div>
1145
+
1146
+ <div className="provider-card-body">
1147
+ <div className="setting-row">
1148
+ <label>Name</label>
1149
+ <input
1150
+ type="text"
1151
+ value={cp.displayName}
1152
+ onChange={(e) => {
1153
+ const name = e.target.value
1154
+ const computedId = name
1155
+ .toLowerCase()
1156
+ .replace(/[^a-z0-9_-]/g, '-')
1157
+ .replace(/-+/g, '-')
1158
+ .replace(/^-|-$/g, '') || `provider-${cpIdx + 1}`
1159
+ updateCp({ displayName: name, id: computedId })
1160
+ }}
1161
+ placeholder="e.g. DeepSeek"
1162
+ />
1163
+ </div>
1164
+
1165
+ <div className="setting-row">
1166
+ <label>Base URL</label>
1167
+ <input
1168
+ type="text"
1169
+ value={cp.baseUrl}
1170
+ onChange={(e) => updateCp({ baseUrl: e.target.value })}
1171
+ placeholder="https://api.myprovider.com/v1"
1172
+ />
1173
+ </div>
1174
+
1175
+ <div className="setting-row">
1176
+ <label>API Key</label>
1177
+ <input
1178
+ type="password"
1179
+ value={cp.apiKey}
1180
+ onChange={(e) => updateCp({ apiKey: e.target.value })}
1181
+ placeholder="Optional. Leave empty if you manage auth via headers."
1182
+ />
1183
+ </div>
1184
+
1185
+ <div style={{ marginTop: '12px' }}>
1186
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Model list</label>
1187
+ {cp.models.map((m, mIdx) => (
1188
+ <div key={mIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1189
+ <input
1190
+ type="text"
1191
+ value={m.modelId}
1192
+ onChange={(e) => updateModel(mIdx, { modelId: e.target.value, displayName: e.target.value })}
1193
+ placeholder="e.g. llama-3.1-8b"
1194
+ style={{ flex: 1 }}
1195
+ />
1196
+ <button
1197
+ className="btn btn-danger"
1198
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1199
+ onClick={() => deleteModel(mIdx)}
1200
+ title="Remove model"
1201
+ >🗑</button>
1202
+ </div>
1203
+ ))}
1204
+ <button
1205
+ className="btn btn-secondary"
1206
+ style={{ fontSize: '12px', marginTop: '4px' }}
1207
+ onClick={addModel}
1208
+ >
1209
+ + Add model
1210
+ </button>
1211
+ </div>
1212
+
1213
+ <div style={{ marginTop: '12px' }}>
1214
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Headers (optional)</label>
1215
+ {cp.headers.map((h, hIdx) => (
1216
+ <div key={hIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1217
+ <input
1218
+ type="text"
1219
+ value={h.name}
1220
+ onChange={(e) => updateHeader(hIdx, { name: e.target.value })}
1221
+ placeholder="Header-Name"
1222
+ style={{ flex: 1 }}
1223
+ />
1224
+ <input
1225
+ type="text"
1226
+ value={h.value}
1227
+ onChange={(e) => updateHeader(hIdx, { value: e.target.value })}
1228
+ placeholder="value"
1229
+ style={{ flex: 1 }}
1230
+ />
1231
+ <button
1232
+ className="btn btn-danger"
1233
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1234
+ onClick={() => deleteHeader(hIdx)}
1235
+ title="Remove header"
1236
+ >🗑</button>
1237
+ </div>
1238
+ ))}
1239
+ <button
1240
+ className="btn btn-secondary"
1241
+ style={{ fontSize: '12px', marginTop: '4px' }}
1242
+ onClick={addHeader}
1243
+ >
1244
+ + Add header
1245
+ </button>
1246
+ </div>
1247
+ </div>
1248
+ </div>
1249
+ )
1250
+ })}
1251
+ </div>
1252
+
1253
+ <div className="setting-actions" style={{ marginTop: '16px' }}>
1254
+ <button
1255
+ className="btn btn-secondary"
1256
+ onClick={() => {
1257
+ const newId = `provider-${(config.customProviders ?? []).length + 1}`
1258
+ updateField('customProviders', [
1259
+ ...(config.customProviders ?? []),
1260
+ { id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
1261
+ ])
1262
+ }}
1263
+ >
1264
+ + Add custom provider
1265
+ </button>
1266
+ </div>
1267
+ </>
1268
+ )}
1269
+
1270
+ {/* ── Section 7: Desktop Updates ── */}
1271
+ {isDesktopApp && renderCollapsibleSection(
1272
+ 'desktop_updates',
1273
+ 'Desktop updates',
1274
+ 'Signed Tauri Channel',
1275
+ 'Check and explicitly install signed Tauri releases from your configured update channel.',
1276
+ <>
890
1277
  <div className="toggle-row">
891
1278
  <div>
892
1279
  <label>Check signed update channel</label>
@@ -916,34 +1303,29 @@ export default function GeneralTab({
916
1303
  {updateAvailable && <button type="button" className="btn-primary" onClick={handleInstallUpdate}>Install signed update</button>}
917
1304
  </div>
918
1305
  {updateMessage && <p className="hint">{updateMessage}</p>}
919
- </section>
1306
+ </>
920
1307
  )}
921
1308
 
922
-
923
-
924
- {isDesktopApp && (
925
- <section className="setting-section">
926
- <div className="section-heading">
927
- <div>
928
- <p className="section-kicker">Desktop</p>
929
- <h2 className="section-title">Assistant Presence</h2>
930
- </div>
931
- </div>
932
- <div className="toggle-row">
933
- <div>
934
- <label>Show Desktop AI Candidate</label>
935
- <p className="hint">Show the mini AI character on your desktop.</p>
936
- </div>
937
- <label className="settings-toggle-switch">
938
- <input
939
- type="checkbox"
940
- checked={config.showDesktopWidget}
941
- onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
942
- />
943
- <span className="settings-toggle-slider"></span>
944
- </label>
1309
+ {/* ── Section 8: Assistant Presence ── */}
1310
+ {isDesktopApp && renderCollapsibleSection(
1311
+ 'assistant_presence',
1312
+ 'Desktop',
1313
+ 'Assistant Presence',
1314
+ 'Configure the mini AI character desktop presence widget.',
1315
+ <div className="toggle-row">
1316
+ <div>
1317
+ <label>Show Desktop AI Candidate</label>
1318
+ <p className="hint">Show the mini AI character on your desktop.</p>
945
1319
  </div>
946
- </section>
1320
+ <label className="settings-toggle-switch">
1321
+ <input
1322
+ type="checkbox"
1323
+ checked={config.showDesktopWidget}
1324
+ onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
1325
+ />
1326
+ <span className="settings-toggle-slider"></span>
1327
+ </label>
1328
+ </div>
947
1329
  )}
948
1330
  </div>
949
1331
  )