@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.
Files changed (55) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/crates/mint-cli/src/agent.rs +1 -1
  5. package/crates/mint-cli/src/main.rs +47 -24
  6. package/crates/mint-cli/src/setup.rs +1 -1
  7. package/crates/mint-core/src/api_server.rs +64 -10
  8. package/crates/mint-core/src/chat.rs +79 -0
  9. package/crates/mint-core/src/lib.rs +1 -1
  10. package/crates/mint-core/src/plugins.rs +4 -1
  11. package/crates/mint-core/src/workflows.rs +8 -0
  12. package/package.json +13 -2
  13. package/src/renderer/src/App.tsx +1 -1
  14. package/src/renderer/src/components/ChatPanel.tsx +27 -14
  15. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  16. package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
  17. package/src/renderer/src/components/MintDashboard.tsx +199 -5
  18. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  19. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  20. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  21. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  22. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  23. package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
  24. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  25. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  26. package/src/renderer/src/components/SettingsWindow.tsx +175 -962
  27. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  28. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  29. package/src/renderer/src/css/image-studio.css +9 -4
  30. package/src/renderer/src/css/settings.css +75 -6
  31. package/src/renderer/src/css/styles.css +355 -38
  32. package/src/renderer/src/css/workflows.css +542 -0
  33. package/src/renderer/src/env.d.ts +2 -0
  34. package/src/renderer/src/index.css +1 -0
  35. package/src/renderer/src/tauri.ts +97 -0
  36. package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
  37. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  38. package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
  39. package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
  40. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  41. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  42. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  43. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  44. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  45. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
  46. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  47. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  48. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
  49. package/src/renderer/src-web/css/image-studio.css +9 -4
  50. package/src/renderer/src-web/css/settings.css +75 -6
  51. package/src/renderer/src-web/css/styles.css +294 -38
  52. package/src/renderer/src-web/env.d.ts +2 -0
  53. package/src/renderer/src-web/tauri.ts +36 -0
  54. package/src-tauri/src/lib.rs +84 -92
  55. package/src-tauri/tauri.conf.json +1 -1
@@ -1,11 +1,13 @@
1
- import { useState } from 'react'
1
+ import { useState, useEffect, useRef } from 'react'
2
2
 
3
- export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspeac' | 'imagine'
3
+ export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspace' | 'imagine' | 'workflows'
4
4
 
5
5
  interface ChatSessionItem {
6
6
  id: string
7
7
  title: string
8
8
  kind: string
9
+ createdAt?: string
10
+ updatedAt?: string
9
11
  }
10
12
 
