@pheem49/mint 1.9.0 → 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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +352 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +353 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/lib.rs +114 -8
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -69,6 +69,33 @@ export default function GeneralTab({
|
|
|
69
69
|
onSaveWithoutClosing
|
|
70
70
|
}: GeneralTabProps) {
|
|
71
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
|
+
}
|
|
72
99
|
|
|
73
100
|
const handleSaveProvider = async (providerId: string) => {
|
|
74
101
|
if (onSaveWithoutClosing) {
|
|
@@ -77,467 +104,547 @@ export default function GeneralTab({
|
|
|
77
104
|
setTimeout(() => setSavedProviderId(null), 2000)
|
|
78
105
|
}
|
|
79
106
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
>
|
|
84
142
|
<div>
|
|
85
|
-
<
|
|
86
|
-
|
|
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>
|
|
87
162
|
</div>
|
|
88
|
-
<p className="section-description">Choose which AI backend Mint uses, then set the default model.</p>
|
|
89
163
|
</div>
|
|
90
164
|
|
|
91
|
-
|
|
92
|
-
<div className="
|
|
93
|
-
|
|
94
|
-
<select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
|
|
95
|
-
<option value="gemini">Google Gemini (Cloud)</option>
|
|
96
|
-
<option value="anthropic">Anthropic Claude</option>
|
|
97
|
-
<option value="openai">OpenAI</option>
|
|
98
|
-
<option value="openrouter">OpenRouter</option>
|
|
99
|
-
<option value="deepseek">DeepSeek</option>
|
|
100
|
-
<option value="ollama">Ollama (Local / Private)</option>
|
|
101
|
-
<option value="huggingface">Hugging Face (Inference API)</option>
|
|
102
|
-
<option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
|
|
103
|
-
{(config.customProviders ?? []).map(cp => (
|
|
104
|
-
<option key={`custom:${cp.id}`} value={`custom:${cp.id}`}>
|
|
105
|
-
{cp.displayName || cp.id} (Custom)
|
|
106
|
-
</option>
|
|
107
|
-
))}
|
|
108
|
-
</select>
|
|
165
|
+
{isOpen && (
|
|
166
|
+
<div className="section-collapsible-body" style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border)' }}>
|
|
167
|
+
{children}
|
|
109
168
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (!cp || cp.models.length === 0) return null
|
|
115
|
-
const currentModel = (config.customModelSelections ?? {})[activeId] ?? cp.models[0]?.modelId ?? ''
|
|
116
|
-
return (
|
|
117
|
-
<div className="setting-row wide">
|
|
118
|
-
<label>Active Model</label>
|
|
119
|
-
<select
|
|
120
|
-
value={currentModel}
|
|
121
|
-
onChange={(e) => updateField('customModelSelections', {
|
|
122
|
-
...(config.customModelSelections ?? {}),
|
|
123
|
-
[activeId]: e.target.value
|
|
124
|
-
})}
|
|
125
|
-
>
|
|
126
|
-
{cp.models.map(m => (
|
|
127
|
-
<option key={m.modelId} value={m.modelId}>
|
|
128
|
-
{m.displayName || m.modelId}
|
|
129
|
-
</option>
|
|
130
|
-
))}
|
|
131
|
-
</select>
|
|
132
|
-
</div>
|
|
133
|
-
)
|
|
134
|
-
})()}
|
|
135
|
-
</div>
|
|
169
|
+
)}
|
|
170
|
+
</section>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
136
173
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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>
|
|
148
219
|
</div>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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>}
|
|
161
257
|
</div>
|
|
162
|
-
|
|
258
|
+
<div className="provider-card-body">
|
|
163
259
|
<div className="setting-row">
|
|
164
|
-
<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>
|
|
165
284
|
<input
|
|
166
|
-
type="
|
|
167
|
-
value={
|
|
168
|
-
onChange={(e) =>
|
|
169
|
-
placeholder="
|
|
285
|
+
type="password"
|
|
286
|
+
value={config.apiKey}
|
|
287
|
+
onChange={(e) => updateField('apiKey', e.target.value)}
|
|
288
|
+
placeholder="Enter Gemini API Key..."
|
|
170
289
|
/>
|
|
171
290
|
</div>
|
|
172
|
-
)}
|
|
173
|
-
<div className="setting-row">
|
|
174
|
-
<label>Gemini API Key</label>
|
|
175
|
-
<input
|
|
176
|
-
type="password"
|
|
177
|
-
value={config.apiKey}
|
|
178
|
-
onChange={(e) => updateField('apiKey', e.target.value)}
|
|
179
|
-
placeholder="Enter Gemini API Key..."
|
|
180
|
-
/>
|
|
181
291
|
</div>
|
|
182
292
|
</div>
|
|
183
|
-
</div>
|
|
184
293
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</div>
|
|
197
|
-
<div className="provider-card-body">
|
|
198
|
-
<div className="setting-row">
|
|
199
|
-
<label>Anthropic Model</label>
|
|
200
|
-
<select
|
|
201
|
-
value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
|
|
202
|
-
onChange={(e) => updateField('anthropicModel', e.target.value)}
|
|
203
|
-
>
|
|
204
|
-
{ANTHROPIC_MODELS.map(model => (
|
|
205
|
-
<option key={model} value={model}>{model}</option>
|
|
206
|
-
))}
|
|
207
|
-
<option value="custom">Custom...</option>
|
|
208
|
-
</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>}
|
|
209
305
|
</div>
|
|
210
|
-
|
|
306
|
+
<div className="provider-card-body">
|
|
307
|
+
<div className="setting-row">
|
|
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
|
+
)}
|
|
211
330
|
<div className="setting-row">
|
|
212
|
-
<label>
|
|
331
|
+
<label>Anthropic API Key</label>
|
|
213
332
|
<input
|
|
214
|
-
type="
|
|
215
|
-
value={
|
|
216
|
-
onChange={(e) =>
|
|
217
|
-
placeholder="
|
|
333
|
+
type="password"
|
|
334
|
+
value={config.anthropicApiKey}
|
|
335
|
+
onChange={(e) => updateField('anthropicApiKey', e.target.value)}
|
|
336
|
+
placeholder="Enter Anthropic API Key..."
|
|
218
337
|
/>
|
|
219
338
|
</div>
|
|
220
|
-
)}
|
|
221
|
-
<div className="setting-row">
|
|
222
|
-
<label>Anthropic API Key</label>
|
|
223
|
-
<input
|
|
224
|
-
type="password"
|
|
225
|
-
value={config.anthropicApiKey}
|
|
226
|
-
onChange={(e) => updateField('anthropicApiKey', e.target.value)}
|
|
227
|
-
placeholder="Enter Anthropic API Key..."
|
|
228
|
-
/>
|
|
229
339
|
</div>
|
|
230
340
|
</div>
|
|
231
|
-
</div>
|
|
232
341
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</div>
|
|
246
|
-
<div className="provider-card-body">
|
|
247
|
-
<div className="setting-row">
|
|
248
|
-
<label>OpenAI Model</label>
|
|
249
|
-
<select
|
|
250
|
-
value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
|
|
251
|
-
onChange={(e) => updateField('openaiModel', e.target.value)}
|
|
252
|
-
>
|
|
253
|
-
{OPENAI_MODELS.map(model => (
|
|
254
|
-
<option key={model} value={model}>{model}</option>
|
|
255
|
-
))}
|
|
256
|
-
<option value="custom">Custom...</option>
|
|
257
|
-
</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>}
|
|
258
354
|
</div>
|
|
259
|
-
|
|
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
|
+
)}
|
|
260
379
|
<div className="setting-row">
|
|
261
|
-
<label>
|
|
380
|
+
<label>OpenAI API Key</label>
|
|
262
381
|
<input
|
|
263
|
-
type="
|
|
264
|
-
value={
|
|
265
|
-
onChange={(e) =>
|
|
266
|
-
placeholder="
|
|
382
|
+
type="password"
|
|
383
|
+
value={config.openaiApiKey}
|
|
384
|
+
onChange={(e) => updateField('openaiApiKey', e.target.value)}
|
|
385
|
+
placeholder="Enter OpenAI API Key..."
|
|
267
386
|
/>
|
|
268
387
|
</div>
|
|
269
|
-
)}
|
|
270
|
-
<div className="setting-row">
|
|
271
|
-
<label>OpenAI API Key</label>
|
|
272
|
-
<input
|
|
273
|
-
type="password"
|
|
274
|
-
value={config.openaiApiKey}
|
|
275
|
-
onChange={(e) => updateField('openaiApiKey', e.target.value)}
|
|
276
|
-
placeholder="Enter OpenAI API Key..."
|
|
277
|
-
/>
|
|
278
388
|
</div>
|
|
279
389
|
</div>
|
|
280
|
-
</div>
|
|
281
390
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</div>
|
|
297
|
-
<div className="provider-card-body">
|
|
298
|
-
<div className="setting-row">
|
|
299
|
-
<label>OpenRouter Model</label>
|
|
300
|
-
<select
|
|
301
|
-
value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
|
|
302
|
-
onChange={(e) => updateField('openrouterModel', e.target.value)}
|
|
303
|
-
>
|
|
304
|
-
{OPENROUTER_MODELS.map(model => (
|
|
305
|
-
<option key={model} value={model}>{model}</option>
|
|
306
|
-
))}
|
|
307
|
-
<option value="custom">Custom...</option>
|
|
308
|
-
</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>}
|
|
309
405
|
</div>
|
|
310
|
-
|
|
406
|
+
<div className="provider-card-body">
|
|
407
|
+
<div className="setting-row">
|
|
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
|
+
)}
|
|
311
430
|
<div className="setting-row">
|
|
312
|
-
<label>
|
|
431
|
+
<label>OpenRouter API Key</label>
|
|
313
432
|
<input
|
|
314
|
-
type="
|
|
315
|
-
value={
|
|
316
|
-
onChange={(e) =>
|
|
317
|
-
placeholder="
|
|
433
|
+
type="password"
|
|
434
|
+
value={config.openrouterApiKey}
|
|
435
|
+
onChange={(e) => updateField('openrouterApiKey', e.target.value)}
|
|
436
|
+
placeholder="Enter OpenRouter API Key..."
|
|
318
437
|
/>
|
|
319
438
|
</div>
|
|
320
|
-
)}
|
|
321
|
-
<div className="setting-row">
|
|
322
|
-
<label>OpenRouter API Key</label>
|
|
323
|
-
<input
|
|
324
|
-
type="password"
|
|
325
|
-
value={config.openrouterApiKey}
|
|
326
|
-
onChange={(e) => updateField('openrouterApiKey', e.target.value)}
|
|
327
|
-
placeholder="Enter OpenRouter API Key..."
|
|
328
|
-
/>
|
|
329
439
|
</div>
|
|
330
440
|
</div>
|
|
331
|
-
</div>
|
|
332
441
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
</div>
|
|
345
|
-
<div className="provider-card-body">
|
|
346
|
-
<div className="setting-row">
|
|
347
|
-
<label>DeepSeek Model</label>
|
|
348
|
-
<select
|
|
349
|
-
value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
|
|
350
|
-
onChange={(e) => updateField('deepseekModel', e.target.value)}
|
|
351
|
-
>
|
|
352
|
-
{DEEPSEEK_MODELS.map(model => (
|
|
353
|
-
<option key={model} value={model}>{model}</option>
|
|
354
|
-
))}
|
|
355
|
-
<option value="custom">Custom...</option>
|
|
356
|
-
</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>}
|
|
357
453
|
</div>
|
|
358
|
-
|
|
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
|
+
)}
|
|
359
478
|
<div className="setting-row">
|
|
360
|
-
<label>
|
|
479
|
+
<label>DeepSeek API Key</label>
|
|
361
480
|
<input
|
|
362
|
-
type="
|
|
363
|
-
value={
|
|
364
|
-
onChange={(e) =>
|
|
365
|
-
placeholder="
|
|
481
|
+
type="password"
|
|
482
|
+
value={config.deepseekApiKey}
|
|
483
|
+
onChange={(e) => updateField('deepseekApiKey', e.target.value)}
|
|
484
|
+
placeholder="Enter DeepSeek API Key..."
|
|
366
485
|
/>
|
|
367
486
|
</div>
|
|
368
|
-
)}
|
|
369
|
-
<div className="setting-row">
|
|
370
|
-
<label>DeepSeek API Key</label>
|
|
371
|
-
<input
|
|
372
|
-
type="password"
|
|
373
|
-
value={config.deepseekApiKey}
|
|
374
|
-
onChange={(e) => updateField('deepseekApiKey', e.target.value)}
|
|
375
|
-
placeholder="Enter DeepSeek API Key..."
|
|
376
|
-
/>
|
|
377
487
|
</div>
|
|
378
488
|
</div>
|
|
379
|
-
</div>
|
|
380
489
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
</div>
|
|
395
|
-
<div className="provider-card-body">
|
|
396
|
-
<div className="setting-row">
|
|
397
|
-
<label>Hugging Face Model</label>
|
|
398
|
-
<select
|
|
399
|
-
value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
|
|
400
|
-
onChange={(e) => updateField('hfModel', e.target.value)}
|
|
401
|
-
>
|
|
402
|
-
{HF_MODELS.map(model => (
|
|
403
|
-
<option key={model} value={model}>{model}</option>
|
|
404
|
-
))}
|
|
405
|
-
<option value="custom">Custom...</option>
|
|
406
|
-
</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>}
|
|
407
503
|
</div>
|
|
408
|
-
|
|
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
|
+
)}
|
|
409
528
|
<div className="setting-row">
|
|
410
|
-
<label>
|
|
529
|
+
<label>Hugging Face API Key</label>
|
|
411
530
|
<input
|
|
412
|
-
type="
|
|
413
|
-
value={
|
|
414
|
-
onChange={(e) =>
|
|
415
|
-
placeholder="
|
|
531
|
+
type="password"
|
|
532
|
+
value={config.hfApiKey}
|
|
533
|
+
onChange={(e) => updateField('hfApiKey', e.target.value)}
|
|
534
|
+
placeholder="Enter Hugging Face API Key..."
|
|
416
535
|
/>
|
|
417
536
|
</div>
|
|
418
|
-
)}
|
|
419
|
-
<div className="setting-row">
|
|
420
|
-
<label>Hugging Face API Key</label>
|
|
421
|
-
<input
|
|
422
|
-
type="password"
|
|
423
|
-
value={config.hfApiKey}
|
|
424
|
-
onChange={(e) => updateField('hfApiKey', e.target.value)}
|
|
425
|
-
placeholder="Enter Hugging Face API Key..."
|
|
426
|
-
/>
|
|
427
537
|
</div>
|
|
428
538
|
</div>
|
|
429
|
-
</div>
|
|
430
539
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
</div>
|
|
444
|
-
<div className="provider-card-body">
|
|
445
|
-
<div className="setting-row">
|
|
446
|
-
<label>LM Studio Model</label>
|
|
447
|
-
<select
|
|
448
|
-
value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
|
|
449
|
-
onChange={(e) => updateField('localModelName', e.target.value)}
|
|
450
|
-
>
|
|
451
|
-
{LOCAL_MODELS.map(model => (
|
|
452
|
-
<option key={model} value={model}>{model}</option>
|
|
453
|
-
))}
|
|
454
|
-
<option value="custom">Custom...</option>
|
|
455
|
-
</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>}
|
|
456
552
|
</div>
|
|
457
|
-
|
|
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
|
+
)}
|
|
458
577
|
<div className="setting-row">
|
|
459
|
-
<label>
|
|
578
|
+
<label>LM Studio Base URL</label>
|
|
460
579
|
<input
|
|
461
580
|
type="text"
|
|
462
|
-
value={
|
|
463
|
-
onChange={(e) =>
|
|
464
|
-
placeholder="e.g.
|
|
581
|
+
value={config.localApiBaseUrl}
|
|
582
|
+
onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
|
|
583
|
+
placeholder="e.g. http://localhost:1234/v1"
|
|
465
584
|
/>
|
|
466
585
|
</div>
|
|
467
|
-
)}
|
|
468
|
-
<div className="setting-row">
|
|
469
|
-
<label>LM Studio Base URL</label>
|
|
470
|
-
<input
|
|
471
|
-
type="text"
|
|
472
|
-
value={config.localApiBaseUrl}
|
|
473
|
-
onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
|
|
474
|
-
placeholder="e.g. http://localhost:1234/v1"
|
|
475
|
-
/>
|
|
476
586
|
</div>
|
|
477
587
|
</div>
|
|
478
|
-
</div>
|
|
479
588
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
</div>
|
|
491
|
-
<div className="provider-card-body">
|
|
492
|
-
<div className="setting-row">
|
|
493
|
-
<label>Ollama Model</label>
|
|
494
|
-
<select
|
|
495
|
-
value={dynamicOllamaModels.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
|
|
496
|
-
onChange={(e) => updateField('ollamaModel', e.target.value)}
|
|
497
|
-
>
|
|
498
|
-
{dynamicOllamaModels.map(model => (
|
|
499
|
-
<option key={model} value={model}>{model}</option>
|
|
500
|
-
))}
|
|
501
|
-
{!dynamicOllamaModels.includes(config.ollamaModel) && config.ollamaModel && config.ollamaModel !== 'custom' && (
|
|
502
|
-
<option value={config.ollamaModel}>{config.ollamaModel}</option>
|
|
503
|
-
)}
|
|
504
|
-
<option value="custom">Custom...</option>
|
|
505
|
-
</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>}
|
|
506
599
|
</div>
|
|
507
|
-
|
|
600
|
+
<div className="provider-card-body">
|
|
508
601
|
<div className="setting-row">
|
|
509
|
-
<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>
|
|
510
629
|
<input
|
|
511
630
|
type="text"
|
|
512
|
-
value={
|
|
513
|
-
onChange={(e) =>
|
|
514
|
-
placeholder="e.g.
|
|
631
|
+
value={config.ollamaHost}
|
|
632
|
+
onChange={(e) => updateField('ollamaHost', e.target.value)}
|
|
633
|
+
placeholder="e.g. http://localhost:11434"
|
|
515
634
|
/>
|
|
516
635
|
</div>
|
|
517
|
-
)}
|
|
518
|
-
<div className="setting-row">
|
|
519
|
-
<label>Ollama Host</label>
|
|
520
|
-
<input
|
|
521
|
-
type="text"
|
|
522
|
-
value={config.ollamaHost}
|
|
523
|
-
onChange={(e) => updateField('ollamaHost', e.target.value)}
|
|
524
|
-
placeholder="e.g. http://localhost:11434"
|
|
525
|
-
/>
|
|
526
636
|
</div>
|
|
527
637
|
</div>
|
|
528
638
|
</div>
|
|
529
|
-
|
|
530
|
-
|
|
639
|
+
</>
|
|
640
|
+
)}
|
|
531
641
|
|
|
532
|
-
{/* ── Search ── */}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
</div>
|
|
539
|
-
<p className="section-description">Choose which search API Mint uses when performing web lookups.</p>
|
|
540
|
-
</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.',
|
|
541
648
|
<div className="provider-cards-container">
|
|
542
649
|
{/* Brave Search */}
|
|
543
650
|
<div className={`provider-card ${config.searchProvider === 'brave' ? 'active-provider' : ''}`}>
|
|
@@ -609,17 +716,14 @@ export default function GeneralTab({
|
|
|
609
716
|
</div>
|
|
610
717
|
</div>
|
|
611
718
|
</div>
|
|
612
|
-
|
|
719
|
+
)}
|
|
613
720
|
|
|
614
|
-
{/* ── Productivity ── */}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
</div>
|
|
621
|
-
<p className="section-description">Enable and configure Gmail, Google Calendar, and Notion plugins directly.</p>
|
|
622
|
-
</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.',
|
|
623
727
|
<div className="provider-cards-container">
|
|
624
728
|
{/* Gmail */}
|
|
625
729
|
<div className={`provider-card ${config.pluginGmailEnabled ? 'active-provider' : ''}`}>
|
|
@@ -764,17 +868,14 @@ export default function GeneralTab({
|
|
|
764
868
|
</div>
|
|
765
869
|
</div>
|
|
766
870
|
</div>
|
|
767
|
-
|
|
871
|
+
)}
|
|
768
872
|
|
|
769
|
-
{/* ── Image Generation ── */}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
</div>
|
|
776
|
-
<p className="section-description">Choose which image generation provider Mint uses.</p>
|
|
777
|
-
</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.',
|
|
778
879
|
<div className="provider-cards-container">
|
|
779
880
|
{/* Gemini Images */}
|
|
780
881
|
<div className={`provider-card ${config.imageGenProvider === 'gemini' ? 'active-provider' : ''}`}>
|
|
@@ -919,225 +1020,260 @@ export default function GeneralTab({
|
|
|
919
1020
|
</div>
|
|
920
1021
|
</div>
|
|
921
1022
|
</div>
|
|
922
|
-
|
|
1023
|
+
)}
|
|
923
1024
|
|
|
924
|
-
{/*
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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>
|
|
1063
|
+
</div>
|
|
930
1064
|
</div>
|
|
931
|
-
<p className="section-description">
|
|
932
|
-
Add any OpenAI-compatible endpoint as a custom provider. Configure its models and optional headers.
|
|
933
|
-
</p>
|
|
934
1065
|
</div>
|
|
1066
|
+
)}
|
|
935
1067
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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}`
|
|
939
1078
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
}
|
|
946
1085
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
}
|
|
952
1092
|
}
|
|
953
|
-
}
|
|
954
1093
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
}
|
|
965
1104
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
+
}
|
|
976
1115
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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>
|
|
1004
1144
|
</div>
|
|
1005
|
-
</div>
|
|
1006
1145
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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>
|
|
1025
1164
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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>
|
|
1035
1174
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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>
|
|
1045
1184
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
</
|
|
1073
|
-
</div>
|
|
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>
|
|
1074
1212
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
</
|
|
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>
|
|
1109
1247
|
</div>
|
|
1110
1248
|
</div>
|
|
1111
|
-
|
|
1112
|
-
)
|
|
1113
|
-
|
|
1114
|
-
</div>
|
|
1115
|
-
|
|
1116
|
-
<div className="setting-actions" style={{ marginTop: '16px' }}>
|
|
1117
|
-
<button
|
|
1118
|
-
className="btn btn-secondary"
|
|
1119
|
-
onClick={() => {
|
|
1120
|
-
const newId = `provider-${(config.customProviders ?? []).length + 1}`
|
|
1121
|
-
updateField('customProviders', [
|
|
1122
|
-
...(config.customProviders ?? []),
|
|
1123
|
-
{ id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
|
|
1124
|
-
])
|
|
1125
|
-
}}
|
|
1126
|
-
>
|
|
1127
|
-
+ Add custom provider
|
|
1128
|
-
</button>
|
|
1129
|
-
</div>
|
|
1130
|
-
</section>
|
|
1249
|
+
)
|
|
1250
|
+
})}
|
|
1251
|
+
</div>
|
|
1131
1252
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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>
|
|
1140
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
|
+
<>
|
|
1141
1277
|
<div className="toggle-row">
|
|
1142
1278
|
<div>
|
|
1143
1279
|
<label>Check signed update channel</label>
|
|
@@ -1167,32 +1303,29 @@ export default function GeneralTab({
|
|
|
1167
1303
|
{updateAvailable && <button type="button" className="btn-primary" onClick={handleInstallUpdate}>Install signed update</button>}
|
|
1168
1304
|
</div>
|
|
1169
1305
|
{updateMessage && <p className="hint">{updateMessage}</p>}
|
|
1170
|
-
|
|
1306
|
+
</>
|
|
1171
1307
|
)}
|
|
1172
1308
|
|
|
1173
|
-
{
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
<
|
|
1183
|
-
<label>Show Desktop AI Candidate</label>
|
|
1184
|
-
<p className="hint">Show the mini AI character on your desktop.</p>
|
|
1185
|
-
</div>
|
|
1186
|
-
<label className="settings-toggle-switch">
|
|
1187
|
-
<input
|
|
1188
|
-
type="checkbox"
|
|
1189
|
-
checked={config.showDesktopWidget}
|
|
1190
|
-
onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
|
|
1191
|
-
/>
|
|
1192
|
-
<span className="settings-toggle-slider"></span>
|
|
1193
|
-
</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>
|
|
1194
1319
|
</div>
|
|
1195
|
-
|
|
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>
|
|
1196
1329
|
)}
|
|
1197
1330
|
</div>
|
|
1198
1331
|
)
|