@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,428 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo, memo, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import {
6
+ ReactFlow,
7
+ Background,
8
+ BackgroundVariant,
9
+ Controls,
10
+ MiniMap,
11
+ Handle,
12
+ Position,
13
+ type NodeProps,
14
+ } from '@xyflow/react';
15
+ import '@xyflow/react/dist/style.css';
16
+ import type { RendererContext } from '@/lib/renderers/registry';
17
+ import type { GraphData, GraphNode, GraphEdge } from '@/app/api/graph/route';
18
+ import { apiFetch } from '@/lib/api';
19
+
20
+ // ─── Force Layout ──────────────────────────────────────────────────────────────
21
+
22
+ interface Pos { x: number; y: number }
23
+
24
+ function forceLayout(
25
+ nodeIds: string[],
26
+ edges: { source: string; target: string }[],
27
+ iterations?: number,
28
+ ): Record<string, Pos> {
29
+ const n = nodeIds.length;
30
+ if (n === 0) return {};
31
+
32
+ const iters = iterations ?? (n > 100 ? 80 : 150);
33
+ const width = 1200;
34
+ const height = 900;
35
+ // k is the "ideal" distance between nodes
36
+ const k = Math.sqrt((width * height) / Math.max(n, 1)) * 0.6;
37
+ const pos: Record<string, Pos> = {};
38
+
39
+ // Initialize in a more spread-out circle or random
40
+ nodeIds.forEach((id, i) => {
41
+ const angle = (2 * Math.PI * i) / n;
42
+ const radius = Math.min(width, height) * 0.4 * Math.random();
43
+ pos[id] = {
44
+ x: width / 2 + Math.cos(angle) * radius,
45
+ y: height / 2 + Math.sin(angle) * radius,
46
+ };
47
+ });
48
+
49
+ const disp: Record<string, Pos> = {};
50
+ const initTemp = width * 0.1;
51
+
52
+ for (let iter = 0; iter < iters; iter++) {
53
+ const temp = initTemp * (1 - iter / iters);
54
+
55
+ for (const id of nodeIds) disp[id] = { x: 0, y: 0 };
56
+
57
+ // Repulsion (nodes push each other away)
58
+ for (let i = 0; i < n; i++) {
59
+ for (let j = i + 1; j < n; j++) {
60
+ const u = nodeIds[i], v = nodeIds[j];
61
+ const dx = pos[u].x - pos[v].x;
62
+ const dy = pos[u].y - pos[v].y;
63
+ const distSq = dx * dx + dy * dy || 0.01;
64
+ const dist = Math.sqrt(distSq);
65
+ const force = (k * k) / dist;
66
+ disp[u].x += (dx / dist) * force;
67
+ disp[u].y += (dy / dist) * force;
68
+ disp[v].x -= (dx / dist) * force;
69
+ disp[v].y -= (dy / dist) * force;
70
+ }
71
+ }
72
+
73
+ // Attraction (edges pull nodes together)
74
+ for (const e of edges) {
75
+ const u = e.source, v = e.target;
76
+ if (!pos[u] || !pos[v]) continue;
77
+ const dx = pos[u].x - pos[v].x;
78
+ const dy = pos[u].y - pos[v].y;
79
+ const distSq = dx * dx + dy * dy || 0.01;
80
+ const dist = Math.sqrt(distSq);
81
+ const force = (dist * dist) / k;
82
+ disp[u].x -= (dx / dist) * force;
83
+ disp[u].y -= (dy / dist) * force;
84
+ disp[v].x += (dx / dist) * force;
85
+ disp[v].y += (dy / dist) * force;
86
+ }
87
+
88
+ // Gravity (pull towards center to avoid drifting)
89
+ for (const id of nodeIds) {
90
+ const dx = pos[id].x - width / 2;
91
+ const dy = pos[id].y - height / 2;
92
+ const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
93
+ const force = 0.05 * dist; // Gentle pull
94
+ disp[id].x -= (dx / dist) * force;
95
+ disp[id].y -= (dy / dist) * force;
96
+ }
97
+
98
+ // Apply displacements
99
+ for (const id of nodeIds) {
100
+ const d = disp[id];
101
+ const dlen = Math.sqrt(d.x * d.x + d.y * d.y) || 0.01;
102
+ pos[id].x += (d.x / dlen) * Math.min(dlen, temp);
103
+ pos[id].y += (d.y / dlen) * Math.min(dlen, temp);
104
+
105
+ // Softer clamping
106
+ pos[id].x = Math.max(0, Math.min(width, pos[id].x));
107
+ pos[id].y = Math.max(0, Math.min(height, pos[id].y));
108
+ }
109
+ }
110
+
111
+ return pos;
112
+ }
113
+
114
+ // ─── WikiNode ──────────────────────────────────────────────────────────────────
115
+
116
+ interface WikiNodeData {
117
+ label: string;
118
+ id: string;
119
+ isCurrent: boolean;
120
+ isOrphan: boolean;
121
+ [key: string]: unknown;
122
+ }
123
+
124
+ const WikiNode = memo(function WikiNode({ data }: NodeProps) {
125
+ const router = useRouter();
126
+ const { label, id, isCurrent, isOrphan, size = 1 } = data as WikiNodeData & { size?: number };
127
+
128
+ const handleClick = useCallback(() => {
129
+ const encoded = (id as string).split('/').map(encodeURIComponent).join('/');
130
+ router.push('/view/' + encoded);
131
+ }, [id, router]);
132
+
133
+ const scale = 0.8 + Math.min(size * 0.1, 1.2);
134
+
135
+ return (
136
+ <div
137
+ onClick={handleClick}
138
+ title={id as string}
139
+ className="group"
140
+ style={{
141
+ fontFamily: "'IBM Plex Mono', monospace",
142
+ fontSize: 10 * scale,
143
+ padding: `${4 * scale}px ${12 * scale}px`,
144
+ borderRadius: 999, // Pill shape
145
+ cursor: 'pointer',
146
+ userSelect: 'none',
147
+ whiteSpace: 'nowrap',
148
+ maxWidth: 240,
149
+ overflow: 'hidden',
150
+ textOverflow: 'ellipsis',
151
+ opacity: isOrphan ? 0.4 : 1,
152
+ background: isCurrent ? 'var(--amber)' : 'var(--card)',
153
+ color: isCurrent ? '#131210' : 'var(--foreground)',
154
+ border: `1.5px solid ${isCurrent ? 'var(--amber)' : 'var(--border)'}`,
155
+ boxShadow: isCurrent
156
+ ? '0 0 20px var(--amber-dim), 0 0 0 2px var(--amber-dim)'
157
+ : '0 2px 4px rgba(0,0,0,0.1)',
158
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
159
+ zIndex: isCurrent ? 10 : 1,
160
+ }}
161
+ onMouseEnter={e => {
162
+ if (!isCurrent) {
163
+ e.currentTarget.style.borderColor = 'var(--amber)';
164
+ e.currentTarget.style.background = 'var(--accent)';
165
+ }
166
+ }}
167
+ onMouseLeave={e => {
168
+ if (!isCurrent) {
169
+ e.currentTarget.style.borderColor = 'var(--border)';
170
+ e.currentTarget.style.background = 'var(--card)';
171
+ }
172
+ }}
173
+ >
174
+ <Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
175
+ {label as string}
176
+ <Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />
177
+ </div>
178
+ );
179
+ });
180
+
181
+ // ─── GraphRenderer ─────────────────────────────────────────────────────────────
182
+
183
+ type Scope = 'global' | 'local';
184
+
185
+ export function GraphRenderer({ filePath }: RendererContext) {
186
+ const [graphData, setGraphData] = useState<GraphData | null>(null);
187
+ const [loading, setLoading] = useState(true);
188
+ const [scope, setScope] = useState<Scope>('local');
189
+ const [mounted, setMounted] = useState(false);
190
+
191
+ useEffect(() => { setMounted(true); }, []);
192
+
193
+ useEffect(() => {
194
+ apiFetch<GraphData>('/api/graph')
195
+ .then((data) => { setGraphData(data); setLoading(false); })
196
+ .catch(() => setLoading(false));
197
+ }, []);
198
+
199
+ // Degree calculation (for node sizing)
200
+ const degrees = useMemo(() => {
201
+ if (!graphData) return new Map<string, number>();
202
+ const d = new Map<string, number>();
203
+ for (const e of graphData.edges) {
204
+ d.set(e.source, (d.get(e.source) || 0) + 1);
205
+ d.set(e.target, (d.get(e.target) || 0) + 1);
206
+ }
207
+ return d;
208
+ }, [graphData]);
209
+
210
+ // Build adjacency for BFS (local scope)
211
+ const adjacency = useMemo(() => {
212
+ if (!graphData) return null;
213
+ const adj = new Map<string, Set<string>>();
214
+ for (const e of graphData.edges) {
215
+ if (!adj.has(e.source)) adj.set(e.source, new Set());
216
+ if (!adj.has(e.target)) adj.set(e.target, new Set());
217
+ adj.get(e.source)!.add(e.target);
218
+ adj.get(e.target)!.add(e.source);
219
+ }
220
+ return adj;
221
+ }, [graphData]);
222
+
223
+ // Scope filter
224
+ const { filteredNodes, filteredEdges } = useMemo(() => {
225
+ if (!graphData) return { filteredNodes: [], filteredEdges: [] };
226
+
227
+ let nodeSubset: GraphNode[];
228
+ let edgeSubset: GraphEdge[];
229
+
230
+ if (scope === 'global') {
231
+ nodeSubset = graphData.nodes;
232
+ edgeSubset = graphData.edges;
233
+ } else {
234
+ // local: BFS 2 hops
235
+ const visited = new Set<string>();
236
+ const queue: Array<{ id: string; depth: number }> = [{ id: filePath, depth: 0 }];
237
+ visited.add(filePath);
238
+ while (queue.length > 0) {
239
+ const { id, depth } = queue.shift()!;
240
+ if (depth >= 2) continue;
241
+ const neighbors = adjacency?.get(id) ?? new Set<string>();
242
+ for (const nb of neighbors) {
243
+ if (!visited.has(nb)) {
244
+ visited.add(nb);
245
+ queue.push({ id: nb, depth: depth + 1 });
246
+ }
247
+ }
248
+ }
249
+ nodeSubset = graphData.nodes.filter(n => visited.has(n.id));
250
+ const nodeIds = new Set(nodeSubset.map(n => n.id));
251
+ edgeSubset = graphData.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
252
+ }
253
+
254
+ return { filteredNodes: nodeSubset, filteredEdges: edgeSubset };
255
+ }, [graphData, scope, filePath, adjacency]);
256
+
257
+ // Orphan detection (nodes with no edges in the current subset)
258
+ const connectedIds = useMemo(() => {
259
+ const s = new Set<string>();
260
+ for (const e of filteredEdges) {
261
+ s.add(e.source);
262
+ s.add(e.target);
263
+ }
264
+ return s;
265
+ }, [filteredEdges]);
266
+
267
+ // Compute layout + build RF nodes/edges
268
+ const { rfNodes, rfEdges } = useMemo(() => {
269
+ if (filteredNodes.length === 0) return { rfNodes: [], rfEdges: [] };
270
+
271
+ const nodeIds = filteredNodes.map(n => n.id);
272
+ const layout = forceLayout(nodeIds, filteredEdges);
273
+
274
+ const rfNodes = filteredNodes.map(n => ({
275
+ id: n.id,
276
+ type: 'wiki' as const,
277
+ position: layout[n.id] ?? { x: 0, y: 0 },
278
+ data: {
279
+ label: n.label,
280
+ id: n.id,
281
+ isCurrent: n.id === filePath,
282
+ isOrphan: !connectedIds.has(n.id),
283
+ size: degrees.get(n.id) || 1,
284
+ },
285
+ }));
286
+
287
+ const rfEdges = filteredEdges.map((e, i) => {
288
+ const isRelatedToCurrent = e.source === filePath || e.target === filePath;
289
+ return {
290
+ id: `e-${i}`,
291
+ source: e.source,
292
+ target: e.target,
293
+ type: 'default' as const, // Curved default
294
+ markerEnd: { type: 'arrowclosed' as const, color: isRelatedToCurrent ? 'var(--amber)' : 'var(--border)' },
295
+ style: {
296
+ stroke: isRelatedToCurrent ? 'var(--amber)' : 'var(--border)',
297
+ strokeWidth: isRelatedToCurrent ? 1.5 : 1,
298
+ opacity: isRelatedToCurrent ? 0.8 : 0.4,
299
+ },
300
+ animated: isRelatedToCurrent,
301
+ };
302
+ });
303
+
304
+ return { rfNodes, rfEdges };
305
+ }, [filteredNodes, filteredEdges, filePath, connectedIds, degrees]);
306
+
307
+ const nodeTypes = useMemo(() => ({ wiki: WikiNode }), []);
308
+
309
+ const scopeButtons: { id: Scope; label: string }[] = [
310
+ { id: 'local', label: 'Local' },
311
+ { id: 'global', label: 'Global' },
312
+ ];
313
+
314
+ if (!mounted || loading) {
315
+ return (
316
+ <div
317
+ style={{
318
+ width: '100%',
319
+ height: 'calc(100vh - 160px)',
320
+ minHeight: 400,
321
+ borderRadius: 12,
322
+ background: 'var(--muted)',
323
+ border: '1px solid var(--border)',
324
+ display: 'flex',
325
+ alignItems: 'center',
326
+ justifyContent: 'center',
327
+ }}
328
+ >
329
+ <span style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace", fontSize: 12 }}>
330
+ {loading ? 'Building graph…' : 'Loading…'}
331
+ </span>
332
+ </div>
333
+ );
334
+ }
335
+
336
+ return (
337
+ <div style={{ width: '100%', position: 'relative', zIndex: 0 }}>
338
+ {/* Toolbar */}
339
+ <div
340
+ style={{
341
+ display: 'flex',
342
+ alignItems: 'center',
343
+ gap: 12,
344
+ marginBottom: 10,
345
+ flexWrap: 'wrap',
346
+ }}
347
+ >
348
+ <span
349
+ style={{
350
+ fontFamily: "'IBM Plex Mono', monospace",
351
+ fontSize: 11,
352
+ color: 'var(--muted-foreground)',
353
+ }}
354
+ >
355
+ {filteredNodes.length} nodes · {filteredEdges.length} edges
356
+ </span>
357
+
358
+ <div
359
+ style={{
360
+ display: 'flex',
361
+ gap: 2,
362
+ padding: 3,
363
+ borderRadius: 8,
364
+ background: 'var(--muted)',
365
+ }}
366
+ >
367
+ {scopeButtons.map(btn => (
368
+ <button
369
+ key={btn.id}
370
+ onClick={() => setScope(btn.id)}
371
+ style={{
372
+ padding: '3px 12px',
373
+ borderRadius: 5,
374
+ fontSize: 11,
375
+ fontFamily: "'IBM Plex Mono', monospace",
376
+ cursor: 'pointer',
377
+ border: 'none',
378
+ outline: 'none',
379
+ background: scope === btn.id ? 'var(--card)' : 'transparent',
380
+ color: scope === btn.id ? 'var(--foreground)' : 'var(--muted-foreground)',
381
+ boxShadow: scope === btn.id ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
382
+ transition: 'all 0.1s',
383
+ }}
384
+ >
385
+ {btn.label}
386
+ </button>
387
+ ))}
388
+ </div>
389
+ </div>
390
+
391
+ {/* React Flow */}
392
+ <div
393
+ style={{
394
+ width: '100%',
395
+ height: 'calc(100vh - 160px)',
396
+ minHeight: 400,
397
+ }}
398
+ >
399
+ <ReactFlow
400
+ nodes={rfNodes}
401
+ edges={rfEdges}
402
+ nodeTypes={nodeTypes}
403
+ fitView
404
+ fitViewOptions={{ padding: 0.15 }}
405
+ proOptions={{ hideAttribution: true }}
406
+ style={{
407
+ background: 'var(--background)',
408
+ borderRadius: 12,
409
+ border: '1px solid var(--border)',
410
+ }}
411
+ >
412
+ <Background
413
+ color="var(--border)"
414
+ gap={24}
415
+ size={1}
416
+ variant={BackgroundVariant.Dots}
417
+ />
418
+ <Controls showInteractive={false} />
419
+ <MiniMap
420
+ nodeColor={(n) =>
421
+ (n.data as WikiNodeData)?.isCurrent ? 'var(--amber)' : 'var(--muted-foreground)'
422
+ }
423
+ />
424
+ </ReactFlow>
425
+ </div>
426
+ </div>
427
+ );
428
+ }
@@ -0,0 +1,251 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import { Sparkles, RefreshCw, Clock, FileText } from 'lucide-react';
5
+ import { encodePath } from '@/lib/utils';
6
+ import { apiFetch } from '@/lib/api';
7
+ import type { RendererContext } from '@/lib/renderers/registry';
8
+
9
+ interface RecentFile {
10
+ path: string;
11
+ mtime: number;
12
+ }
13
+
14
+ function relativeTime(mtime: number): string {
15
+ const diff = Date.now() - mtime;
16
+ const m = Math.floor(diff / 60000);
17
+ const h = Math.floor(diff / 3600000);
18
+ const d = Math.floor(diff / 86400000);
19
+ if (m < 1) return 'just now';
20
+ if (m < 60) return `${m}m ago`;
21
+ if (h < 24) return `${h}h ago`;
22
+ return `${d}d ago`;
23
+ }
24
+
25
+ function basename(p: string) {
26
+ return p.split('/').pop()?.replace(/\.(md|csv)$/, '') ?? p;
27
+ }
28
+
29
+ // Minimal markdown→HTML for the streamed summary
30
+ function renderMarkdown(md: string): string {
31
+ return md
32
+ .replace(/^### (.+)$/gm, '<h3 style="font-size:.8rem;font-weight:600;color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em;margin:1em 0 .3em">$1</h3>')
33
+ .replace(/^## (.+)$/gm, '<h2 style="font-size:.9rem;font-weight:700;color:var(--foreground);margin:1.2em 0 .4em">$1</h2>')
34
+ .replace(/^# (.+)$/gm, '<h1 style="font-size:1rem;font-weight:700;color:var(--foreground);margin:1.2em 0 .4em">$1</h1>')
35
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
36
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
37
+ .replace(/`(.+?)`/g, '<code style="font-family:\'IBM Plex Mono\',monospace;font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
38
+ .replace(/^[-*] (.+)$/gm, '<li style="margin:.2em 0;padding-left:.3em">$1</li>')
39
+ .replace(/(<li[^>]*>.*<\/li>\n?)+/g, s => `<ul style="margin:.4em 0;padding-left:1.4em;list-style:disc">${s}</ul>`)
40
+ .replace(/\n{2,}/g, '</p><p style="margin:.5em 0;font-size:.85rem;line-height:1.7;color:var(--foreground)">')
41
+ .replace(/^(?!<[hulo])(.+)$/gm, '<p style="margin:.5em 0;font-size:.85rem;line-height:1.7;color:var(--foreground)">$1</p>');
42
+ }
43
+
44
+ const LIMIT = 8;
45
+
46
+ export function SummaryRenderer({ filePath }: RendererContext) {
47
+ const [recentFiles, setRecentFiles] = useState<RecentFile[]>([]);
48
+ const [summary, setSummary] = useState('');
49
+ const [streaming, setStreaming] = useState(false);
50
+ const [error, setError] = useState('');
51
+ const [generated, setGenerated] = useState(false);
52
+ const abortRef = useRef<AbortController | null>(null);
53
+
54
+ // Fetch recent files once
55
+ useEffect(() => {
56
+ apiFetch<RecentFile[]>(`/api/recent-files?limit=${LIMIT}`)
57
+ .then((data) => setRecentFiles(data.filter(f => f.path.endsWith('.md'))))
58
+ .catch(() => {});
59
+ }, [filePath]);
60
+
61
+ async function generate() {
62
+ if (recentFiles.length === 0) return;
63
+ abortRef.current?.abort();
64
+ const ctrl = new AbortController();
65
+ abortRef.current = ctrl;
66
+
67
+ setSummary('');
68
+ setError('');
69
+ setStreaming(true);
70
+ setGenerated(false);
71
+
72
+ const attachedFiles = recentFiles.map(f => f.path);
73
+ const fileListMd = recentFiles
74
+ .map(f => `- **${basename(f.path)}** (${f.path}, modified ${relativeTime(f.mtime)})`)
75
+ .join('\n');
76
+
77
+ const prompt = `You are summarizing recent changes in a personal knowledge base (MindOS).
78
+
79
+ The following files were recently modified:
80
+ ${fileListMd}
81
+
82
+ Please provide a concise daily briefing in this format:
83
+ 1. **Key changes**: What was added or updated in each file (1–2 sentences per file)
84
+ 2. **Themes**: Any patterns or recurring topics across the changes
85
+ 3. **Suggested next actions**: 2–3 actionable follow-ups based on the content
86
+
87
+ Be specific. Reference actual content from the files. Keep the total response under 300 words.`;
88
+
89
+ try {
90
+ const res = await fetch('/api/ask', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({
94
+ messages: [{ role: 'user', content: prompt }],
95
+ attachedFiles,
96
+ }),
97
+ signal: ctrl.signal,
98
+ });
99
+
100
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
101
+ if (!res.body) throw new Error('No response body');
102
+
103
+ const reader = res.body.getReader();
104
+ const decoder = new TextDecoder();
105
+ let acc = '';
106
+
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done) break;
110
+ // Vercel AI SDK text stream: each chunk may have "0:..." prefix
111
+ const raw = decoder.decode(value, { stream: true });
112
+ for (const line of raw.split('\n')) {
113
+ const m = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
114
+ if (m) {
115
+ acc += m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
116
+ } else if (line && !line.startsWith('d:') && !line.startsWith('e:') && !line.startsWith('0:')) {
117
+ // plain text stream fallback
118
+ acc += line;
119
+ }
120
+ }
121
+ setSummary(acc);
122
+ }
123
+ setGenerated(true);
124
+ } catch (err: unknown) {
125
+ if (err instanceof Error && err.name !== 'AbortError') {
126
+ setError(err.message || 'Failed to generate summary');
127
+ }
128
+ } finally {
129
+ setStreaming(false);
130
+ }
131
+ }
132
+
133
+ useEffect(() => () => { abortRef.current?.abort(); }, []);
134
+
135
+ return (
136
+ <div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
137
+ {/* header row */}
138
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.5rem', flexWrap: 'wrap' }}>
139
+ <span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
140
+ {recentFiles.length > 0
141
+ ? `${recentFiles.length} recently modified files`
142
+ : 'Loading recent files…'}
143
+ </span>
144
+ <button
145
+ onClick={generate}
146
+ disabled={streaming || recentFiles.length === 0}
147
+ style={{
148
+ display: 'flex',
149
+ alignItems: 'center',
150
+ gap: 6,
151
+ padding: '5px 14px',
152
+ borderRadius: 7,
153
+ fontSize: 12,
154
+ fontFamily: "'IBM Plex Mono',monospace",
155
+ cursor: streaming || recentFiles.length === 0 ? 'not-allowed' : 'pointer',
156
+ border: 'none',
157
+ background: streaming ? 'var(--muted)' : 'var(--amber)',
158
+ color: streaming ? 'var(--muted-foreground)' : '#131210',
159
+ opacity: recentFiles.length === 0 ? 0.5 : 1,
160
+ transition: 'opacity .15s',
161
+ }}
162
+ >
163
+ {streaming ? (
164
+ <RefreshCw size={12} style={{ animation: 'spin 1s linear infinite' }} />
165
+ ) : (
166
+ <Sparkles size={12} />
167
+ )}
168
+ {streaming ? 'Generating…' : generated ? 'Regenerate' : 'Generate briefing'}
169
+ </button>
170
+ </div>
171
+
172
+ {/* source files */}
173
+ {recentFiles.length > 0 && (
174
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: '1.5rem' }}>
175
+ {recentFiles.map(f => (
176
+ <a
177
+ key={f.path}
178
+ href={`/view/${encodePath(f.path)}`}
179
+ style={{
180
+ display: 'inline-flex',
181
+ alignItems: 'center',
182
+ gap: 5,
183
+ padding: '3px 10px',
184
+ borderRadius: 999,
185
+ fontSize: '0.7rem',
186
+ fontFamily: "'IBM Plex Mono',monospace",
187
+ background: 'var(--muted)',
188
+ color: 'var(--muted-foreground)',
189
+ textDecoration: 'none',
190
+ border: '1px solid var(--border)',
191
+ transition: 'color .15s',
192
+ }}
193
+ onMouseEnter={e => (e.currentTarget.style.color = 'var(--foreground)')}
194
+ onMouseLeave={e => (e.currentTarget.style.color = 'var(--muted-foreground)')}
195
+ title={f.path}
196
+ >
197
+ <FileText size={10} />
198
+ {basename(f.path)}
199
+ <span style={{ opacity: 0.5 }}>
200
+ <Clock size={9} style={{ display: 'inline', marginLeft: 2 }} />
201
+ {' '}{relativeTime(f.mtime)}
202
+ </span>
203
+ </a>
204
+ ))}
205
+ </div>
206
+ )}
207
+
208
+ {/* error */}
209
+ {error && (
210
+ <div style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12, marginBottom: '1rem' }}>
211
+ {error}
212
+ </div>
213
+ )}
214
+
215
+ {/* summary output */}
216
+ {summary ? (
217
+ <div style={{
218
+ background: 'var(--card)',
219
+ border: '1px solid var(--border)',
220
+ borderRadius: 10,
221
+ padding: '18px 20px',
222
+ position: 'relative',
223
+ }}>
224
+ {streaming && (
225
+ <div style={{ position: 'absolute', top: 12, right: 14, width: 6, height: 6, borderRadius: '50%', background: 'var(--amber)', animation: 'pulse 1.2s ease-in-out infinite' }} />
226
+ )}
227
+ <div dangerouslySetInnerHTML={{ __html: renderMarkdown(summary) }} />
228
+ </div>
229
+ ) : !streaming && !generated && recentFiles.length > 0 ? (
230
+ <div style={{
231
+ border: '1px dashed var(--border)',
232
+ borderRadius: 10,
233
+ padding: '2.5rem 1.5rem',
234
+ textAlign: 'center',
235
+ color: 'var(--muted-foreground)',
236
+ }}>
237
+ <Sparkles size={28} style={{ margin: '0 auto 10px', opacity: 0.3, color: 'var(--amber)' }} />
238
+ <p style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
239
+ Click <strong style={{ color: 'var(--foreground)' }}>Generate briefing</strong> to summarize recent changes with AI.
240
+ </p>
241
+ </div>
242
+ ) : null}
243
+
244
+ {/* CSS keyframes injected inline */}
245
+ <style>{`
246
+ @keyframes spin { to { transform: rotate(360deg); } }
247
+ @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
248
+ `}</style>
249
+ </div>
250
+ );
251
+ }