@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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +297 -107
- 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 +382 -127
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +391 -2506
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +152 -109
- package/crates/mint-cli/src/setup.rs +143 -143
- 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 +431 -65
- package/crates/mint-core/src/browser.rs +630 -102
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +174 -11
- package/crates/mint-core/src/code_tools.rs +47 -31
- package/crates/mint-core/src/config.rs +302 -17
- package/crates/mint-core/src/image_gen.rs +299 -38
- package/crates/mint-core/src/lib.rs +10 -7
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +237 -96
- package/crates/mint-core/src/pictures.rs +67 -5
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +128 -25
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- 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/components/AgentActivityTable.tsx +25 -6
- 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/fileIcons.ts +135 -0
- 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 +486 -69
- 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 +195 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
- 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 +106 -3
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- 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 +368 -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 +385 -93
- 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 +156 -5
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
- 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 +110 -3
- 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 +369 -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/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +155 -25
- 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
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
<
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
<div className="
|
|
79
|
-
|
|
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
|
-
|
|
169
|
+
)}
|
|
170
|
+
</section>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
92
173
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
258
|
+
<div className="provider-card-body">
|
|
119
259
|
<div className="setting-row">
|
|
120
|
-
<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="
|
|
123
|
-
value={
|
|
124
|
-
onChange={(e) =>
|
|
125
|
-
placeholder="
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
306
|
+
<div className="provider-card-body">
|
|
167
307
|
<div className="setting-row">
|
|
168
|
-
<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="
|
|
171
|
-
value={
|
|
172
|
-
onChange={(e) =>
|
|
173
|
-
placeholder="
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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>
|
|
380
|
+
<label>OpenAI API Key</label>
|
|
218
381
|
<input
|
|
219
|
-
type="
|
|
220
|
-
value={
|
|
221
|
-
onChange={(e) =>
|
|
222
|
-
placeholder="
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
406
|
+
<div className="provider-card-body">
|
|
267
407
|
<div className="setting-row">
|
|
268
|
-
<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="
|
|
271
|
-
value={
|
|
272
|
-
onChange={(e) =>
|
|
273
|
-
placeholder="
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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>
|
|
479
|
+
<label>DeepSeek API Key</label>
|
|
317
480
|
<input
|
|
318
|
-
type="
|
|
319
|
-
value={
|
|
320
|
-
onChange={(e) =>
|
|
321
|
-
placeholder="
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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>
|
|
529
|
+
<label>Hugging Face API Key</label>
|
|
367
530
|
<input
|
|
368
|
-
type="
|
|
369
|
-
value={
|
|
370
|
-
onChange={(e) =>
|
|
371
|
-
placeholder="
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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>
|
|
578
|
+
<label>LM Studio Base URL</label>
|
|
416
579
|
<input
|
|
417
580
|
type="text"
|
|
418
|
-
value={
|
|
419
|
-
onChange={(e) =>
|
|
420
|
-
placeholder="e.g.
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
600
|
+
<div className="provider-card-body">
|
|
464
601
|
<div className="setting-row">
|
|
465
|
-
<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={
|
|
469
|
-
onChange={(e) =>
|
|
470
|
-
placeholder="e.g.
|
|
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
|
-
|
|
486
|
-
|
|
639
|
+
</>
|
|
640
|
+
)}
|
|
487
641
|
|
|
488
|
-
{/* ── Search ── */}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
719
|
+
)}
|
|
569
720
|
|
|
570
|
-
{/* ── Productivity ── */}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
871
|
+
)}
|
|
724
872
|
|
|
725
|
-
{/* ── Image Generation ── */}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
879
|
-
|
|
1023
|
+
)}
|
|
880
1024
|
|
|
881
|
-
{
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
1306
|
+
</>
|
|
920
1307
|
)}
|
|
921
1308
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
</
|
|
931
|
-
|
|
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
|
-
|
|
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
|
)
|