@pheem49/mint 1.5.5 → 1.6.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 (222) hide show
  1. package/.codex +0 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/.github/workflows/ci.yml +45 -0
  4. package/.github/workflows/release.yml +79 -0
  5. package/Cargo.lock +5792 -0
  6. package/Cargo.toml +32 -0
  7. package/README.md +387 -353
  8. package/assets/icon.png +0 -0
  9. package/bin/mint +0 -0
  10. package/crates/mint-cli/Cargo.toml +23 -0
  11. package/crates/mint-cli/src/agent.rs +851 -0
  12. package/crates/mint-cli/src/gmail.rs +216 -0
  13. package/crates/mint-cli/src/image.rs +142 -0
  14. package/crates/mint-cli/src/main.rs +2837 -0
  15. package/crates/mint-cli/src/mcp.rs +63 -0
  16. package/crates/mint-cli/src/onboard.rs +1149 -0
  17. package/crates/mint-cli/src/setup.rs +390 -0
  18. package/crates/mint-cli/src/skills.rs +8 -0
  19. package/crates/mint-cli/src/updater.rs +279 -0
  20. package/crates/mint-core/Cargo.toml +22 -0
  21. package/crates/mint-core/src/agent_loop.rs +94 -0
  22. package/crates/mint-core/src/api_server.rs +991 -0
  23. package/crates/mint-core/src/channels.rs +248 -0
  24. package/crates/mint-core/src/chat.rs +895 -0
  25. package/crates/mint-core/src/code_tools.rs +729 -0
  26. package/crates/mint-core/src/config.rs +368 -0
  27. package/crates/mint-core/src/files.rs +159 -0
  28. package/crates/mint-core/src/knowledge.rs +541 -0
  29. package/crates/mint-core/src/lib.rs +84 -0
  30. package/crates/mint-core/src/mcp.rs +273 -0
  31. package/crates/mint-core/src/memory.rs +673 -0
  32. package/crates/mint-core/src/orchestration.rs +2157 -0
  33. package/crates/mint-core/src/pictures.rs +314 -0
  34. package/crates/mint-core/src/plugins.rs +727 -0
  35. package/crates/mint-core/src/safety.rs +416 -0
  36. package/crates/mint-core/src/semantic.rs +254 -0
  37. package/crates/mint-core/src/shell.rs +317 -0
  38. package/crates/mint-core/src/skills.rs +71 -0
  39. package/crates/mint-core/src/symbols.rs +157 -0
  40. package/crates/mint-core/src/tasks.rs +308 -0
  41. package/crates/mint-core/src/tts.rs +92 -0
  42. package/crates/mint-core/src/weather.rs +93 -0
  43. package/crates/mint-core/src/web_search.rs +200 -0
  44. package/crates/mint-core/src/workflows.rs +81 -0
  45. package/crates/mint-core/tests/mcp_stdio.rs +45 -0
  46. package/crates/mint-core/tests/memory_persistence.rs +172 -0
  47. package/crates/mint-core/tests/pictures_storage.rs +14 -0
  48. package/crates/mint-core/tests/task_lifecycle.rs +87 -0
  49. package/package.json +35 -99
  50. package/src/bin/index.js +16 -0
  51. package/src/renderer/index-web.html +17 -0
  52. package/src/renderer/index.html +17 -0
  53. package/src/renderer/public/Live2DCubismCore.js +9 -0
  54. package/src/renderer/public/assets/icon.png +0 -0
  55. package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
  56. package/src/renderer/src/App.tsx +33 -0
  57. package/src/renderer/src/calculator.ts +47 -0
  58. package/src/renderer/src/components/ChatPanel.tsx +1598 -0
  59. package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
  60. package/src/renderer/src/components/Live2DStage.tsx +374 -0
  61. package/src/renderer/src/components/MintDashboard.tsx +950 -0
  62. package/src/renderer/src/components/ModelPanel.tsx +154 -0
  63. package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
  64. package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
  65. package/src/renderer/src/components/ScreenPicker.tsx +579 -0
  66. package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
  67. package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
  68. package/src/renderer/src/components/WidgetWindow.tsx +36 -0
  69. package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
  70. package/src/{UI → renderer/src/css}/settings.css +69 -16
  71. package/src/renderer/src/css/spotlight.css +113 -0
  72. package/src/renderer/src/css/styles.css +3722 -0
  73. package/src/renderer/src/css/widget.css +185 -0
  74. package/src/renderer/src/env.d.ts +116 -0
  75. package/src/renderer/src/index.css +379 -0
  76. package/src/renderer/src/main.tsx +13 -0
  77. package/src/renderer/src/tauri.ts +996 -0
  78. package/src/renderer/src-web/App.tsx +25 -0
  79. package/src/renderer/src-web/calculator.ts +47 -0
  80. package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
  81. package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
  82. package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
  83. package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
  84. package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
  85. package/src/renderer/src-web/css/settings.css +1100 -0
  86. package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
  87. package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
  88. package/src/{UI → renderer/src-web/css}/widget.css +2 -2
  89. package/src/renderer/src-web/env.d.ts +107 -0
  90. package/src/renderer/src-web/index.css +379 -0
  91. package/src/renderer/src-web/main.tsx +13 -0
  92. package/src/renderer/src-web/tauri.ts +983 -0
  93. package/tsconfig.json +30 -0
  94. package/vite.config.ts +33 -0
  95. package/vite.config.web.ts +51 -0
  96. package/GUIDE_TH.md +0 -125
  97. package/assets/Agent_Mint.png +0 -0
  98. package/assets/CLI_Screen.png +0 -0
  99. package/assets/Settings.png +0 -0
  100. package/benchmark_ai.js +0 -71
  101. package/install.ps1 +0 -64
  102. package/install.sh +0 -54
  103. package/main.js +0 -139
  104. package/mint-cli-logic.js +0 -3
  105. package/mint-cli.js +0 -410
  106. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
  107. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
  108. package/preload-picker.js +0 -11
  109. package/preload-settings.js +0 -11
  110. package/preload.js +0 -41
  111. package/scripts/install_linux_desktop_entry.js +0 -48
  112. package/src/AI_Brain/Gemini_API.js +0 -813
  113. package/src/AI_Brain/agent_orchestrator.js +0 -73
  114. package/src/AI_Brain/autonomous_brain.js +0 -179
  115. package/src/AI_Brain/behavior_memory.js +0 -135
  116. package/src/AI_Brain/headless_agent.js +0 -143
  117. package/src/AI_Brain/knowledge_base.js +0 -349
  118. package/src/AI_Brain/memory_store.js +0 -662
  119. package/src/AI_Brain/proactive_engine.js +0 -172
  120. package/src/AI_Brain/provider_adapter.js +0 -365
  121. package/src/Automation_Layer/browser_automation.js +0 -149
  122. package/src/Automation_Layer/file_operations.js +0 -286
  123. package/src/Automation_Layer/open_app.js +0 -85
  124. package/src/Automation_Layer/open_website.js +0 -38
  125. package/src/CLI/approval_handler.js +0 -47
  126. package/src/CLI/chat_router.js +0 -247
  127. package/src/CLI/chat_ui.js +0 -1159
  128. package/src/CLI/cli_colors.js +0 -115
  129. package/src/CLI/cli_formatters.js +0 -94
  130. package/src/CLI/code_agent.js +0 -1667
  131. package/src/CLI/code_session_memory.js +0 -62
  132. package/src/CLI/gmail_auth.js +0 -210
  133. package/src/CLI/image_input.js +0 -90
  134. package/src/CLI/intent_detectors.js +0 -181
  135. package/src/CLI/interactive_chat.js +0 -658
  136. package/src/CLI/list_features.js +0 -64
  137. package/src/CLI/onboarding.js +0 -416
  138. package/src/CLI/repo_summarizer.js +0 -282
  139. package/src/CLI/semantic_code_search.js +0 -312
  140. package/src/CLI/skill_manager.js +0 -41
  141. package/src/CLI/slash_command_handler.js +0 -418
  142. package/src/CLI/symbol_indexer.js +0 -231
  143. package/src/CLI/updater.js +0 -230
  144. package/src/CLI/workspace_manager.js +0 -90
  145. package/src/Channels/brave_search_bridge.js +0 -35
  146. package/src/Channels/discord_bridge.js +0 -66
  147. package/src/Channels/google_search_bridge.js +0 -38
  148. package/src/Channels/line_bridge.js +0 -60
  149. package/src/Channels/slack_bridge.js +0 -48
  150. package/src/Channels/telegram_bridge.js +0 -41
  151. package/src/Channels/whatsapp_bridge.js +0 -57
  152. package/src/Command_Parser/parser.js +0 -45
  153. package/src/Plugins/dev_tools.js +0 -41
  154. package/src/Plugins/discord.js +0 -20
  155. package/src/Plugins/docker.js +0 -47
  156. package/src/Plugins/gmail.js +0 -251
  157. package/src/Plugins/google_calendar.js +0 -252
  158. package/src/Plugins/mcp_manager.js +0 -95
  159. package/src/Plugins/notion.js +0 -256
  160. package/src/Plugins/obsidian.js +0 -54
  161. package/src/Plugins/plugin_manager.js +0 -81
  162. package/src/Plugins/spotify.js +0 -173
  163. package/src/Plugins/system_metrics.js +0 -31
  164. package/src/Plugins/system_monitor.js +0 -72
  165. package/src/System/action_executor.js +0 -178
  166. package/src/System/bridge_manager.js +0 -76
  167. package/src/System/chat_history_manager.js +0 -83
  168. package/src/System/config_manager.js +0 -194
  169. package/src/System/custom_workflows.js +0 -163
  170. package/src/System/daemon_manager.js +0 -67
  171. package/src/System/google_tts_urls.js +0 -51
  172. package/src/System/granular_automation.js +0 -157
  173. package/src/System/ipc_handlers.js +0 -332
  174. package/src/System/notifications.js +0 -23
  175. package/src/System/optional_require.js +0 -23
  176. package/src/System/picture_store.js +0 -109
  177. package/src/System/proactive_loop.js +0 -153
  178. package/src/System/safety_manager.js +0 -273
  179. package/src/System/sandbox_runner.js +0 -182
  180. package/src/System/screen_capture.js +0 -175
  181. package/src/System/smart_context.js +0 -227
  182. package/src/System/system_automation.js +0 -162
  183. package/src/System/system_events.js +0 -79
  184. package/src/System/system_info.js +0 -125
  185. package/src/System/task_manager.js +0 -222
  186. package/src/System/tool_registry.js +0 -293
  187. package/src/System/window_manager.js +0 -220
  188. package/src/UI/floating.css +0 -80
  189. package/src/UI/floating.html +0 -17
  190. package/src/UI/floating.js +0 -67
  191. package/src/UI/live2d_manager.js +0 -600
  192. package/src/UI/preload-floating.js +0 -7
  193. package/src/UI/preload-spotlight.js +0 -11
  194. package/src/UI/preload-widget.js +0 -5
  195. package/src/UI/proactive-glow.html +0 -42
  196. package/src/UI/renderer.js +0 -2127
  197. package/src/UI/screenPicker.html +0 -214
  198. package/src/UI/screenPicker.js +0 -262
  199. package/src/UI/settings.html +0 -577
  200. package/src/UI/settings.js +0 -770
  201. package/src/UI/spotlight.html +0 -23
  202. package/src/UI/spotlight.js +0 -185
  203. package/src/UI/widget.html +0 -29
  204. package/src/UI/widget.js +0 -10
  205. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  206. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
  207. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
  208. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
  209. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
  210. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
  211. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
  212. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
  213. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
  214. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
  215. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
  216. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
  217. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
  218. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
  219. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
  220. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
  221. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
  222. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
