@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,325 @@
1
+ 'use client';
2
+
3
+ import { useMemo, useState, useRef, useCallback, useEffect } from 'react';
4
+ import Papa from 'papaparse';
5
+ import { ChevronUp, ChevronDown, ChevronsUpDown, Plus, Trash2 } from 'lucide-react';
6
+
7
+ interface CsvViewProps {
8
+ content: string;
9
+ filePath?: string;
10
+ appendAction?: (newRow: string[]) => Promise<{ newContent: string }>;
11
+ saveAction?: (newContent: string) => Promise<void>;
12
+ }
13
+
14
+ type SortDir = 'asc' | 'desc' | null;
15
+
16
+ function serializeRows(headers: string[], rows: string[][]): string {
17
+ return Papa.unparse([headers, ...rows]);
18
+ }
19
+
20
+ // ─── Inline cell editor ───────────────────────────────────────────────────────
21
+
22
+ function Cell({
23
+ value,
24
+ editable,
25
+ onCommit,
26
+ }: {
27
+ value: string;
28
+ editable: boolean;
29
+ onCommit: (newVal: string) => void;
30
+ }) {
31
+ const [editing, setEditing] = useState(false);
32
+ const [draft, setDraft] = useState(value);
33
+ const inputRef = useRef<HTMLInputElement>(null);
34
+
35
+ function startEdit() {
36
+ if (!editable) return;
37
+ setDraft(value);
38
+ setEditing(true);
39
+ }
40
+
41
+ useEffect(() => {
42
+ if (editing) inputRef.current?.select();
43
+ }, [editing]);
44
+
45
+ function commit() {
46
+ setEditing(false);
47
+ if (draft !== value) onCommit(draft);
48
+ }
49
+
50
+ if (editing) {
51
+ return (
52
+ <input
53
+ ref={inputRef}
54
+ value={draft}
55
+ onChange={e => setDraft(e.target.value)}
56
+ onBlur={commit}
57
+ onKeyDown={e => {
58
+ if (e.key === 'Enter') commit();
59
+ if (e.key === 'Escape') { setDraft(value); setEditing(false); }
60
+ }}
61
+ className="w-full bg-transparent outline-none text-sm"
62
+ style={{
63
+ color: 'var(--foreground)',
64
+ borderBottom: '1px solid var(--amber)',
65
+ minWidth: 60,
66
+ }}
67
+ onClick={e => e.stopPropagation()}
68
+ />
69
+ );
70
+ }
71
+
72
+ return (
73
+ <div
74
+ className={`truncate text-sm ${editable ? 'cursor-text' : ''}`}
75
+ style={{ color: 'var(--foreground)', minWidth: 60 }}
76
+ onClick={editable ? startEdit : undefined}
77
+ title={value}
78
+ >
79
+ {value || <span style={{ color: 'var(--muted-foreground)', opacity: 0.4 }}>—</span>}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ // ─── Add row form ─────────────────────────────────────────────────────────────
85
+
86
+ function AddRowForm({
87
+ headers,
88
+ onAdd,
89
+ onCancel,
90
+ }: {
91
+ headers: string[];
92
+ onAdd: (row: string[]) => void;
93
+ onCancel: () => void;
94
+ }) {
95
+ const [values, setValues] = useState<string[]>(() => Array(headers.length).fill(''));
96
+ const firstRef = useRef<HTMLInputElement>(null);
97
+
98
+ function set(i: number, v: string) {
99
+ setValues(prev => { const next = [...prev]; next[i] = v; return next; });
100
+ }
101
+
102
+ function submit() {
103
+ onAdd(values);
104
+ }
105
+
106
+ return (
107
+ <tr style={{ background: 'color-mix(in srgb, var(--amber) 6%, transparent)', borderTop: '1px solid var(--amber)' }}>
108
+ {headers.map((h, i) => (
109
+ <td key={i} className="px-3 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
110
+ <input
111
+ ref={i === 0 ? firstRef : undefined}
112
+ autoFocus={i === 0}
113
+ value={values[i]}
114
+ onChange={e => set(i, e.target.value)}
115
+ onKeyDown={e => {
116
+ if (e.key === 'Enter') submit();
117
+ if (e.key === 'Escape') onCancel();
118
+ }}
119
+ placeholder={h}
120
+ className="w-full bg-transparent outline-none text-sm placeholder:opacity-30"
121
+ style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--border)' }}
122
+ />
123
+ </td>
124
+ ))}
125
+ {/* spacer for delete column */}
126
+ <td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }} />
127
+ </tr>
128
+ );
129
+ }
130
+
131
+ // ─── Main component ───────────────────────────────────────────────────────────
132
+
133
+ export default function CsvView({ content: initialContent, appendAction, saveAction }: CsvViewProps) {
134
+ const [content, setContent] = useState(initialContent);
135
+ const [sortCol, setSortCol] = useState<number | null>(null);
136
+ const [sortDir, setSortDir] = useState<SortDir>(null);
137
+ const [showAdd, setShowAdd] = useState(false);
138
+
139
+ const parsed = useMemo(() => {
140
+ const result = Papa.parse<string[]>(content, { skipEmptyLines: true });
141
+ return result.data as string[][];
142
+ }, [content]);
143
+
144
+ const headers = parsed[0] || [];
145
+ const rows = parsed.slice(1);
146
+
147
+ const sortedRows = useMemo(() => {
148
+ if (sortCol === null || sortDir === null) return rows;
149
+ return [...rows].sort((a, b) => {
150
+ const va = a[sortCol] ?? '', vb = b[sortCol] ?? '';
151
+ const na = parseFloat(va), nb = parseFloat(vb);
152
+ if (!isNaN(na) && !isNaN(nb)) return sortDir === 'asc' ? na - nb : nb - na;
153
+ return sortDir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
154
+ });
155
+ }, [rows, sortCol, sortDir]);
156
+
157
+ function toggleSort(col: number) {
158
+ if (sortCol !== col) { setSortCol(col); setSortDir('asc'); return; }
159
+ if (sortDir === 'asc') { setSortDir('desc'); return; }
160
+ setSortCol(null); setSortDir(null);
161
+ }
162
+
163
+ // Update a single cell and persist
164
+ const handleCellCommit = useCallback(async (rowIdx: number, colIdx: number, newVal: string) => {
165
+ if (!saveAction) return;
166
+ // rowIdx here is index into sortedRows — need to map back to original rows
167
+ const updatedRows = rows.map((r, i) => {
168
+ // find which original row matches this sorted row
169
+ const sorted = sortedRows[rowIdx];
170
+ if (r === sorted) return r.map((cell, ci) => ci === colIdx ? newVal : cell);
171
+ return r;
172
+ });
173
+ const newContent = serializeRows(headers, updatedRows);
174
+ setContent(newContent);
175
+ await saveAction(newContent);
176
+ }, [saveAction, rows, sortedRows, headers]);
177
+
178
+ // Delete a row and persist
179
+ const handleDeleteRow = useCallback(async (rowIdx: number) => {
180
+ if (!saveAction) return;
181
+ const sorted = sortedRows[rowIdx];
182
+ const updatedRows = rows.filter(r => r !== sorted);
183
+ const newContent = serializeRows(headers, updatedRows);
184
+ setContent(newContent);
185
+ await saveAction(newContent);
186
+ }, [saveAction, rows, sortedRows, headers]);
187
+
188
+ // Append a new row
189
+ const handleAddRow = useCallback(async (newRow: string[]) => {
190
+ setShowAdd(false);
191
+ if (appendAction) {
192
+ const result = await appendAction(newRow);
193
+ setContent(result.newContent);
194
+ } else if (saveAction) {
195
+ const newContent = serializeRows(headers, [...rows, newRow]);
196
+ setContent(newContent);
197
+ await saveAction(newContent);
198
+ }
199
+ }, [appendAction, saveAction, headers, rows]);
200
+
201
+ const canEdit = !!saveAction || !!appendAction;
202
+
203
+ if (headers.length === 0) {
204
+ return <div className="text-sm py-4" style={{ color: 'var(--muted-foreground)' }}>Empty CSV file</div>;
205
+ }
206
+
207
+ return (
208
+ <div className="w-full rounded-xl overflow-hidden border" style={{ borderColor: 'var(--border)' }}>
209
+ <div className="overflow-x-auto">
210
+ <table className="min-w-full text-sm border-collapse">
211
+ <thead>
212
+ <tr style={{ background: 'var(--muted)' }}>
213
+ {headers.map((header, i) => (
214
+ <th
215
+ key={i}
216
+ onClick={() => toggleSort(i)}
217
+ className="px-4 py-2.5 text-left font-semibold cursor-pointer select-none whitespace-nowrap transition-colors hover:bg-accent"
218
+ style={{
219
+ color: 'var(--foreground)',
220
+ borderBottom: '1px solid var(--border)',
221
+ fontFamily: "'IBM Plex Sans', sans-serif",
222
+ fontSize: '0.75rem',
223
+ letterSpacing: '0.04em',
224
+ textTransform: 'uppercase',
225
+ }}
226
+ >
227
+ <div className="flex items-center gap-1.5">
228
+ <span>{header}</span>
229
+ {sortCol === i
230
+ ? sortDir === 'asc'
231
+ ? <ChevronUp size={11} style={{ color: 'var(--amber)' }} />
232
+ : <ChevronDown size={11} style={{ color: 'var(--amber)' }} />
233
+ : <ChevronsUpDown size={11} style={{ color: 'var(--muted-foreground)', opacity: 0.4 }} />
234
+ }
235
+ </div>
236
+ </th>
237
+ ))}
238
+ {canEdit && <th className="w-8" style={{ borderBottom: '1px solid var(--border)', background: 'var(--muted)' }} />}
239
+ </tr>
240
+ </thead>
241
+ <tbody>
242
+ {sortedRows.map((row, rowIdx) => (
243
+ <tr
244
+ key={rowIdx}
245
+ className="group transition-colors"
246
+ style={{
247
+ background: rowIdx % 2 === 0 ? 'var(--background)' : 'var(--card)',
248
+ }}
249
+ onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
250
+ onMouseLeave={e => (e.currentTarget.style.background = rowIdx % 2 === 0 ? 'var(--background)' : 'var(--card)')}
251
+ >
252
+ {headers.map((_, colIdx) => (
253
+ <td
254
+ key={colIdx}
255
+ className="px-4 py-2.5 max-w-xs"
256
+ style={{ borderBottom: '1px solid var(--border)' }}
257
+ >
258
+ <Cell
259
+ value={row[colIdx] ?? ''}
260
+ editable={!!saveAction}
261
+ onCommit={(v) => handleCellCommit(rowIdx, colIdx, v)}
262
+ />
263
+ </td>
264
+ ))}
265
+ {canEdit && (
266
+ <td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
267
+ {saveAction && (
268
+ <button
269
+ onClick={() => handleDeleteRow(rowIdx)}
270
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-destructive/10"
271
+ style={{ color: 'var(--muted-foreground)' }}
272
+ title="Delete row"
273
+ >
274
+ <Trash2 size={12} />
275
+ </button>
276
+ )}
277
+ </td>
278
+ )}
279
+ </tr>
280
+ ))}
281
+
282
+ {/* Add row form */}
283
+ {showAdd && (
284
+ <AddRowForm
285
+ headers={headers}
286
+ onAdd={handleAddRow}
287
+ onCancel={() => setShowAdd(false)}
288
+ />
289
+ )}
290
+ </tbody>
291
+ </table>
292
+ </div>
293
+
294
+ {/* Footer */}
295
+ <div
296
+ className="px-4 py-2 flex items-center justify-between"
297
+ style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}
298
+ >
299
+ <span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
300
+ {rows.length} rows · {headers.length} cols
301
+ </span>
302
+
303
+ {canEdit && !showAdd && (
304
+ <button
305
+ onClick={() => setShowAdd(true)}
306
+ className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors"
307
+ style={{ color: 'var(--amber)', background: 'var(--amber-dim)', fontFamily: "'IBM Plex Mono', monospace" }}
308
+ >
309
+ <Plus size={12} />
310
+ Add row
311
+ </button>
312
+ )}
313
+ {showAdd && (
314
+ <button
315
+ onClick={() => setShowAdd(false)}
316
+ className="text-xs px-2.5 py-1 rounded-md transition-colors"
317
+ style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
318
+ >
319
+ Cancel
320
+ </button>
321
+ )}
322
+ </div>
323
+ </div>
324
+ );
325
+ }
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import Link from 'next/link';
5
+ import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
6
+ import Breadcrumb from '@/components/Breadcrumb';
7
+ import { encodePath } from '@/lib/utils';
8
+ import { FileNode } from '@/lib/types';
9
+ import { useLocale } from '@/lib/LocaleContext';
10
+
11
+ interface DirViewProps {
12
+ dirPath: string;
13
+ entries: FileNode[];
14
+ }
15
+
16
+ function FileIcon({ node }: { node: FileNode }) {
17
+ if (node.type === 'directory') return <Folder size={16} className="text-yellow-400 shrink-0" />;
18
+ if (node.extension === '.csv') return <Table size={16} className="text-emerald-400 shrink-0" />;
19
+ return <FileText size={16} className="text-zinc-400 shrink-0" />;
20
+ }
21
+
22
+ function FileIconLarge({ node }: { node: FileNode }) {
23
+ if (node.type === 'directory') return <FolderOpen size={28} className="text-yellow-400" />;
24
+ if (node.extension === '.csv') return <Table size={28} className="text-emerald-400" />;
25
+ return <FileText size={28} className="text-zinc-400" />;
26
+ }
27
+
28
+ function countFiles(node: FileNode): number {
29
+ if (node.type === 'file') return 1;
30
+ return (node.children || []).reduce((acc, c) => acc + countFiles(c), 0);
31
+ }
32
+
33
+ const DIR_VIEW_KEY = 'mindos-dir-view';
34
+
35
+ function useDirViewPref() {
36
+ const [view, setViewState] = useState<'grid' | 'list'>('grid');
37
+
38
+ useEffect(() => {
39
+ const saved = localStorage.getItem(DIR_VIEW_KEY);
40
+ if (saved === 'list' || saved === 'grid') setViewState(saved);
41
+ }, []);
42
+
43
+ const setView = (v: 'grid' | 'list') => {
44
+ setViewState(v);
45
+ localStorage.setItem(DIR_VIEW_KEY, v);
46
+ };
47
+
48
+ return [view, setView] as const;
49
+ }
50
+
51
+ export default function DirView({ dirPath, entries }: DirViewProps) {
52
+ const [view, setView] = useDirViewPref();
53
+ const { t } = useLocale();
54
+ const fileCounts = useMemo(() => {
55
+ const map = new Map<string, number>();
56
+ for (const e of entries) map.set(e.path, countFiles(e));
57
+ return map;
58
+ }, [entries]);
59
+
60
+ return (
61
+ <div className="flex flex-col min-h-screen">
62
+ {/* Topbar */}
63
+ <div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
64
+ <div className="max-w-[860px] mx-auto flex items-center justify-between gap-2">
65
+ <div className="min-w-0 flex-1">
66
+ <Breadcrumb filePath={dirPath} />
67
+ </div>
68
+ {/* View toggle */}
69
+ <div className="flex items-center gap-1 p-1 bg-muted rounded-lg shrink-0">
70
+ <button
71
+ onClick={() => setView('grid')}
72
+ className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
73
+ title={t.dirView.gridView}
74
+ >
75
+ <LayoutGrid size={14} />
76
+ </button>
77
+ <button
78
+ onClick={() => setView('list')}
79
+ className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
80
+ title={t.dirView.listView}
81
+ >
82
+ <List size={14} />
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ {/* Content */}
89
+ <div className="flex-1 px-4 md:px-6 py-6">
90
+ <div className="max-w-[860px] mx-auto">
91
+ {entries.length === 0 ? (
92
+ <p className="text-muted-foreground text-sm">{t.dirView.emptyFolder}</p>
93
+ ) : view === 'grid' ? (
94
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-3">
95
+ {entries.map(entry => (
96
+ <Link
97
+ key={entry.path}
98
+ href={`/view/${encodePath(entry.path)}`}
99
+ className="flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center"
100
+ >
101
+ <FileIconLarge node={entry} />
102
+ <span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
103
+ {entry.name}
104
+ </span>
105
+ {entry.type === 'directory' && (
106
+ <span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
107
+ )}
108
+ </Link>
109
+ ))}
110
+ </div>
111
+ ) : (
112
+ <div className="flex flex-col divide-y divide-border border border-border rounded-xl overflow-hidden">
113
+ {entries.map(entry => (
114
+ <Link
115
+ key={entry.path}
116
+ href={`/view/${encodePath(entry.path)}`}
117
+ className="flex items-center gap-3 px-4 py-3 bg-card hover:bg-accent transition-colors duration-100"
118
+ >
119
+ <FileIcon node={entry} />
120
+ <span className="flex-1 text-sm text-foreground truncate" suppressHydrationWarning>
121
+ {entry.name}
122
+ </span>
123
+ {entry.type === 'directory' ? (
124
+ <span className="text-xs text-muted-foreground shrink-0">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
125
+ ) : entry.mtime ? (
126
+ <span className="text-xs text-muted-foreground shrink-0 tabular-nums" style={{ fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
127
+ {new Date(entry.mtime).toLocaleDateString()}
128
+ </span>
129
+ ) : null}
130
+ </Link>
131
+ ))}
132
+ </div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useCallback } from 'react';
4
+ import { EditorView, basicSetup } from 'codemirror';
5
+ import { markdown } from '@codemirror/lang-markdown';
6
+ import { oneDark } from '@codemirror/theme-one-dark';
7
+ import { EditorState } from '@codemirror/state';
8
+
9
+ interface EditorProps {
10
+ value: string;
11
+ onChange: (value: string) => void;
12
+ language?: 'markdown' | 'plain';
13
+ }
14
+
15
+ const darkTheme = EditorView.theme({
16
+ '&': {
17
+ backgroundColor: '#09090b',
18
+ height: '100%',
19
+ fontSize: '0.875rem',
20
+ fontFamily: 'var(--font-geist-mono), ui-monospace, monospace',
21
+ },
22
+ '.cm-scroller': {
23
+ overflow: 'auto',
24
+ lineHeight: '1.6',
25
+ },
26
+ '.cm-content': {
27
+ padding: '16px',
28
+ caretColor: '#60a5fa',
29
+ },
30
+ '.cm-focused': {
31
+ outline: 'none',
32
+ },
33
+ '.cm-line': {
34
+ padding: '0 4px',
35
+ },
36
+ '.cm-gutters': {
37
+ backgroundColor: '#0d0d0f',
38
+ borderRight: '1px solid #27272a',
39
+ color: '#52525b',
40
+ },
41
+ '.cm-activeLineGutter': {
42
+ backgroundColor: '#18181b',
43
+ },
44
+ '.cm-activeLine': {
45
+ backgroundColor: '#18181b50',
46
+ },
47
+ '.cm-selectionBackground': {
48
+ backgroundColor: '#2563eb40',
49
+ },
50
+ '&.cm-focused .cm-selectionBackground': {
51
+ backgroundColor: '#2563eb60',
52
+ },
53
+ '.cm-cursor': {
54
+ borderLeftColor: '#60a5fa',
55
+ borderLeftWidth: '2px',
56
+ },
57
+ });
58
+
59
+ export default function Editor({ value, onChange, language = 'markdown' }: EditorProps) {
60
+ const containerRef = useRef<HTMLDivElement>(null);
61
+ const viewRef = useRef<EditorView | null>(null);
62
+ const onChangeRef = useRef(onChange);
63
+ onChangeRef.current = onChange;
64
+
65
+ // Track whether update is from external value change
66
+ const isExternalUpdate = useRef(false);
67
+
68
+ useEffect(() => {
69
+ if (!containerRef.current) return;
70
+
71
+ const updateListener = EditorView.updateListener.of((update) => {
72
+ if (update.docChanged && !isExternalUpdate.current) {
73
+ onChangeRef.current(update.state.doc.toString());
74
+ }
75
+ });
76
+
77
+ const state = EditorState.create({
78
+ doc: value,
79
+ extensions: [
80
+ basicSetup,
81
+ oneDark,
82
+ darkTheme,
83
+ language === 'markdown' ? markdown() : [],
84
+ updateListener,
85
+ EditorView.lineWrapping,
86
+ ],
87
+ });
88
+
89
+ const view = new EditorView({
90
+ state,
91
+ parent: containerRef.current,
92
+ });
93
+
94
+ viewRef.current = view;
95
+
96
+ return () => {
97
+ view.destroy();
98
+ viewRef.current = null;
99
+ };
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ }, []);
102
+
103
+ // Sync external value changes to editor
104
+ useEffect(() => {
105
+ const view = viewRef.current;
106
+ if (!view) return;
107
+ const current = view.state.doc.toString();
108
+ if (current !== value) {
109
+ isExternalUpdate.current = true;
110
+ view.dispatch({
111
+ changes: { from: 0, to: current.length, insert: value },
112
+ });
113
+ isExternalUpdate.current = false;
114
+ }
115
+ }, [value]);
116
+
117
+ return (
118
+ <div
119
+ ref={containerRef}
120
+ className="h-full w-full overflow-hidden rounded-lg border border-zinc-800"
121
+ style={{ minHeight: '400px' }}
122
+ />
123
+ );
124
+ }
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+
5
+ const Editor = dynamic(() => import('./Editor'), {
6
+ ssr: false,
7
+ loading: () => (
8
+ <div className="h-full w-full min-h-[400px] rounded-lg border border-zinc-800 bg-zinc-900 flex items-center justify-center">
9
+ <div className="flex items-center gap-2 text-zinc-500 text-sm">
10
+ <div className="w-4 h-4 border-2 border-zinc-600 border-t-zinc-300 rounded-full animate-spin" />
11
+ <span>Loading editor...</span>
12
+ </div>
13
+ </div>
14
+ ),
15
+ });
16
+
17
+ export default Editor;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { Component, type ReactNode } from 'react';
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error?: Error;
13
+ }
14
+
15
+ export default class ErrorBoundary extends Component<Props, State> {
16
+ constructor(props: Props) {
17
+ super(props);
18
+ this.state = { hasError: false };
19
+ }
20
+
21
+ static getDerivedStateFromError(error: Error): State {
22
+ return { hasError: true, error };
23
+ }
24
+
25
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
26
+ console.error('[ErrorBoundary]', error, info.componentStack);
27
+ }
28
+
29
+ render() {
30
+ if (this.state.hasError) {
31
+ if (this.props.fallback) return this.props.fallback;
32
+
33
+ return (
34
+ <div className="flex items-center justify-center min-h-[50vh] px-4">
35
+ <div className="text-center max-w-md">
36
+ <h2 className="text-lg font-semibold text-foreground mb-2">Something went wrong</h2>
37
+ <p className="text-sm text-muted-foreground mb-4">
38
+ {this.state.error?.message || 'An unexpected error occurred.'}
39
+ </p>
40
+ <button
41
+ onClick={() => this.setState({ hasError: false, error: undefined })}
42
+ className="px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
43
+ >
44
+ Try again
45
+ </button>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return this.props.children;
52
+ }
53
+ }