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