@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,95 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+ import { Columns2, PanelLeft, Eye, Pencil } from 'lucide-react';
5
+ import EditorWrapper from './EditorWrapper';
6
+ import MarkdownView from './MarkdownView';
7
+
8
+ // WysiwygEditor uses browser APIs — load client-side only
9
+ const WysiwygEditor = dynamic(() => import('./WysiwygEditor'), { ssr: false });
10
+
11
+ export type MdViewMode = 'wysiwyg' | 'split' | 'source' | 'preview';
12
+
13
+ interface MarkdownEditorProps {
14
+ value: string;
15
+ onChange: (v: string) => void;
16
+ viewMode: MdViewMode;
17
+ onViewModeChange: (m: MdViewMode) => void;
18
+ }
19
+
20
+ const MODES: { id: MdViewMode; icon: React.ReactNode; label: string }[] = [
21
+ { id: 'wysiwyg', icon: <Pencil size={12} />, label: 'WYSIWYG' },
22
+ { id: 'split', icon: <Columns2 size={12} />, label: 'Split' },
23
+ { id: 'source', icon: <PanelLeft size={12} />, label: 'Source' },
24
+ { id: 'preview', icon: <Eye size={12} />, label: 'Preview' },
25
+ ];
26
+
27
+ const EDITOR_HEIGHT = 'calc(100vh - 160px)';
28
+
29
+ export default function MarkdownEditor({ value, onChange, viewMode, onViewModeChange }: MarkdownEditorProps) {
30
+ return (
31
+ <div className="flex flex-col gap-2">
32
+ {/* Mode toolbar */}
33
+ <div className="flex items-center gap-1 p-1 bg-muted rounded-lg self-start">
34
+ {MODES.map(m => (
35
+ <button
36
+ key={m.id}
37
+ onClick={() => onViewModeChange(m.id)}
38
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-medium transition-colors ${
39
+ viewMode === m.id
40
+ ? 'bg-card text-foreground shadow-sm'
41
+ : 'text-muted-foreground hover:text-foreground'
42
+ }`}
43
+ style={{ fontFamily: "'IBM Plex Mono', monospace" }}
44
+ >
45
+ {m.icon}
46
+ {m.label}
47
+ </button>
48
+ ))}
49
+ </div>
50
+
51
+ {/* Panes */}
52
+ <div
53
+ className="rounded-xl overflow-hidden border border-border flex"
54
+ style={{ height: EDITOR_HEIGHT }}
55
+ >
56
+ {/* WYSIWYG */}
57
+ {viewMode === 'wysiwyg' && (
58
+ <div className="w-full h-full overflow-hidden">
59
+ <WysiwygEditor value={value} onChange={onChange} />
60
+ </div>
61
+ )}
62
+
63
+ {/* Split: source left + preview right */}
64
+ {viewMode === 'split' && (
65
+ <>
66
+ <div className="w-1/2 h-full overflow-auto border-r border-border">
67
+ <EditorWrapper value={value} onChange={onChange} language="markdown" />
68
+ </div>
69
+ <div className="w-1/2 h-full overflow-auto bg-background">
70
+ <div className="px-6 py-5">
71
+ <MarkdownView content={value} />
72
+ </div>
73
+ </div>
74
+ </>
75
+ )}
76
+
77
+ {/* Source only */}
78
+ {viewMode === 'source' && (
79
+ <div className="w-full h-full overflow-auto">
80
+ <EditorWrapper value={value} onChange={onChange} language="markdown" />
81
+ </div>
82
+ )}
83
+
84
+ {/* Preview only */}
85
+ {viewMode === 'preview' && (
86
+ <div className="w-full h-full overflow-auto bg-background">
87
+ <div className="px-6 py-5">
88
+ <MarkdownView content={value} />
89
+ </div>
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ import ReactMarkdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+ import rehypeHighlight from 'rehype-highlight';
6
+ import rehypeRaw from 'rehype-raw';
7
+ import rehypeSlug from 'rehype-slug';
8
+ import { useState, useCallback } from 'react';
9
+ import { Copy, Check } from 'lucide-react';
10
+ import type { Components } from 'react-markdown';
11
+
12
+ interface MarkdownViewProps {
13
+ content: string;
14
+ }
15
+
16
+ function CopyButton({ code }: { code: string }) {
17
+ const [copied, setCopied] = useState(false);
18
+ const handleCopy = useCallback(() => {
19
+ navigator.clipboard.writeText(code).then(() => {
20
+ setCopied(true);
21
+ setTimeout(() => setCopied(false), 2000);
22
+ });
23
+ }, [code]);
24
+
25
+ return (
26
+ <button
27
+ onClick={handleCopy}
28
+ className="
29
+ absolute top-2.5 right-2.5
30
+ p-1.5 rounded-md
31
+ bg-zinc-700 hover:bg-zinc-600
32
+ text-zinc-400 hover:text-zinc-200
33
+ transition-colors duration-100
34
+ opacity-0 group-hover:opacity-100
35
+ "
36
+ title="Copy code"
37
+ >
38
+ {copied ? <Check size={13} /> : <Copy size={13} />}
39
+ </button>
40
+ );
41
+ }
42
+
43
+ // Heading components with suppressHydrationWarning to prevent
44
+ // rehype-slug + emoji hydration mismatches between server and client
45
+ function makeHeading(Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') {
46
+ const HeadingComponent = ({ children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
47
+ <Tag {...props} suppressHydrationWarning>{children}</Tag>
48
+ );
49
+ HeadingComponent.displayName = Tag;
50
+ return HeadingComponent;
51
+ }
52
+
53
+ const components: Components = {
54
+ h1: makeHeading('h1'),
55
+ h2: makeHeading('h2'),
56
+ h3: makeHeading('h3'),
57
+ h4: makeHeading('h4'),
58
+ h5: makeHeading('h5'),
59
+ h6: makeHeading('h6'),
60
+ code({ children, ...props }) {
61
+ return <code {...props} suppressHydrationWarning>{children}</code>;
62
+ },
63
+ pre({ children, ...props }) {
64
+ // Extract code string from children
65
+ let codeString = '';
66
+ if (children && typeof children === 'object' && 'props' in children) {
67
+ const codeEl = children as React.ReactElement<{ children?: React.ReactNode }>;
68
+ codeString = extractText(codeEl.props?.children);
69
+ }
70
+ return (
71
+ <div className="relative group">
72
+ <pre {...props} suppressHydrationWarning>{children}</pre>
73
+ <CopyButton code={codeString} />
74
+ </div>
75
+ );
76
+ },
77
+ a({ href, children, ...props }) {
78
+ const isExternal = href?.startsWith('http');
79
+ return (
80
+ <a
81
+ href={href}
82
+ target={isExternal ? '_blank' : undefined}
83
+ rel={isExternal ? 'noopener noreferrer' : undefined}
84
+ {...props}
85
+ >
86
+ {children}
87
+ </a>
88
+ );
89
+ },
90
+ img({ src, alt, ...props }) {
91
+ if (!src) return null;
92
+ // eslint-disable-next-line @next/next/no-img-element
93
+ return <img src={src} alt={alt ?? ''} {...props} />;
94
+ },
95
+ };
96
+
97
+ function extractText(node: React.ReactNode): string {
98
+ if (typeof node === 'string') return node;
99
+ if (Array.isArray(node)) return node.map(extractText).join('');
100
+ if (node && typeof node === 'object' && 'props' in node) {
101
+ return extractText((node as React.ReactElement<{ children?: React.ReactNode }>).props?.children);
102
+ }
103
+ return '';
104
+ }
105
+
106
+ export default function MarkdownView({ content }: MarkdownViewProps) {
107
+ return (
108
+ <div className="prose max-w-none">
109
+ <ReactMarkdown
110
+ remarkPlugins={[remarkGfm]}
111
+ rehypePlugins={[rehypeSlug, rehypeHighlight, rehypeRaw]}
112
+ components={components}
113
+ >
114
+ {content}
115
+ </ReactMarkdown>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,193 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Search, X, FileText, Table } from 'lucide-react';
6
+ import { SearchResult } from '@/lib/types';
7
+ import { encodePath } from '@/lib/utils';
8
+ import { apiFetch } from '@/lib/api';
9
+ import { useLocale } from '@/lib/LocaleContext';
10
+
11
+ interface SearchModalProps {
12
+ open: boolean;
13
+ onClose: () => void;
14
+ }
15
+
16
+ /** Highlight matched text fragments in a snippet based on the query */
17
+ function highlightSnippet(snippet: string, query: string): React.ReactNode {
18
+ if (!query.trim()) return snippet;
19
+ // Split query into words and escape for regex
20
+ const words = query.trim().split(/\s+/).filter(Boolean);
21
+ const escaped = words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
22
+ const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
23
+ const parts = snippet.split(pattern);
24
+ return parts.map((part, i) =>
25
+ pattern.test(part) ? <mark key={i} className="bg-yellow-300/40 text-foreground rounded-sm px-0.5">{part}</mark> : part
26
+ );
27
+ }
28
+
29
+ export default function SearchModal({ open, onClose }: SearchModalProps) {
30
+ const [query, setQuery] = useState('');
31
+ const [results, setResults] = useState<SearchResult[]>([]);
32
+ const [loading, setLoading] = useState(false);
33
+ const [selectedIndex, setSelectedIndex] = useState(0);
34
+ const inputRef = useRef<HTMLInputElement>(null);
35
+ const router = useRouter();
36
+ const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
37
+ const { t } = useLocale();
38
+
39
+ // Focus input when modal opens
40
+ useEffect(() => {
41
+ if (open) {
42
+ setTimeout(() => inputRef.current?.focus(), 50);
43
+ setQuery('');
44
+ setResults([]);
45
+ setSelectedIndex(0);
46
+ }
47
+ }, [open]);
48
+
49
+ // Debounced search
50
+ const doSearch = useCallback((q: string) => {
51
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
52
+ if (!q.trim()) {
53
+ setResults([]);
54
+ setLoading(false);
55
+ return;
56
+ }
57
+ setLoading(true);
58
+ debounceTimer.current = setTimeout(async () => {
59
+ try {
60
+ const data = await apiFetch<SearchResult[]>(`/api/search?q=${encodeURIComponent(q)}`);
61
+ setResults(Array.isArray(data) ? data : []);
62
+ setSelectedIndex(0);
63
+ } catch {
64
+ setResults([]);
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }, 300);
69
+ }, []);
70
+
71
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
72
+ const val = e.target.value;
73
+ setQuery(val);
74
+ doSearch(val);
75
+ }, [doSearch]);
76
+
77
+ const navigate = useCallback((result: SearchResult) => {
78
+ router.push(`/view/${encodePath(result.path)}`);
79
+ onClose();
80
+ }, [router, onClose]);
81
+
82
+ // Keyboard navigation
83
+ useEffect(() => {
84
+ if (!open) return;
85
+ const handler = (e: KeyboardEvent) => {
86
+ if (e.key === 'Escape') {
87
+ onClose();
88
+ } else if (e.key === 'ArrowDown') {
89
+ e.preventDefault();
90
+ setSelectedIndex(i => Math.min(i + 1, results.length - 1));
91
+ } else if (e.key === 'ArrowUp') {
92
+ e.preventDefault();
93
+ setSelectedIndex(i => Math.max(i - 1, 0));
94
+ } else if (e.key === 'Enter') {
95
+ if (results[selectedIndex]) navigate(results[selectedIndex]);
96
+ }
97
+ };
98
+ window.addEventListener('keydown', handler);
99
+ return () => window.removeEventListener('keydown', handler);
100
+ }, [open, onClose, results, selectedIndex, navigate]);
101
+
102
+ if (!open) return null;
103
+
104
+ return (
105
+ <div
106
+ className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[15vh] modal-backdrop"
107
+ onClick={(e) => e.target === e.currentTarget && onClose()}
108
+ >
109
+ <div role="dialog" aria-modal="true" aria-label="Search" className="w-full md:max-w-xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl overflow-hidden max-h-[85vh] md:max-h-none flex flex-col">
110
+ {/* Mobile drag indicator */}
111
+ <div className="flex justify-center pt-2 pb-0 md:hidden">
112
+ <div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
113
+ </div>
114
+ {/* Search input */}
115
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border">
116
+ <Search size={16} className="text-muted-foreground shrink-0" />
117
+ <input
118
+ ref={inputRef}
119
+ type="text"
120
+ value={query}
121
+ onChange={handleChange}
122
+ placeholder={t.search.placeholder}
123
+ className="flex-1 bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none"
124
+ />
125
+ {loading && (
126
+ <div className="w-4 h-4 border-2 border-muted-foreground/40 border-t-foreground rounded-full animate-spin shrink-0" />
127
+ )}
128
+ {!loading && query && (
129
+ <button onClick={() => { setQuery(''); setResults([]); inputRef.current?.focus(); }}>
130
+ <X size={14} className="text-muted-foreground hover:text-foreground" />
131
+ </button>
132
+ )}
133
+ <kbd className="hidden md:inline text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5 font-mono">ESC</kbd>
134
+ </div>
135
+
136
+ {/* Results */}
137
+ <div className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
138
+ {results.length === 0 && query && !loading && (
139
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
140
+ )}
141
+ {results.length === 0 && !query && (
142
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground/60">{t.search.prompt}</div>
143
+ )}
144
+ {results.map((result, i) => {
145
+ const ext = result.path.endsWith('.csv') ? '.csv' : '.md';
146
+ const parts = result.path.split('/');
147
+ const fileName = parts[parts.length - 1];
148
+ const dirPath = parts.slice(0, -1).join('/');
149
+ return (
150
+ <button
151
+ key={result.path}
152
+ onClick={() => navigate(result)}
153
+ onMouseEnter={() => setSelectedIndex(i)}
154
+ className={`
155
+ w-full px-4 py-3 flex items-start gap-3 text-left transition-colors duration-75
156
+ ${i === selectedIndex ? 'bg-muted' : 'hover:bg-muted/50'}
157
+ ${i < results.length - 1 ? 'border-b border-border' : ''}
158
+ `}
159
+ >
160
+ {ext === '.csv'
161
+ ? <Table size={14} className="text-emerald-400 shrink-0 mt-0.5" />
162
+ : <FileText size={14} className="text-muted-foreground shrink-0 mt-0.5" />
163
+ }
164
+ <div className="min-w-0 flex-1">
165
+ <div className="flex items-baseline gap-2 flex-wrap">
166
+ <span className="text-sm text-foreground font-medium truncate">{fileName}</span>
167
+ {dirPath && (
168
+ <span className="text-xs text-muted-foreground truncate">{dirPath}</span>
169
+ )}
170
+ </div>
171
+ {result.snippet && (
172
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
173
+ {highlightSnippet(result.snippet, query)}
174
+ </p>
175
+ )}
176
+ </div>
177
+ </button>
178
+ );
179
+ })}
180
+ </div>
181
+
182
+ {/* Footer — desktop only */}
183
+ {results.length > 0 && (
184
+ <div className="hidden md:flex px-4 py-2 border-t border-border items-center gap-3 text-xs text-muted-foreground/60">
185
+ <span><kbd className="font-mono">↑↓</kbd> {t.search.navigate}</span>
186
+ <span><kbd className="font-mono">↵</kbd> {t.search.open}</span>
187
+ <span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
188
+ </div>
189
+ )}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,237 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback } from 'react';
4
+ import { X, Settings, Save, Loader2, AlertCircle, CheckCircle2, RotateCcw } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { getAllRenderers, loadDisabledState, isRendererEnabled } from '@/lib/renderers/registry';
7
+ import { apiFetch } from '@/lib/api';
8
+ import '@/lib/renderers/index';
9
+ import type { AiSettings, SettingsData, Tab } from './settings/types';
10
+ import { FONTS } from './settings/types';
11
+ import { AiTab } from './settings/AiTab';
12
+ import { AppearanceTab } from './settings/AppearanceTab';
13
+ import { KnowledgeTab } from './settings/KnowledgeTab';
14
+ import { PluginsTab } from './settings/PluginsTab';
15
+ import { ShortcutsTab } from './settings/ShortcutsTab';
16
+
17
+ interface SettingsModalProps {
18
+ open: boolean;
19
+ onClose: () => void;
20
+ }
21
+
22
+ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
23
+ const [tab, setTab] = useState<Tab>('ai');
24
+ const [data, setData] = useState<SettingsData | null>(null);
25
+ const [saving, setSaving] = useState(false);
26
+ const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
27
+ const { t, locale, setLocale } = useLocale();
28
+
29
+ // Appearance state (localStorage-based)
30
+ const [font, setFont] = useState('lora');
31
+ const [contentWidth, setContentWidth] = useState('780px');
32
+ const [dark, setDark] = useState(true);
33
+ // Plugin enabled state
34
+ const [pluginStates, setPluginStates] = useState<Record<string, boolean>>({});
35
+
36
+ useEffect(() => {
37
+ if (!open) return;
38
+ apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('load-error'));
39
+ setFont(localStorage.getItem('prose-font') ?? 'lora');
40
+ setContentWidth(localStorage.getItem('content-width') ?? '780px');
41
+ const stored = localStorage.getItem('theme');
42
+ setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
43
+ loadDisabledState();
44
+ const initial: Record<string, boolean> = {};
45
+ for (const r of getAllRenderers()) initial[r.id] = isRendererEnabled(r.id);
46
+ setPluginStates(initial);
47
+ setStatus('idle');
48
+ }, [open]);
49
+
50
+ // Apply font immediately
51
+ useEffect(() => {
52
+ const fontMap: Record<string, string> = {
53
+ 'lora': "'Lora', Georgia, serif",
54
+ 'ibm-plex-sans': "'IBM Plex Sans', sans-serif",
55
+ 'geist': 'var(--font-geist-sans), sans-serif',
56
+ 'ibm-plex-mono': "'IBM Plex Mono', monospace",
57
+ };
58
+ document.documentElement.style.setProperty('--prose-font-override', fontMap[font] ?? '');
59
+ localStorage.setItem('prose-font', font);
60
+ }, [font]);
61
+
62
+ // Apply content width immediately
63
+ useEffect(() => {
64
+ document.documentElement.style.setProperty('--content-width-override', contentWidth);
65
+ localStorage.setItem('content-width', contentWidth);
66
+ }, [contentWidth]);
67
+
68
+ // Esc to close
69
+ useEffect(() => {
70
+ if (!open) return;
71
+ const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
72
+ window.addEventListener('keydown', handler);
73
+ return () => window.removeEventListener('keydown', handler);
74
+ }, [open, onClose]);
75
+
76
+ const handleSave = useCallback(async () => {
77
+ if (!data) return;
78
+ setSaving(true);
79
+ try {
80
+ await apiFetch('/api/settings', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ ai: data.ai, mindRoot: data.mindRoot, webPassword: data.webPassword, authToken: data.authToken }),
84
+ });
85
+ setStatus('saved');
86
+ setTimeout(() => setStatus('idle'), 2500);
87
+ } catch {
88
+ setStatus('error');
89
+ setTimeout(() => setStatus('idle'), 2500);
90
+ } finally {
91
+ setSaving(false);
92
+ }
93
+ }, [data]);
94
+
95
+ const updateAi = useCallback((patch: Partial<AiSettings>) => {
96
+ setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
97
+ }, []);
98
+
99
+ const restoreFromEnv = useCallback(async () => {
100
+ if (!data) return;
101
+ const defaults: AiSettings = {
102
+ provider: 'anthropic',
103
+ providers: {
104
+ anthropic: { apiKey: '', model: '' },
105
+ openai: { apiKey: '', model: '', baseUrl: '' },
106
+ },
107
+ };
108
+ setData(d => d ? { ...d, ai: defaults } : d);
109
+ setSaving(true);
110
+ try {
111
+ await apiFetch('/api/settings', {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({ ai: defaults, mindRoot: data.mindRoot }),
115
+ });
116
+ setStatus('saved');
117
+ } catch {
118
+ setStatus('error');
119
+ } finally {
120
+ setSaving(false);
121
+ }
122
+ apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('error'));
123
+ setTimeout(() => setStatus('idle'), 2500);
124
+ }, [data]);
125
+
126
+ if (!open) return null;
127
+
128
+ const env = data?.envOverrides ?? {};
129
+
130
+ const TABS: { id: Tab; label: string }[] = [
131
+ { id: 'ai', label: t.settings.tabs.ai },
132
+ { id: 'appearance', label: t.settings.tabs.appearance },
133
+ { id: 'knowledge', label: t.settings.tabs.knowledge },
134
+ { id: 'plugins', label: t.settings.tabs.plugins },
135
+ { id: 'shortcuts', label: t.settings.tabs.shortcuts },
136
+ ];
137
+
138
+ return (
139
+ <div
140
+ className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[10vh] modal-backdrop"
141
+ onClick={(e) => e.target === e.currentTarget && onClose()}
142
+ >
143
+ <div role="dialog" aria-modal="true" aria-label="Settings" className="w-full md:max-w-xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl flex flex-col h-[88vh] md:h-auto md:max-h-[78vh]">
144
+ {/* Mobile drag indicator */}
145
+ <div className="flex justify-center pt-2 pb-0 md:hidden">
146
+ <div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
147
+ </div>
148
+
149
+ {/* Header */}
150
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
151
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
152
+ <Settings size={15} className="text-muted-foreground" />
153
+ <span style={{ fontFamily: "'IBM Plex Mono', monospace" }}>{t.settings.title}</span>
154
+ </div>
155
+ <button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
156
+ <X size={15} />
157
+ </button>
158
+ </div>
159
+
160
+ {/* Tabs */}
161
+ <div className="flex border-b border-border px-4 shrink-0 overflow-x-auto scrollbar-none">
162
+ {TABS.map(t => (
163
+ <button
164
+ key={t.id}
165
+ onClick={() => setTab(t.id)}
166
+ className={`px-3 py-2.5 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
167
+ tab === t.id
168
+ ? 'border-amber-500 text-foreground'
169
+ : 'border-transparent text-muted-foreground hover:text-foreground'
170
+ }`}
171
+ >
172
+ {t.label}
173
+ </button>
174
+ ))}
175
+ </div>
176
+
177
+ {/* Content */}
178
+ <div className="flex-1 overflow-y-auto min-h-0 px-5 py-5 space-y-5">
179
+ {status === 'load-error' && (tab === 'ai' || tab === 'knowledge') ? (
180
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
181
+ <AlertCircle size={20} className="text-destructive" />
182
+ <p className="text-sm text-destructive font-medium">Failed to load settings</p>
183
+ <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>
184
+ </div>
185
+ ) : !data && tab !== 'shortcuts' && tab !== 'appearance' ? (
186
+ <div className="flex justify-center py-8">
187
+ <Loader2 size={18} className="animate-spin text-muted-foreground" />
188
+ </div>
189
+ ) : (
190
+ <>
191
+ {tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} t={t} />}
192
+ {tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
193
+ {tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
194
+ {tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
195
+ {tab === 'shortcuts' && <ShortcutsTab t={t} />}
196
+ </>
197
+ )}
198
+ </div>
199
+
200
+ {/* Footer */}
201
+ {(tab === 'ai' || tab === 'knowledge') && (
202
+ <div className="px-5 py-3 border-t border-border shrink-0 flex items-center justify-between">
203
+ <div className="flex items-center gap-3">
204
+ {tab === 'ai' && Object.values(env).some(Boolean) && (
205
+ <button
206
+ onClick={restoreFromEnv}
207
+ disabled={saving || !data}
208
+ className="flex items-center gap-1.5 px-4 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
209
+ >
210
+ <RotateCcw size={13} />
211
+ {t.settings.ai.restoreFromEnv}
212
+ </button>
213
+ )}
214
+ <div className="flex items-center gap-1.5 text-xs">
215
+ {status === 'saved' && (
216
+ <><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{t.settings.saved}</span></>
217
+ )}
218
+ {status === 'error' && (
219
+ <><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>
220
+ )}
221
+ </div>
222
+ </div>
223
+ <button
224
+ onClick={handleSave}
225
+ disabled={saving || !data}
226
+ className="flex items-center gap-1.5 px-4 py-1.5 text-sm rounded-lg disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
227
+ style={{ background: 'var(--amber)', color: '#131210' }}
228
+ >
229
+ {saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
230
+ {t.settings.save}
231
+ </button>
232
+ </div>
233
+ )}
234
+ </div>
235
+ </div>
236
+ );
237
+ }