@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.
Files changed (66) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +40 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -68,15 +68,7 @@ const LOCAL_MODELS = [
68
68
  'lmstudio-community/gemma-2-9b-it-GGUF'
69
69
  ]
70
70
 
71
- const OLLAMA_MODELS = [
72
- 'llama3:latest',
73
- 'llama3.1:latest',
74
- 'llama3.2:latest',
75
- 'gemma2:latest',
76
- 'mistral:latest',
77
- 'phi3:latest',
78
- 'qwen2.5:latest'
79
- ]
71
+ const OLLAMA_MODELS: string[] = []
80
72
 
81
73
  interface ApprovalDetails {
82
74
  title: string
@@ -122,6 +114,7 @@ interface AgentActivity {
122
114
  target: string
123
115
  kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
124
116
  state: 'active' | 'done' | 'error'
117
+ action?: string
125
118
  }
126
119
 
127
120
  interface AgentActivityView {
@@ -153,7 +146,20 @@ function describeTool(action: string, input: Record<string, unknown>): AgentActi
153
146
  const command = activityDetail(input, 'command')
154
147
  const name = activityDetail(input, 'name')
155
148
  const tool = activityDetail(input, 'tool')
156
- const target = path || query || command || name || tool || action.replaceAll('_', ' ')
149
+ const rawTarget = path || query || command || name || tool || action.replaceAll('_', ' ')
150
+
151
+ // Append line range for read_file when startLine / endLine are available
152
+ let target = rawTarget
153
+ if (action === 'read_file' && path) {
154
+ const startLine = typeof input.startLine === 'number' ? input.startLine : undefined
155
+ const endLine = typeof input.endLine === 'number' ? input.endLine : undefined
156
+ if (startLine !== undefined && endLine !== undefined) {
157
+ target = `${rawTarget} #L${startLine}-${endLine}`
158
+ } else if (startLine !== undefined) {
159
+ target = `${rawTarget} #L${startLine}`
160
+ }
161
+ }
162
+
157
163
  const labels: Record<string, string> = {
158
164
  apply_patch: 'Applying patch',
159
165
  ask_user: 'Asking user',
@@ -189,6 +195,7 @@ function describeTool(action: string, input: Record<string, unknown>): AgentActi
189
195
  target: formatActivityTarget(target),
190
196
  kind: activityKind(action, target),
191
197
  state: 'active',
198
+ action,
192
199
  }
193
200
  }
194
201
 
@@ -215,7 +222,13 @@ function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
215
222
  for (let index = activities.length - 1; index >= 0; index -= 1) {
216
223
  if (activities[index].state !== 'active') continue
217
224
  activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
218
- if (activities[index].state === 'error') activities[index].label = 'Failed'
225
+ if (activities[index].state === 'error') {
226
+ activities[index].label = 'Failed'
227
+ }
228
+ if (activities[index].action === 'ask_user' && event.data.result.startsWith('User answered:')) {
229
+ const answer = event.data.result.replace('User answered:', '').trim()
230
+ activities[index].target = `(Answered: "${answer}") ${activities[index].target}`
231
+ }
219
232
  break
220
233
  }
221
234
  }
@@ -224,7 +237,16 @@ function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
224
237
  return { summary: activitySummary(activities), items }
225
238
  }
226
239
 
