@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
package/app/app/icon.svg
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g-human" x1="14" y1="16" x2="2" y2="16" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0%" stop-color="#c8873a" stop-opacity="0.8"/>
|
|
5
|
+
<stop offset="100%" stop-color="#c8873a" stop-opacity="0.3"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="g-agent" x1="14" y1="16" x2="30" y2="16" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop offset="0%" stop-color="#c8873a" stop-opacity="0.8"/>
|
|
9
|
+
<stop offset="100%" stop-color="#c8873a" stop-opacity="1"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<!-- Human Loop (Left) — dashed, lighter -->
|
|
14
|
+
<path
|
|
15
|
+
d="M14,16 C10,23 3.2,23 3.2,16 C3.2,9 10,9 14,16"
|
|
16
|
+
stroke="url(#g-human)"
|
|
17
|
+
stroke-width="1.8"
|
|
18
|
+
stroke-dasharray="1.2 2.4"
|
|
19
|
+
stroke-linecap="round"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<!-- Agent Loop (Right) — solid, bolder -->
|
|
23
|
+
<path
|
|
24
|
+
d="M14,16 C18,7 28.8,7 28.8,16 C28.8,25 18,25 14,16"
|
|
25
|
+
stroke="url(#g-agent)"
|
|
26
|
+
stroke-width="2.6"
|
|
27
|
+
stroke-linecap="round"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Spark center -->
|
|
31
|
+
<path
|
|
32
|
+
d="M14,14.5 Q14,16 15.5,16 Q14,16 14,17.5 Q14,16 12.5,16 Q14,16 14,14.5 Z"
|
|
33
|
+
fill="#FEF3C7"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Geist, Geist_Mono, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
|
|
3
|
+
import { headers } from 'next/headers';
|
|
4
|
+
import './globals.css';
|
|
5
|
+
import { getFileTree } from '@/lib/fs';
|
|
6
|
+
import SidebarLayout from '@/components/SidebarLayout';
|
|
7
|
+
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
8
|
+
import { LocaleProvider } from '@/lib/LocaleContext';
|
|
9
|
+
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
10
|
+
|
|
11
|
+
const geistSans = Geist({
|
|
12
|
+
variable: '--font-geist-sans',
|
|
13
|
+
subsets: ['latin'],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const geistMono = Geist_Mono({
|
|
17
|
+
variable: '--font-geist-mono',
|
|
18
|
+
subsets: ['latin'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const ibmPlexMono = IBM_Plex_Mono({
|
|
22
|
+
variable: '--font-ibm-plex-mono',
|
|
23
|
+
weight: ['400', '500', '600'],
|
|
24
|
+
subsets: ['latin'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const ibmPlexSans = IBM_Plex_Sans({
|
|
28
|
+
variable: '--font-ibm-plex-sans',
|
|
29
|
+
weight: ['300', '400', '500', '600'],
|
|
30
|
+
subsets: ['latin'],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const lora = Lora({
|
|
34
|
+
variable: '--font-lora',
|
|
35
|
+
weight: ['400', '500', '600'],
|
|
36
|
+
style: ['normal', 'italic'],
|
|
37
|
+
subsets: ['latin'],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const metadata: Metadata = {
|
|
41
|
+
title: 'MindOS',
|
|
42
|
+
description: 'Personal knowledge base',
|
|
43
|
+
icons: { icon: '/logo-square.svg' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const viewport = {
|
|
47
|
+
width: 'device-width',
|
|
48
|
+
initialScale: 1,
|
|
49
|
+
viewportFit: 'cover' as const,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default async function RootLayout({
|
|
53
|
+
children,
|
|
54
|
+
}: Readonly<{
|
|
55
|
+
children: React.ReactNode;
|
|
56
|
+
}>) {
|
|
57
|
+
let fileTree: import('@/lib/types').FileNode[] = [];
|
|
58
|
+
try {
|
|
59
|
+
fileTree = getFileTree();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('[RootLayout] Failed to load file tree:', err);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headersList = await headers();
|
|
65
|
+
const isLoginPage = headersList.get('x-pathname') === '/login';
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<html lang="en" suppressHydrationWarning>
|
|
69
|
+
<head>
|
|
70
|
+
{/* Patch Node.removeChild/insertBefore to swallow errors caused by browser
|
|
71
|
+
extensions (translators, Grammarly, etc.) that mutate the DOM between SSR
|
|
72
|
+
and hydration. See: https://github.com/facebook/react/issues/17256 */}
|
|
73
|
+
<script
|
|
74
|
+
dangerouslySetInnerHTML={{
|
|
75
|
+
__html: `(function(){if(typeof Node!=='undefined'){var o=Node.prototype.removeChild;Node.prototype.removeChild=function(c){if(c.parentNode!==this){try{return o.call(c.parentNode,c)}catch(e){return c}}return o.call(this,c)};var i=Node.prototype.insertBefore;Node.prototype.insertBefore=function(n,r){if(r&&r.parentNode!==this){try{return i.call(r.parentNode,n,r)}catch(e){return i.call(this,n,null)}}return i.call(this,n,r)}}})();`,
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
{/* Apply user appearance settings before first paint, preventing flash */}
|
|
79
|
+
<script
|
|
80
|
+
dangerouslySetInnerHTML={{
|
|
81
|
+
__html: `(function(){try{var s=localStorage.getItem('theme');var dark=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',dark);var cw=localStorage.getItem('content-width');if(cw)document.documentElement.style.setProperty('--content-width-override',cw);var pf=localStorage.getItem('prose-font');var fm={lora:'"Lora", Georgia, serif','ibm-plex-sans':'"IBM Plex Sans", sans-serif',geist:'var(--font-geist-sans), sans-serif','ibm-plex-mono':'"IBM Plex Mono", monospace'};if(pf&&fm[pf])document.documentElement.style.setProperty('--prose-font-override',fm[pf]);}catch(e){}})();`,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
</head>
|
|
85
|
+
<body
|
|
86
|
+
className={`${geistSans.variable} ${geistMono.variable} ${ibmPlexMono.variable} ${ibmPlexSans.variable} ${lora.variable} antialiased bg-background text-foreground`}
|
|
87
|
+
suppressHydrationWarning
|
|
88
|
+
>
|
|
89
|
+
<LocaleProvider>
|
|
90
|
+
<TooltipProvider delay={300}>
|
|
91
|
+
<ErrorBoundary>
|
|
92
|
+
{isLoginPage ? children : (
|
|
93
|
+
<SidebarLayout fileTree={fileTree}>
|
|
94
|
+
{children}
|
|
95
|
+
</SidebarLayout>
|
|
96
|
+
)}
|
|
97
|
+
</ErrorBoundary>
|
|
98
|
+
</TooltipProvider>
|
|
99
|
+
</LocaleProvider>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, Suspense } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Loader2, Lock } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
function LoginForm() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const searchParams = useSearchParams();
|
|
10
|
+
const [password, setPassword] = useState('');
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
|
|
14
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError('');
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch('/api/auth', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ password }),
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
const rawRedirect = searchParams.get('redirect') ?? '/';
|
|
26
|
+
// Safety: only allow relative paths starting with / to prevent open redirect
|
|
27
|
+
const safe = rawRedirect.startsWith('/') && !rawRedirect.startsWith('//')
|
|
28
|
+
? rawRedirect
|
|
29
|
+
: '/';
|
|
30
|
+
router.replace(safe);
|
|
31
|
+
} else {
|
|
32
|
+
setError('Incorrect password. Please try again.');
|
|
33
|
+
setPassword('');
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
setError('Connection error. Please try again.');
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="min-h-screen bg-background flex items-center justify-center px-4">
|
|
44
|
+
<div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-sm p-8">
|
|
45
|
+
{/* Logo + title */}
|
|
46
|
+
<div className="flex flex-col items-center gap-3 mb-8">
|
|
47
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none" width={40} height={40}>
|
|
48
|
+
<defs>
|
|
49
|
+
<linearGradient id="lp-grad-human" x1="35" y1="40" x2="5" y2="40" gradientUnits="userSpaceOnUse">
|
|
50
|
+
<stop offset="0%" stopColor="#c8873a" stopOpacity="0.8"/>
|
|
51
|
+
<stop offset="100%" stopColor="#c8873a" stopOpacity="0.3"/>
|
|
52
|
+
</linearGradient>
|
|
53
|
+
<linearGradient id="lp-grad-agent" x1="35" y1="40" x2="75" y2="40" gradientUnits="userSpaceOnUse">
|
|
54
|
+
<stop offset="0%" stopColor="#c8873a" stopOpacity="0.8"/>
|
|
55
|
+
<stop offset="100%" stopColor="#c8873a" stopOpacity="1"/>
|
|
56
|
+
</linearGradient>
|
|
57
|
+
</defs>
|
|
58
|
+
<g transform="translate(0, 20)">
|
|
59
|
+
<path d="M35,20 C25,35 8,35 8,20 C8,5 25,5 35,20" stroke="url(#lp-grad-human)" strokeWidth="3" strokeDasharray="2 4" strokeLinecap="round"/>
|
|
60
|
+
<path d="M35,20 C45,2 75,2 75,20 C75,38 45,38 35,20" stroke="url(#lp-grad-agent)" strokeWidth="4.5" strokeLinecap="round"/>
|
|
61
|
+
<path d="M35,17.5 Q35,20 37.5,20 Q35,20 35,22.5 Q35,20 32.5,20 Q35,20 35,17.5 Z" fill="#FEF3C7"/>
|
|
62
|
+
</g>
|
|
63
|
+
</svg>
|
|
64
|
+
<h1
|
|
65
|
+
className="text-xl font-semibold text-foreground tracking-tight"
|
|
66
|
+
style={{ fontFamily: "'IBM Plex Mono', monospace" }}
|
|
67
|
+
>
|
|
68
|
+
MindOS
|
|
69
|
+
</h1>
|
|
70
|
+
<p className="text-sm text-muted-foreground">Enter your password to continue</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
74
|
+
<div className="space-y-1.5">
|
|
75
|
+
<label className="text-sm font-medium text-foreground" htmlFor="password">
|
|
76
|
+
Password
|
|
77
|
+
</label>
|
|
78
|
+
<input
|
|
79
|
+
id="password"
|
|
80
|
+
type="password"
|
|
81
|
+
value={password}
|
|
82
|
+
onChange={e => setPassword(e.target.value)}
|
|
83
|
+
placeholder="Enter password"
|
|
84
|
+
autoFocus
|
|
85
|
+
autoComplete="current-password"
|
|
86
|
+
required
|
|
87
|
+
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{error && (
|
|
92
|
+
<p className="text-xs text-destructive">{error}</p>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<button
|
|
96
|
+
type="submit"
|
|
97
|
+
disabled={loading || !password}
|
|
98
|
+
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
|
99
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
100
|
+
>
|
|
101
|
+
{loading ? (
|
|
102
|
+
<Loader2 size={14} className="animate-spin" />
|
|
103
|
+
) : (
|
|
104
|
+
<Lock size={14} />
|
|
105
|
+
)}
|
|
106
|
+
{loading ? 'Signing in…' : 'Sign in'}
|
|
107
|
+
</button>
|
|
108
|
+
</form>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default function LoginPage() {
|
|
115
|
+
return (
|
|
116
|
+
<Suspense>
|
|
117
|
+
<LoginForm />
|
|
118
|
+
</Suspense>
|
|
119
|
+
);
|
|
120
|
+
}
|
package/app/app/page.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getRecentlyModified } from '@/lib/fs';
|
|
2
|
+
import HomeContent from '@/components/HomeContent';
|
|
3
|
+
|
|
4
|
+
export default function HomePage() {
|
|
5
|
+
let recent: { path: string; mtime: number }[] = [];
|
|
6
|
+
try {
|
|
7
|
+
recent = getRecentlyModified(15);
|
|
8
|
+
} catch (err) {
|
|
9
|
+
console.error('[HomePage] Failed to load recent files:', err);
|
|
10
|
+
}
|
|
11
|
+
return <HomeContent recent={recent} />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition, useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Edit3, Save, X, Loader2, LayoutTemplate } from 'lucide-react';
|
|
6
|
+
import MarkdownView from '@/components/MarkdownView';
|
|
7
|
+
import JsonView from '@/components/JsonView';
|
|
8
|
+
import CsvView from '@/components/CsvView';
|
|
9
|
+
import Backlinks from '@/components/Backlinks';
|
|
10
|
+
import Breadcrumb from '@/components/Breadcrumb';
|
|
11
|
+
import MarkdownEditor, { MdViewMode } from '@/components/MarkdownEditor';
|
|
12
|
+
import TableOfContents from '@/components/TableOfContents';
|
|
13
|
+
import { resolveRenderer } from '@/lib/renderers/registry';
|
|
14
|
+
import { encodePath } from '@/lib/utils';
|
|
15
|
+
import '@/lib/renderers/index'; // registers all renderers
|
|
16
|
+
|
|
17
|
+
interface ViewPageClientProps {
|
|
18
|
+
filePath: string;
|
|
19
|
+
content: string;
|
|
20
|
+
extension: string;
|
|
21
|
+
saveAction: (content: string) => Promise<void>;
|
|
22
|
+
appendRowAction?: (newRow: string[]) => Promise<{ newContent: string }>;
|
|
23
|
+
initialEditing?: boolean;
|
|
24
|
+
isDraft?: boolean;
|
|
25
|
+
draftDirectories?: string[];
|
|
26
|
+
createDraftAction?: (targetPath: string, content: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function ViewPageClient({
|
|
30
|
+
filePath,
|
|
31
|
+
content,
|
|
32
|
+
extension,
|
|
33
|
+
saveAction,
|
|
34
|
+
appendRowAction,
|
|
35
|
+
initialEditing = false,
|
|
36
|
+
isDraft = false,
|
|
37
|
+
draftDirectories = [],
|
|
38
|
+
createDraftAction,
|
|
39
|
+
}: ViewPageClientProps) {
|
|
40
|
+
const hydrated = useSyncExternalStore(
|
|
41
|
+
() => () => {},
|
|
42
|
+
() => true,
|
|
43
|
+
() => false,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const useRaw = useSyncExternalStore(
|
|
47
|
+
(onStoreChange) => {
|
|
48
|
+
const listener = () => onStoreChange();
|
|
49
|
+
window.addEventListener('storage', listener);
|
|
50
|
+
window.addEventListener('mindos-use-raw-change', listener);
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener('storage', listener);
|
|
53
|
+
window.removeEventListener('mindos-use-raw-change', listener);
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
() => {
|
|
57
|
+
const saved = localStorage.getItem('mindos-use-raw');
|
|
58
|
+
return saved !== null ? saved === 'true' : true;
|
|
59
|
+
},
|
|
60
|
+
() => false,
|
|
61
|
+
);
|
|
62
|
+
const router = useRouter();
|
|
63
|
+
const [editing, setEditing] = useState(initialEditing || content === '');
|
|
64
|
+
const [editContent, setEditContent] = useState(content);
|
|
65
|
+
const [savedContent, setSavedContent] = useState(content);
|
|
66
|
+
const [isPending, startTransition] = useTransition();
|
|
67
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
68
|
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
69
|
+
const [mdViewMode, setMdViewMode] = useState<MdViewMode>('wysiwyg');
|
|
70
|
+
|
|
71
|
+
const inferredName = filePath.split('/').pop() || 'Untitled.md';
|
|
72
|
+
const [showSaveAs, setShowSaveAs] = useState(isDraft);
|
|
73
|
+
const [saveDir, setSaveDir] = useState('');
|
|
74
|
+
const [saveName, setSaveName] = useState(inferredName);
|
|
75
|
+
|
|
76
|
+
// Keep first paint deterministic between server and client to avoid hydration mismatch.
|
|
77
|
+
const effectiveUseRaw = hydrated ? useRaw : false;
|
|
78
|
+
|
|
79
|
+
const handleToggleRaw = useCallback(() => {
|
|
80
|
+
const next = !useRaw;
|
|
81
|
+
localStorage.setItem('mindos-use-raw', String(next));
|
|
82
|
+
window.dispatchEvent(new Event('mindos-use-raw-change'));
|
|
83
|
+
}, [useRaw]);
|
|
84
|
+
|
|
85
|
+
const renderer = resolveRenderer(filePath, extension);
|
|
86
|
+
const isCsv = extension === 'csv';
|
|
87
|
+
const showRenderer = !editing && !effectiveUseRaw && !!renderer;
|
|
88
|
+
|
|
89
|
+
const handleEdit = useCallback(() => {
|
|
90
|
+
setEditContent(savedContent);
|
|
91
|
+
setEditing(true);
|
|
92
|
+
setSaveError(null);
|
|
93
|
+
setSaveSuccess(false);
|
|
94
|
+
}, [savedContent]);
|
|
95
|
+
|
|
96
|
+
const handleCancel = useCallback(() => {
|
|
97
|
+
if (isDraft) {
|
|
98
|
+
router.push('/');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setEditing(false);
|
|
102
|
+
setSaveError(null);
|
|
103
|
+
}, [isDraft, router]);
|
|
104
|
+
|
|
105
|
+
const handleConfirmDraftSave = useCallback(() => {
|
|
106
|
+
const trimmed = saveName.trim();
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
setSaveError('Please enter a file name');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!createDraftAction) {
|
|
112
|
+
setSaveError('Draft save is not available');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const finalName = trimmed.endsWith('.md') || trimmed.endsWith('.csv') ? trimmed : `${trimmed}.md`;
|
|
117
|
+
const targetPath = saveDir ? `${saveDir}/${finalName}` : finalName;
|
|
118
|
+
|
|
119
|
+
setSaveError(null);
|
|
120
|
+
startTransition(async () => {
|
|
121
|
+
try {
|
|
122
|
+
await createDraftAction(targetPath, editContent);
|
|
123
|
+
setSavedContent(editContent);
|
|
124
|
+
setEditing(false);
|
|
125
|
+
setShowSaveAs(false);
|
|
126
|
+
setSaveSuccess(true);
|
|
127
|
+
setTimeout(() => setSaveSuccess(false), 2500);
|
|
128
|
+
router.push(`/view/${encodePath(targetPath)}`);
|
|
129
|
+
router.refresh();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}, [saveName, createDraftAction, saveDir, editContent, router]);
|
|
135
|
+
|
|
136
|
+
const handleSave = useCallback(() => {
|
|
137
|
+
if (isCsv) {
|
|
138
|
+
setEditing(false);
|
|
139
|
+
setSaveSuccess(true);
|
|
140
|
+
setTimeout(() => setSaveSuccess(false), 2500);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isDraft) {
|
|
145
|
+
setShowSaveAs(true);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setSaveError(null);
|
|
150
|
+
startTransition(async () => {
|
|
151
|
+
try {
|
|
152
|
+
await saveAction(editContent);
|
|
153
|
+
setSavedContent(editContent);
|
|
154
|
+
setEditing(false);
|
|
155
|
+
setSaveSuccess(true);
|
|
156
|
+
setTimeout(() => setSaveSuccess(false), 2500);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}, [isCsv, isDraft, saveAction, editContent]);
|
|
162
|
+
|
|
163
|
+
// Renderer's inline save — updates local savedContent without entering edit mode
|
|
164
|
+
const handleRendererSave = useCallback(async (newContent: string) => {
|
|
165
|
+
await saveAction(newContent);
|
|
166
|
+
setSavedContent(newContent);
|
|
167
|
+
}, [saveAction]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const handler = (e: KeyboardEvent) => {
|
|
171
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
if (editing) handleSave();
|
|
174
|
+
}
|
|
175
|
+
if (e.key === 'e' && !editing && document.activeElement?.tagName === 'BODY') {
|
|
176
|
+
handleEdit();
|
|
177
|
+
}
|
|
178
|
+
if (e.key === 'Escape' && editing) handleCancel();
|
|
179
|
+
};
|
|
180
|
+
window.addEventListener('keydown', handler);
|
|
181
|
+
return () => window.removeEventListener('keydown', handler);
|
|
182
|
+
}, [editing, handleSave, handleEdit, handleCancel]);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex flex-col min-h-screen">
|
|
186
|
+
{/* Top bar */}
|
|
187
|
+
<div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
|
|
188
|
+
<div className="content-width xl:mr-[220px] flex items-center justify-between gap-2">
|
|
189
|
+
<div className="min-w-0 flex-1">
|
|
190
|
+
<Breadcrumb filePath={filePath} />
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
|
|
194
|
+
{saveSuccess && (
|
|
195
|
+
<span className="text-xs flex items-center gap-1.5" style={{ color: '#7aad80', fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
196
|
+
<span className="w-1.5 h-1.5 rounded-full" style={{ background: '#7aad80' }} />
|
|
197
|
+
<span className="hidden sm:inline">saved</span>
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
{saveError && (
|
|
201
|
+
<span className="text-xs text-red-400 hidden sm:inline">{saveError}</span>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{/* Renderer toggle — only shown when a custom renderer exists */}
|
|
205
|
+
{renderer && !editing && !isDraft && (
|
|
206
|
+
<button
|
|
207
|
+
onClick={handleToggleRaw}
|
|
208
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
|
209
|
+
style={{
|
|
210
|
+
background: effectiveUseRaw ? 'var(--muted)' : `${'var(--amber)'}22`,
|
|
211
|
+
color: effectiveUseRaw ? 'var(--muted-foreground)' : 'var(--amber)',
|
|
212
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
213
|
+
}}
|
|
214
|
+
title={effectiveUseRaw ? `Switch to ${renderer.name}` : 'View raw'}
|
|
215
|
+
>
|
|
216
|
+
<LayoutTemplate size={13} />
|
|
217
|
+
<span className="hidden sm:inline">{effectiveUseRaw ? renderer.name : 'Raw'}</span>
|
|
218
|
+
</button>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{!editing && !showRenderer && !isDraft && (
|
|
222
|
+
<button
|
|
223
|
+
onClick={handleEdit}
|
|
224
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
|
225
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
226
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--foreground)'; e.currentTarget.style.background = 'var(--accent)'; }}
|
|
227
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'var(--muted)'; }}
|
|
228
|
+
>
|
|
229
|
+
<Edit3 size={13} />
|
|
230
|
+
<span className="hidden sm:inline">Edit</span>
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
{editing && (
|
|
234
|
+
<>
|
|
235
|
+
<button
|
|
236
|
+
onClick={handleCancel}
|
|
237
|
+
disabled={isPending}
|
|
238
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors disabled:opacity-50"
|
|
239
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
240
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--accent)'; }}
|
|
241
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--muted)'; }}
|
|
242
|
+
>
|
|
243
|
+
<X size={13} />
|
|
244
|
+
<span className="hidden sm:inline">Cancel</span>
|
|
245
|
+
</button>
|
|
246
|
+
<button
|
|
247
|
+
onClick={isDraft && showSaveAs ? handleConfirmDraftSave : handleSave}
|
|
248
|
+
disabled={isPending}
|
|
249
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50"
|
|
250
|
+
style={{ background: 'var(--amber)', color: '#131210', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
251
|
+
>
|
|
252
|
+
{isPending ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
|
|
253
|
+
<span className="hidden sm:inline">Save</span>
|
|
254
|
+
</button>
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Content */}
|
|
262
|
+
<div className="flex-1 px-4 md:px-6 py-6 md:py-8">
|
|
263
|
+
{editing ? (
|
|
264
|
+
<div className="content-width xl:mr-[220px]">
|
|
265
|
+
{isDraft && showSaveAs && (
|
|
266
|
+
<div className="mb-3 rounded-lg border border-border bg-card p-3 flex flex-col md:flex-row md:items-end gap-2">
|
|
267
|
+
<div className="flex-1 min-w-[180px]">
|
|
268
|
+
<label className="text-xs text-muted-foreground">Directory</label>
|
|
269
|
+
<select
|
|
270
|
+
value={saveDir}
|
|
271
|
+
onChange={(e) => setSaveDir(e.target.value)}
|
|
272
|
+
className="mt-1 w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground"
|
|
273
|
+
>
|
|
274
|
+
<option value="">/</option>
|
|
275
|
+
{draftDirectories.map((dir) => (
|
|
276
|
+
<option key={dir} value={dir}>{dir}</option>
|
|
277
|
+
))}
|
|
278
|
+
</select>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flex-1 min-w-[200px]">
|
|
281
|
+
<label className="text-xs text-muted-foreground">File name</label>
|
|
282
|
+
<input
|
|
283
|
+
value={saveName}
|
|
284
|
+
onChange={(e) => setSaveName(e.target.value)}
|
|
285
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleConfirmDraftSave(); }}
|
|
286
|
+
className="mt-1 w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground"
|
|
287
|
+
placeholder="Untitled.md"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
{isCsv ? (
|
|
293
|
+
<CsvView
|
|
294
|
+
content={editContent}
|
|
295
|
+
filePath={filePath}
|
|
296
|
+
appendAction={appendRowAction}
|
|
297
|
+
saveAction={async (c) => {
|
|
298
|
+
await saveAction(c);
|
|
299
|
+
setEditContent(c);
|
|
300
|
+
setSavedContent(c);
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
) : (
|
|
304
|
+
<MarkdownEditor
|
|
305
|
+
value={editContent}
|
|
306
|
+
onChange={setEditContent}
|
|
307
|
+
viewMode={mdViewMode}
|
|
308
|
+
onViewModeChange={setMdViewMode}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
) : showRenderer ? (
|
|
313
|
+
<div className="content-width xl:mr-[220px]">
|
|
314
|
+
<renderer.component
|
|
315
|
+
filePath={filePath}
|
|
316
|
+
content={savedContent}
|
|
317
|
+
extension={extension}
|
|
318
|
+
saveAction={handleRendererSave}
|
|
319
|
+
/>
|
|
320
|
+
<Backlinks filePath={filePath} />
|
|
321
|
+
</div>
|
|
322
|
+
) : (
|
|
323
|
+
<div className="content-width xl:mr-[220px]">
|
|
324
|
+
{extension === 'csv' ? (
|
|
325
|
+
<CsvView
|
|
326
|
+
content={savedContent}
|
|
327
|
+
filePath={filePath}
|
|
328
|
+
/>
|
|
329
|
+
) : extension === 'json' ? (
|
|
330
|
+
<JsonView content={savedContent} />
|
|
331
|
+
) : (
|
|
332
|
+
<>
|
|
333
|
+
<MarkdownView content={savedContent} />
|
|
334
|
+
<TableOfContents content={savedContent} />
|
|
335
|
+
</>
|
|
336
|
+
)}
|
|
337
|
+
<Backlinks filePath={filePath} />
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function ViewError({
|
|
6
|
+
error,
|
|
7
|
+
reset,
|
|
8
|
+
}: {
|
|
9
|
+
error: Error & { digest?: string };
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
console.error('View error:', error);
|
|
14
|
+
}, [error]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4 px-6">
|
|
18
|
+
<div className="text-center">
|
|
19
|
+
<h2 className="text-lg font-semibold text-foreground mb-2">Failed to load file</h2>
|
|
20
|
+
<p className="text-sm text-muted-foreground max-w-md">
|
|
21
|
+
{error.message || 'The file could not be read or rendered.'}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
<button
|
|
25
|
+
onClick={reset}
|
|
26
|
+
className="px-4 py-2 rounded-md text-sm font-medium transition-colors"
|
|
27
|
+
style={{ background: 'var(--muted)', color: 'var(--foreground)' }}
|
|
28
|
+
>
|
|
29
|
+
Try again
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default function Loading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex items-center justify-center min-h-[50vh]">
|
|
4
|
+
<div className="flex flex-col items-center gap-3">
|
|
5
|
+
<div
|
|
6
|
+
className="w-6 h-6 border-2 rounded-full animate-spin"
|
|
7
|
+
style={{ borderColor: 'var(--border)', borderTopColor: 'var(--amber)' }}
|
|
8
|
+
/>
|
|
9
|
+
<span className="text-xs text-muted-foreground" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
10
|
+
Loading...
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|