@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,93 @@
1
+ import { notFound } from 'next/navigation';
2
+ import { getFileContent, saveFileContent, isDirectory, getDirEntries, createFile, getFileTree } from '@/lib/fs';
3
+ import type { FileNode } from '@/lib/types';
4
+ import ViewPageClient from './ViewPageClient';
5
+ import DirView from '@/components/DirView';
6
+ import Papa from 'papaparse';
7
+
8
+ interface PageProps {
9
+ params: Promise<{ path: string[] }>;
10
+ }
11
+
12
+ function collectDirectories(nodes: FileNode[]): string[] {
13
+ const dirs: string[] = [];
14
+ for (const n of nodes) {
15
+ if (n.type === 'directory') {
16
+ dirs.push(n.path);
17
+ if (n.children) dirs.push(...collectDirectories(n.children));
18
+ }
19
+ }
20
+ return dirs;
21
+ }
22
+
23
+ export default async function ViewPage({ params }: PageProps) {
24
+ const { path: segments } = await params;
25
+ const filePath = segments.map(decodeURIComponent).join('/');
26
+
27
+ // Directory: show folder listing
28
+ if (isDirectory(filePath)) {
29
+ const entries = getDirEntries(filePath);
30
+ return <DirView dirPath={filePath} entries={entries} />;
31
+ }
32
+
33
+ const extension = filePath.split('.').pop()?.toLowerCase() || '';
34
+
35
+ async function saveAction(newContent: string) {
36
+ 'use server';
37
+ saveFileContent(filePath, newContent);
38
+ }
39
+
40
+ async function appendRowAction(newRow: string[]): Promise<{ newContent: string }> {
41
+ 'use server';
42
+ const current = getFileContent(filePath);
43
+ const parsed = Papa.parse<string[]>(current, { skipEmptyLines: true });
44
+ const rows = parsed.data as string[][];
45
+ rows.push(newRow);
46
+ const newContent = Papa.unparse(rows);
47
+ saveFileContent(filePath, newContent);
48
+ return { newContent };
49
+ }
50
+
51
+ async function createDraftAction(targetPath: string, draftContent: string) {
52
+ 'use server';
53
+ createFile(targetPath, draftContent);
54
+ }
55
+
56
+ let content = '';
57
+ let exists = true;
58
+ try {
59
+ content = getFileContent(filePath);
60
+ } catch {
61
+ exists = false;
62
+ }
63
+
64
+ if (!exists) {
65
+ // Special draft entry used by homepage "New Notes"
66
+ if (filePath === 'Untitled.md') {
67
+ const draftDirectories = collectDirectories(getFileTree());
68
+ return (
69
+ <ViewPageClient
70
+ filePath={filePath}
71
+ content=""
72
+ extension="md"
73
+ saveAction={saveAction}
74
+ initialEditing
75
+ isDraft
76
+ draftDirectories={draftDirectories}
77
+ createDraftAction={createDraftAction}
78
+ />
79
+ );
80
+ }
81
+ notFound();
82
+ }
83
+
84
+ return (
85
+ <ViewPageClient
86
+ filePath={filePath}
87
+ content={content}
88
+ extension={extension}
89
+ saveAction={saveAction}
90
+ appendRowAction={extension === 'csv' ? appendRowAction : undefined}
91
+ />
92
+ );
93
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { Sparkles } from 'lucide-react';
6
+ import AskModal from './AskModal';
7
+
8
+ export default function AskFab() {
9
+ const [open, setOpen] = useState(false);
10
+ const pathname = usePathname();
11
+ const currentFile = pathname.startsWith('/view/')
12
+ ? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
13
+ : undefined;
14
+
15
+ return (
16
+ <>
17
+ <button
18
+ onClick={() => setOpen(true)}
19
+ className="
20
+ group
21
+ fixed z-40
22
+ bottom-5 right-5
23
+ md:bottom-5 md:right-5
24
+ flex items-center justify-center
25
+ gap-0 hover:gap-2
26
+ p-3 md:p-[11px] rounded-xl
27
+ text-white font-medium text-[13px]
28
+ shadow-md shadow-amber-900/15
29
+ transition-all duration-200 ease-out
30
+ hover:shadow-lg hover:shadow-amber-800/25
31
+ active:scale-95
32
+ cursor-pointer
33
+ overflow-hidden
34
+ "
35
+ style={{
36
+ fontFamily: "'IBM Plex Mono', monospace",
37
+ background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
38
+ marginBottom: 'env(safe-area-inset-bottom, 0px)',
39
+ }}
40
+ title="MindOS Agent (⌘/)"
41
+ aria-label="MindOS Agent"
42
+ >
43
+ <Sparkles size={16} className="relative z-10 shrink-0" />
44
+
45
+ <span className="
46
+ relative z-10
47
+ max-w-0 group-hover:max-w-[120px]
48
+ opacity-0 group-hover:opacity-100
49
+ transition-all duration-200 ease-out
50
+ whitespace-nowrap overflow-hidden
51
+ ">
52
+ MindOS Agent
53
+ </span>
54
+ </button>
55
+
56
+ <AskModal open={open} onClose={() => setOpen(false)} currentFile={currentFile} />
57
+ </>
58
+ );
59
+ }
@@ -0,0 +1,398 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+ import { X, Sparkles, Send, AtSign, Paperclip, StopCircle, RotateCcw, History } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import type { Message } from '@/lib/types';
7
+ import { useAskSession } from '@/hooks/useAskSession';
8
+ import { useFileUpload } from '@/hooks/useFileUpload';
9
+ import { useMention } from '@/hooks/useMention';
10
+ import MessageList from '@/components/ask/MessageList';
11
+ import MentionPopover from '@/components/ask/MentionPopover';
12
+ import SessionHistory from '@/components/ask/SessionHistory';
13
+ import FileChip from '@/components/ask/FileChip';
14
+
15
+ interface AskModalProps {
16
+ open: boolean;
17
+ onClose: () => void;
18
+ currentFile?: string;
19
+ }
20
+
21
+ export default function AskModal({ open, onClose, currentFile }: AskModalProps) {
22
+ const inputRef = useRef<HTMLInputElement>(null);
23
+ const abortRef = useRef<AbortController | null>(null);
24
+ const { t } = useLocale();
25
+
26
+ const [input, setInput] = useState('');
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
29
+ const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
30
+ const [maxSteps, setMaxSteps] = useState(20);
31
+ const [showHistory, setShowHistory] = useState(false);
32
+
33
+ const session = useAskSession(currentFile);
34
+ const upload = useFileUpload();
35
+ const mention = useMention();
36
+
37
+ // Focus and reset on open
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+ if (open) {
41
+ setTimeout(() => inputRef.current?.focus(), 50);
42
+ void (async () => {
43
+ if (cancelled) return;
44
+ await session.initSessions();
45
+ })();
46
+ setInput('');
47
+ setAttachedFiles(currentFile ? [currentFile] : []);
48
+ upload.clearAttachments();
49
+ mention.resetMention();
50
+ setShowHistory(false);
51
+ } else {
52
+ abortRef.current?.abort();
53
+ }
54
+ return () => { cancelled = true; };
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [open, currentFile]);
57
+
58
+ // Persist session on message changes
59
+ useEffect(() => {
60
+ if (!open || !session.activeSessionId) return;
61
+ session.persistSession(session.messages, session.activeSessionId);
62
+ return () => session.clearPersistTimer();
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, [open, session.messages, session.activeSessionId]);
65
+
66
+ // Esc to close (or dismiss mention)
67
+ useEffect(() => {
68
+ if (!open) return;
69
+ const handler = (e: KeyboardEvent) => {
70
+ if (e.key === 'Escape') {
71
+ if (mention.mentionQuery !== null) { mention.resetMention(); return; }
72
+ onClose();
73
+ }
74
+ };
75
+ window.addEventListener('keydown', handler);
76
+ return () => window.removeEventListener('keydown', handler);
77
+ }, [open, onClose, mention]);
78
+
79
+ const handleInputChange = useCallback((val: string) => {
80
+ setInput(val);
81
+ mention.updateMentionFromInput(val);
82
+ }, [mention]);
83
+
84
+ const selectMention = useCallback((filePath: string) => {
85
+ const atIdx = input.lastIndexOf('@');
86
+ setInput(input.slice(0, atIdx));
87
+ mention.resetMention();
88
+ if (!attachedFiles.includes(filePath)) {
89
+ setAttachedFiles(prev => [...prev, filePath]);
90
+ }
91
+ setTimeout(() => inputRef.current?.focus(), 0);
92
+ }, [input, attachedFiles, mention]);
93
+
94
+ const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
95
+ if (mention.mentionQuery === null) return;
96
+ if (e.key === 'ArrowDown') {
97
+ e.preventDefault();
98
+ mention.navigateMention('down');
99
+ } else if (e.key === 'ArrowUp') {
100
+ e.preventDefault();
101
+ mention.navigateMention('up');
102
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
103
+ if (mention.mentionResults.length > 0) {
104
+ e.preventDefault();
105
+ selectMention(mention.mentionResults[mention.mentionIndex]);
106
+ }
107
+ }
108
+ }, [mention, selectMention]);
109
+
110
+ const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
111
+
112
+ const handleSubmit = useCallback(async (e: React.FormEvent) => {
113
+ e.preventDefault();
114
+ if (mention.mentionQuery !== null) return;
115
+ const text = input.trim();
116
+ if (!text || isLoading) return;
117
+
118
+ const userMsg: Message = { role: 'user', content: text };
119
+ const requestMessages = [...session.messages, userMsg];
120
+ session.setMessages([...requestMessages, { role: 'assistant', content: '' }]);
121
+ setInput('');
122
+ setAttachedFiles(currentFile ? [currentFile] : []);
123
+ setIsLoading(true);
124
+ setLoadingPhase('connecting');
125
+
126
+ const controller = new AbortController();
127
+ abortRef.current = controller;
128
+
129
+ try {
130
+ const res = await fetch('/api/ask', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({
134
+ messages: requestMessages,
135
+ currentFile,
136
+ attachedFiles,
137
+ uploadedFiles: upload.localAttachments,
138
+ maxSteps,
139
+ }),
140
+ signal: controller.signal,
141
+ });
142
+
143
+ if (!res.ok) {
144
+ let errorMsg = `Request failed (${res.status})`;
145
+ try {
146
+ const errBody = await res.json();
147
+ if (errBody.error) errorMsg = errBody.error;
148
+ } catch {}
149
+ throw new Error(errorMsg);
150
+ }
151
+
152
+ if (!res.body) throw new Error('No response body');
153
+
154
+ const reader = res.body.getReader();
155
+ const decoder = new TextDecoder();
156
+ let assistantContent = '';
157
+ setLoadingPhase('thinking');
158
+
159
+ while (true) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ const chunk = decoder.decode(value, { stream: true });
163
+ if (chunk) setLoadingPhase('streaming');
164
+ assistantContent += chunk;
165
+ session.setMessages(prev => {
166
+ const updated = [...prev];
167
+ updated[updated.length - 1] = { role: 'assistant', content: assistantContent };
168
+ return updated;
169
+ });
170
+ }
171
+
172
+ if (!assistantContent.trim()) {
173
+ session.setMessages(prev => {
174
+ const updated = [...prev];
175
+ updated[updated.length - 1] = { role: 'assistant', content: `__error__${t.ask.errorNoResponse}` };
176
+ return updated;
177
+ });
178
+ }
179
+ } catch (err) {
180
+ if ((err as Error).name === 'AbortError') {
181
+ session.setMessages(prev => {
182
+ const updated = [...prev];
183
+ const lastIdx = updated.length - 1;
184
+ if (lastIdx >= 0 && updated[lastIdx].role === 'assistant' && !updated[lastIdx].content.trim()) {
185
+ updated[lastIdx] = { role: 'assistant', content: `__error__${t.ask.stopped}` };
186
+ }
187
+ return updated;
188
+ });
189
+ } else {
190
+ const errMsg = err instanceof Error ? err.message : 'Something went wrong';
191
+ session.setMessages(prev => {
192
+ const updated = [...prev];
193
+ const lastIdx = updated.length - 1;
194
+ if (lastIdx >= 0 && updated[lastIdx].role === 'assistant' && !updated[lastIdx].content.trim()) {
195
+ updated[lastIdx] = { role: 'assistant', content: `__error__${errMsg}` };
196
+ return updated;
197
+ }
198
+ return [...updated, { role: 'assistant', content: `__error__${errMsg}` }];
199
+ });
200
+ }
201
+ } finally {
202
+ setIsLoading(false);
203
+ abortRef.current = null;
204
+ }
205
+ }, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, maxSteps, t.ask.errorNoResponse, t.ask.stopped]);
206
+
207
+ const handleResetSession = useCallback(() => {
208
+ if (isLoading) return;
209
+ session.resetSession();
210
+ setInput('');
211
+ setAttachedFiles(currentFile ? [currentFile] : []);
212
+ upload.clearAttachments();
213
+ mention.resetMention();
214
+ setShowHistory(false);
215
+ setTimeout(() => inputRef.current?.focus(), 0);
216
+ }, [isLoading, currentFile, session, upload, mention]);
217
+
218
+ const handleLoadSession = useCallback((id: string) => {
219
+ session.loadSession(id);
220
+ setShowHistory(false);
221
+ setInput('');
222
+ setAttachedFiles(currentFile ? [currentFile] : []);
223
+ upload.clearAttachments();
224
+ mention.resetMention();
225
+ setTimeout(() => inputRef.current?.focus(), 0);
226
+ }, [session, currentFile, upload, mention]);
227
+
228
+ if (!open) return null;
229
+
230
+ return (
231
+ <div
232
+ className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[10vh] modal-backdrop"
233
+ onClick={(e) => e.target === e.currentTarget && onClose()}
234
+ >
235
+ <div
236
+ role="dialog"
237
+ aria-modal="true"
238
+ aria-label={t.ask.title}
239
+ className="w-full md:max-w-2xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl flex flex-col h-[92vh] md:h-auto md:max-h-[75vh]"
240
+ >
241
+ {/* Header */}
242
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
243
+ {/* Mobile drag indicator */}
244
+ <div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
245
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
246
+ <Sparkles size={15} style={{ color: 'var(--amber)' }} />
247
+ <span style={{ fontFamily: "'IBM Plex Mono', monospace" }}>{t.ask.title}</span>
248
+ {currentFile && (
249
+ <span className="text-xs text-muted-foreground font-normal truncate max-w-[200px]">
250
+ — {currentFile.split('/').pop()}
251
+ </span>
252
+ )}
253
+ </div>
254
+ <div className="flex items-center gap-1">
255
+ <button type="button" onClick={() => setShowHistory(v => !v)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title="Session history">
256
+ <History size={14} />
257
+ </button>
258
+ <button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
259
+ <RotateCcw size={14} />
260
+ </button>
261
+ <button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
262
+ <X size={15} />
263
+ </button>
264
+ </div>
265
+ </div>
266
+
267
+ {showHistory && (
268
+ <SessionHistory
269
+ sessions={session.sessions}
270
+ activeSessionId={session.activeSessionId}
271
+ onLoad={handleLoadSession}
272
+ onDelete={session.deleteSession}
273
+ />
274
+ )}
275
+
276
+ {/* Messages */}
277
+ <MessageList
278
+ messages={session.messages}
279
+ isLoading={isLoading}
280
+ loadingPhase={loadingPhase}
281
+ emptyPrompt={t.ask.emptyPrompt}
282
+ suggestions={t.ask.suggestions}
283
+ onSuggestionClick={setInput}
284
+ labels={{ connecting: t.ask.connecting, thinking: t.ask.thinking, generating: t.ask.generating }}
285
+ />
286
+
287
+ {/* Input area */}
288
+ <div className="border-t border-border shrink-0">
289
+ {/* Attached file chips */}
290
+ {attachedFiles.length > 0 && (
291
+ <div className="px-4 pt-2.5 pb-1">
292
+ <div className="text-[11px] text-muted-foreground/70 mb-1.5">Knowledge Base Context</div>
293
+ <div className="flex flex-wrap gap-1.5">
294
+ {attachedFiles.map(f => (
295
+ <FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
296
+ ))}
297
+ </div>
298
+ </div>
299
+ )}
300
+
301
+ {upload.localAttachments.length > 0 && (
302
+ <div className="px-4 pb-1">
303
+ <div className="text-[11px] text-muted-foreground/70 mb-1.5">Uploaded Files</div>
304
+ <div className="flex flex-wrap gap-1.5">
305
+ {upload.localAttachments.map((f, idx) => (
306
+ <FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
307
+ ))}
308
+ </div>
309
+ </div>
310
+ )}
311
+
312
+ {upload.uploadError && (
313
+ <div className="px-4 pb-1 text-xs text-red-400">{upload.uploadError}</div>
314
+ )}
315
+
316
+ {/* @-mention dropdown */}
317
+ {mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
318
+ <MentionPopover
319
+ results={mention.mentionResults}
320
+ selectedIndex={mention.mentionIndex}
321
+ onSelect={selectMention}
322
+ />
323
+ )}
324
+
325
+ <form onSubmit={handleSubmit} className="flex items-center gap-2 px-3 py-3">
326
+ <button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title="Attach local file">
327
+ <Paperclip size={15} />
328
+ </button>
329
+
330
+ <input
331
+ ref={upload.uploadInputRef}
332
+ type="file"
333
+ className="hidden"
334
+ multiple
335
+ accept=".txt,.md,.markdown,.csv,.json,.yaml,.yml,.xml,.html,.htm,.pdf,text/plain,text/markdown,text/csv,application/json,application/pdf"
336
+ onChange={async (e) => {
337
+ const inputEl = e.currentTarget;
338
+ await upload.pickFiles(inputEl.files);
339
+ inputEl.value = '';
340
+ }}
341
+ />
342
+
343
+ <button
344
+ type="button"
345
+ onClick={() => {
346
+ const el = inputRef.current;
347
+ if (!el) return;
348
+ const pos = el.selectionStart ?? input.length;
349
+ const newVal = input.slice(0, pos) + '@' + input.slice(pos);
350
+ handleInputChange(newVal);
351
+ setTimeout(() => { el.focus(); el.setSelectionRange(pos + 1, pos + 1); }, 0);
352
+ }}
353
+ className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
354
+ title="@ mention file"
355
+ >
356
+ <AtSign size={15} />
357
+ </button>
358
+
359
+ <input
360
+ ref={inputRef}
361
+ value={input}
362
+ onChange={e => handleInputChange(e.target.value)}
363
+ onKeyDown={handleInputKeyDown}
364
+ placeholder={t.ask.placeholder}
365
+ disabled={isLoading}
366
+ className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
367
+ />
368
+
369
+ {isLoading ? (
370
+ <button type="button" onClick={handleStop} className="p-1.5 rounded-md transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted" title={t.ask.stopTitle}>
371
+ <StopCircle size={15} />
372
+ </button>
373
+ ) : (
374
+ <button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: '#131210' }}>
375
+ <Send size={14} />
376
+ </button>
377
+ )}
378
+ </form>
379
+ </div>
380
+
381
+ {/* Footer hint — desktop only */}
382
+ <div className="hidden md:flex px-4 pb-2 items-center gap-3 text-xs text-muted-foreground/50 shrink-0">
383
+ <span><kbd className="font-mono">↵</kbd> {t.ask.send}</span>
384
+ <span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
385
+ <span className="inline-flex items-center gap-1">
386
+ <span>Agent steps</span>
387
+ <select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-[11px] text-foreground">
388
+ <option value={10}>10</option>
389
+ <option value={20}>20</option>
390
+ <option value={30}>30</option>
391
+ </select>
392
+ </span>
393
+ <span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ );
398
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { Link as LinkIcon, FileText } from 'lucide-react';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { apiFetch } from '@/lib/api';
8
+ import type { BacklinkItem } from '@/lib/types';
9
+
10
+ export default function Backlinks({ filePath }: { filePath: string }) {
11
+ const [backlinks, setBacklinks] = useState<BacklinkItem[]>([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const { t } = useLocale();
14
+
15
+ useEffect(() => {
16
+ setLoading(true);
17
+ apiFetch<BacklinkItem[]>(`/api/backlinks?path=${encodeURIComponent(filePath)}`)
18
+ .then(data => {
19
+ setBacklinks(Array.isArray(data) ? data : []);
20
+ setLoading(false);
21
+ })
22
+ .catch(() => {
23
+ setBacklinks([]);
24
+ setLoading(false);
25
+ });
26
+ }, [filePath]);
27
+
28
+ if (!loading && backlinks.length === 0) return null;
29
+
30
+ return (
31
+ <div className="mt-12 pt-8 border-t border-border">
32
+ <div className="flex items-center gap-2 mb-6 text-muted-foreground">
33
+ <LinkIcon size={16} className="text-amber-500/70" />
34
+ <h3 className="text-sm font-semibold tracking-wider uppercase" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
35
+ {t.common?.relatedFiles || 'Related Files'}
36
+ </h3>
37
+ <span className="text-xs bg-muted px-1.5 py-0.5 rounded-full font-mono">
38
+ {backlinks.length}
39
+ </span>
40
+ </div>
41
+
42
+ <div className="grid gap-3">
43
+ {loading ? (
44
+ <div className="space-y-3">
45
+ {[1, 2].map(i => (
46
+ <div key={i} className="h-20 bg-muted/30 rounded-lg animate-pulse" />
47
+ ))}
48
+ </div>
49
+ ) : (
50
+ backlinks.map((link) => (
51
+ <Link
52
+ key={link.filePath}
53
+ href={`/view/${link.filePath.split('/').map(encodeURIComponent).join('/')}`}
54
+ className="group block p-4 rounded-xl border border-border/50 bg-card/30 hover:bg-muted/30 hover:border-amber-500/30 transition-all duration-200"
55
+ >
56
+ <div className="flex items-start gap-3">
57
+ <div className="mt-1 p-1.5 rounded-md bg-muted group-hover:bg-amber-500/10 transition-colors">
58
+ <FileText size={14} className="text-muted-foreground group-hover:text-amber-500" />
59
+ </div>
60
+ <div className="min-w-0 flex-1">
61
+ <div className="font-medium text-sm text-foreground group-hover:text-amber-500 transition-colors truncate mb-1">
62
+ {link.filePath}
63
+ </div>
64
+ <div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed italic opacity-80 group-hover:opacity-100 transition-opacity">
65
+ {link.snippets[0] || ''}
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </Link>
70
+ ))
71
+ )}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { ChevronRight, Home } from 'lucide-react';
5
+
6
+ export default function Breadcrumb({ filePath }: { filePath: string }) {
7
+ const parts = filePath.split('/');
8
+ return (
9
+ <nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
10
+ <Link href="/" className="hover:text-foreground transition-colors">
11
+ <Home size={14} />
12
+ </Link>
13
+ {parts.map((part, i) => {
14
+ const isLast = i === parts.length - 1;
15
+ const href = '/view/' + parts.slice(0, i + 1).map(encodeURIComponent).join('/');
16
+ return (
17
+ <span key={i} className="flex items-center gap-1">
18
+ <ChevronRight size={12} className="text-muted-foreground/50" />
19
+ {isLast ? (
20
+ <span className="text-foreground font-medium" suppressHydrationWarning>{part}</span>
21
+ ) : (
22
+ <Link href={href} className="hover:text-foreground transition-colors truncate max-w-[200px]" suppressHydrationWarning>
23
+ {part}
24
+ </Link>
25
+ )}
26
+ </span>
27
+ );
28
+ })}
29
+ </nav>
30
+ );
31
+ }