@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.
- package/.env.local.example +38 -0
- package/LICENSE +21 -0
- package/README.md +423 -0
- package/README_zh.md +423 -0
- package/app/README.md +152 -0
- package/app/app/api/ask/route.ts +170 -0
- package/app/app/api/ask-sessions/route.ts +90 -0
- package/app/app/api/auth/route.ts +37 -0
- package/app/app/api/backlinks/route.ts +22 -0
- package/app/app/api/bootstrap/route.ts +37 -0
- package/app/app/api/extract-pdf/route.ts +82 -0
- package/app/app/api/file/route.ts +138 -0
- package/app/app/api/files/route.ts +12 -0
- package/app/app/api/git/route.ts +42 -0
- package/app/app/api/graph/route.ts +113 -0
- package/app/app/api/recent-files/route.ts +10 -0
- package/app/app/api/search/route.ts +17 -0
- package/app/app/api/settings/reset-token/route.ts +21 -0
- package/app/app/api/settings/route.ts +123 -0
- package/app/app/error.tsx +33 -0
- package/app/app/globals.css +368 -0
- package/app/app/icon.svg +35 -0
- package/app/app/layout.tsx +103 -0
- package/app/app/login/page.tsx +120 -0
- package/app/app/page.tsx +12 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +343 -0
- package/app/app/view/[...path]/error.tsx +33 -0
- package/app/app/view/[...path]/loading.tsx +15 -0
- package/app/app/view/[...path]/page.tsx +93 -0
- package/app/components/AskFab.tsx +59 -0
- package/app/components/AskModal.tsx +398 -0
- package/app/components/Backlinks.tsx +75 -0
- package/app/components/Breadcrumb.tsx +31 -0
- package/app/components/CsvView.tsx +325 -0
- package/app/components/DirView.tsx +138 -0
- package/app/components/Editor.tsx +124 -0
- package/app/components/EditorWrapper.tsx +17 -0
- package/app/components/ErrorBoundary.tsx +53 -0
- package/app/components/FileTree.tsx +369 -0
- package/app/components/HomeContent.tsx +262 -0
- package/app/components/JsonView.tsx +27 -0
- package/app/components/MarkdownEditor.tsx +95 -0
- package/app/components/MarkdownView.tsx +118 -0
- package/app/components/SearchModal.tsx +193 -0
- package/app/components/SettingsModal.tsx +237 -0
- package/app/components/Sidebar.tsx +136 -0
- package/app/components/SidebarLayout.tsx +36 -0
- package/app/components/TableOfContents.tsx +150 -0
- package/app/components/ThemeToggle.tsx +34 -0
- package/app/components/WysiwygEditor.tsx +75 -0
- package/app/components/ask/FileChip.tsx +30 -0
- package/app/components/ask/MentionPopover.tsx +52 -0
- package/app/components/ask/MessageList.tsx +126 -0
- package/app/components/ask/SessionHistory.tsx +49 -0
- package/app/components/renderers/AgentInspectorRenderer.tsx +277 -0
- package/app/components/renderers/BacklinksRenderer.tsx +147 -0
- package/app/components/renderers/ConfigRenderer.tsx +236 -0
- package/app/components/renderers/CsvRenderer.tsx +77 -0
- package/app/components/renderers/DiffRenderer.tsx +310 -0
- package/app/components/renderers/GraphRenderer.tsx +428 -0
- package/app/components/renderers/SummaryRenderer.tsx +251 -0
- package/app/components/renderers/TimelineRenderer.tsx +213 -0
- package/app/components/renderers/TodoRenderer.tsx +474 -0
- package/app/components/renderers/WorkflowRenderer.tsx +404 -0
- package/app/components/renderers/csv/BoardView.tsx +146 -0
- package/app/components/renderers/csv/ConfigPanel.tsx +117 -0
- package/app/components/renderers/csv/EditableCell.tsx +43 -0
- package/app/components/renderers/csv/GalleryView.tsx +40 -0
- package/app/components/renderers/csv/TableView.tsx +164 -0
- package/app/components/renderers/csv/types.ts +87 -0
- package/app/components/settings/AiTab.tsx +111 -0
- package/app/components/settings/AppearanceTab.tsx +101 -0
- package/app/components/settings/KnowledgeTab.tsx +157 -0
- package/app/components/settings/PluginsTab.tsx +82 -0
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/ShortcutsTab.tsx +22 -0
- package/app/components/settings/types.ts +41 -0
- package/app/components/ui/button.tsx +60 -0
- package/app/components/ui/dialog.tsx +157 -0
- package/app/components/ui/input.tsx +20 -0
- package/app/components/ui/scroll-area.tsx +55 -0
- package/app/components/ui/toggle.tsx +44 -0
- package/app/components/ui/tooltip.tsx +66 -0
- package/app/components.json +25 -0
- package/app/data/pages/home-dark.png +0 -0
- package/app/data/pages/home-mobile-crop.png +0 -0
- package/app/data/pages/home-mobile.png +0 -0
- package/app/data/pages/home.png +0 -0
- package/app/data/pages/view-dir.png +0 -0
- package/app/data/pages/view-file-bot.png +0 -0
- package/app/data/pages/view-file-dark-crop.png +0 -0
- package/app/data/pages/view-file-dark.png +0 -0
- package/app/data/pages/view-file-mobile.png +0 -0
- package/app/data/pages/view-file-sm.png +0 -0
- package/app/data/pages/view-file-top.png +0 -0
- package/app/data/pages/view-file.png +0 -0
- package/app/eslint.config.mjs +18 -0
- package/app/hooks/useAskSession.ts +181 -0
- package/app/hooks/useFileUpload.ts +126 -0
- package/app/hooks/useMention.ts +65 -0
- package/app/lib/LocaleContext.tsx +40 -0
- package/app/lib/actions.ts +40 -0
- package/app/lib/agent/index.ts +3 -0
- package/app/lib/agent/model.ts +18 -0
- package/app/lib/agent/prompt.ts +32 -0
- package/app/lib/agent/tools.ts +151 -0
- package/app/lib/api.ts +55 -0
- package/app/lib/core/backlinks.ts +40 -0
- package/app/lib/core/csv.ts +28 -0
- package/app/lib/core/fs-ops.ts +118 -0
- package/app/lib/core/git.ts +50 -0
- package/app/lib/core/index.ts +58 -0
- package/app/lib/core/lines.ts +89 -0
- package/app/lib/core/search.ts +79 -0
- package/app/lib/core/security.ts +43 -0
- package/app/lib/core/tree.ts +113 -0
- package/app/lib/core/types.ts +40 -0
- package/app/lib/fs.ts +467 -0
- package/app/lib/i18n.ts +300 -0
- package/app/lib/jwt.ts +58 -0
- package/app/lib/renderers/index.ts +79 -0
- package/app/lib/renderers/registry.ts +70 -0
- package/app/lib/settings.ts +150 -0
- package/app/lib/types.ts +32 -0
- package/app/lib/utils.ts +34 -0
- package/app/next-env.d.ts +6 -0
- package/app/next.config.ts +10 -0
- package/app/package-lock.json +15306 -0
- package/app/package.json +71 -0
- package/app/postcss.config.mjs +7 -0
- package/app/proxy.ts +64 -0
- package/app/public/file.svg +1 -0
- package/app/public/globe.svg +1 -0
- package/app/public/landing/index.html +353 -0
- package/app/public/landing/style.css +216 -0
- package/app/public/logo-square.svg +37 -0
- package/app/public/logo.svg +37 -0
- package/app/public/next.svg +1 -0
- package/app/public/vercel.svg +1 -0
- package/app/public/window.svg +1 -0
- package/app/scripts/extract-pdf.cjs +56 -0
- package/app/tsconfig.json +34 -0
- package/app/vitest.config.ts +14 -0
- package/assets/demo-flow-zh.html +622 -0
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/assets/images/gui-sync-cv.png +0 -0
- package/assets/logo-square.svg +37 -0
- package/bin/cli.js +894 -0
- package/mcp/README.md +113 -0
- package/mcp/package-lock.json +1717 -0
- package/mcp/package.json +18 -0
- package/mcp/src/index.ts +494 -0
- package/mcp/tsconfig.json +13 -0
- package/package.json +49 -0
- package/scripts/setup.js +675 -0
- package/scripts/upgrade-prompt.md +147 -0
- package/skills/mindos/SKILL.md +319 -0
- package/skills/mindos-zh/SKILL.md +318 -0
- package/templates/README.md +31 -0
- package/templates/empty/CHANGELOG.md +9 -0
- package/templates/empty/CONFIG.json +197 -0
- package/templates/empty/CONFIG.md +73 -0
- package/templates/empty/INSTRUCTION.md +177 -0
- package/templates/empty/README.md +27 -0
- package/templates/en/CHANGELOG.md +9 -0
- package/templates/en/CONFIG.json +197 -0
- package/templates/en/CONFIG.md +73 -0
- package/templates/en/INSTRUCTION.md +177 -0
- package/templates/en/README.md +27 -0
- package/templates/en/TODO.md +13 -0
- package/templates/en//360/237/221/244 Profile/INSTRUCTION.md" +21 -0
- package/templates/en//360/237/221/244 Profile/README.md" +15 -0
- package/templates/en//360/237/221/244 Profile//342/232/231/357/270/217 Preferences.md" +21 -0
- package/templates/en//360/237/221/244 Profile//360/237/216/257 Focus.md" +31 -0
- package/templates/en//360/237/221/244 Profile//360/237/221/244 Identity.md" +22 -0
- package/templates/en//360/237/223/232 Resources/INSTRUCTION.md" +29 -0
- package/templates/en//360/237/223/232 Resources/README.md" +21 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +1 -0
- package/templates/en//360/237/223/235 Notes/INSTRUCTION.md" +31 -0
- package/templates/en//360/237/223/235 Notes/Ideas/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +16 -0
- package/templates/en//360/237/223/235 Notes/Inbox/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Inbox//360/237/247/252_example_quick_capture.md" +14 -0
- package/templates/en//360/237/223/235 Notes/Meetings/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Meetings//360/237/247/252_example_meeting_note.md" +17 -0
- package/templates/en//360/237/223/235 Notes/README.md" +24 -0
- package/templates/en//360/237/223/235 Notes/Waiting/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/Waiting//360/237/247/252_example_blocked_item.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Configurations/README.md" +3 -0
- package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +14 -0
- package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +21 -0
- package/templates/en//360/237/224/204 Workflows/Information/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Information//360/237/247/252_example_info_capture_sop.md" +13 -0
- package/templates/en//360/237/224/204 Workflows/Media/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Media//360/237/247/252_example_content_publish_sop.md" +13 -0
- package/templates/en//360/237/224/204 Workflows/README.md" +22 -0
- package/templates/en//360/237/224/204 Workflows/Research/README.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Research//360/237/247/252_example_lit_review_sop.md" +16 -0
- package/templates/en//360/237/224/204 Workflows/Startup/README.md" +3 -0
- package/templates/en//360/237/224/204 Workflows/Startup//360/237/247/252_example_weekly_founder_ops.md" +22 -0
- package/templates/en//360/237/224/227 Connections/Classmates/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Classmates//360/237/247/252_example_leo_chen.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Colleagues/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Colleagues//360/237/247/252_example_ethan_zhao.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Connections Overview.csv" +5 -0
- package/templates/en//360/237/224/227 Connections/Family/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Family//360/237/247/252_example_james_wang.md" +16 -0
- package/templates/en//360/237/224/227 Connections/Friends/README.md" +11 -0
- package/templates/en//360/237/224/227 Connections/Friends//360/237/247/252_example_lily_lin.md" +16 -0
- package/templates/en//360/237/224/227 Connections/INSTRUCTION.md" +56 -0
- package/templates/en//360/237/224/227 Connections/README.md" +20 -0
- package/templates/en//360/237/232/200 Projects/Archived/README.md" +9 -0
- package/templates/en//360/237/232/200 Projects/Archived//360/237/247/252_example_archived_project_note.md" +14 -0
- package/templates/en//360/237/232/200 Projects/INSTRUCTION.md" +29 -0
- package/templates/en//360/237/232/200 Projects/Products/README.md" +16 -0
- package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +20 -0
- package/templates/en//360/237/232/200 Projects/README.md" +21 -0
- package/templates/en//360/237/232/200 Projects/Research/README.md" +16 -0
- package/templates/en//360/237/232/200 Projects/Research//360/237/247/252_example_research_project_brief.md" +16 -0
- package/templates/template-generation-skill.md +79 -0
- package/templates/zh/CHANGELOG.md +9 -0
- package/templates/zh/CONFIG.json +197 -0
- package/templates/zh/CONFIG.md +66 -0
- package/templates/zh/INSTRUCTION.md +177 -0
- package/templates/zh/README.md +27 -0
- package/templates/zh/TODO.md +13 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/INSTRUCTION.md" +28 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +20 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/217 Preferences.md" +21 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/257 Focus.md" +31 -0
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/244 Identity.md" +22 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/INSTRUCTION.md" +29 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +21 -0
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +1 -0
- 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
- 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
- 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
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260/INSTRUCTION.md" +31 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +24 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//344/274/232/350/256/256/README.md" +8 -0
- 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
- 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
- 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
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225/README.md" +8 -0
- 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
- 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
- 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
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/INSTRUCTION.md" +29 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +21 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//344/277/241/346/201/257/README.md" +16 -0
- 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
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/252/222/344/275/223/README.md" +16 -0
- 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
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//347/247/221/347/240/224/README.md" +16 -0
- 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
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256/README.md" +3 -0
- 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
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273/INSTRUCTION.md" +62 -0
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273/README.md" +20 -0
- 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
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/344/272/213/README.md" +11 -0
- 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
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/220/214/345/255/246/README.md" +11 -0
- 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
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//345/256/266/344/272/272/README.md" +11 -0
- 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
- package/templates/zh//360/237/224/227 /345/205/263/347/263/273//346/234/213/345/217/213/README.md" +11 -0
- 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
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256/INSTRUCTION.md" +31 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256/README.md" +21 -0
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201/README.md" +16 -0
- 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
- 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
- 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
- package/templates/zh//360/237/232/200 /351/241/271/347/233/256//347/247/221/347/240/224/README.md" +16 -0
- 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
|
+
}
|