@pheem49/mint 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +9 -0
- package/crates/mint-cli/src/agent.rs +1 -1
- package/crates/mint-cli/src/main.rs +47 -24
- package/crates/mint-cli/src/setup.rs +1 -1
- package/crates/mint-core/src/api_server.rs +64 -10
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/lib.rs +1 -1
- package/crates/mint-core/src/plugins.rs +4 -1
- package/crates/mint-core/src/workflows.rs +8 -0
- package/package.json +13 -2
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +27 -14
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src/components/MintDashboard.tsx +199 -5
- 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 +452 -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 -962
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/css/image-studio.css +9 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +355 -38
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +97 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
- 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 +452 -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 -968
- package/src/renderer/src-web/css/image-studio.css +9 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +294 -38
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/tauri.ts +36 -0
- package/src-tauri/src/lib.rs +84 -92
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
interface MemoryTabProps {
|
|
4
|
+
userName: string
|
|
5
|
+
setUserName: (val: string) => void
|
|
6
|
+
userPreferences: string
|
|
7
|
+
setUserPreferences: (val: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function MemoryTab({
|
|
11
|
+
userName,
|
|
12
|
+
setUserName,
|
|
13
|
+
userPreferences,
|
|
14
|
+
setUserPreferences
|
|
15
|
+
}: MemoryTabProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="tab-pane active animate-fade-in" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
18
|
+
<section className="setting-section">
|
|
19
|
+
<div className="section-heading">
|
|
20
|
+
<div>
|
|
21
|
+
<p className="section-kicker">Cross-session Memory</p>
|
|
22
|
+
<h2 className="section-title">User Profile & Preferences</h2>
|
|
23
|
+
</div>
|
|
24
|
+
<p className="section-description">
|
|
25
|
+
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.
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="form-grid single">
|
|
30
|
+
<div className="setting-row">
|
|
31
|
+
<label>Your Name / Nickname</label>
|
|
32
|
+
<div className="memory-field-container">
|
|
33
|
+
<input
|
|
34
|
+
type="text"
|
|
35
|
+
value={userName}
|
|
36
|
+
onChange={(e) => setUserName(e.target.value)}
|
|
37
|
+
placeholder="e.g. Pheem, Jane"
|
|
38
|
+
/>
|
|
39
|
+
<p className="hint">Used by the assistant to address you in conversation.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="setting-row">
|
|
44
|
+
<label>Custom Instructions & Preferences</label>
|
|
45
|
+
<div className="memory-field-container">
|
|
46
|
+
<textarea
|
|
47
|
+
value={userPreferences}
|
|
48
|
+
onChange={(e) => setUserPreferences(e.target.value)}
|
|
49
|
+
placeholder="e.g. Explain coding concepts step-by-step. Prefer TypeScript. Talk in Thai. Keep explanations concise."
|
|
50
|
+
style={{ minHeight: '120px', resize: 'vertical' }}
|
|
51
|
+
/>
|
|
52
|
+
<p className="hint">Preferences, guidelines, or persona instructions for the assistant.</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { DEFAULT_CONFIG } from '../SettingsWindow'
|
|
3
|
+
import { listLearnedSkills, addLearnedSkill, deleteLearnedSkill, LearnedSkill } from '../../tauri'
|
|
4
|
+
|
|
5
|
+
interface PluginsTabProps {
|
|
6
|
+
config: typeof DEFAULT_CONFIG
|
|
7
|
+
updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
|
|
8
|
+
mcpName: string
|
|
9
|
+
setMcpName: (val: string) => void
|
|
10
|
+
mcpCmd: string
|
|
11
|
+
setMcpCmd: (val: string) => void
|
|
12
|
+
mcpArgs: string
|
|
13
|
+
setMcpArgs: (val: string) => void
|
|
14
|
+
mcpEnv: string
|
|
15
|
+
setMcpEnv: (val: string) => void
|
|
16
|
+
handleAddMcpServer: () => void
|
|
17
|
+
handleRemoveMcpServer: (name: string) => void
|
|
18
|
+
handleConnectPlugin: (plugin: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function PluginsTab({
|
|
22
|
+
config,
|
|
23
|
+
updateField,
|
|
24
|
+
mcpName,
|
|
25
|
+
setMcpName,
|
|
26
|
+
mcpCmd,
|
|
27
|
+
setMcpCmd,
|
|
28
|
+
mcpArgs,
|
|
29
|
+
setMcpArgs,
|
|
30
|
+
mcpEnv,
|
|
31
|
+
setMcpEnv,
|
|
32
|
+
handleAddMcpServer,
|
|
33
|
+
handleRemoveMcpServer,
|
|
34
|
+
handleConnectPlugin
|
|
35
|
+
}: PluginsTabProps) {
|
|
36
|
+
// Local state for learned skills
|
|
37
|
+
const [skills, setSkills] = useState<LearnedSkill[]>([])
|
|
38
|
+
const [newSkillName, setNewSkillName] = useState('')
|
|
39
|
+
const [newSkillContent, setNewSkillContent] = useState('')
|
|
40
|
+
const [skillsLoading, setSkillsLoading] = useState(false)
|
|
41
|
+
const [skillsError, setSkillsError] = useState('')
|
|
42
|
+
|
|
43
|
+
// Load skills on mount
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
fetchSkills()
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const fetchSkills = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const list = await listLearnedSkills()
|
|
51
|
+
setSkills(list)
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("Failed to load skills:", e)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleAddSkill = async (e: React.FormEvent) => {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
if (!newSkillName.trim() || !newSkillContent.trim()) {
|
|
60
|
+
setSkillsError('Please fill in both name and content.')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cleanName = newSkillName.trim().replace(/\s+/g, '-').toLowerCase()
|
|
65
|
+
setSkillsLoading(true)
|
|
66
|
+
setSkillsError('')
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await addLearnedSkill(cleanName, newSkillContent)
|
|
70
|
+
setNewSkillName('')
|
|
71
|
+
setNewSkillContent('')
|
|
72
|
+
await fetchSkills()
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
setSkillsError(err.message || String(err))
|
|
75
|
+
} finally {
|
|
76
|
+
setSkillsLoading(false)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleDeleteSkill = async (name: string) => {
|
|
81
|
+
if (confirm(`Forget skill "${name}"?`)) {
|
|
82
|
+
try {
|
|
83
|
+
await deleteLearnedSkill(name)
|
|
84
|
+
await fetchSkills()
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error("Failed to delete skill:", e)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Local state to toggle expansion of configuration sections
|
|
92
|
+
const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null)
|
|
93
|
+
|
|
94
|
+
const toggleExpand = (pluginKey: string) => {
|
|
95
|
+
setExpandedPlugin(expandedPlugin === pluginKey ? null : pluginKey)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pluginsList: Array<{
|
|
99
|
+
key: string
|
|
100
|
+
name: string
|
|
101
|
+
desc: string
|
|
102
|
+
icon: string
|
|
103
|
+
enabledField: 'pluginSpotifyEnabled' | 'pluginDiscordEnabled'
|
|
104
|
+
hasCredentials: boolean
|
|
105
|
+
fields?: Array<{ label: string; field: any; type: string; placeholder: string }>
|
|
106
|
+
}> = [
|
|
107
|
+
{
|
|
108
|
+
key: 'spotify',
|
|
109
|
+
name: 'Spotify',
|
|
110
|
+
desc: 'Control playback with AI. Requires playerctl locally.',
|
|
111
|
+
icon: '🎵',
|
|
112
|
+
enabledField: 'pluginSpotifyEnabled' as const,
|
|
113
|
+
hasCredentials: false
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: 'discord',
|
|
117
|
+
name: 'Discord RPC',
|
|
118
|
+
desc: 'Show "Using Mint Assistant" status in your local Discord client.',
|
|
119
|
+
icon: '💬',
|
|
120
|
+
enabledField: 'pluginDiscordEnabled' as const,
|
|
121
|
+
hasCredentials: false
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="tab-pane active" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
|
|
127
|
+
{/* ── Learned AI Skills ── */}
|
|
128
|
+
<section className="setting-section">
|
|
129
|
+
<div className="section-heading">
|
|
130
|
+
<div>
|
|
131
|
+
<p className="section-kicker">Knowledge Base</p>
|
|
132
|
+
<h2 className="section-title">Learned AI Skills</h2>
|
|
133
|
+
</div>
|
|
134
|
+
<p className="section-description">
|
|
135
|
+
Skills are special instructions or guides taught to Mint (equivalent to <code>mint learn</code> in CLI). The AI reads active skills before every prompt to align with your guidelines.
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{skills.length === 0 ? (
|
|
140
|
+
<div className="empty-skills-notice" style={{
|
|
141
|
+
padding: '24px',
|
|
142
|
+
border: '1px dashed var(--border)',
|
|
143
|
+
borderRadius: '12px',
|
|
144
|
+
textAlign: 'center',
|
|
145
|
+
color: 'var(--text-muted)',
|
|
146
|
+
background: 'rgba(0,0,0,0.06)',
|
|
147
|
+
fontSize: '0.9rem',
|
|
148
|
+
marginBottom: '20px'
|
|
149
|
+
}}>
|
|
150
|
+
No learned skills found. Teach Mint a skill below or run <code>/learn <path></code> in chat!
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
<div className="skills-list" style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
|
|
154
|
+
{skills.map((s) => (
|
|
155
|
+
<div className="skill-card" key={s.id} style={{
|
|
156
|
+
display: 'flex',
|
|
157
|
+
alignItems: 'flex-start',
|
|
158
|
+
justifyContent: 'space-between',
|
|
159
|
+
padding: '16px 20px',
|
|
160
|
+
border: '1px solid var(--border)',
|
|
161
|
+
borderRadius: '12px',
|
|
162
|
+
background: 'var(--surface-bg)',
|
|
163
|
+
gap: '16px'
|
|
164
|
+
}}>
|
|
165
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
166
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
|
167
|
+
<span className="badge" style={{
|
|
168
|
+
background: 'var(--accent)',
|
|
169
|
+
color: '#fff',
|
|
170
|
+
fontSize: '0.75rem',
|
|
171
|
+
fontWeight: '600',
|
|
172
|
+
padding: '2px 8px',
|
|
173
|
+
borderRadius: '4px'
|
|
174
|
+
}}>{s.name}</span>
|
|
175
|
+
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
|
|
176
|
+
Source: {s.sourcePath}
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div style={{
|
|
180
|
+
fontSize: '0.85rem',
|
|
181
|
+
color: 'var(--text-main)',
|
|
182
|
+
whiteSpace: 'pre-wrap',
|
|
183
|
+
maxHeight: '120px',
|
|
184
|
+
overflowY: 'auto',
|
|
185
|
+
background: 'rgba(0,0,0,0.12)',
|
|
186
|
+
padding: '10px 14px',
|
|
187
|
+
borderRadius: '8px',
|
|
188
|
+
fontFamily: 'monospace',
|
|
189
|
+
lineHeight: '1.4'
|
|
190
|
+
}}>
|
|
191
|
+
{s.content}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<button
|
|
195
|
+
className="btn btn-danger"
|
|
196
|
+
onClick={() => handleDeleteSkill(s.name)}
|
|
197
|
+
style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px', flexShrink: 0 }}
|
|
198
|
+
>
|
|
199
|
+
Forget
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<form className="add-skill-box" onSubmit={handleAddSkill} style={{
|
|
207
|
+
padding: '20px',
|
|
208
|
+
border: '1px solid var(--border)',
|
|
209
|
+
borderRadius: '12px',
|
|
210
|
+
background: 'var(--surface-bg)'
|
|
211
|
+
}}>
|
|
212
|
+
<h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Teach New Skill</h3>
|
|
213
|
+
{skillsError && <div style={{ color: '#ef4444', fontSize: '0.85rem', marginBottom: '12px', fontWeight: '500' }}>{skillsError}</div>}
|
|
214
|
+
|
|
215
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
|
|
216
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Name (e.g. coding-guidelines)</label>
|
|
217
|
+
<input
|
|
218
|
+
type="text"
|
|
219
|
+
placeholder="e.g. angular-standard"
|
|
220
|
+
value={newSkillName}
|
|
221
|
+
onChange={(e) => setNewSkillName(e.target.value)}
|
|
222
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
|
|
227
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Instructions / Content</label>
|
|
228
|
+
<textarea
|
|
229
|
+
placeholder="# Instructions Write only clean TypeScript. Use async/await. Avoid let where const is possible."
|
|
230
|
+
value={newSkillContent}
|
|
231
|
+
onChange={(e) => setNewSkillContent(e.target.value)}
|
|
232
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '100px', resize: 'vertical' }}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<button
|
|
237
|
+
type="submit"
|
|
238
|
+
className="btn-primary"
|
|
239
|
+
disabled={skillsLoading}
|
|
240
|
+
style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}
|
|
241
|
+
>
|
|
242
|
+
{skillsLoading ? 'Learning...' : 'Teach Skill'}
|
|
243
|
+
</button>
|
|
244
|
+
</form>
|
|
245
|
+
</section>
|
|
246
|
+
|
|
247
|
+
{/* ── External tools (MCP Servers) ── */}
|
|
248
|
+
<section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
|
|
249
|
+
<div className="section-heading">
|
|
250
|
+
<div>
|
|
251
|
+
<p className="section-kicker">External tools</p>
|
|
252
|
+
<h2 className="section-title">MCP Servers</h2>
|
|
253
|
+
</div>
|
|
254
|
+
<p className="section-description">Connect Mint to tools like search, GitHub, or filesystem servers.</p>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div className="mcp-list">
|
|
258
|
+
{Object.entries(config.mcpServers || {}).map(([name, srv]: [string, any]) => (
|
|
259
|
+
<div className="plugin-card" key={name} style={{
|
|
260
|
+
display: 'flex',
|
|
261
|
+
alignItems: 'center',
|
|
262
|
+
justifyContent: 'space-between',
|
|
263
|
+
padding: '16px 20px',
|
|
264
|
+
border: '1px solid var(--border)',
|
|
265
|
+
borderRadius: '12px',
|
|
266
|
+
background: 'var(--surface-bg)',
|
|
267
|
+
marginBottom: '10px'
|
|
268
|
+
}}>
|
|
269
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
270
|
+
<div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>⚙</div>
|
|
271
|
+
<div className="plugin-info">
|
|
272
|
+
<div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{name}</div>
|
|
273
|
+
<div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
|
|
274
|
+
Command: {srv.command} {srv.args?.join(' ')}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="plugin-actions">
|
|
279
|
+
<button className="btn btn-danger" onClick={() => handleRemoveMcpServer(name)} style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}>Remove</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
))}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div className="add-mcp-box" style={{
|
|
286
|
+
marginTop: '20px',
|
|
287
|
+
padding: '20px',
|
|
288
|
+
border: '1px solid var(--border)',
|
|
289
|
+
borderRadius: '12px',
|
|
290
|
+
background: 'var(--surface-bg)'
|
|
291
|
+
}}>
|
|
292
|
+
<h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Add MCP Server</h3>
|
|
293
|
+
<div className="form-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
|
|
294
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
295
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Server Name</label>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
placeholder="e.g. google-search"
|
|
299
|
+
value={mcpName}
|
|
300
|
+
onChange={(e) => setMcpName(e.target.value)}
|
|
301
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
305
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Command</label>
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
placeholder="e.g. npx"
|
|
309
|
+
value={mcpCmd}
|
|
310
|
+
onChange={(e) => setMcpCmd(e.target.value)}
|
|
311
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
|
|
316
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Arguments</label>
|
|
317
|
+
<input
|
|
318
|
+
type="text"
|
|
319
|
+
placeholder="e.g. -y @modelcontextprotocol/server-brave-search"
|
|
320
|
+
value={mcpArgs}
|
|
321
|
+
onChange={(e) => setMcpArgs(e.target.value)}
|
|
322
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%' }}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
|
|
326
|
+
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Environment Variables (JSON)</label>
|
|
327
|
+
<textarea
|
|
328
|
+
placeholder='e.g. {"BRAVE_API_KEY": "your_key_here"}'
|
|
329
|
+
value={mcpEnv}
|
|
330
|
+
onChange={(e) => setMcpEnv(e.target.value)}
|
|
331
|
+
style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '70px', resize: 'vertical' }}
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
<button className="btn-primary" onClick={handleAddMcpServer} style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}>Add MCP Server</button>
|
|
335
|
+
</div>
|
|
336
|
+
</section>
|
|
337
|
+
|
|
338
|
+
{/* ── Built-in Plugins ── */}
|
|
339
|
+
<section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
|
|
340
|
+
<div className="section-heading">
|
|
341
|
+
<div>
|
|
342
|
+
<p className="section-kicker">Integrations</p>
|
|
343
|
+
<h2 className="section-title">Built-in Plugins</h2>
|
|
344
|
+
</div>
|
|
345
|
+
<p className="section-description">Enable and configure credentials for native Mint plugins.</p>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="plugin-list" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
348
|
+
{pluginsList.map(p => {
|
|
349
|
+
const isEnabled = config[p.enabledField]
|
|
350
|
+
const isExpanded = expandedPlugin === p.key
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div className={`plugin-card-wrapper ${isEnabled ? 'active-plugin-card' : ''}`} key={p.key} style={{
|
|
354
|
+
border: '1px solid var(--border)',
|
|
355
|
+
borderRadius: '12px',
|
|
356
|
+
background: 'var(--surface-bg)',
|
|
357
|
+
transition: 'all 0.2s ease',
|
|
358
|
+
overflow: 'hidden'
|
|
359
|
+
}}>
|
|
360
|
+
<div className="plugin-card" style={{
|
|
361
|
+
display: 'flex',
|
|
362
|
+
alignItems: 'center',
|
|
363
|
+
justifyContent: 'space-between',
|
|
364
|
+
padding: '16px 20px',
|
|
365
|
+
background: isEnabled ? 'color-mix(in srgb, var(--accent) 3%, var(--surface-bg))' : 'transparent'
|
|
366
|
+
}}>
|
|
367
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
368
|
+
<div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
|
|
369
|
+
{p.icon}
|
|
370
|
+
</div>
|
|
371
|
+
<div className="plugin-info">
|
|
372
|
+
<div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{p.name}</div>
|
|
373
|
+
<div className="plugin-desc" style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>{p.desc}</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
377
|
+
{p.hasCredentials && (
|
|
378
|
+
<button
|
|
379
|
+
className="btn-secondary"
|
|
380
|
+
onClick={() => toggleExpand(p.key)}
|
|
381
|
+
style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
|
|
382
|
+
>
|
|
383
|
+
{isExpanded ? 'Hide Config' : 'Configure'}
|
|
384
|
+
</button>
|
|
385
|
+
)}
|
|
386
|
+
{p.key === 'discord' && isEnabled && (
|
|
387
|
+
<button
|
|
388
|
+
className="btn-secondary"
|
|
389
|
+
onClick={() => handleConnectPlugin('discord')}
|
|
390
|
+
style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
|
|
391
|
+
>
|
|
392
|
+
Update RPC
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
395
|
+
<label className="settings-toggle-switch">
|
|
396
|
+
<input
|
|
397
|
+
type="checkbox"
|
|
398
|
+
checked={isEnabled}
|
|
399
|
+
onChange={(e) => {
|
|
400
|
+
updateField(p.enabledField, e.target.checked)
|
|
401
|
+
if (!e.target.checked && expandedPlugin === p.key) {
|
|
402
|
+
// Don't auto-collapse config on disable so user can still edit
|
|
403
|
+
}
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
<span className="settings-toggle-slider" />
|
|
407
|
+
</label>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
{p.hasCredentials && isExpanded && (
|
|
412
|
+
<div className="plugin-config-panel" style={{
|
|
413
|
+
padding: '20px',
|
|
414
|
+
borderTop: '1px solid var(--border)',
|
|
415
|
+
background: 'rgba(0,0,0,0.12)',
|
|
416
|
+
display: 'flex',
|
|
417
|
+
flexDirection: 'column',
|
|
418
|
+
gap: '12px'
|
|
419
|
+
}}>
|
|
420
|
+
<h4 style={{ fontSize: '0.9rem', fontWeight: '600', color: 'var(--text-soft)', marginBottom: '4px' }}>Credentials Configuration</h4>
|
|
421
|
+
<div className="form-grid compact" style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
|
|
422
|
+
{p.fields?.map(f => (
|
|
423
|
+
<div className="setting-row" key={f.field} style={{ display: 'flex', flexDirection: 'column', gap: '6px', border: 'none', padding: 0 }}>
|
|
424
|
+
<label style={{ fontSize: '0.8rem', fontWeight: '500', color: 'var(--text-muted)' }}>{f.label}</label>
|
|
425
|
+
<input
|
|
426
|
+
type={f.type}
|
|
427
|
+
placeholder={f.placeholder}
|
|
428
|
+
value={(config as any)[f.field] || ''}
|
|
429
|
+
onChange={(e) => updateField(f.field, e.target.value)}
|
|
430
|
+
style={{
|
|
431
|
+
width: '100%',
|
|
432
|
+
padding: '10px 14px',
|
|
433
|
+
borderRadius: '8px',
|
|
434
|
+
border: '1px solid var(--border)',
|
|
435
|
+
background: 'var(--input-bg)',
|
|
436
|
+
color: 'var(--text-main)',
|
|
437
|
+
fontSize: '0.9rem'
|
|
438
|
+
}}
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
))}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
})}
|
|
448
|
+
</div>
|
|
449
|
+
</section>
|
|
450
|
+
</div>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export default function ShortcutsTab() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="tab-pane active">
|
|
6
|
+
<section className="setting-section">
|
|
7
|
+
<div className="section-heading">
|
|
8
|
+
<div>
|
|
9
|
+
<p className="section-kicker">Keyboard</p>
|
|
10
|
+
<h2 className="section-title">Shortcuts</h2>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<div className="shortcut-list">
|
|
14
|
+
<div className="shortcut-item">
|
|
15
|
+
<span>Show / Hide Mint Window</span>
|
|
16
|
+
<div className="keys"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>Space</kbd></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="shortcut-item">
|
|
19
|
+
<span>Open Spotlight</span>
|
|
20
|
+
<div className="keys"><kbd>Alt</kbd><kbd>Space</kbd></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="shortcut-item">
|
|
23
|
+
<span>Close / Dismiss</span>
|
|
24
|
+
<div className="keys"><kbd>Esc</kbd></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { DEFAULT_CONFIG } from '../SettingsWindow' // we can import it or define type locally
|
|
3
|
+
|
|
4
|
+
interface ThemeTabProps {
|
|
5
|
+
config: typeof DEFAULT_CONFIG
|
|
6
|
+
updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function ThemeTab({ config, updateField }: ThemeTabProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="tab-pane active">
|
|
12
|
+
<section className="setting-section">
|
|
13
|
+
<div className="section-heading">
|
|
14
|
+
<div>
|
|
15
|
+
<p className="section-kicker">Appearance</p>
|
|
16
|
+
<h2 className="section-title">Theme</h2>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="theme-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
|
|
20
|
+
{(['dark', 'light', 'midnight', 'custom'] as const).map(t => (
|
|
21
|
+
<button
|
|
22
|
+
key={t}
|
|
23
|
+
className={`theme-card ${config.theme === t ? 'active' : ''}`}
|
|
24
|
+
onClick={() => updateField('theme', t)}
|
|
25
|
+
>
|
|
26
|
+
<div className={`theme-preview ${t}-preview`}></div>
|
|
27
|
+
<span style={{ textTransform: 'capitalize' }}>{t}</span>
|
|
28
|
+
</button>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{config.theme === 'custom' && (
|
|
33
|
+
<div className="custom-theme-panel" style={{ marginTop: '15px' }}>
|
|
34
|
+
<div className="setting-row">
|
|
35
|
+
<label>Background Gradient</label>
|
|
36
|
+
<div className="color-range" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
37
|
+
<input type="color" value={config.customBgStart} onChange={(e) => updateField('customBgStart', e.target.value)} />
|
|
38
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
|
|
39
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
40
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
41
|
+
<polyline points="12 5 19 12 12 19"></polyline>
|
|
42
|
+
</svg>
|
|
43
|
+
</span>
|
|
44
|
+
<input type="color" value={config.customBgEnd} onChange={(e) => updateField('customBgEnd', e.target.value)} />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="setting-row">
|
|
48
|
+
<label>Panel Background</label>
|
|
49
|
+
<input type="color" value={config.customPanelBg} onChange={(e) => updateField('customPanelBg', e.target.value)} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section className="setting-section">
|
|
56
|
+
<div className="section-heading">
|
|
57
|
+
<div>
|
|
58
|
+
<p className="section-kicker">Color</p>
|
|
59
|
+
<h2 className="section-title">Accent & Text</h2>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="color-section">
|
|
63
|
+
<div>
|
|
64
|
+
<label>Accent Color</label>
|
|
65
|
+
<div className="color-presets">
|
|
66
|
+
{['#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899'].map(c => (
|
|
67
|
+
<button
|
|
68
|
+
key={c}
|
|
69
|
+
className="color-dot"
|
|
70
|
+
style={{ backgroundColor: c, border: config.accentColor === c ? '2px solid white' : 'none' }}
|
|
71
|
+
onClick={() => updateField('accentColor', c)}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="color-inputs" style={{ display: 'flex', gap: '15px', marginTop: '12px' }}>
|
|
77
|
+
<div>
|
|
78
|
+
<label>Custom Accent</label>
|
|
79
|
+
<input type="color" value={config.accentColor} onChange={(e) => updateField('accentColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label>System Text</label>
|
|
83
|
+
<input type="color" value={config.systemTextColor} onChange={(e) => updateField('systemTextColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
<section className="setting-section">
|
|
90
|
+
<div className="section-heading">
|
|
91
|
+
<div>
|
|
92
|
+
<p className="section-kicker">Surface</p>
|
|
93
|
+
<h2 className="section-title">Interface Style</h2>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="form-grid">
|
|
97
|
+
<div className="setting-row">
|
|
98
|
+
<label>Glass Blur</label>
|
|
99
|
+
<select value={config.glassBlur} onChange={(e) => updateField('glassBlur', e.target.value)}>
|
|
100
|
+
<option value="blur(4px)">Low (4px)</option>
|
|
101
|
+
<option value="blur(16px)">Medium (16px) - Default</option>
|
|
102
|
+
<option value="blur(32px)">High (32px)</option>
|
|
103
|
+
<option value="none">Off (Solid)</option>
|
|
104
|
+
</select>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="setting-row">
|
|
107
|
+
<label>Font Family</label>
|
|
108
|
+
<select value={config.fontFamily} onChange={(e) => updateField('fontFamily', e.target.value)}>
|
|
109
|
+
<option value="'Outfit', sans-serif">Outfit (Default)</option>
|
|
110
|
+
<option value="'Mali', cursive">Mali (Cute Thai Font)</option>
|
|
111
|
+
<option value="'Prompt', sans-serif">Prompt (Modern Thai)</option>
|
|
112
|
+
<option value="'Sarabun', sans-serif">Sarabun (Formal Thai)</option>
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="setting-row">
|
|
116
|
+
<label>Font Size</label>
|
|
117
|
+
<select value={config.fontSize} onChange={(e) => updateField('fontSize', e.target.value)}>
|
|
118
|
+
<option value="16px">Small</option>
|
|
119
|
+
<option value="18px">Medium (Default)</option>
|
|
120
|
+
<option value="22px">Large</option>
|
|
121
|
+
<option value="26px">Extra Large</option>
|
|
122
|
+
<option value="30px">Extra Extra Large</option>
|
|
123
|
+
</select>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|