@geminilight/mindos 0.1.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 (282) hide show
  1. package/.env.local.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +423 -0
  4. package/README_zh.md +423 -0
  5. package/app/README.md +152 -0
  6. package/app/app/api/ask/route.ts +170 -0
  7. package/app/app/api/ask-sessions/route.ts +90 -0
  8. package/app/app/api/auth/route.ts +37 -0
  9. package/app/app/api/backlinks/route.ts +22 -0
  10. package/app/app/api/bootstrap/route.ts +37 -0
  11. package/app/app/api/extract-pdf/route.ts +82 -0
  12. package/app/app/api/file/route.ts +138 -0
  13. package/app/app/api/files/route.ts +12 -0
  14. package/app/app/api/git/route.ts +42 -0
  15. package/app/app/api/graph/route.ts +113 -0
  16. package/app/app/api/recent-files/route.ts +10 -0
  17. package/app/app/api/search/route.ts +17 -0
  18. package/app/app/api/settings/reset-token/route.ts +21 -0
  19. package/app/app/api/settings/route.ts +123 -0
  20. package/app/app/error.tsx +33 -0
  21. package/app/app/globals.css +368 -0
  22. package/app/app/icon.svg +35 -0
  23. package/app/app/layout.tsx +103 -0
  24. package/app/app/login/page.tsx +120 -0
  25. package/app/app/page.tsx +12 -0
  26. package/app/app/view/[...path]/ViewPageClient.tsx +343 -0
  27. package/app/app/view/[...path]/error.tsx +33 -0
  28. package/app/app/view/[...path]/loading.tsx +15 -0
  29. package/app/app/view/[...path]/page.tsx +93 -0
  30. package/app/components/AskFab.tsx +59 -0
  31. package/app/components/AskModal.tsx +398 -0
  32. package/app/components/Backlinks.tsx +75 -0
  33. package/app/components/Breadcrumb.tsx +31 -0
  34. package/app/components/CsvView.tsx +325 -0
  35. package/app/components/DirView.tsx +138 -0
  36. package/app/components/Editor.tsx +124 -0
  37. package/app/components/EditorWrapper.tsx +17 -0
  38. package/app/components/ErrorBoundary.tsx +53 -0
  39. package/app/components/FileTree.tsx +369 -0
  40. package/app/components/HomeContent.tsx +262 -0
  41. package/app/components/JsonView.tsx +27 -0
  42. package/app/components/MarkdownEditor.tsx +95 -0
  43. package/app/components/MarkdownView.tsx +118 -0
  44. package/app/components/SearchModal.tsx +193 -0
  45. package/app/components/SettingsModal.tsx +237 -0
  46. package/app/components/Sidebar.tsx +136 -0
  47. package/app/components/SidebarLayout.tsx +36 -0
  48. package/app/components/TableOfContents.tsx +150 -0
  49. package/app/components/ThemeToggle.tsx +34 -0
  50. package/app/components/WysiwygEditor.tsx +75 -0
  51. package/app/components/ask/FileChip.tsx +30 -0
  52. package/app/components/ask/MentionPopover.tsx +52 -0
  53. package/app/components/ask/MessageList.tsx +126 -0
  54. package/app/components/ask/SessionHistory.tsx +49 -0
  55. package/app/components/renderers/AgentInspectorRenderer.tsx +277 -0
  56. package/app/components/renderers/BacklinksRenderer.tsx +147 -0
  57. package/app/components/renderers/ConfigRenderer.tsx +236 -0
  58. package/app/components/renderers/CsvRenderer.tsx +77 -0
  59. package/app/components/renderers/DiffRenderer.tsx +310 -0
  60. package/app/components/renderers/GraphRenderer.tsx +428 -0
  61. package/app/components/renderers/SummaryRenderer.tsx +251 -0
  62. package/app/components/renderers/TimelineRenderer.tsx +213 -0
  63. package/app/components/renderers/TodoRenderer.tsx +474 -0
  64. package/app/components/renderers/WorkflowRenderer.tsx +404 -0
  65. package/app/components/renderers/csv/BoardView.tsx +146 -0
  66. package/app/components/renderers/csv/ConfigPanel.tsx +117 -0
  67. package/app/components/renderers/csv/EditableCell.tsx +43 -0
  68. package/app/components/renderers/csv/GalleryView.tsx +40 -0
  69. package/app/components/renderers/csv/TableView.tsx +164 -0
  70. package/app/components/renderers/csv/types.ts +87 -0
  71. package/app/components/settings/AiTab.tsx +111 -0
  72. package/app/components/settings/AppearanceTab.tsx +101 -0
  73. package/app/components/settings/KnowledgeTab.tsx +157 -0
  74. package/app/components/settings/PluginsTab.tsx +82 -0
  75. package/app/components/settings/Primitives.tsx +60 -0
  76. package/app/components/settings/ShortcutsTab.tsx +22 -0
  77. package/app/components/settings/types.ts +41 -0
  78. package/app/components/ui/button.tsx +60 -0
  79. package/app/components/ui/dialog.tsx +157 -0
  80. package/app/components/ui/input.tsx +20 -0
  81. package/app/components/ui/scroll-area.tsx +55 -0
  82. package/app/components/ui/toggle.tsx +44 -0
  83. package/app/components/ui/tooltip.tsx +66 -0
  84. package/app/components.json +25 -0
  85. package/app/data/pages/home-dark.png +0 -0
  86. package/app/data/pages/home-mobile-crop.png +0 -0
  87. package/app/data/pages/home-mobile.png +0 -0
  88. package/app/data/pages/home.png +0 -0
  89. package/app/data/pages/view-dir.png +0 -0
  90. package/app/data/pages/view-file-bot.png +0 -0
  91. package/app/data/pages/view-file-dark-crop.png +0 -0
  92. package/app/data/pages/view-file-dark.png +0 -0
  93. package/app/data/pages/view-file-mobile.png +0 -0
  94. package/app/data/pages/view-file-sm.png +0 -0
  95. package/app/data/pages/view-file-top.png +0 -0
  96. package/app/data/pages/view-file.png +0 -0
  97. package/app/eslint.config.mjs +18 -0
  98. package/app/hooks/useAskSession.ts +181 -0
  99. package/app/hooks/useFileUpload.ts +126 -0
  100. package/app/hooks/useMention.ts +65 -0
  101. package/app/lib/LocaleContext.tsx +40 -0
  102. package/app/lib/actions.ts +40 -0
  103. package/app/lib/agent/index.ts +3 -0
  104. package/app/lib/agent/model.ts +18 -0
  105. package/app/lib/agent/prompt.ts +32 -0
  106. package/app/lib/agent/tools.ts +151 -0
  107. package/app/lib/api.ts +55 -0
  108. package/app/lib/core/backlinks.ts +40 -0
  109. package/app/lib/core/csv.ts +28 -0
  110. package/app/lib/core/fs-ops.ts +118 -0
  111. package/app/lib/core/git.ts +50 -0
  112. package/app/lib/core/index.ts +58 -0
  113. package/app/lib/core/lines.ts +89 -0
  114. package/app/lib/core/search.ts +79 -0
  115. package/app/lib/core/security.ts +43 -0
  116. package/app/lib/core/tree.ts +113 -0
  117. package/app/lib/core/types.ts +40 -0
  118. package/app/lib/fs.ts +467 -0
  119. package/app/lib/i18n.ts +300 -0
  120. package/app/lib/jwt.ts +58 -0
  121. package/app/lib/renderers/index.ts +79 -0
  122. package/app/lib/renderers/registry.ts +70 -0
  123. package/app/lib/settings.ts +150 -0
  124. package/app/lib/types.ts +32 -0
  125. package/app/lib/utils.ts +34 -0
  126. package/app/next-env.d.ts +6 -0
  127. package/app/next.config.ts +10 -0
  128. package/app/package-lock.json +15306 -0
  129. package/app/package.json +71 -0
  130. package/app/postcss.config.mjs +7 -0
  131. package/app/proxy.ts +64 -0
  132. package/app/public/file.svg +1 -0
  133. package/app/public/globe.svg +1 -0
  134. package/app/public/landing/index.html +353 -0
  135. package/app/public/landing/style.css +216 -0
  136. package/app/public/logo-square.svg +37 -0
  137. package/app/public/logo.svg +37 -0
  138. package/app/public/next.svg +1 -0
  139. package/app/public/vercel.svg +1 -0
  140. package/app/public/window.svg +1 -0
  141. package/app/scripts/extract-pdf.cjs +56 -0
  142. package/app/tsconfig.json +34 -0
  143. package/app/vitest.config.ts +14 -0
  144. package/assets/demo-flow-zh.html +622 -0
  145. package/assets/images/demo-flow-dark.png +0 -0
  146. package/assets/images/demo-flow-light.png +0 -0
  147. package/assets/images/demo-flow-zh-dark.png +0 -0
  148. package/assets/images/demo-flow-zh-light.png +0 -0
  149. package/assets/images/gui-sync-cv.png +0 -0
  150. package/assets/logo-square.svg +37 -0
  151. package/bin/cli.js +894 -0
  152. package/mcp/README.md +113 -0
  153. package/mcp/package-lock.json +1717 -0
  154. package/mcp/package.json +18 -0
  155. package/mcp/src/index.ts +494 -0
  156. package/mcp/tsconfig.json +13 -0
  157. package/package.json +49 -0
  158. package/scripts/setup.js +675 -0
  159. package/scripts/upgrade-prompt.md +147 -0
  160. package/skills/mindos/SKILL.md +319 -0
  161. package/skills/mindos-zh/SKILL.md +318 -0
  162. package/templates/README.md +31 -0
  163. package/templates/empty/CHANGELOG.md +9 -0
  164. package/templates/empty/CONFIG.json +197 -0
  165. package/templates/empty/CONFIG.md +73 -0
  166. package/templates/empty/INSTRUCTION.md +177 -0
  167. package/templates/empty/README.md +27 -0
  168. package/templates/en/CHANGELOG.md +9 -0
  169. package/templates/en/CONFIG.json +197 -0
  170. package/templates/en/CONFIG.md +73 -0
  171. package/templates/en/INSTRUCTION.md +177 -0
  172. package/templates/en/README.md +27 -0
  173. package/templates/en/TODO.md +13 -0
  174. package/templates/en//360/237/221/244 Profile/INSTRUCTION.md" +21 -0
  175. package/templates/en//360/237/221/244 Profile/README.md" +15 -0
  176. package/templates/en//360/237/221/244 Profile//342/232/231/357/270/217 Preferences.md" +21 -0
  177. package/templates/en//360/237/221/244 Profile//360/237/216/257 Focus.md" +31 -0
  178. package/templates/en//360/237/221/244 Profile//360/237/221/244 Identity.md" +22 -0
  179. package/templates/en//360/237/223/232 Resources/INSTRUCTION.md" +29 -0
  180. package/templates/en//360/237/223/232 Resources/README.md" +21 -0
  181. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +1 -0
  182. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +1 -0
  183. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +1 -0
  184. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +1 -0
  185. package/templates/en//360/237/223/235 Notes/INSTRUCTION.md" +31 -0
  186. package/templates/en//360/237/223/235 Notes/Ideas/README.md" +8 -0
  187. package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +16 -0
  188. package/templates/en//360/237/223/235 Notes/Inbox/README.md" +8 -0
  189. package/templates/en//360/237/223/235 Notes/Inbox//360/237/247/252_example_quick_capture.md" +14 -0
  190. package/templates/en//360/237/223/235 Notes/Meetings/README.md" +8 -0
  191. package/templates/en//360/237/223/235 Notes/Meetings//360/237/247/252_example_meeting_note.md" +17 -0
  192. package/templates/en//360/237/223/235 Notes/README.md" +24 -0
  193. package/templates/en//360/237/223/235 Notes/Waiting/README.md" +8 -0
  194. package/templates/en//360/237/223/235 Notes/Waiting//360/237/247/252_example_blocked_item.md" +16 -0
  195. package/templates/en//360/237/224/204 Workflows/Configurations/README.md" +3 -0
  196. package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +14 -0
  197. package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +21 -0
  198. package/templates/en//360/237/224/204 Workflows/Information/README.md" +16 -0
  199. package/templates/en//360/237/224/204 Workflows/Information//360/237/247/252_example_info_capture_sop.md" +13 -0
  200. package/templates/en//360/237/224/204 Workflows/Media/README.md" +16 -0
  201. package/templates/en//360/237/224/204 Workflows/Media//360/237/247/252_example_content_publish_sop.md" +13 -0
  202. package/templates/en//360/237/224/204 Workflows/README.md" +22 -0
  203. package/templates/en//360/237/224/204 Workflows/Research/README.md" +16 -0
  204. package/templates/en//360/237/224/204 Workflows/Research//360/237/247/252_example_lit_review_sop.md" +16 -0
  205. package/templates/en//360/237/224/204 Workflows/Startup/README.md" +3 -0
  206. package/templates/en//360/237/224/204 Workflows/Startup//360/237/247/252_example_weekly_founder_ops.md" +22 -0
  207. package/templates/en//360/237/224/227 Connections/Classmates/README.md" +11 -0
  208. package/templates/en//360/237/224/227 Connections/Classmates//360/237/247/252_example_leo_chen.md" +16 -0
  209. package/templates/en//360/237/224/227 Connections/Colleagues/README.md" +11 -0
  210. package/templates/en//360/237/224/227 Connections/Colleagues//360/237/247/252_example_ethan_zhao.md" +16 -0
  211. package/templates/en//360/237/224/227 Connections/Connections Overview.csv" +5 -0
  212. package/templates/en//360/237/224/227 Connections/Family/README.md" +11 -0
  213. package/templates/en//360/237/224/227 Connections/Family//360/237/247/252_example_james_wang.md" +16 -0
  214. package/templates/en//360/237/224/227 Connections/Friends/README.md" +11 -0
  215. package/templates/en//360/237/224/227 Connections/Friends//360/237/247/252_example_lily_lin.md" +16 -0
  216. package/templates/en//360/237/224/227 Connections/INSTRUCTION.md" +56 -0
  217. package/templates/en//360/237/224/227 Connections/README.md" +20 -0
  218. package/templates/en//360/237/232/200 Projects/Archived/README.md" +9 -0
  219. package/templates/en//360/237/232/200 Projects/Archived//360/237/247/252_example_archived_project_note.md" +14 -0
  220. package/templates/en//360/237/232/200 Projects/INSTRUCTION.md" +29 -0
  221. package/templates/en//360/237/232/200 Projects/Products/README.md" +16 -0
  222. package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +20 -0
  223. package/templates/en//360/237/232/200 Projects/README.md" +21 -0
  224. package/templates/en//360/237/232/200 Projects/Research/README.md" +16 -0
  225. package/templates/en//360/237/232/200 Projects/Research//360/237/247/252_example_research_project_brief.md" +16 -0
  226. package/templates/template-generation-skill.md +79 -0
  227. package/templates/zh/CHANGELOG.md +9 -0
  228. package/templates/zh/CONFIG.json +197 -0
  229. package/templates/zh/CONFIG.md +66 -0
  230. package/templates/zh/INSTRUCTION.md +177 -0
  231. package/templates/zh/README.md +27 -0
  232. package/templates/zh/TODO.md +13 -0
  233. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/INSTRUCTION.md" +28 -0
  234. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +20 -0
  235. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/217 Preferences.md" +21 -0
  236. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/257 Focus.md" +31 -0
  237. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/244 Identity.md" +22 -0
  238. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/INSTRUCTION.md" +29 -0
  239. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +21 -0
  240. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +1 -0
  241. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /344/272/247/345/223/201.csv" +1 -0
  242. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/255/246/350/200/205/346/270/205/345/215/225.csv" +1 -0
  243. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/267/245/345/205/267/346/270/205/345/215/225.csv" +1 -0
  244. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/INSTRUCTION.md" +31 -0
  245. package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +24 -0
  246. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256/README.md" +8 -0
  247. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256//360/237/247/252_example_/344/274/232/350/256/256/347/272/252/350/246/201.md" +17 -0
  248. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210/README.md" +8 -0
  249. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//345/276/205/345/217/215/351/246/210//360/237/247/252_example_/345/276/205/345/217/215/351/246/210/344/272/213/351/241/271.md" +16 -0
  250. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225/README.md" +8 -0
  251. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225//360/237/247/252_example_/344/272/247/345/223/201/346/203/263/346/263/225.md" +16 -0
  252. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261/README.md" +8 -0
  253. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/224/266/344/273/266/347/256/261//360/237/247/252_example_/344/270/264/346/227/266/351/200/237/350/256/260.md" +13 -0
  254. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/INSTRUCTION.md" +29 -0
  255. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +21 -0
  256. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257/README.md" +16 -0
  257. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257//360/237/247/252_example_/344/277/241/346/201/257/351/207/207/351/233/206/346/265/201/347/250/213.md" +13 -0
  258. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223/README.md" +16 -0
  259. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223//360/237/247/252_example_/345/206/205/345/256/271/345/217/221/345/270/203/346/265/201/347/250/213.md" +13 -0
  260. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224/README.md" +16 -0
  261. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224//360/237/247/252_example_/346/226/207/347/214/256/347/273/274/350/277/260/346/265/201/347/250/213.md" +16 -0
  262. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256/README.md" +3 -0
  263. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256//360/237/247/252_example_/351/205/215/347/275/256/346/233/264/346/226/260/346/265/201/347/250/213.md" +26 -0
  264. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/INSTRUCTION.md" +62 -0
  265. package/templates/zh//360/237/224/227 /345/205/263/347/263/273/README.md" +20 -0
  266. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/205/263/347/263/273/346/200/273/350/247/210.csv" +5 -0
  267. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213/README.md" +11 -0
  268. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213//360/237/247/252_example_/345/220/214/344/272/213/350/265/265/344/270/200/350/276/260.md" +16 -0
  269. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246/README.md" +11 -0
  270. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246//360/237/247/252_example_/345/220/214/345/255/246/351/231/210/347/253/213/346/254/247.md" +16 -0
  271. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272/README.md" +11 -0
  272. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272//360/237/247/252_example_/345/256/266/344/272/272/347/216/213/345/273/272/345/233/275.md" +16 -0
  273. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213/README.md" +11 -0
  274. package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213//360/237/247/252_example_/346/234/213/345/217/213/346/236/227/345/260/217/344/270/275.md" +16 -0
  275. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/INSTRUCTION.md" +31 -0
  276. package/templates/zh//360/237/232/200 /351/241/271/347/233/256/README.md" +21 -0
  277. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201/README.md" +16 -0
  278. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201//360/237/247/252_example_/344/272/247/345/223/201/351/241/271/347/233/256/347/256/200/346/212/245.md" +20 -0
  279. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243/README.md" +9 -0
  280. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//345/267/262/345/275/222/346/241/243//360/237/247/252_example_/345/275/222/346/241/243/351/241/271/347/233/256/350/256/260/345/275/225.md" +15 -0
  281. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224/README.md" +16 -0
  282. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224//360/237/247/252_example_/347/247/221/347/240/224/351/241/271/347/233/256/347/256/200/346/212/245.md" +16 -0
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { usePathname } from 'next/navigation';
6
+ import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
7
+ import FileTree from './FileTree';
8
+ import SearchModal from './SearchModal';
9
+ import AskModal from './AskModal';
10
+ import SettingsModal from './SettingsModal';
11
+ import { FileNode } from '@/lib/types';
12
+ import { useLocale } from '@/lib/LocaleContext';
13
+
14
+ interface SidebarProps {
15
+ fileTree: FileNode[];
16
+ collapsed?: boolean;
17
+ onCollapse?: () => void;
18
+ onExpand?: () => void;
19
+ }
20
+
21
+ const Logo = ({ id }: { id: string }) => (
22
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40" fill="none" className="w-8 h-4 text-[var(--amber)]" aria-hidden="true">
23
+ <defs>
24
+ <linearGradient id={`grad-human-${id}`} x1="35" y1="20" x2="5" y2="20" gradientUnits="userSpaceOnUse">
25
+ <stop offset="0%" stopColor="currentColor" stopOpacity="0.8"/>
26
+ <stop offset="100%" stopColor="currentColor" stopOpacity="0.3"/>
27
+ </linearGradient>
28
+ <linearGradient id={`grad-agent-${id}`} x1="35" y1="20" x2="75" y2="20" gradientUnits="userSpaceOnUse">
29
+ <stop offset="0%" stopColor="currentColor" stopOpacity="0.8"/>
30
+ <stop offset="100%" stopColor="currentColor" stopOpacity="1"/>
31
+ </linearGradient>
32
+ </defs>
33
+ <path d="M35,20 C25,35 8,35 8,20 C8,5 25,5 35,20" stroke={`url(#grad-human-${id})`} strokeWidth="3" strokeDasharray="2 4" strokeLinecap="round"/>
34
+ <path d="M35,20 C45,2 75,2 75,20 C75,38 45,38 35,20" stroke={`url(#grad-agent-${id})`} strokeWidth="4.5" strokeLinecap="round"/>
35
+ <path d="M35,17.5 Q35,20 37.5,20 Q35,20 35,22.5 Q35,20 32.5,20 Q35,20 35,17.5 Z" fill="#FEF3C7"/>
36
+ </svg>
37
+ );
38
+
39
+ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExpand }: SidebarProps) {
40
+ const [searchOpen, setSearchOpen] = useState(false);
41
+ const [askOpen, setAskOpen] = useState(false);
42
+ const [settingsOpen, setSettingsOpen] = useState(false);
43
+ const [mobileOpen, setMobileOpen] = useState(false);
44
+ const { t } = useLocale();
45
+
46
+ const pathname = usePathname();
47
+ const currentFile = pathname.startsWith('/view/')
48
+ ? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
49
+ : undefined;
50
+
51
+ useEffect(() => {
52
+ const handler = (e: KeyboardEvent) => {
53
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(v => !v); }
54
+ if ((e.metaKey || e.ctrlKey) && e.key === '/') { e.preventDefault(); setAskOpen(v => !v); }
55
+ if ((e.metaKey || e.ctrlKey) && e.key === ',') { e.preventDefault(); setSettingsOpen(v => !v); }
56
+ };
57
+ window.addEventListener('keydown', handler);
58
+ return () => window.removeEventListener('keydown', handler);
59
+ }, []);
60
+
61
+ useEffect(() => { setMobileOpen(false); }, [pathname]);
62
+
63
+ const sidebarContent = (
64
+ <div className="flex flex-col h-full">
65
+ <div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
66
+ <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
67
+ <Logo id="desktop" />
68
+ <span className="font-semibold text-foreground text-sm tracking-wide" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>MindOS</span>
69
+ </Link>
70
+ {/* Mobile close */}
71
+ <button onClick={() => setMobileOpen(false)} className="md:hidden p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
72
+ <X size={16} />
73
+ </button>
74
+ {/* Desktop action buttons — trimmed to 4 */}
75
+ <div className="hidden md:flex items-center gap-1">
76
+ <button onClick={() => setSearchOpen(true)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.searchTitle} aria-label={t.sidebar.searchTitle}>
77
+ <Search size={15} />
78
+ </button>
79
+ <button onClick={() => setSettingsOpen(true)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.settingsTitle} aria-label={t.sidebar.settingsTitle}>
80
+ <Settings size={15} />
81
+ </button>
82
+ <button onClick={onCollapse} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.collapseTitle} aria-label={t.sidebar.collapseTitle}>
83
+ <PanelLeftClose size={15} />
84
+ </button>
85
+ </div>
86
+ </div>
87
+
88
+ <div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
89
+ <FileTree nodes={fileTree} onNavigate={() => setMobileOpen(false)} />
90
+ </div>
91
+ </div>
92
+ );
93
+
94
+ return (
95
+ <>
96
+ <aside className={`sidebar-panel hidden md:flex fixed top-0 left-0 h-screen w-[280px] z-30 bg-card border-r border-border flex-col transition-transform duration-300 ${collapsed ? '-translate-x-full' : 'translate-x-0'}`}>
97
+ {sidebarContent}
98
+ </aside>
99
+
100
+ {collapsed && (
101
+ <button onClick={onExpand} className="hidden md:flex fixed top-4 left-0 z-30 items-center justify-center w-6 h-10 bg-card border border-border rounded-r-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.sidebar.expandTitle}>
102
+ <PanelLeftOpen size={14} />
103
+ </button>
104
+ )}
105
+
106
+ {/* Mobile navbar */}
107
+ <header className="md:hidden fixed top-0 left-0 right-0 z-30 bg-card border-b border-border flex items-center justify-between px-3 py-2" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
108
+ <button onClick={() => setMobileOpen(true)} className="p-2.5 -ml-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label="Open menu">
109
+ <Menu size={20} />
110
+ </button>
111
+ <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
112
+ <Logo id="mobile" />
113
+ <span className="font-semibold text-foreground text-sm tracking-wide">MindOS</span>
114
+ </Link>
115
+ <div className="flex items-center gap-0.5">
116
+ <button onClick={() => setSearchOpen(true)} className="p-2.5 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.searchTitle}>
117
+ <Search size={20} />
118
+ </button>
119
+ <button onClick={() => setSettingsOpen(true)} className="p-2.5 -mr-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.settingsTitle}>
120
+ <Settings size={20} />
121
+ </button>
122
+ </div>
123
+ </header>
124
+
125
+ {mobileOpen && <div className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />}
126
+
127
+ <aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
128
+ {sidebarContent}
129
+ </aside>
130
+
131
+ <SearchModal open={searchOpen} onClose={() => setSearchOpen(false)} />
132
+ <AskModal open={askOpen} onClose={() => setAskOpen(false)} currentFile={currentFile} />
133
+ <SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
134
+ </>
135
+ );
136
+ }
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Sidebar from './Sidebar';
5
+ import AskFab from './AskFab';
6
+ import { FileNode } from '@/lib/types';
7
+
8
+ interface SidebarLayoutProps {
9
+ fileTree: FileNode[];
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps) {
14
+ const [collapsed, setCollapsed] = useState(false);
15
+
16
+ return (
17
+ <>
18
+ <Sidebar
19
+ fileTree={fileTree}
20
+ collapsed={collapsed}
21
+ onCollapse={() => setCollapsed(true)}
22
+ onExpand={() => setCollapsed(false)}
23
+ />
24
+ <main
25
+ className={`min-h-screen transition-all duration-300 pt-[52px] md:pt-0 ${
26
+ collapsed ? 'md:pl-0' : 'md:pl-[280px]'
27
+ }`}
28
+ >
29
+ <div className="min-h-screen bg-background">
30
+ {children}
31
+ </div>
32
+ </main>
33
+ <AskFab />
34
+ </>
35
+ );
36
+ }
@@ -0,0 +1,150 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { ChevronRight } from 'lucide-react';
5
+ import GithubSlugger from 'github-slugger';
6
+
7
+ interface Heading {
8
+ id: string;
9
+ text: string;
10
+ level: number;
11
+ }
12
+
13
+ function parseHeadings(content: string): Heading[] {
14
+ const slugger = new GithubSlugger();
15
+ const lines = content.split('\n');
16
+ const headings: Heading[] = [];
17
+ for (const line of lines) {
18
+ const match = line.match(/^(#{1,4})\s+(.+)/);
19
+ if (match) {
20
+ const level = match[1].length;
21
+ const text = match[2].trim();
22
+ const id = slugger.slug(text);
23
+ headings.push({ id, text, level });
24
+ }
25
+ }
26
+ return headings;
27
+ }
28
+
29
+ const TOPBAR_H = 44;
30
+ const SCROLL_OFFSET = TOPBAR_H + 12;
31
+ const NAV_W = 212;
32
+
33
+ interface TableOfContentsProps {
34
+ content: string;
35
+ }
36
+
37
+ export default function TableOfContents({ content }: TableOfContentsProps) {
38
+ const headings = parseHeadings(content);
39
+ const [activeId, setActiveId] = useState<string>('');
40
+ const [collapsed, setCollapsed] = useState(false);
41
+ const observerRef = useRef<IntersectionObserver | null>(null);
42
+
43
+ useEffect(() => {
44
+ if (headings.length === 0) return;
45
+ const timer = setTimeout(() => {
46
+ const elements = headings
47
+ .map(h => document.getElementById(h.id))
48
+ .filter(Boolean) as HTMLElement[];
49
+ if (elements.length === 0) return;
50
+ observerRef.current?.disconnect();
51
+ observerRef.current = new IntersectionObserver(
52
+ (entries) => {
53
+ for (const entry of entries) {
54
+ if (entry.isIntersecting) { setActiveId(entry.target.id); break; }
55
+ }
56
+ },
57
+ { rootMargin: `-${SCROLL_OFFSET}px 0% -70% 0%`, threshold: 0 }
58
+ );
59
+ elements.forEach(el => observerRef.current?.observe(el));
60
+ }, 150);
61
+ return () => { clearTimeout(timer); observerRef.current?.disconnect(); };
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [content]);
64
+
65
+ if (headings.length < 2) return null;
66
+
67
+ const minLevel = Math.min(...headings.map(h => h.level));
68
+
69
+ const handleClick = (e: React.MouseEvent, id: string) => {
70
+ e.preventDefault();
71
+ const el = document.getElementById(id);
72
+ if (!el) return;
73
+ const top = el.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET;
74
+ window.scrollTo({ top, behavior: 'smooth' });
75
+ setActiveId(id);
76
+ };
77
+
78
+ return (
79
+ <aside
80
+ className="hidden xl:block fixed right-0 z-10"
81
+ style={{
82
+ top: TOPBAR_H,
83
+ height: `calc(100vh - ${TOPBAR_H}px)`,
84
+ // Always reserve full width so content margin doesn't jump
85
+ width: NAV_W,
86
+ // Slide the entire panel off the right edge when collapsed
87
+ transform: collapsed ? `translateX(${NAV_W}px)` : 'translateX(0)',
88
+ transition: 'transform 200ms ease-in-out',
89
+ }}
90
+ >
91
+ {/* Collapse / expand button — tab attached to left edge of the panel */}
92
+ <button
93
+ onClick={() => setCollapsed(v => !v)}
94
+ className="absolute top-6 flex items-center justify-center w-5 h-8 rounded-l-md border border-r-0 border-border hover:bg-muted transition-colors"
95
+ style={{
96
+ left: -20, // sticks out to the left of the panel
97
+ background: 'var(--background)',
98
+ }}
99
+ title={collapsed ? 'Expand TOC' : 'Collapse TOC'}
100
+ >
101
+ <ChevronRight
102
+ size={11}
103
+ className="text-muted-foreground/60 transition-transform duration-200"
104
+ style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
105
+ />
106
+ </button>
107
+
108
+ {/* Nav list */}
109
+ <nav
110
+ className="flex flex-col gap-0.5 overflow-y-auto py-5 pl-2 pr-3 h-full border-l border-border"
111
+ style={{ background: 'var(--background)' }}
112
+ >
113
+ <p
114
+ className="text-[10px] font-semibold uppercase tracking-wider px-2 mb-1"
115
+ style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}
116
+ >
117
+ On this page
118
+ </p>
119
+ {headings.map((heading, i) => {
120
+ const indent = (heading.level - minLevel) * 14;
121
+ const isActive = activeId === heading.id;
122
+ const isNested = heading.level > minLevel;
123
+ return (
124
+ <a
125
+ key={`${heading.id}-${i}`}
126
+ href={`#${heading.id}`}
127
+ onClick={(e) => handleClick(e, heading.id)}
128
+ className="block text-xs py-1 rounded transition-colors duration-100 leading-snug"
129
+ style={{
130
+ paddingLeft: `${8 + indent}px`,
131
+ paddingRight: '8px',
132
+ borderLeft: isNested ? '1px solid var(--border)' : 'none',
133
+ marginLeft: isNested ? '8px' : '0',
134
+ whiteSpace: 'nowrap',
135
+ overflow: 'hidden',
136
+ textOverflow: 'ellipsis',
137
+ ...(isActive
138
+ ? { color: 'var(--amber)', background: 'var(--amber-dim)' }
139
+ : { color: 'var(--muted-foreground)' }
140
+ )
141
+ }}
142
+ >
143
+ {heading.text}
144
+ </a>
145
+ );
146
+ })}
147
+ </nav>
148
+ </aside>
149
+ );
150
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { Sun, Moon } from 'lucide-react';
5
+
6
+ export default function ThemeToggle() {
7
+ const [dark, setDark] = useState(true);
8
+
9
+ useEffect(() => {
10
+ const stored = localStorage.getItem('theme');
11
+ const isDark = stored
12
+ ? stored === 'dark'
13
+ : window.matchMedia('(prefers-color-scheme: dark)').matches;
14
+ setDark(isDark);
15
+ document.documentElement.classList.toggle('dark', isDark);
16
+ }, []);
17
+
18
+ const toggle = () => {
19
+ const next = !dark;
20
+ setDark(next);
21
+ document.documentElement.classList.toggle('dark', next);
22
+ localStorage.setItem('theme', next ? 'dark' : 'light');
23
+ };
24
+
25
+ return (
26
+ <button
27
+ onClick={toggle}
28
+ className="p-1.5 rounded-lg hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
29
+ title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
30
+ >
31
+ {dark ? <Sun size={15} /> : <Moon size={15} />}
32
+ </button>
33
+ );
34
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { useEditor, EditorContent } from '@tiptap/react';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+ import Placeholder from '@tiptap/extension-placeholder';
7
+ import TaskList from '@tiptap/extension-task-list';
8
+ import TaskItem from '@tiptap/extension-task-item';
9
+ import Link from '@tiptap/extension-link';
10
+ import { Table } from '@tiptap/extension-table';
11
+ import { TableRow } from '@tiptap/extension-table';
12
+ import { TableCell } from '@tiptap/extension-table';
13
+ import { TableHeader } from '@tiptap/extension-table';
14
+ import { Markdown } from 'tiptap-markdown';
15
+
16
+ interface WysiwygEditorProps {
17
+ value: string;
18
+ onChange: (markdown: string) => void;
19
+ }
20
+
21
+ export default function WysiwygEditor({ value, onChange }: WysiwygEditorProps) {
22
+ const onChangeRef = useRef(onChange);
23
+ onChangeRef.current = onChange;
24
+
25
+ const editor = useEditor({
26
+ extensions: [
27
+ StarterKit.configure({
28
+ codeBlock: { HTMLAttributes: { class: 'not-prose' } },
29
+ }),
30
+ Markdown.configure({
31
+ html: true,
32
+ transformPastedText: true,
33
+ transformCopiedText: true,
34
+ }),
35
+ Placeholder.configure({ placeholder: 'Start writing…' }),
36
+ TaskList,
37
+ TaskItem.configure({ nested: true }),
38
+ Link.configure({ openOnClick: false, autolink: true }),
39
+ Table.configure({ resizable: false }),
40
+ TableRow,
41
+ TableHeader,
42
+ TableCell,
43
+ ],
44
+ content: value,
45
+ editorProps: {
46
+ attributes: {
47
+ class: 'prose max-w-none focus:outline-none wysiwyg-editor',
48
+ },
49
+ },
50
+ onUpdate({ editor }) {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const md = (editor.storage as any).markdown?.getMarkdown?.() ?? editor.getText();
53
+ onChangeRef.current(md);
54
+ },
55
+ immediatelyRender: false,
56
+ });
57
+
58
+ // Sync external value changes (e.g. cancel → revert) without full re-mount
59
+ const lastMd = useRef(value);
60
+ useEffect(() => {
61
+ if (!editor || value === lastMd.current) return;
62
+ lastMd.current = value;
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ const current = (editor.storage as any).markdown?.getMarkdown?.() ?? '';
65
+ if (current !== value) {
66
+ editor.commands.setContent(value);
67
+ }
68
+ }, [editor, value]);
69
+
70
+ return (
71
+ <div className="wysiwyg-wrapper h-full overflow-y-auto px-8 py-6">
72
+ <EditorContent editor={editor} />
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { X, FileText, Table, Paperclip } from 'lucide-react';
4
+
5
+ interface FileChipProps {
6
+ path: string;
7
+ onRemove: () => void;
8
+ variant?: 'kb' | 'upload';
9
+ }
10
+
11
+ export default function FileChip({ path, onRemove, variant = 'kb' }: FileChipProps) {
12
+ const name = path.split('/').pop() ?? path;
13
+ const isCsv = name.endsWith('.csv');
14
+ const Icon = variant === 'upload' ? Paperclip : isCsv ? Table : FileText;
15
+ const iconClass = variant === 'upload' ? 'text-zinc-400' : isCsv ? 'text-emerald-400' : 'text-zinc-400';
16
+
17
+ return (
18
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs border border-border bg-muted text-foreground max-w-[220px]">
19
+ <Icon size={11} className={`${iconClass} shrink-0`} />
20
+ <span className="truncate" title={path}>{name}</span>
21
+ <button
22
+ type="button"
23
+ onClick={onRemove}
24
+ className="text-muted-foreground hover:text-foreground ml-0.5 shrink-0"
25
+ >
26
+ <X size={10} />
27
+ </button>
28
+ </span>
29
+ );
30
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { FileText, Table } from 'lucide-react';
4
+
5
+ interface MentionPopoverProps {
6
+ results: string[];
7
+ selectedIndex: number;
8
+ onSelect: (filePath: string) => void;
9
+ }
10
+
11
+ export default function MentionPopover({ results, selectedIndex, onSelect }: MentionPopoverProps) {
12
+ if (results.length === 0) return null;
13
+
14
+ return (
15
+ <div className="mx-4 mb-1 border border-border rounded-lg bg-card shadow-lg overflow-hidden">
16
+ {results.map((f, idx) => {
17
+ const name = f.split('/').pop() ?? f;
18
+ const isCsv = name.endsWith('.csv');
19
+ return (
20
+ <button
21
+ key={f}
22
+ type="button"
23
+ onMouseDown={(e) => {
24
+ e.preventDefault();
25
+ onSelect(f);
26
+ }}
27
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
28
+ idx === selectedIndex
29
+ ? 'bg-accent text-foreground'
30
+ : 'text-muted-foreground hover:bg-muted'
31
+ }`}
32
+ >
33
+ {isCsv ? (
34
+ <Table size={13} className="text-emerald-400 shrink-0" />
35
+ ) : (
36
+ <FileText size={13} className="text-zinc-400 shrink-0" />
37
+ )}
38
+ <span className="truncate flex-1">{name}</span>
39
+ <span className="text-[10px] text-muted-foreground/50 truncate max-w-[140px] shrink-0">
40
+ {f.split('/').slice(0, -1).join('/')}
41
+ </span>
42
+ </button>
43
+ );
44
+ })}
45
+ <div className="px-3 py-1.5 border-t border-border flex gap-3 text-[10px] text-muted-foreground/50">
46
+ <span>↑↓ navigate</span>
47
+ <span>↵ select</span>
48
+ <span>ESC dismiss</span>
49
+ </div>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect } from 'react';
4
+ import { Sparkles, Loader2, AlertCircle } from 'lucide-react';
5
+ import ReactMarkdown from 'react-markdown';
6
+ import remarkGfm from 'remark-gfm';
7
+ import type { Message } from '@/lib/types';
8
+
9
+ function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
10
+ return (
11
+ <div className="prose prose-sm dark:prose-invert max-w-none text-foreground
12
+ prose-p:my-1 prose-p:leading-relaxed
13
+ prose-headings:font-semibold prose-headings:my-2
14
+ prose-ul:my-1 prose-li:my-0.5
15
+ prose-ol:my-1
16
+ prose-code:text-[0.8em] prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none
17
+ prose-pre:bg-muted prose-pre:text-foreground prose-pre:text-xs
18
+ prose-blockquote:border-l-amber-400 prose-blockquote:text-muted-foreground
19
+ prose-a:text-amber-500 prose-a:no-underline hover:prose-a:underline
20
+ prose-strong:text-foreground prose-strong:font-semibold
21
+ prose-table:text-xs prose-th:py-1 prose-td:py-1
22
+ ">
23
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
24
+ {isStreaming && (
25
+ <span className="inline-block w-1.5 h-3.5 bg-amber-400 ml-0.5 align-middle animate-pulse rounded-sm" />
26
+ )}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ interface MessageListProps {
32
+ messages: Message[];
33
+ isLoading: boolean;
34
+ loadingPhase: 'connecting' | 'thinking' | 'streaming';
35
+ emptyPrompt: string;
36
+ suggestions: readonly string[];
37
+ onSuggestionClick: (text: string) => void;
38
+ labels: {
39
+ connecting: string;
40
+ thinking: string;
41
+ generating: string;
42
+ };
43
+ }
44
+
45
+ export default function MessageList({
46
+ messages,
47
+ isLoading,
48
+ loadingPhase,
49
+ emptyPrompt,
50
+ suggestions,
51
+ onSuggestionClick,
52
+ labels,
53
+ }: MessageListProps) {
54
+ const endRef = useRef<HTMLDivElement>(null);
55
+
56
+ useEffect(() => {
57
+ endRef.current?.scrollIntoView({ behavior: 'smooth' });
58
+ }, [messages]);
59
+
60
+ return (
61
+ <div className="flex-1 overflow-y-auto px-4 py-4 space-y-4 min-h-0">
62
+ {messages.length === 0 && (
63
+ <div className="mt-6 space-y-3">
64
+ <p className="text-center text-sm text-muted-foreground/60">{emptyPrompt}</p>
65
+ <div className="flex flex-col gap-2 px-2">
66
+ {suggestions.map((s, i) => (
67
+ <button
68
+ key={i}
69
+ type="button"
70
+ onClick={() => onSuggestionClick(s)}
71
+ className="text-left text-xs px-3 py-2 rounded-lg border border-border bg-muted/50 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
72
+ >
73
+ {s}
74
+ </button>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ )}
79
+ {messages.map((m, i) => (
80
+ <div key={i} className={`flex gap-3 ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
81
+ {m.role === 'assistant' && (
82
+ <div
83
+ className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-0.5"
84
+ style={{ background: 'var(--amber-dim)' }}
85
+ >
86
+ <Sparkles size={12} style={{ color: 'var(--amber)' }} />
87
+ </div>
88
+ )}
89
+ {m.role === 'user' ? (
90
+ <div
91
+ className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap"
92
+ style={{ background: 'var(--amber)', color: '#131210' }}
93
+ >
94
+ {m.content}
95
+ </div>
96
+ ) : m.content.startsWith('__error__') ? (
97
+ <div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-red-500/20 bg-red-500/8 text-sm">
98
+ <div className="flex items-start gap-2 text-red-400">
99
+ <AlertCircle size={14} className="shrink-0 mt-0.5" />
100
+ <span className="leading-relaxed">{m.content.slice(9)}</span>
101
+ </div>
102
+ </div>
103
+ ) : (
104
+ <div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
105
+ {m.content ? (
106
+ <AssistantMessage content={m.content} isStreaming={isLoading && i === messages.length - 1} />
107
+ ) : isLoading && i === messages.length - 1 ? (
108
+ <div className="flex items-center gap-2 py-1">
109
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
110
+ <span className="text-xs text-muted-foreground animate-pulse">
111
+ {loadingPhase === 'connecting'
112
+ ? labels.connecting
113
+ : loadingPhase === 'thinking'
114
+ ? labels.thinking
115
+ : labels.generating}
116
+ </span>
117
+ </div>
118
+ ) : null}
119
+ </div>
120
+ )}
121
+ </div>
122
+ ))}
123
+ <div ref={endRef} />
124
+ </div>
125
+ );
126
+ }