227
- function renderAgentActivityTable(activityView: AgentActivityView) {
240
+ function AgentActivityTable({ activityView }: { activityView: AgentActivityView }) {
241
+ const [expandedIndices, setExpandedIndices] = useState<Record<number, boolean>>({})
242
+
243
+ const toggleExpand = (index: number) => {
244
+ setExpandedIndices(prev => ({
245
+ ...prev,
246
+ [index]: !prev[index]
247
+ }))
248
+ }
249
+
228
250
  return (
229
251
  <div className="agent-activity-list">
230
252
  <div className="agent-activity-table-head" aria-hidden="true">
@@ -233,14 +255,39 @@ function renderAgentActivityTable(activityView: AgentActivityView) {
233
255
  <span>Target</span>
234
256
  <span />
235
257
  </div>
236
- {activityView.items.map((activity, index) => (
237
- <div className="agent-activity-item" data-kind={activity.kind} data-state={activity.state} key={`${index}-${activity.label}-${activity.target}`}>
238
- <span className="agent-activity-label">{activity.label}</span>
239
- <span className="agent-activity-icon" aria-hidden="true" />
240
- <span className="agent-activity-text">{activity.target}</span>
241
- <span className="agent-activity-chevron" aria-hidden="true">&gt;</span>
242
- </div>
243
- ))}
258
+ {activityView.items.map((activity, index) => {
259
+ const isExpanded = !!expandedIndices[index]
260
+ return (
261
+ <div
262
+ className="agent-activity-item"
263
+ data-kind={activity.kind}
264
+ data-state={activity.state}
265
+ key={`${index}-${activity.label}-${activity.target}`}
266
+ style={{ cursor: 'pointer' }}
267
+ onClick={() => toggleExpand(index)}
268
+ >
269
+ <span className="agent-activity-label">{activity.label}</span>
270
+ <span className="agent-activity-icon" aria-hidden="true" />
271
+ <span
272
+ className="agent-activity-text"
273
+ style={isExpanded ? { whiteSpace: 'normal', wordBreak: 'break-all', overflow: 'visible', textOverflow: 'clip' } : undefined}
274
+ >
275
+ {activity.target}
276
+ </span>
277
+ <span
278
+ className="agent-activity-chevron"
279
+ style={{
280
+ transform: isExpanded ? 'rotate(90deg)' : 'none',
281
+ transition: 'transform 0.2s ease',
282
+ display: 'inline-block'
283
+ }}
284
+ aria-hidden="true"
285
+ >
286
+ &gt;
287
+ </span>
288
+ </div>
289
+ )
290
+ })}
244
291
  </div>
245
292
  )
246
293
  }
@@ -257,7 +304,7 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
257
304
  return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
258
305
  }
259
306
  if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
260
- if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Approve to continue without a typed answer, or cancel to decline.', isDangerous: false }
307
+ if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Type your answer below and submit to respond to the agent.', isDangerous: false }
261
308
  return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
262
309
  }
263
310
 
@@ -355,7 +402,7 @@ interface ChatPanelProps {
355
402
  onSetAgentMode: (enabled: boolean) => void
356
403
  onSetProvider: (provider: string) => void
357
404
  onSelectWorkspace: () => void
358
- onApproval: (approved: boolean, autoApproveSession?: boolean) => void
405
+ onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
359
406
  settingsConfig: any
360
407
  onSetModel: (model: string) => void
361
408
  }
@@ -450,7 +497,17 @@ function renderFormattedMessage(text: string) {
450
497
  </strong>
451
498
  )
452
499
  }
453
- return boldPart
500
+ const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
501
+ return mentionParts.map((mentionPart, mentionIndex) => {
502
+ if (mentionIndex % 2 === 1) {
503
+ return (
504
+ <span key={`mention-${mentionIndex}`} className="chat-mention">
505
+ {mentionPart}
506
+ </span>
507
+ )
508
+ }
509
+ return mentionPart
510
+ })
454
511
  })
455
512
  })
456
513
  }