@@ -0,0 +1,763 @@
1
+ import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ clearChatHistory,
4
+ deleteChatSession,
5
+ renameChatSession,
6
+ getRecentInteractions,
7
+ getRuntimeStatus,
8
+ listChatSessions,
9
+ listSavedPictures,
10
+ streamChatMessage,
11
+ submitToolApproval,
12
+ listen,
13
+ readClipboardImage as readTauriClipboardImage,
14
+ type AgentProgress,
15
+ type ChatResponse,
16
+ type ChatSession,
17
+ type DocumentAttachment,
18
+ type PictureEntry,
19
+ type RuntimeStatus,
20
+ } from '../tauri'
21
+ import ChatPanel from './ChatPanel'
22
+ import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
23
+ import PicturesLibrary from './PicturesLibrary'
24
+
25
+ const DEFAULT_CONFIG = {
26
+ theme: 'dark',
27
+ accentColor: '#4f83e6',
28
+ systemTextColor: '#f8fafc',
29
+ customBgStart: '#0f172a',
30
+ customBgEnd: '#1e1b4b',
31
+ customPanelBg: '#1e293b',
32
+ glassBlur: 'blur(16px)',
33
+ fontFamily: "'Outfit', sans-serif",
34
+ fontSize: '15px',
35
+ }
36
+
37
+ const ACTIVE_CONVERSATION_ID_KEY = 'mint:web-active-conversation-id'
38
+
39
+ function createConversationId() {
40
+ const random = Math.random().toString(36).slice(2, 10)
41
+ return `conversation-${Date.now().toString(36)}-${random}`
42
+ }
43
+
44
+ function activeConversationId() {
45
+ const existing = window.localStorage.getItem(ACTIVE_CONVERSATION_ID_KEY)
46
+ if (existing === 'conversation-default') {
47
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, 'cli')
48
+ return 'cli'
49
+ }
50
+ if (existing) return existing
51
+ const next = createConversationId()
52
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, next)
53
+ return next
54
+ }
55
+
56
+ const MOCK_WELCOME_INTERACTION = {
57
+ id: -1,
58
+ userText: '',
59
+ aiText: `มิ้นท์กำลังรอสแตนด์บายเตรียมพร้อมช่วยคุณทีมอยู่เลยค่ะ! ✨ แล้วก็แอบนั่งจัดระเบียบข้อมูลนิดๆ หน่อยๆ ให้พร้อมใช้ด้วยค่ะ 😊💖\n\nแต่พอคุณทีมทักมา มิ้นท์ก็วางมือจากทุกอย่างมาคุยกับคุณทีมก่อนเลยนะคะค้าา! ช่วงนี้มีอะไรให้มิ้นท์ช่วยดูแล หรืออยากชวนคุยเรื่องไหนเป็นพิเศษไหมคะ มิ้นท์พร้อมมว๊ากกกค่ะ! 🚀🎯`,
60
+ provider: 'gemini',
61
+ model: 'gemini-3-flash-preview',
62
+ createdAt: new Date().toISOString(),
63
+ }
64
+
65
+ function errorMessage(reason: unknown) {
66
+ return reason instanceof Error ? reason.message : String(reason)
67
+ }
68
+
69
+ function readImage(file: File): Promise<string> {
70
+ return new Promise((resolve, reject) => {
71
+ const reader = new FileReader()
72
+ reader.onload = () => resolve(String(reader.result))
73
+ reader.onerror = () => reject(reader.error ?? new Error('Unable to read image'))
74
+ reader.readAsDataURL(file)
75
+ })
76
+ }
77
+
78
+ function readDocument(file: File): Promise<string> {
79
+ return new Promise((resolve, reject) => {
80
+ const reader = new FileReader()
81
+ reader.onload = () => resolve(String(reader.result))
82
+ reader.onerror = () => reject(reader.error ?? new Error('Unable to read document'))
83
+ reader.readAsDataURL(file)
84
+ })
85
+ }
86
+
87
+ async function createTrimmedImagePreview(dataUri: string): Promise<string> {
88
+ const image = await new Promise<HTMLImageElement>((resolve, reject) => {
89
+ const nextImage = new Image()
90
+ nextImage.onload = () => resolve(nextImage)
91
+ nextImage.onerror = () => reject(new Error('Unable to prepare image preview'))
92
+ nextImage.src = dataUri
93
+ })
94
+
95
+ const canvas = document.createElement('canvas')
96
+ canvas.width = image.naturalWidth || image.width
97
+ canvas.height = image.naturalHeight || image.height
98
+ const context = canvas.getContext('2d', { willReadFrequently: true })
99
+ if (!context || canvas.width === 0 || canvas.height === 0) return dataUri
100
+
101
+ context.drawImage(image, 0, 0)
102
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height)
103
+ let minX = canvas.width
104
+ let minY = canvas.height
105
+ let maxX = -1
106
+ let maxY = -1
107
+
108
+ for (let y = 0; y < canvas.height; y += 1) {
109
+ for (let x = 0; x < canvas.width; x += 1) {
110
+ const alpha = pixels.data[(y * canvas.width + x) * 4 + 3]
111
+ if (alpha > 12) {
112
+ minX = Math.min(minX, x)
113
+ minY = Math.min(minY, y)
114
+ maxX = Math.max(maxX, x)
115
+ maxY = Math.max(maxY, y)
116
+ }
117
+ }
118
+ }
119
+
120
+ if (maxX < minX || maxY < minY) return dataUri
121
+
122
+ const padding = 8
123
+ const sx = Math.max(0, minX - padding)
124
+ const sy = Math.max(0, minY - padding)
125
+ const sw = Math.min(canvas.width - sx, maxX - minX + 1 + padding * 2)
126
+ const sh = Math.min(canvas.height - sy, maxY - minY + 1 + padding * 2)
127
+
128
+ if (sw >= canvas.width * 0.92 && sh >= canvas.height * 0.92) return dataUri
129
+
130
+ const previewCanvas = document.createElement('canvas')
131
+ previewCanvas.width = sw
132
+ previewCanvas.height = sh
133
+ const previewContext = previewCanvas.getContext('2d')
134
+ if (!previewContext) return dataUri
135
+ previewContext.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh)
136
+ return previewCanvas.toDataURL('image/png')
137
+ }
138
+
139
+ const lightenColor = (hex: string, amount: number) => {
140
+ const clean = hex.replace('#', '')
141
+ if (clean.length !== 6) return hex
142
+ const num = parseInt(clean, 16)
143
+ const r = Math.min(255, (num >> 16) + amount)
144
+ const g = Math.min(255, ((num >> 8) & 0x00FF) + amount)
145
+ const b = Math.min(255, (num & 0x0000FF) + amount)
146
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
147
+ }
148
+
149
+ const hexToRgb = (hex: string) => {
150
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
151
+ return result ? {
152
+ r: parseInt(result[1], 16),
153
+ g: parseInt(result[2], 16),
154
+ b: parseInt(result[3], 16),
155
+ } : { r: 15, g: 23, b: 42 }
156
+ }
157
+
158
+ const applyThemeStyles = (cfg: any) => {
159
+ const theme = cfg.theme || 'dark'
160
+ const accentColor = cfg.accentColor || '#4f83e6'
161
+ const systemTextColor = cfg.systemTextColor || '#f8fafc'
162
+
163
+ document.documentElement.setAttribute('data-theme', theme)
164
+ document.documentElement.style.setProperty('--accent', accentColor)
165
+ document.documentElement.style.setProperty('--accent-hover', lightenColor(accentColor, 20))
166
+ document.documentElement.style.setProperty('--text-main', systemTextColor)
167
+ document.documentElement.style.setProperty('--glass-blur', cfg.glassBlur || 'blur(16px)')
168
+ document.body.style.fontFamily = cfg.fontFamily || "'Outfit', sans-serif"
169
+ document.documentElement.style.fontSize = cfg.fontSize || '15px'
170
+
171
+ if (theme === 'custom') {
172
+ if (cfg.customBgStart && cfg.customBgEnd) {
173
+ document.documentElement.style.setProperty('--bg-color', cfg.customBgStart)
174
+ document.documentElement.style.setProperty('--bg-gradient', `linear-gradient(135deg, ${cfg.customBgStart} 0%, ${cfg.customBgEnd} 100%)`)
175
+ }
176
+ if (cfg.customPanelBg) {
177
+ const rgb = hexToRgb(cfg.customPanelBg)
178
+ document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`)
179
+ document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`)
180
+ document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`)
181
+ document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`)
182
+ document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`)
183
+ document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`)
184
+ document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`)
185
+ }
186
+ return
187
+ }
188
+
189
+ ;[
190
+ '--bg-color',
191
+ '--bg-gradient',
192
+ '--panel-bg',
193
+ '--panel-raised',
194
+ '--panel-soft',
195
+ '--chrome-bg',
196
+ '--surface-bg',
197
+ '--surface-strong',
198
+ '--input-bg',
199
+ ].forEach((name) => document.documentElement.style.removeProperty(name))
200
+ }
201
+
202
+ export default function MintDashboard() {
203
+ const [view, setView] = useState<DashboardView>('chat')
204
+ const [status, setStatus] = useState<RuntimeStatus | null>(null)
205
+ const [error, setError] = useState('')
206
+ const [message, setMessage] = useState('')
207
+ const [interactions, setInteractions] = useState<any[]>([])
208
+ const [pictures, setPictures] = useState<PictureEntry[]>([])
209
+ const [sending, setSending] = useState(false)
210
+ const [sendingMessage, setSendingMessage] = useState('')
211
+ const [sendingImageCount, setSendingImageCount] = useState(0)
212
+ const [streamedReply, setStreamedReply] = useState('')
213
+ const [streamedResponse, setStreamedResponse] = useState<ChatResponse | null>(null)
214
+ const [agentProgress, setAgentProgress] = useState<AgentProgress[]>([])
215
+ const [agentActivitySnapshots, setAgentActivitySnapshots] = useState<Record<string, AgentProgress[]>>({})
216
+ const [imageAttachments, setImageAttachments] = useState<Array<{ dataUri: string; name: string; previewDataUri?: string }>>([])
217
+ const [documentAttachment, setDocumentAttachment] = useState<DocumentAttachment | null>(null)
218
+ const [pendingApproval, setPendingApproval] = useState<any | null>(null)
219
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('mint:sidebar-collapsed') === 'true')
220
+ const [smartContext, setSmartContext] = useState(() => window.localStorage.getItem('mint:smart-context') !== 'false')
221
+ const [agentMode, setAgentMode] = useState(() => window.localStorage.getItem('mint:agent-mode') === 'true')
222
+ const [toastMessage, setToastMessage] = useState('')
223
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
224
+ const [dashboardDataReady, setDashboardDataReady] = useState(false)
225
+ const [startupTimedOut, setStartupTimedOut] = useState(false)
226
+ const [settingsConfig, setSettingsConfig] = useState<any>(null)
227
+ const [conversationId, setConversationId] = useState(activeConversationId)
228
+ const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
229
+ const chatEnd = useRef<HTMLDivElement | null>(null)
230
+ const startupReady = dashboardDataReady || startupTimedOut
231
+
232
+ async function refreshHistory() {
233
+ const history = await getRecentInteractions(50, conversationId)
234
+ setInteractions(history.reverse())
235
+ }
236
+
237
+ async function refreshChatSessions(nextActiveId = conversationId) {
238
+ const sessions = await listChatSessions()
239
+ const isKnown = sessions.some((session) => session.id === nextActiveId)
240
+ setChatSessions(
241
+ isKnown || nextActiveId === 'cli'
242
+ ? sessions
243
+ : [
244
+ {
245
+ id: nextActiveId,
246
+ title: 'New chat',
247
+ kind: 'conversation',
248
+ createdAt: new Date().toISOString(),
249
+ updatedAt: new Date().toISOString(),
250
+ },
251
+ ...sessions,
252
+ ],
253
+ )
254
+ }
255
+
256
+ async function refreshPictures() {
257
+ setPictures(await listSavedPictures())
258
+ }
259
+
260
+ useEffect(() => {
261
+ Promise.allSettled([
262
+ getRuntimeStatus().then(setStatus),
263
+ refreshHistory(),
264
+ refreshChatSessions(),
265
+ window.settingsApi?.getSettings()
266
+ .then((loaded: any) => {
267
+ setSettingsConfig(loaded)
268
+ applyThemeStyles({ ...DEFAULT_CONFIG, ...loaded })
269
+ }),
270
+ ]).then((results) => {
271
+ const failure = results.find((result) => result.status === 'rejected')
272
+ if (failure?.status === 'rejected') setError(errorMessage(failure.reason))
273
+ setDashboardDataReady(true)
274
+ })
275
+ window.api?.onSettingsChanged?.((loaded: any) => {
276
+ setSettingsConfig(loaded)
277
+ applyThemeStyles(loaded)
278
+ })
279
+ }, [])
280
+
281
+ useEffect(() => {
282
+ const timer = window.setTimeout(() => setStartupTimedOut(true), 10000)
283
+ return () => window.clearTimeout(timer)
284
+ }, [])
285
+
286
+ useEffect(() => {
287
+ if (view === 'pictures') refreshPictures().catch((reason: unknown) => setError(errorMessage(reason)))
288
+ }, [view])
289
+
290
+ useEffect(() => {
291
+ chatEnd.current?.scrollIntoView({ behavior: 'smooth' })
292
+ }, [interactions, sending, streamedReply, pendingApproval, agentProgress])
293
+
294
+ const showToast = (nextMessage: string) => {
295
+ setToastMessage(nextMessage)
296
+ setTimeout(() => setToastMessage((current) => current === nextMessage ? '' : current), 3000)
297
+ }
298
+
299
+ const changeView = (newView: DashboardView) => {
300
+ setView(newView)
301
+ setMobileSidebarOpen(false)
302
+ }
303
+
304
+ const toggleSidebar = () => {
305
+ const next = !sidebarCollapsed
306
+ window.localStorage.setItem('mint:sidebar-collapsed', String(next))
307
+ setSidebarCollapsed(next)
308
+ setMobileSidebarOpen(false)
309
+ }
310
+
311
+ const updateSmartContext = (enabled: boolean) => {
312
+ window.localStorage.setItem('mint:smart-context', String(enabled))
313
+ setSmartContext(enabled)
314
+ }
315
+
316
+ const updateAgentMode = (enabled: boolean) => {
317
+ window.localStorage.setItem('mint:agent-mode', String(enabled))
318
+ setAgentMode(enabled)
319
+ }
320
+
321
+ async function handleApproval(approved: boolean, _autoApproveSession = false) {
322
+ if (!pendingApproval) return
323
+ try {
324
+ await submitToolApproval(pendingApproval.token, approved)
325
+ } catch (reason) {
326
+ setError(errorMessage(reason))
327
+ } finally {
328
+ setPendingApproval(null)
329
+ }
330
+ }
331
+
332
+ async function sendPrompt(
333
+ promptText: string,
334
+ options: {
335
+ imageAttachments?: Array<{ dataUri: string; name: string; previewDataUri?: string }>
336
+ audioDataUri?: string | null
337
+ documentAttachment?: DocumentAttachment | null
338
+ systemInstruction?: string
339
+ clearComposer?: boolean
340
+ } = {},
341
+ ) {
342
+ if (sending) return
343
+ const outgoingImages = options.imageAttachments ?? []
344
+ const outgoingDocument = options.documentAttachment ?? null
345
+ const shouldUseAgentMode = agentMode || promptText.toLowerCase().startsWith('search web:')
346
+ const outgoingImage = outgoingImages.map((img) => img.dataUri).join(' ')
347
+ const outgoingImageCount = outgoingImages.length
348
+ setSending(true)
349
+ setSendingMessage(promptText)
350
+ setSendingImageCount(outgoingImageCount)
351
+ setError('')
352
+ setStreamedReply('')
353
+ setStreamedResponse(null)
354
+ setAgentProgress([])
355
+ const progressSnapshot: AgentProgress[] = []
356
+ if (options.clearComposer) {
357
+ setMessage('')
358
+ setImageAttachments([])
359
+ setDocumentAttachment(null)
360
+ }
361
+
362
+ try {
363
+ const response = await streamChatMessage(
364
+ shouldUseAgentMode ? promptText : `/chat ${promptText}`,
365
+ (chunk) => setStreamedReply((current) => `${current}${chunk}`),
366
+ outgoingImage,
367
+ options.audioDataUri ?? null,
368
+ options.systemInstruction ?? '',
369
+ (progress) => {
370
+ progressSnapshot.push(progress)
371
+ setAgentProgress((current) => [...current, progress].slice(-24))
372
+ },
373
+ outgoingDocument,
374
+ null,
375
+ conversationId,
376
+ )
377
+ setStreamedResponse(response)
378
+ const history = (await getRecentInteractions(50, conversationId)).reverse()
379
+ if (progressSnapshot.length > 0) {
380
+ const newestInteraction = [...history]
381
+ .reverse()
382
+ .find((interaction) => interaction.aiText === response.text || interaction.userText === promptText) ?? history[history.length - 1]
383
+ if (newestInteraction?.id != null) {
384
+ setAgentActivitySnapshots((current) => ({
385
+ ...current,
386
+ [String(newestInteraction.id)]: progressSnapshot.slice(),
387
+ }))
388
+ }
389
+ }
390
+ setInteractions(history)
391
+ await refreshChatSessions()
392
+ await refreshPictures()
393
+ setStreamedReply('')
394
+ setStreamedResponse(null)
395
+ } catch (reason) {
396
+ setError(errorMessage(reason))
397
+ } finally {
398
+ setSending(false)
399
+ setSendingMessage('')
400
+ setSendingImageCount(0)
401
+ }
402
+ }
403
+
404
+ async function handleSubmit(event: FormEvent<HTMLFormElement>) {
405
+ event.preventDefault()
406
+ const trimmed = message.trim()
407
+ const currentImages = imageAttachments
408
+ const currentDocument = documentAttachment
409
+ const hasAttachments = currentImages.length > 0 || Boolean(currentDocument)
410
+ if ((!trimmed && !hasAttachments) || sending) return
411
+ const promptText = trimmed || (currentImages.length > 1 ? 'Describe these images.' : currentImages.length === 1 ? 'Describe this image.' : 'Summarize this document.')
412
+ await sendPrompt(promptText, {
413
+ imageAttachments: currentImages,
414
+ documentAttachment: currentDocument,
415
+ clearComposer: true,
416
+ })
417
+ }
418
+
419
+ async function sendVoiceMessage(transcript: string, audioDataUri?: string | null) {
420
+ const promptText = transcript.trim() || 'ข้อความเสียง'
421
+ if (!promptText || sending) return
422
+ await sendPrompt(promptText, {
423
+ audioDataUri,
424
+ systemInstruction: audioDataUri
425
+ ? 'The user attached a voice message. Listen to the audio and reply naturally in the same language as the user. Do not mention transcription or this instruction.'
426
+ : '',
427
+ })
428
+ }
429
+
430
+ async function selectImage(event: ChangeEvent<HTMLInputElement>) {
431
+ const file = event.target.files?.[0]
432
+ if (!file) return
433
+ try {
434
+ const dataUri = await readImage(file)
435
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
436
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: file.name }])
437
+ } catch (reason) {
438
+ setError(errorMessage(reason))
439
+ } finally {
440
+ event.target.value = ''
441
+ }
442
+ }
443
+
444
+ function pasteImage(clipboardData: DataTransfer) {
445
+ let file: File | null = null
446
+
447
+ if (clipboardData.files && clipboardData.files.length > 0) {
448
+ for (let i = 0; i < clipboardData.files.length; i++) {
449
+ const f = clipboardData.files[i]
450
+ if (f && f.type.startsWith('image/')) {
451
+ file = f
452
+ break
453
+ }
454
+ }
455
+ }
456
+
457
+ if (!file && clipboardData.items && clipboardData.items.length > 0) {
458
+ for (let i = 0; i < clipboardData.items.length; i++) {
459
+ const item = clipboardData.items[i]
460
+ if (item && item.type.startsWith('image/')) {
461
+ const f = item.getAsFile()
462
+ if (f) {
463
+ file = f
464
+ break
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ if (!file) return false
471
+
472
+ readImage(file)
473
+ .then((dataUri) => {
474
+ const name = file.name && file.name !== 'image.png' ? file.name : 'Pasted image'
475
+ createTrimmedImagePreview(dataUri)
476
+ .catch(() => dataUri)
477
+ .then((previewDataUri) => {
478
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name }])
479
+ })
480
+ })
481
+ .catch((reason) => setError(errorMessage(reason)))
482
+ return true
483
+ }
484
+
485
+ async function readClipboardImage() {
486
+ try {
487
+ const dataUri = await readTauriClipboardImage()
488
+ if (dataUri) {
489
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
490
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
491
+ return true
492
+ }
493
+ } catch (err) {
494
+ console.warn('Tauri clipboard fallback error:', err)
495
+ }
496
+
497
+ try {
498
+ if (!navigator.clipboard?.read) return false
499
+ const items = await navigator.clipboard.read()
500
+ for (const item of items) {
501
+ const imageType = item.types.find((type) => type.startsWith('image/'))
502
+ if (!imageType) continue
503
+ const blob = await item.getType(imageType)
504
+ const file = new File([blob], 'Pasted image', { type: imageType })
505
+ const dataUri = await readImage(file)
506
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
507
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
508
+ return true
509
+ }
510
+ return false
511
+ } catch {
512
+ // Some environments expose pasted images only through ClipboardEvent.
513
+ return false
514
+ }
515
+ }
516
+
517
+ async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
518
+ const file = event.target.files?.[0]
519
+ if (!file) return
520
+ try {
521
+ if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
522
+ throw new Error('Only PDF files are supported')
523
+ }
524
+ setDocumentAttachment({
525
+ filename: file.name,
526
+ dataUri: await readDocument(file),
527
+ })
528
+ } catch (reason) {
529
+ setError(errorMessage(reason))
530
+ } finally {
531
+ event.target.value = ''
532
+ }
533
+ }
534
+
535
+ function startWebSearch() {
536
+ updateAgentMode(true)
537
+ setMessage((current) => current.trim() ? `Search web: ${current.trim()}` : 'Search web: ')
538
+ }
539
+
540
+ async function captureScreen() {
541
+ try {
542
+ await window.api.startVision()
543
+ } catch (reason) {
544
+ setError(errorMessage(reason))
545
+ }
546
+ }
547
+
548
+ async function clearHistory(action: 'New chat' | 'Clear history') {
549
+ try {
550
+ if (action === 'New chat') {
551
+ const next = createConversationId()
552
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, next)
553
+ setConversationId(next)
554
+ await refreshChatSessions(next)
555
+ } else {
556
+ if (!window.confirm(`${action} will clear the current conversation history. Continue?`)) return
557
+ await clearChatHistory(conversationId)
558
+ }
559
+ setInteractions([])
560
+ setAgentActivitySnapshots({})
561
+ setStreamedReply('')
562
+ setStreamedResponse(null)
563
+ setMessage('')
564
+ setImageAttachments([])
565
+ } catch (reason) {
566
+ setError(errorMessage(reason))
567
+ }
568
+ }
569
+
570
+ async function selectConversation(id: string) {
571
+ if (id === conversationId) {
572
+ changeView('chat')
573
+ return
574
+ }
575
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, id)
576
+ setConversationId(id)
577
+ changeView('chat')
578
+ setStreamedReply('')
579
+ setStreamedResponse(null)
580
+ setMessage('')
581
+ setImageAttachments([])
582
+ setDocumentAttachment(null)
583
+ setAgentProgress([])
584
+ const history = await getRecentInteractions(50, id)
585
+ setInteractions(history.reverse())
586
+ }
587
+
588
+ async function deleteConversation(id: string) {
589
+ if (id === 'cli') return
590
+ const session = chatSessions.find((item) => item.id === id)
591
+ const title = session?.title || 'this chat'
592
+ if (!window.confirm(`Delete "${title}"? This will remove the conversation and its messages.`)) return
593
+
594
+ try {
595
+ await deleteChatSession(id)
596
+ const remaining = chatSessions.filter((item) => item.id !== id && item.kind !== 'cli' && item.id !== 'conversation-default')
597
+ const nextActive = id === conversationId
598
+ ? (remaining[0]?.id ?? createConversationId())
599
+ : conversationId
600
+
601
+ if (nextActive !== conversationId) {
602
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, nextActive)
603
+ setConversationId(nextActive)
604
+ setAgentProgress([])
605
+ const history = await getRecentInteractions(50, nextActive)
606
+ setInteractions(history.reverse())
607
+ }
608
+
609
+ await refreshChatSessions(nextActive)
610
+ if (id !== conversationId) return
611
+ setStreamedReply('')
612
+ setStreamedResponse(null)
613
+ setMessage('')
614
+ setImageAttachments([])
615
+ setDocumentAttachment(null)
616
+ } catch (reason) {
617
+ setError(errorMessage(reason))
618
+ }
619
+ }
620
+
621
+ async function renameConversation(id: string, newTitle: string) {
622
+ if (!newTitle.trim()) return
623
+ try {
624
+ await renameChatSession(id, newTitle.trim())
625
+ await refreshChatSessions(conversationId)
626
+ } catch (reason) {
627
+ setError(errorMessage(reason))
628
+ }
629
+ }
630
+
631
+ async function changeProvider(provider: string) {
632
+ try {
633
+ const config = await window.settingsApi.getSettings()
634
+ config.aiProvider = provider
635
+ await window.settingsApi.saveSettings(config)
636
+ setSettingsConfig(config)
637
+ setStatus(await getRuntimeStatus())
638
+ } catch (reason) {
639
+ setError(errorMessage(reason))
640
+ }
641
+ }
642
+
643
+ async function changeModel(modelName: string) {
644
+ try {
645
+ const config = await window.settingsApi.getSettings()
646
+ const provider = config.aiProvider
647
+ if (provider === 'gemini') {
648
+ config.geminiModel = modelName
649
+ } else if (provider === 'openai') {
650
+ config.openaiModel = modelName
651
+ } else if (provider === 'openrouter') {
652
+ config.openrouterModel = modelName
653
+ } else if (provider === 'deepseek') {
654
+ config.deepseekModel = modelName
655
+ } else if (provider === 'anthropic') {
656
+ config.anthropicModel = modelName
657
+ } else if (provider === 'huggingface') {
658
+ config.hfModel = modelName
659
+ } else if (provider === 'local_openai') {
660
+ config.localModelName = modelName
661
+ } else if (provider === 'ollama') {
662
+ config.ollamaModel = modelName
663
+ }
664
+ await window.settingsApi.saveSettings(config)
665
+ setSettingsConfig(config)
666
+ setStatus(await getRuntimeStatus())
667
+ } catch (reason) {
668
+ setError(errorMessage(reason))
669
+ }
670
+ }
671
+
672
+
673
+
674
+ return (
675
+ <div className={`app-container ${startupReady ? '' : 'is-loading'}`}>
676
+ <div className={`app-body ${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${view === 'pictures' ? 'pictures-open' : ''} ${mobileSidebarOpen ? 'mobile-sidebar-open' : ''}`}>
677
+ {mobileSidebarOpen && (
678
+ <div
679
+ className="sidebar-backdrop"
680
+ onClick={() => setMobileSidebarOpen(false)}
681
+ style={{
682
+ position: 'fixed',
683
+ top: 0,
684
+ left: 0,
685
+ right: 0,
686
+ bottom: 0,
687
+ background: 'rgba(0, 0, 0, 0.5)',
688
+ backdropFilter: 'blur(4px)',
689
+ zIndex: 9998,
690
+ }}
691
+ />
692
+ )}
693
+ <DashboardSidebar
694
+ view={view}
695
+ sidebarCollapsed={sidebarCollapsed}
696
+ sending={sending}
697
+ chatSessions={chatSessions}
698
+ activeConversationId={conversationId}
699
+ onToggleSidebar={toggleSidebar}
700
+ onClearHistory={clearHistory}
701
+ onSelectConversation={selectConversation}
702
+ onDeleteConversation={deleteConversation}
703
+ onRenameConversation={renameConversation}
704
+ onSetView={changeView}
705
+ />
706
+ <main className="assistant-workspace model-hidden">
707
+ <ChatPanel
708
+ interactions={interactions}
709
+ sending={sending}
710
+ sendingMessage={sendingMessage}
711
+ sendingImageCount={sendingImageCount}
712
+ streamedReply={streamedReply}
713
+ streamedResponse={streamedResponse}
714
+ agentProgress={agentProgress}
715
+ agentActivitySnapshots={agentActivitySnapshots}
716
+ message={message}
717
+ imageAttachments={imageAttachments}
718
+ documentName={documentAttachment?.filename ?? ''}
719
+ pendingApproval={pendingApproval}
720
+ smartContext={smartContext}
721
+ agentMode={agentMode}
722
+ status={status}
723
+ chatEnd={chatEnd}
724
+ welcomeInteraction={MOCK_WELCOME_INTERACTION}
725
+ onSubmit={handleSubmit}
726
+ onSelectImage={selectImage}
727
+ onSelectDocument={selectDocument}
728
+ onPasteImage={pasteImage}
729
+ onReadClipboardImage={readClipboardImage}
730
+ onSetMessage={setMessage}
731
+ onSendVoiceMessage={sendVoiceMessage}
732
+ onRemoveImage={(idx: number) => {
733
+ setImageAttachments((current) => current.filter((_, i) => i !== idx))
734
+ }}
735
+ onRemoveDocument={() => setDocumentAttachment(null)}
736
+ onStartWebSearch={startWebSearch}
737
+ onCaptureScreen={captureScreen}
738
+ onSetSmartContext={updateSmartContext}
739
+ onSetAgentMode={updateAgentMode}
740
+ onSetProvider={changeProvider}
741
+ settingsConfig={settingsConfig}
742
+ onSetModel={changeModel}
743
+ onApproval={handleApproval}
744
+ onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
745
+ />
746
+ </main>
747
+ <PicturesLibrary view={view} pictures={pictures} onSetView={changeView} />
748
+ </div>
749
+ <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
750
+ <div className="startup-loading-content">
751
+ <div className="startup-loading-dots" aria-hidden="true"><span /><span /><span /></div>
752
+ <div className="startup-loading-text">Loading Agent Mint</div>
753
+ </div>
754
+ </div>
755
+ {error && (
756
+ <div className="mint-error" style={{ position: 'absolute', bottom: '20px', right: '20px', zIndex: 100, margin: 0, boxShadow: '0 8px 24px rgba(0,0,0,0.3)' }}>
757
+ {error}
758
+ <button onClick={() => setError('')} style={{ marginLeft: '12px', background: 'transparent', border: 0, color: 'white', cursor: 'pointer' }}>✕</button>
759
+ </div>
760
+ )}
761
+ </div>
762
+ )
763
+ }