11
13
  interface DashboardSidebarProps {
@@ -33,6 +35,8 @@ interface DashboardSidebarProps {
33
35
  onSetInteractionEnabled: (enabled: boolean) => void
34
36
  onSetShowInteractionGuide: (visible: boolean) => void
35
37
  onShowToast: (message: string) => void
38
+ isSearchOpen: boolean
39
+ onSetSearchOpen: (open: boolean) => void
36
40
  }
37
41
 
38
42
  export default function DashboardSidebar({
@@ -60,9 +64,23 @@ export default function DashboardSidebar({
60
64
  onSetInteractionEnabled,
61
65
  onSetShowInteractionGuide,
62
66
  onShowToast,
67
+ isSearchOpen,
68
+ onSetSearchOpen,
63
69
  }: DashboardSidebarProps) {
64
70
  const [editingSessionId, setEditingSessionId] = useState<string | null>(null)
65
71
  const [editTitleValue, setEditTitleValue] = useState('')
72
+ const [isMoreOpen, setIsMoreOpen] = useState(false)
73
+ const moreContainerRef = useRef<HTMLDivElement>(null)
74
+
75
+ useEffect(() => {
76
+ function handleClickOutside(event: MouseEvent) {
77
+ if (moreContainerRef.current && !moreContainerRef.current.contains(event.target as Node)) {
78
+ setIsMoreOpen(false)
79
+ }
80
+ }
81
+ document.addEventListener('mousedown', handleClickOutside)
82
+ return () => document.removeEventListener('mousedown', handleClickOutside)
83
+ }, [])
66
84
 
67
85
  const handleSaveRename = (id: string) => {
68
86
  if (editTitleValue.trim() && editTitleValue.trim() !== chatSessions.find(s => s.id === id)?.title) {
@@ -115,6 +133,16 @@ export default function DashboardSidebar({
115
133
  <span>New Chat</span>
116
134
  </button>
117
135
 
136
+ <button className="sidebar-top-action sidebar-search-btn" onClick={() => onSetSearchOpen(true)}>
137
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
138
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
139
+ <circle cx="11" cy="11" r="8"></circle>
140
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
141
+ </svg>
142
+ </span>
143
+ <span>Search Chats</span>
144
+ </button>
145
+
118
146
  <button className={`sidebar-top-action ${view === 'chat' ? 'is-active' : ''}`} onClick={() => onSetView('chat')}>
119
147
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
120
148
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -133,22 +161,14 @@ export default function DashboardSidebar({
133
161
  </span>
134
162
  <span>Pictures</span>
135
163
  </button>
136
- <button className={`sidebar-top-action ${view === 'imagine' ? 'is-active' : ''}`} onClick={() => onSetView('imagine')}>
137
- <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
138
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
139
- <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
140
- </svg>
141
- </span>
142
- <span>Image Studio</span>
143
- </button>
144
- <button className={`sidebar-top-action ${view === 'workspeac' ? 'is-active' : ''}`} onClick={() => onSetView('workspeac')}>
164
+ <button className={`sidebar-top-action ${view === 'workspace' ? 'is-active' : ''}`} onClick={() => onSetView('workspace')}>
145
165
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
146
166
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
147
167
  <path d="M3 6h7l2 2h9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>
148
168
  <path d="M3 6v12"></path>
149
169
  </svg>
150
170
  </span>
151
- <span>Workspeac</span>
171
+ <span>Workspace</span>
152
172
  </button>
153
173
  <button className={`sidebar-top-action ${modelVisible ? 'is-active' : ''}`} onClick={onToggleModel}>
154
174
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
@@ -160,6 +180,45 @@ export default function DashboardSidebar({
160
180
  <span>Live2D Model</span>
161
181
  </button>
162
182
 
183
+ <div className="sidebar-more-container" ref={moreContainerRef}>
184
+ <button className={`sidebar-top-action ${isMoreOpen || view === 'imagine' || view === 'workflows' ? 'is-active' : ''}`} onClick={() => setIsMoreOpen(!isMoreOpen)}>
185
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
186
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
187
+ <circle cx="12" cy="12" r="1.5"></circle>
188
+ <circle cx="19" cy="12" r="1.5"></circle>
189
+ <circle cx="5" cy="12" r="1.5"></circle>
190
+ </svg>
191
+ </span>
192
+ <span>More</span>
193
+ </button>
194
+ {isMoreOpen && (
195
+ <div className="sidebar-more-popover">
196
+ <button className={`popover-item ${view === 'imagine' ? 'active' : ''}`} onClick={() => { onSetView('imagine'); setIsMoreOpen(false); }}>
197
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
198
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
199
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
200
+ </svg>
201
+ </span>
202
+ <span>Image Studio</span>
203
+ </button>
204
+ <button className={`popover-item ${view === 'workflows' ? 'active' : ''}`} onClick={() => { onSetView('workflows'); setIsMoreOpen(false); }}>
205
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
206
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
207
+ <path d="M4 12h16"></path>
208
+ <path d="M12 4v16"></path>
209
+ <rect x="2" y="9" width="4" height="6" rx="1"></rect>
210
+ <rect x="18" y="9" width="4" height="6" rx="1"></rect>
211
+ <rect x="10" y="2" width="4" height="4" rx="1"></rect>
212
+ <rect x="10" y="18" width="4" height="4" rx="1"></rect>
213
+ </svg>
214
+ </span>
215
+ <span>Workflow (Beta)</span>
216
+ </button>
217
+ </div>
218
+ )}
219
+ </div>
220
+
221
+
163
222
  {modelVisible && (
164
223
  <div className="sidebar-model-controls">
165
224
  <button
@@ -193,12 +193,16 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
193
193
  document.body.removeChild(a)
194
194
  }
195
195
 
196
- if (view !== 'imagine') return null
197
-
198
196
  const modelOptions = PROVIDER_MODELS[selectedProvider] ?? []
199
197
 
200
198
  return (
201
- <div className="img-studio" id="image-studio-panel" role="main" aria-label="Image Studio">
199
+ <div
200
+ className={`img-studio ${view === 'imagine' ? 'is-visible' : ''}`}
201
+ id="image-studio-panel"
202
+ role="main"
203
+ aria-label="Image Studio"
204
+ aria-hidden={view !== 'imagine'}
205
+ >
202
206
  {/* Header */}
203
207
  <header className="img-studio-header">
204
208
  <div className="img-studio-header-title">
@@ -5,6 +5,7 @@ import {
5
5
  deleteChatSession,
6
6
  renameChatSession,
7
7
  getRecentInteractions,
8
+ saveSystemInteraction,
8
9
  getRuntimeStatus,
9
10
  listChatSessions,
10
11
  listSavedPictures,
@@ -28,6 +29,7 @@ import ModelPanel from './ModelPanel'
28
29
  import type { ModelInteraction } from './ModelPanel'
29
30
  import PicturesLibrary from './PicturesLibrary'
30
31
  import WorkspacePanel from './WorkspacePanel'
32
+ import WorkflowBuilderPanel from './WorkflowBuilderPanel'
31
33
 
32
34
  const EXPRESSIONS = [
33
35
  "Default",
@@ -267,6 +269,79 @@ export default function MintDashboard() {
267
269
  const chatEnd = useRef<HTMLDivElement | null>(null)
268
270
  const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
269
271
 
272
+ const [isSearchOpen, setIsSearchOpen] = useState(false)
273
+ const [searchQuery, setSearchQuery] = useState('')
274
+
275
+ useEffect(() => {
276
+ function handleKeyDown(event: KeyboardEvent) {
277
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
278
+ event.preventDefault()
279
+ setIsSearchOpen((prev) => !prev)
280
+ } else if (event.key === 'Escape') {
281
+ setIsSearchOpen(false)
282
+ }
283
+ }
284
+ window.addEventListener('keydown', handleKeyDown)
285
+ return () => window.removeEventListener('keydown', handleKeyDown)
286
+ }, [])
287
+
288
+ useEffect(() => {
289
+ if (!isSearchOpen) {
290
+ setSearchQuery('')
291
+ }
292
+ }, [isSearchOpen])
293
+
294
+ const filteredSessions = chatSessions.filter((session) => {
295
+ if (session.kind === 'cli' || session.id === 'conversation-default') return false
296
+ return session.title.toLowerCase().includes(searchQuery.toLowerCase())
297
+ })
298
+
299
+ const groupSessionsByDate = (sessions: ChatSession[]) => {
300
+ const groups: { [key: string]: ChatSession[] } = {}
301
+
302
+ sessions.forEach((session) => {
303
+ const dateStr = session.updatedAt || session.createdAt
304
+ if (!dateStr) {
305
+ const groupName = 'Other'
306
+ if (!groups[groupName]) groups[groupName] = []
307
+ groups[groupName].push(session)
308
+ return
309
+ }
310
+
311
+ const date = new Date(dateStr)
312
+ const today = new Date()
313
+ const yesterday = new Date()
314
+ yesterday.setDate(today.getDate() - 1)
315
+
316
+ let groupName = ''
317
+ if (date.toDateString() === today.toDateString()) {
318
+ groupName = 'Today'
319
+ } else if (date.toDateString() === yesterday.toDateString()) {
320
+ groupName = 'Yesterday'
321
+ } else {
322
+ const diffTime = Math.abs(today.getTime() - date.getTime())
323
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
324
+ if (diffDays <= 7) {
325
+ groupName = 'Previous 7 days'
326
+ } else if (diffDays <= 30) {
327
+ groupName = 'Previous 30 days'
328
+ } else {
329
+ groupName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
330
+ }
331
+ }
332
+
333
+ if (!groups[groupName]) {
334
+ groups[groupName] = []
335
+ }
336
+ groups[groupName].push(session)
337
+ })
338
+
339
+ return groups
340
+ }
341
+
342
+ const groupedSearchSessions = groupSessionsByDate(filteredSessions)
343
+
344
+
270
345
  async function refreshHistory() {
271
346
  const history = await getRecentInteractions(50, conversationId)
272
347
  const reversed = history.reverse()
@@ -361,7 +436,7 @@ export default function MintDashboard() {
361
436
  }, [view])
362
437
 
363
438
  useEffect(() => {
364
- if (view === 'workspeac' && !agentMode) updateAgentMode(true)
439
+ if (view === 'workspace' && !agentMode) updateAgentMode(true)
365
440
  }, [view, agentMode])
366
441
 
367
442
  useEffect(() => {
@@ -674,7 +749,7 @@ export default function MintDashboard() {
674
749
  const selected = await selectWorkspaceDirectory()
675
750
  if (selected) {
676
751
  updateWorkspacePath(selected)
677
- setView('workspeac')
752
+ setView('workspace')
678
753
  }
679
754
  } catch (reason) {
680
755
  setError(errorMessage(reason))
@@ -781,6 +856,37 @@ export default function MintDashboard() {
781
856
  }
782
857
  }
783
858
 
859
+ function formatProviderChangeText(provider: string, model: string) {
860
+ let providerName = provider
861
+ if (provider === 'gemini') providerName = 'Gemini'
862
+ else if (provider === 'openai') providerName = 'OpenAI'
863
+ else if (provider === 'openrouter') providerName = 'OpenRouter'
864
+ else if (provider === 'deepseek') providerName = 'DeepSeek'
865
+ else if (provider === 'anthropic') providerName = 'Claude'
866
+ else if (provider === 'huggingface') providerName = 'HF'
867
+ else if (provider === 'local_openai') providerName = 'Local'
868
+ else if (provider === 'ollama') providerName = 'Ollama'
869
+
870
+ if (providerName && providerName === provider) {
871
+ providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
872
+ }
873
+ return [providerName, model].filter(Boolean).join(' • ')
874
+ }
875
+
876
+ function getActiveModelName(config: any, provider: string) {
877
+ switch (provider) {
878
+ case 'gemini': return config.geminiModel || 'gemini-1.5-flash'
879
+ case 'openai': return config.openaiModel || 'gpt-4o'
880
+ case 'openrouter': return config.openrouterModel || 'anthropic/claude-3.5-sonnet'
881
+ case 'deepseek': return config.deepseekModel || 'deepseek-chat'
882
+ case 'anthropic': return config.anthropicModel || 'claude-3-5-sonnet-20240620'
883
+ case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
884
+ case 'local_openai': return config.localModelName || 'llama3'
885
+ case 'ollama': return config.ollamaModel || 'llama3:latest'
886
+ default: return ''
887
+ }
888
+ }
889
+
784
890
  async function changeProvider(provider: string) {
785
891
  try {
786
892
  const config = await window.settingsApi.getSettings()
@@ -788,6 +894,12 @@ export default function MintDashboard() {
788
894
  await window.settingsApi.saveSettings(config)
789
895
  setSettingsConfig(config)
790
896
  setStatus(await getRuntimeStatus())
897
+
898
+ // Record system event in chat history
899
+ const activeModel = getActiveModelName(config, provider)
900
+ const displayName = formatProviderChangeText(provider, activeModel)
901
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
902
+ await refreshHistory()
791
903
  } catch (reason) {
792
904
  setError(errorMessage(reason))
793
905
  }
@@ -817,6 +929,11 @@ export default function MintDashboard() {
817
929
  await window.settingsApi.saveSettings(config)
818
930
  setSettingsConfig(config)
819
931
  setStatus(await getRuntimeStatus())
932
+
933
+ // Record system event in chat history
934
+ const displayName = formatProviderChangeText(provider, modelName)
935
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
936
+ await refreshHistory()
820
937
  } catch (reason) {
821
938
  setError(errorMessage(reason))
822
939
  }
@@ -897,9 +1014,11 @@ export default function MintDashboard() {
897
1014
  onSetInteractionEnabled={updateInteractionEnabled}
898
1015
  onSetShowInteractionGuide={updateInteractionGuide}
899
1016
  onShowToast={showToast}
1017
+ isSearchOpen={isSearchOpen}
1018
+ onSetSearchOpen={setIsSearchOpen}
900
1019
  />
901
- <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspeac' ? '' : 'model-hidden'} ${view === 'workspeac' ? 'workspace-open' : ''}`}>
902
- {view === 'workspeac' && (
1020
+ <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspace' ? '' : 'model-hidden'} ${view === 'workspace' ? 'workspace-open' : ''}`}>
1021
+ {view === 'workspace' && (
903
1022
  <WorkspacePanel
904
1023
  agentMode={agentMode}
905
1024
  sending={sending}
@@ -914,7 +1033,7 @@ export default function MintDashboard() {
914
1033
  expressionIndex={expressionIndex}
915
1034
  accessoryIndex={accessoryIndex}
916
1035
  isLocked={isLocked}
917
- isActive={modelVisible && view !== 'pictures' && view !== 'workspeac'}
1036
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows'}
918
1037
  layoutPreset={layoutPreset}
919
1038
  sending={sending}
920
1039
  interactionEnabled={interactionEnabled}
@@ -980,6 +1099,10 @@ export default function MintDashboard() {
980
1099
  setMessage(imgPrompt)
981
1100
  }}
982
1101
  />
1102
+ <WorkflowBuilderPanel
1103
+ view={view}
1104
+ onShowToast={showToast}
1105
+ />
983
1106
  </div>
984
1107
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
985
1108
  <div className="startup-loading-content">
@@ -998,6 +1121,77 @@ export default function MintDashboard() {
998
1121
  </button>
999
1122
  </div>
1000
1123
  )}
1124
+
1125
+ {isSearchOpen && (
1126
+ <div className="sidebar-search-modal-backdrop" onClick={() => setIsSearchOpen(false)}>
1127
+ <div className="sidebar-search-modal" onClick={(e) => e.stopPropagation()}>
1128
+ <div className="search-modal-header">
1129
+ <span className="search-icon-wrapper">
1130
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1131
+ <circle cx="11" cy="11" r="8"></circle>
1132
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
1133
+ </svg>
1134
+ </span>
1135
+ <input
1136
+ type="text"
1137
+ placeholder="Search chats..."
1138
+ value={searchQuery}
1139
+ onChange={(e) => setSearchQuery(e.target.value)}
1140
+ autoFocus
1141
+ />
1142
+ <button className="search-modal-close" onClick={() => setIsSearchOpen(false)}>
1143
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1144
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1145
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1146
+ </svg>
1147
+ </button>
1148
+ </div>
1149
+
1150
+ <div className="search-modal-body">
1151
+ <button
1152
+ className="search-new-chat-btn"
1153
+ onClick={() => {
1154
+ clearHistory('New chat')
1155
+ setIsSearchOpen(false)
1156
+ }}
1157
+ >
1158
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1159
+ <line x1="12" y1="5" x2="12" y2="19"></line>
1160
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1161
+ </svg>
1162
+ <span>New Chat</span>
1163
+ </button>
1164
+
1165
+ <div className="search-modal-results">
1166
+ {Object.keys(groupedSearchSessions).length > 0 ? (
1167
+ Object.entries(groupedSearchSessions).map(([groupName, sessions]) => (
1168
+ <div key={groupName} className="search-results-group">
1169
+ <div className="search-group-title">{groupName}</div>
1170
+ {sessions.map((session) => (
1171
+ <button
1172
+ key={session.id}
1173
+ className={`search-result-item ${session.id === conversationId ? 'active' : ''}`}
1174
+ onClick={() => {
1175
+ selectConversation(session.id)
1176
+ setIsSearchOpen(false)
1177
+ }}
1178
+ >
1179
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1180
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1181
+ </svg>
1182
+ <span className="search-result-title">{session.title || 'New chat'}</span>
1183
+ </button>
1184
+ ))}
1185
+ </div>
1186
+ ))
1187
+ ) : (
1188
+ <div className="search-no-results">No matching chats found</div>
1189
+ )}
1190
+ </div>
1191
+ </div>
1192
+ </div>
1193
+ </div>
1194
+ )}
1001
1195
  </div>
1002
1196
  )
1003
1197
  }
@@ -9,10 +9,8 @@ interface PicturesLibraryProps {
9
9
  }
10
10
 
11
11
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
12
- if (view !== 'pictures') return null
13
-
14
12
  return (
15
- <section className="pictures-library">
13
+ <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
16
14
  <header className="pictures-header">
17
15
  <div><span className="pictures-kicker">Gallery</span><h2>Saved Pictures</h2></div>
18
16
  <div className="pictures-header-actions">
@@ -0,0 +1,97 @@
1
+ import React from 'react'
2
+ import { DEFAULT_CONFIG } from '../SettingsWindow'
3
+
4
+ interface AudioTabProps {
5
+ config: typeof DEFAULT_CONFIG
6
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
7
+ }
8
+
9
+ export default function AudioTab({ config, updateField }: AudioTabProps) {
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">Speech</p>
16
+ <h2 className="section-title">Voice Reply</h2>
17
+ </div>
18
+ <p className="section-description">Control spoken responses and TTS behavior.</p>
19
+ </div>
20
+
21
+ <div className="toggle-row">
22
+ <div>
23
+ <label>Enable Voice Reply</label>
24
+ <p className="hint">Mint will speak responses out loud when this is enabled.</p>
25
+ </div>
26
+ <label className="settings-toggle-switch">
27
+ <input
28
+ type="checkbox"
29
+ checked={config.enableVoiceReply}
30
+ onChange={(e) => updateField('enableVoiceReply', e.target.checked)}
31
+ />
32
+ <span className="settings-toggle-slider"></span>
33
+ </label>
34
+ </div>
35
+
36
+ <div className="form-grid single">
37
+ <div className="setting-row">
38
+ <label>Voice Engine</label>
39
+ <select value={config.ttsProvider} onChange={(e) => updateField('ttsProvider', e.target.value)}>
40
+ <option value="google">Google Cloud (Natural, Auto Lang)</option>
41
+ <option value="native">OS Native (Supports Pitch)</option>
42
+ </select>
43
+ </div>
44
+ </div>
45
+
46
+ <div className="slider-stack">
47
+ <div className="setting-row">
48
+ <label>Volume</label>
49
+ <div className="slider-group">
50
+ <input
51
+ type="range"
52
+ min="0"
53
+ max="1"
54
+ step="0.1"
55
+ value={config.ttsVolume}
56
+ onChange={(e) => updateField('ttsVolume', parseFloat(e.target.value))}
57
+ className="range-slider"
58
+ />
59
+ <span className="range-value">{Math.round(config.ttsVolume * 100)}%</span>
60
+ </div>
61
+ </div>
62
+ <div className="setting-row">
63
+ <label>Speed</label>
64
+ <div className="slider-group">
65
+ <input
66
+ type="range"
67
+ min="0.5"
68
+ max="2"
69
+ step="0.1"
70
+ value={config.ttsSpeed}
71
+ onChange={(e) => updateField('ttsSpeed', parseFloat(e.target.value))}
72
+ className="range-slider"
73
+ />
74
+ <span className="range-value">{parseFloat(String(config.ttsSpeed)).toFixed(1)}x</span>
75
+ </div>
76
+ </div>
77
+ <div className="setting-row">
78
+ <label>Pitch</label>
79
+ <div className="slider-group">
80
+ <input
81
+ type="range"
82
+ min="0"
83
+ max="2"
84
+ step="0.1"
85
+ value={config.ttsPitch}
86
+ onChange={(e) => updateField('ttsPitch', parseFloat(e.target.value))}
87
+ className="range-slider"
88
+ />
89
+ <span className="range-value">{parseFloat(String(config.ttsPitch)).toFixed(1)}</span>
90
+ </div>
91
+ <p className="hint">Pitch applies to OS native voice only.</p>
92
+ </div>
93
+ </div>
94
+ </section>
95
+ </div>
96
+ )
97
+ }