@@ -977,6 +1034,34 @@ export default function ChatPanel({
977
1034
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
978
1035
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
979
1036
  const [elapsedSeconds, setElapsedSeconds] = useState(0)
1037
+ const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
1038
+ const [askAnswer, setAskAnswer] = useState('')
1039
+
1040
+ useEffect(() => {
1041
+ setAskAnswer('')
1042
+ }, [pendingApproval])
1043
+
1044
+ useEffect(() => {
1045
+ const fetchOllamaModels = async () => {
1046
+ if (status?.activeProvider !== 'ollama') return;
1047
+ const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
1048
+ const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
1049
+ try {
1050
+ const res = await fetch(`${cleanHost}/api/tags`);
1051
+ if (res.ok) {
1052
+ const data = await res.json();
1053
+ if (data && Array.isArray(data.models)) {
1054
+ setDynamicOllamaModels(data.models.map((m: any) => m.name));
1055
+ return;
1056
+ }
1057
+ }
1058
+ } catch (err) {
1059
+ // fallback to default if fetch fails
1060
+ }
1061
+ setDynamicOllamaModels(OLLAMA_MODELS);
1062
+ }
1063
+ fetchOllamaModels();
1064
+ }, [status?.activeProvider, settingsConfig?.ollamaHost])
980
1065
 
981
1066
  useEffect(() => {
982
1067
  if (!sending) {
@@ -1037,7 +1122,7 @@ export default function ChatPanel({
1037
1122
  case 'local_openai':
1038
1123
  return LOCAL_MODELS
1039
1124
  case 'ollama':
1040
- return OLLAMA_MODELS
1125
+ return dynamicOllamaModels
1041
1126
  default:
1042
1127
  return []
1043
1128
  }
@@ -1473,7 +1558,7 @@ export default function ChatPanel({
1473
1558
  const appendWorkspaceReference = (reference: string) => {
1474
1559
  const trimmed = reference.trim()
1475
1560
  if (!trimmed) return
1476
- onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed}` : trimmed)
1561
+ onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed} ` : `${trimmed} `)
1477
1562
  }
1478
1563
  const handleWorkspaceDrop = (event: DragEvent<HTMLElement>) => {
1479
1564
  const reference =
@@ -1502,7 +1587,7 @@ export default function ChatPanel({
1502
1587
  </button>
1503
1588
  {isOpen && (
1504
1589
  <div className="agent-activity-card agent-activity-card-history">
1505
- {renderAgentActivityTable(activityView)}
1590
+ <AgentActivityTable activityView={activityView} />
1506
1591
  </div>
1507
1592
  )}
1508
1593
  </div>
@@ -1701,22 +1786,40 @@ export default function ChatPanel({
1701
1786
 
1702
1787
  return (
1703
1788
  <section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
1789
+ <div className="chat-header">
1790
+ <div className="chat-header-title">
1791
+ <img src="./assets/icon.png" alt="Logo" className="chat-header-logo" />
1792
+ <span>Mint Agent</span>
1793
+ </div>
1794
+ </div>
1704
1795
  <div className="chat-container">
1705
- {interactions.map((interaction) => (
1706
- <div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1707
- {interaction.isSystemEvent ? (
1708
- <div className="system-event" style={{ background: 'rgba(16, 185, 129, 0.06)', border: '1px solid rgba(16, 185, 129, 0.2)', borderRadius: '8px', padding: '10px 14px', color: '#a7f3d0', fontSize: '0.82rem', lineHeight: '1.45', alignSelf: 'stretch' }}>
1709
- {interaction.userText}
1710
- </div>
1711
- ) : interaction.userText && (
1712
- <div className="message user-message">
1713
- <div className="bubble-wrapper">
1714
- <div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
1715
- <div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
1796
+ {interactions.map((interaction) => {
1797
+ const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
1798
+ if (isSystemEvent) {
1799
+ return (
1800
+ <div key={interaction.id} className="system-event-divider">
1801
+ <div className="system-event-line" />
1802
+ <div className="system-event-pill">
1803
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
1804
+ <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
1805
+ </svg>
1806
+ <span>{interaction.userText}</span>
1716
1807
  </div>
1808
+ <div className="system-event-line" />
1717
1809
  </div>
1718
- )}
1719
- <div className="message ai-message">
1810
+ );
1811
+ }
1812
+ return (
1813
+ <div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1814
+ {interaction.userText && (
1815
+ <div className="message user-message">
1816
+ <div className="bubble-wrapper">
1817
+ <div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
1818
+ <div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
1819
+ </div>
1820
+ </div>
1821
+ )}
1822
+ <div className="message ai-message">
1720
1823
  <div className="bubble-wrapper">
1721
1824
  {renderCompletedActivity(interaction)}
1722
1825
  {renderFileChanges(interaction)}
@@ -1750,7 +1853,8 @@ export default function ChatPanel({
1750
1853
  </div>
1751
1854
  </div>
1752
1855
  </div>
1753
- ))}
1856
+ );
1857
+ })}
1754
1858
 
1755
1859
  {sending && (
1756
1860
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
@@ -1764,7 +1868,7 @@ export default function ChatPanel({
1764
1868
  {pendingApproval ? 'Waiting for approval' : 'Working'}
1765
1869
  </span>
1766
1870
  </div>
1767
- {renderAgentActivityTable(agentActivities)}
1871
+ <AgentActivityTable activityView={agentActivities} />
1768
1872
  </div>
1769
1873
  </div>
1770
1874
  )}
@@ -1834,6 +1938,7 @@ export default function ChatPanel({
1834
1938
  const writeFile = pendingApproval.approval?.WriteFile
1835
1939
  const applyPatch = pendingApproval.approval?.ApplyPatch
1836
1940
  const diffText = writeFile?.diff || applyPatch?.diff
1941
+ const isAskUser = !!pendingApproval.approval?.AskUser
1837
1942
 
1838
1943
  return (
1839
1944
  <div className="message ai-message" style={{ width: '100%' }}>
@@ -1850,12 +1955,54 @@ export default function ChatPanel({
1850
1955
  ) : (
1851
1956
  details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1852
1957
  )}
1958
+ {isAskUser && (
1959
+ <div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
1960
+ <textarea
1961
+ style={{
1962
+ width: '100%',
1963
+ minHeight: '70px',
1964
+ padding: '10px',
1965
+ background: 'rgba(0, 0, 0, 0.2)',
1966
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1967
+ borderRadius: '6px',
1968
+ color: 'var(--text-main, #f8fafc)',
1969
+ fontSize: '0.9rem',
1970
+ fontFamily: 'inherit',
1971
+ resize: 'vertical',
1972
+ outline: 'none',
1973
+ }}
1974
+ placeholder="Type your answer here..."
1975
+ value={askAnswer}
1976
+ onChange={(e) => setAskAnswer(e.target.value)}
1977
+ />
1978
+ </div>
1979
+ )}
1853
1980
  </div>
1854
- <div className="approval-card-actions">
1855
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1856
- <button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
1857
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1858
- </div>
1981
+ {isAskUser ? (
1982
+ <div className="approval-card-actions">
1983
+ <button
1984
+ type="button"
1985
+ className="approval-btn approval-btn-approve"
1986
+ disabled={!askAnswer.trim()}
1987
+ onClick={() => onApproval(true, false, askAnswer)}
1988
+ >
1989
+ Submit Answer
1990
+ </button>
1991
+ <button
1992
+ type="button"
1993
+ className="approval-btn approval-btn-cancel"
1994
+ onClick={() => onApproval(false)}
1995
+ >
1996
+ Decline
1997
+ </button>
1998
+ </div>
1999
+ ) : (
2000
+ <div className="approval-card-actions">
2001
+ <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
2002
+ <button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
2003
+ <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
2004
+ </div>
2005
+ )}
1859
2006
  </div>
1860
2007
  </div>
1861
2008
  </div>
@@ -2035,7 +2182,7 @@ export default function ChatPanel({
2035
2182
  return <option key={provider} value={provider}>{displayName}</option>
2036
2183
  })}
2037
2184
  </select>
2038
- {availableModels.length > 0 && (
2185
+ {(availableModels.length > 0 || activeModel) && (
2039
2186
  <select
2040
2187
  value={activeModel}
2041
2188
  onChange={(event) => onSetModel(event.target.value)}
@@ -2093,6 +2240,9 @@ export default function ChatPanel({
2093
2240
  </button>
2094
2241
  </form>
2095
2242
  </div>
2243
+ <p className="input-disclaimer">
2244
+ Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
2245
+ </p>
2096
2246
  </section>
2097
2247
  )
2098
2248
  }
@@ -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
@@ -16,6 +16,7 @@ interface ImageStudioPanelProps {
16
16
  view: string
17
17
  onRefreshPictures?: () => void
18
18
  onSendToChat?: (dataUri: string, prompt: string) => void
19
+ onToggleMobileSidebar?: () => void
19
20
  }
20
21
 
21
22
  const ASPECT_OPTIONS: { value: AspectRatio; label: string; icon: string }[] = [
@@ -85,7 +86,7 @@ function SkeletonCard() {
85
86
  )
86
87
  }
87
88
 
88
- export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat }: ImageStudioPanelProps) {
89
+ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat, onToggleMobileSidebar }: ImageStudioPanelProps) {
89
90
  const [prompt, setPrompt] = useState('')
90
91
  const [negativePrompt, setNegativePrompt] = useState('')
91
92
  const [showNegative, setShowNegative] = useState(false)
@@ -193,15 +194,28 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
193
194
  document.body.removeChild(a)
194
195
  }
195
196
 
196
- if (view !== 'imagine') return null
197
-
198
197
  const modelOptions = PROVIDER_MODELS[selectedProvider] ?? []
199
198
 
200
199
  return (
201
- <div className="img-studio" id="image-studio-panel" role="main" aria-label="Image Studio">
200
+ <div
201
+ className={`img-studio ${view === 'imagine' ? 'is-visible' : ''}`}
202
+ id="image-studio-panel"
203
+ role="main"
204
+ aria-label="Image Studio"
205
+ aria-hidden={view !== 'imagine'}
206
+ >
202
207
  {/* Header */}
203
208
  <header className="img-studio-header">
204
209
  <div className="img-studio-header-title">
210
+ <button
211
+ className="mobile-menu-btn"
212
+ type="button"
213
+ onClick={onToggleMobileSidebar}
214
+ aria-label="Toggle menu"
215
+ style={{ marginRight: '8px' }}
216
+ >
217
+ ☰
218
+ </button>
205
219
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
206
220
  <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"/>
207
221
  </svg>