@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,369 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
|
|
4
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
5
|
+
import { FileNode } from '@/lib/types';
|
|
6
|
+
import { encodePath } from '@/lib/utils';
|
|
7
|
+
import { ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2, Trash2, Pencil } from 'lucide-react';
|
|
8
|
+
import { createFileAction, deleteFileAction, renameFileAction } from '@/lib/actions';
|
|
9
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
+
|
|
11
|
+
interface FileTreeProps {
|
|
12
|
+
nodes: FileNode[];
|
|
13
|
+
depth?: number;
|
|
14
|
+
onNavigate?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getIcon(node: FileNode) {
|
|
18
|
+
if (node.type === 'directory') return null;
|
|
19
|
+
if (node.extension === '.csv') return <Table size={14} className="text-emerald-400 shrink-0" />;
|
|
20
|
+
return <FileText size={14} className="text-zinc-400 shrink-0" />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCurrentFilePath(pathname: string): string {
|
|
24
|
+
const prefix = '/view/';
|
|
25
|
+
if (!pathname.startsWith(prefix)) return '';
|
|
26
|
+
const encoded = pathname.slice(prefix.length);
|
|
27
|
+
return encoded.split('/').map(decodeURIComponent).join('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: number; onDone: () => void }) {
|
|
31
|
+
const [value, setValue] = useState('');
|
|
32
|
+
const [isPending, startTransition] = useTransition();
|
|
33
|
+
const [error, setError] = useState('');
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
const { t } = useLocale();
|
|
36
|
+
|
|
37
|
+
const handleSubmit = useCallback(() => {
|
|
38
|
+
const name = value.trim();
|
|
39
|
+
if (!name) { setError(t.fileTree.enterFileName); return; }
|
|
40
|
+
startTransition(async () => {
|
|
41
|
+
const result = await createFileAction(dirPath, name);
|
|
42
|
+
if (result.success && result.filePath) {
|
|
43
|
+
onDone();
|
|
44
|
+
router.push(`/view/${encodePath(result.filePath)}`);
|
|
45
|
+
router.refresh();
|
|
46
|
+
} else {
|
|
47
|
+
setError(result.error || t.fileTree.failed);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}, [value, dirPath, onDone, router, t]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="px-2 pb-1" style={{ paddingLeft: `${depth * 12 + 20}px` }}>
|
|
54
|
+
<div className="flex items-center gap-1">
|
|
55
|
+
<input
|
|
56
|
+
autoFocus
|
|
57
|
+
type="text"
|
|
58
|
+
value={value}
|
|
59
|
+
onChange={(e) => { setValue(e.target.value); setError(''); }}
|
|
60
|
+
onKeyDown={(e) => {
|
|
61
|
+
if (e.key === 'Enter') handleSubmit();
|
|
62
|
+
if (e.key === 'Escape') onDone();
|
|
63
|
+
}}
|
|
64
|
+
placeholder="filename.md"
|
|
65
|
+
className="
|
|
66
|
+
flex-1 bg-muted border border-border rounded px-2 py-1
|
|
67
|
+
text-xs text-foreground placeholder:text-muted-foreground
|
|
68
|
+
focus:outline-none focus:border-blue-500/60
|
|
69
|
+
"
|
|
70
|
+
/>
|
|
71
|
+
{isPending
|
|
72
|
+
? <Loader2 size={13} className="text-zinc-500 animate-spin shrink-0" />
|
|
73
|
+
: (
|
|
74
|
+
<button
|
|
75
|
+
onClick={handleSubmit}
|
|
76
|
+
className="text-xs text-blue-400 hover:text-blue-300 shrink-0 px-1"
|
|
77
|
+
>
|
|
78
|
+
{t.fileTree.create}
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</div>
|
|
83
|
+
{error && <p className="text-xs text-red-400 mt-0.5 px-1">{error}</p>}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function DirectoryNode({ node, depth, currentPath, onNavigate }: {
|
|
89
|
+
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
90
|
+
}) {
|
|
91
|
+
const router = useRouter();
|
|
92
|
+
const isActive = currentPath.startsWith(node.path + '/') || currentPath === node.path;
|
|
93
|
+
const [open, setOpen] = useState(depth === 0 ? true : isActive);
|
|
94
|
+
const [showNewFile, setShowNewFile] = useState(false);
|
|
95
|
+
const [renaming, setRenaming] = useState(false);
|
|
96
|
+
const [renameValue, setRenameValue] = useState(node.name);
|
|
97
|
+
const [isPending, startTransition] = useTransition();
|
|
98
|
+
const renameRef = useRef<HTMLInputElement>(null);
|
|
99
|
+
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
100
|
+
const { t } = useLocale();
|
|
101
|
+
|
|
102
|
+
const toggle = useCallback(() => setOpen(v => !v), []);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
return () => {
|
|
106
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
107
|
+
};
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const startRename = useCallback((e?: React.MouseEvent) => {
|
|
111
|
+
e?.preventDefault();
|
|
112
|
+
e?.stopPropagation();
|
|
113
|
+
if (clickTimerRef.current) {
|
|
114
|
+
clearTimeout(clickTimerRef.current);
|
|
115
|
+
clickTimerRef.current = null;
|
|
116
|
+
}
|
|
117
|
+
setRenameValue(node.name);
|
|
118
|
+
setRenaming(true);
|
|
119
|
+
setTimeout(() => renameRef.current?.select(), 0);
|
|
120
|
+
}, [node.name]);
|
|
121
|
+
|
|
122
|
+
const commitRename = useCallback(() => {
|
|
123
|
+
const newName = renameValue.trim();
|
|
124
|
+
if (!newName || newName === node.name) { setRenaming(false); return; }
|
|
125
|
+
startTransition(async () => {
|
|
126
|
+
const result = await renameFileAction(node.path, newName);
|
|
127
|
+
if (result.success && result.newPath) {
|
|
128
|
+
setRenaming(false);
|
|
129
|
+
router.push(`/view/${encodePath(result.newPath)}`);
|
|
130
|
+
router.refresh();
|
|
131
|
+
} else {
|
|
132
|
+
setRenaming(false);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}, [renameValue, node.name, node.path, router]);
|
|
136
|
+
|
|
137
|
+
const handleSingleClick = useCallback(() => {
|
|
138
|
+
if (renaming) return;
|
|
139
|
+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
|
140
|
+
clickTimerRef.current = setTimeout(() => {
|
|
141
|
+
router.push(`/view/${encodePath(node.path)}`);
|
|
142
|
+
onNavigate?.();
|
|
143
|
+
clickTimerRef.current = null;
|
|
144
|
+
}, 180);
|
|
145
|
+
}, [renaming, router, node.path, onNavigate]);
|
|
146
|
+
|
|
147
|
+
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
|
148
|
+
startRename(e);
|
|
149
|
+
}, [startRename]);
|
|
150
|
+
|
|
151
|
+
if (renaming) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
|
|
154
|
+
<input
|
|
155
|
+
ref={renameRef}
|
|
156
|
+
autoFocus
|
|
157
|
+
value={renameValue}
|
|
158
|
+
onChange={e => setRenameValue(e.target.value)}
|
|
159
|
+
onKeyDown={e => {
|
|
160
|
+
if (e.key === 'Enter') commitRename();
|
|
161
|
+
if (e.key === 'Escape') setRenaming(false);
|
|
162
|
+
}}
|
|
163
|
+
onBlur={commitRename}
|
|
164
|
+
className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
|
|
165
|
+
/>
|
|
166
|
+
{isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div>
|
|
173
|
+
<div className="relative group/dir flex items-center">
|
|
174
|
+
<button
|
|
175
|
+
onClick={toggle}
|
|
176
|
+
className="shrink-0 p-1 rounded hover:bg-muted text-zinc-500 transition-colors"
|
|
177
|
+
style={{ marginLeft: `${depth * 12 + 4}px` }}
|
|
178
|
+
aria-label={open ? 'Collapse' : 'Expand'}
|
|
179
|
+
>
|
|
180
|
+
<span className="block transition-transform duration-150" style={{ transform: open ? 'rotate(0deg)' : 'rotate(-90deg)' }}>
|
|
181
|
+
<ChevronDown size={13} />
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={handleSingleClick}
|
|
187
|
+
onDoubleClick={handleDoubleClick}
|
|
188
|
+
className={`
|
|
189
|
+
flex-1 flex items-center gap-1.5 px-1 py-1 rounded text-left min-w-0 pr-16
|
|
190
|
+
text-sm transition-colors duration-100
|
|
191
|
+
hover:bg-muted
|
|
192
|
+
${isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'}
|
|
193
|
+
`}
|
|
194
|
+
>
|
|
195
|
+
{open
|
|
196
|
+
? <FolderOpen size={14} className="text-yellow-400 shrink-0" />
|
|
197
|
+
: <Folder size={14} className="text-yellow-400 shrink-0" />
|
|
198
|
+
}
|
|
199
|
+
<span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
|
|
200
|
+
</button>
|
|
201
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/dir:flex items-center gap-0.5 z-10">
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={(e) => {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
setOpen(true);
|
|
208
|
+
setShowNewFile(true);
|
|
209
|
+
}}
|
|
210
|
+
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
211
|
+
title={t.fileTree.newFileTitle}
|
|
212
|
+
>
|
|
213
|
+
<Plus size={13} />
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={startRename}
|
|
218
|
+
className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
219
|
+
title={t.fileTree.rename}
|
|
220
|
+
>
|
|
221
|
+
<Pencil size={12} />
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div
|
|
227
|
+
className="overflow-hidden transition-all duration-200"
|
|
228
|
+
style={{ maxHeight: open ? '9999px' : '0px' }}
|
|
229
|
+
>
|
|
230
|
+
{node.children && (
|
|
231
|
+
<FileTree nodes={node.children} depth={depth + 1} onNavigate={onNavigate} />
|
|
232
|
+
)}
|
|
233
|
+
{showNewFile && (
|
|
234
|
+
<NewFileInline
|
|
235
|
+
dirPath={node.path}
|
|
236
|
+
depth={depth}
|
|
237
|
+
onDone={() => setShowNewFile(false)}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
246
|
+
node: FileNode; depth: number; currentPath: string; onNavigate?: () => void;
|
|
247
|
+
}) {
|
|
248
|
+
const router = useRouter();
|
|
249
|
+
const isActive = currentPath === node.path;
|
|
250
|
+
const [renaming, setRenaming] = useState(false);
|
|
251
|
+
const [renameValue, setRenameValue] = useState(node.name);
|
|
252
|
+
const [isPending, startTransition] = useTransition();
|
|
253
|
+
const renameRef = useRef<HTMLInputElement>(null);
|
|
254
|
+
const { t } = useLocale();
|
|
255
|
+
|
|
256
|
+
const handleClick = useCallback(() => {
|
|
257
|
+
if (renaming) return;
|
|
258
|
+
router.push(`/view/${encodePath(node.path)}`);
|
|
259
|
+
onNavigate?.();
|
|
260
|
+
}, [router, node.path, onNavigate, renaming]);
|
|
261
|
+
|
|
262
|
+
const startRename = useCallback((e: React.MouseEvent) => {
|
|
263
|
+
e.stopPropagation();
|
|
264
|
+
setRenameValue(node.name);
|
|
265
|
+
setRenaming(true);
|
|
266
|
+
setTimeout(() => renameRef.current?.select(), 0);
|
|
267
|
+
}, [node.name]);
|
|
268
|
+
|
|
269
|
+
const commitRename = useCallback(() => {
|
|
270
|
+
const newName = renameValue.trim();
|
|
271
|
+
if (!newName || newName === node.name) { setRenaming(false); return; }
|
|
272
|
+
startTransition(async () => {
|
|
273
|
+
const result = await renameFileAction(node.path, newName);
|
|
274
|
+
if (result.success && result.newPath) {
|
|
275
|
+
setRenaming(false);
|
|
276
|
+
router.push(`/view/${encodePath(result.newPath)}`);
|
|
277
|
+
router.refresh();
|
|
278
|
+
} else {
|
|
279
|
+
setRenaming(false);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}, [renameValue, node.name, node.path, router]);
|
|
283
|
+
|
|
284
|
+
const handleDelete = useCallback((e: React.MouseEvent) => {
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
if (!confirm(t.fileTree.confirmDelete(node.name))) return;
|
|
287
|
+
startTransition(async () => {
|
|
288
|
+
await deleteFileAction(node.path);
|
|
289
|
+
if (currentPath === node.path) router.push('/');
|
|
290
|
+
router.refresh();
|
|
291
|
+
});
|
|
292
|
+
}, [node.name, node.path, currentPath, router, t]);
|
|
293
|
+
|
|
294
|
+
if (renaming) {
|
|
295
|
+
return (
|
|
296
|
+
<div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
|
|
297
|
+
<input
|
|
298
|
+
ref={renameRef}
|
|
299
|
+
autoFocus
|
|
300
|
+
value={renameValue}
|
|
301
|
+
onChange={e => setRenameValue(e.target.value)}
|
|
302
|
+
onKeyDown={e => {
|
|
303
|
+
if (e.key === 'Enter') commitRename();
|
|
304
|
+
if (e.key === 'Escape') setRenaming(false);
|
|
305
|
+
}}
|
|
306
|
+
onBlur={commitRename}
|
|
307
|
+
className="w-full bg-muted border border-blue-500/60 rounded px-2 py-0.5 text-xs text-foreground focus:outline-none"
|
|
308
|
+
/>
|
|
309
|
+
{isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="relative group/file">
|
|
316
|
+
<button
|
|
317
|
+
onClick={handleClick}
|
|
318
|
+
onDoubleClick={startRename}
|
|
319
|
+
data-filepath={node.path}
|
|
320
|
+
className={`
|
|
321
|
+
w-full flex items-center gap-1.5 px-2 py-1 rounded text-left
|
|
322
|
+
text-sm transition-colors duration-100 cursor-pointer pr-16
|
|
323
|
+
${isActive
|
|
324
|
+
? 'bg-accent text-foreground'
|
|
325
|
+
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
|
326
|
+
}
|
|
327
|
+
`}
|
|
328
|
+
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
329
|
+
>
|
|
330
|
+
{getIcon(node)}
|
|
331
|
+
<span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
|
|
332
|
+
</button>
|
|
333
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
|
|
334
|
+
<button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
|
|
335
|
+
<Pencil size={12} />
|
|
336
|
+
</button>
|
|
337
|
+
<button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-red-400 hover:bg-muted transition-colors" title={t.fileTree.delete}>
|
|
338
|
+
<Trash2 size={12} />
|
|
339
|
+
</button>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default function FileTree({ nodes, depth = 0, onNavigate }: FileTreeProps) {
|
|
346
|
+
const pathname = usePathname();
|
|
347
|
+
const currentPath = getCurrentFilePath(pathname);
|
|
348
|
+
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
if (!currentPath || depth !== 0) return;
|
|
351
|
+
const timer = setTimeout(() => {
|
|
352
|
+
const el = document.querySelector(`[data-filepath="${CSS.escape(currentPath)}"]`) as HTMLElement | null;
|
|
353
|
+
el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
354
|
+
}, 120);
|
|
355
|
+
return () => clearTimeout(timer);
|
|
356
|
+
}, [currentPath, depth]);
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div className="flex flex-col gap-0.5">
|
|
360
|
+
{nodes.map((node) =>
|
|
361
|
+
node.type === 'directory' ? (
|
|
362
|
+
<DirectoryNode key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
|
|
363
|
+
) : (
|
|
364
|
+
<FileNodeItem key={node.path} node={node} depth={depth} currentPath={currentPath} onNavigate={onNavigate} />
|
|
365
|
+
)
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
|
+
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
9
|
+
import '@/lib/renderers/index'; // registers all renderers
|
|
10
|
+
|
|
11
|
+
interface RecentFile {
|
|
12
|
+
path: string;
|
|
13
|
+
mtime: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Maps a renderer id to a canonical entry file path
|
|
17
|
+
const RENDERER_ENTRY: Record<string, string> = {
|
|
18
|
+
todo: 'TODO.md',
|
|
19
|
+
csv: 'Resources/Products.csv',
|
|
20
|
+
graph: 'README.md',
|
|
21
|
+
timeline: 'CHANGELOG.md',
|
|
22
|
+
backlinks: 'BACKLINKS.md',
|
|
23
|
+
summary: 'DAILY.md',
|
|
24
|
+
'agent-inspector': 'Agent-Audit.md',
|
|
25
|
+
workflow: 'Workflow.md',
|
|
26
|
+
'diff-viewer': 'Agent-Diff.md',
|
|
27
|
+
'config-panel': 'CONFIG.json',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function deriveEntryPath(id: string): string | null {
|
|
31
|
+
return RENDERER_ENTRY[id] ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function triggerSearch() {
|
|
35
|
+
// Dispatch ⌘K to open the Sidebar's SearchModal
|
|
36
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, bubbles: true }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function triggerAsk() {
|
|
40
|
+
// Dispatch ⌘/ to open the Sidebar's AskModal
|
|
41
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: '/', metaKey: true, bubbles: true }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function HomeContent({ recent }: { recent: RecentFile[] }) {
|
|
45
|
+
const { t } = useLocale();
|
|
46
|
+
const [showAll, setShowAll] = useState(false);
|
|
47
|
+
|
|
48
|
+
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
49
|
+
|
|
50
|
+
const renderers = getAllRenderers();
|
|
51
|
+
|
|
52
|
+
const lastFile = recent[0];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-12">
|
|
56
|
+
{/* Hero */}
|
|
57
|
+
<div className="mb-10">
|
|
58
|
+
<div className="flex items-center gap-2 mb-3">
|
|
59
|
+
<div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
|
|
60
|
+
<h1 className="text-2xl font-semibold tracking-tight" style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}>
|
|
61
|
+
MindOS
|
|
62
|
+
</h1>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-sm leading-relaxed mb-5" style={{ color: 'var(--muted-foreground)', paddingLeft: '1rem' }}>
|
|
65
|
+
{t.app.tagline}
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
{/* AI-first command bar */}
|
|
69
|
+
<div
|
|
70
|
+
className="w-full max-w-[620px] flex flex-col sm:flex-row items-stretch sm:items-center gap-2"
|
|
71
|
+
style={{
|
|
72
|
+
marginLeft: '1rem',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{/* Ask AI (primary) */}
|
|
76
|
+
<button
|
|
77
|
+
onClick={triggerAsk}
|
|
78
|
+
title="⌘/"
|
|
79
|
+
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
|
|
80
|
+
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
81
|
+
>
|
|
82
|
+
<Sparkles size={15} style={{ color: 'var(--amber)' }} className="shrink-0" />
|
|
83
|
+
<span className="text-sm flex-1 text-left" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
|
|
84
|
+
{t.home.shortcuts.askAI}
|
|
85
|
+
</span>
|
|
86
|
+
<kbd
|
|
87
|
+
className="hidden sm:inline-flex items-center gap-0.5 px-2 py-0.5 rounded text-[11px] font-mono font-medium"
|
|
88
|
+
style={{ background: 'var(--amber-dim)', color: 'var(--amber)' }}
|
|
89
|
+
>
|
|
90
|
+
⌘/
|
|
91
|
+
</kbd>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
{/* Search files (secondary) */}
|
|
95
|
+
<button
|
|
96
|
+
onClick={triggerSearch}
|
|
97
|
+
title="⌘K"
|
|
98
|
+
className="flex items-center gap-2 px-3 py-3 rounded-xl border text-sm transition-colors shrink-0 hover:bg-muted"
|
|
99
|
+
style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}
|
|
100
|
+
>
|
|
101
|
+
<Search size={14} />
|
|
102
|
+
<span className="hidden sm:inline">{t.home.shortcuts.searchFiles}</span>
|
|
103
|
+
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono" style={{ background: 'var(--muted)' }}>
|
|
104
|
+
⌘K
|
|
105
|
+
</kbd>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Quick Actions */}
|
|
110
|
+
<div className="flex flex-wrap gap-2.5 mt-4" style={{ paddingLeft: '1rem' }}>
|
|
111
|
+
{lastFile && (
|
|
112
|
+
<Link
|
|
113
|
+
href={`/view/${encodePath(lastFile.path)}`}
|
|
114
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
|
|
115
|
+
style={{
|
|
116
|
+
background: 'var(--amber-dim)',
|
|
117
|
+
color: 'var(--amber)',
|
|
118
|
+
fontFamily: "'IBM Plex Sans', sans-serif",
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
<ArrowRight size={14} />
|
|
122
|
+
<span>{t.home.continueEditing}</span>
|
|
123
|
+
<span className="text-xs opacity-60 truncate max-w-[160px]" suppressHydrationWarning>
|
|
124
|
+
{lastFile.path.split('/').pop()}
|
|
125
|
+
</span>
|
|
126
|
+
</Link>
|
|
127
|
+
)}
|
|
128
|
+
<Link
|
|
129
|
+
href="/view/Untitled.md"
|
|
130
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors"
|
|
131
|
+
style={{
|
|
132
|
+
background: 'var(--muted)',
|
|
133
|
+
color: 'var(--muted-foreground)',
|
|
134
|
+
fontFamily: "'IBM Plex Sans', sans-serif",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<FilePlus size={14} />
|
|
138
|
+
<span>{t.home.newNote}</span>
|
|
139
|
+
</Link>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Plugins — compact 3-column grid */}
|
|
145
|
+
{renderers.length > 0 && (
|
|
146
|
+
<section className="mb-12">
|
|
147
|
+
<div className="flex items-center gap-2 mb-4">
|
|
148
|
+
<Puzzle size={13} style={{ color: 'var(--amber)' }} />
|
|
149
|
+
<h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
150
|
+
{t.home.plugins}
|
|
151
|
+
</h2>
|
|
152
|
+
<span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>
|
|
153
|
+
{renderers.length}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
|
158
|
+
{renderers.map((r) => {
|
|
159
|
+
const entryPath = deriveEntryPath(r.id);
|
|
160
|
+
return (
|
|
161
|
+
<Link
|
|
162
|
+
key={r.id}
|
|
163
|
+
href={entryPath ? `/view/${encodePath(entryPath)}` : '#'}
|
|
164
|
+
className="group flex items-center gap-2.5 px-3 py-2.5 rounded-lg border transition-all hover:border-amber-500/30 hover:bg-muted/50"
|
|
165
|
+
style={{ borderColor: 'var(--border)' }}
|
|
166
|
+
>
|
|
167
|
+
<span className="text-base leading-none shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
168
|
+
<div className="flex-1 min-w-0">
|
|
169
|
+
<span className="text-xs font-semibold truncate block" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans', sans-serif" }}>
|
|
170
|
+
{r.name}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</Link>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{/* Recently modified — timeline feed */}
|
|
181
|
+
{recent.length > 0 && (() => {
|
|
182
|
+
const INITIAL_COUNT = 5;
|
|
183
|
+
const visibleRecent = showAll ? recent : recent.slice(0, INITIAL_COUNT);
|
|
184
|
+
const hasMore = recent.length > INITIAL_COUNT;
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<section className="mb-12">
|
|
188
|
+
<div className="flex items-center gap-2 mb-5">
|
|
189
|
+
<Clock size={13} style={{ color: 'var(--amber)' }} />
|
|
190
|
+
<h2 className="text-xs font-semibold uppercase tracking-[0.08em]" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
191
|
+
{t.home.recentlyModified}
|
|
192
|
+
</h2>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="relative pl-4">
|
|
196
|
+
{/* Timeline line */}
|
|
197
|
+
<div className="absolute left-0 top-1 bottom-1 w-px" style={{ background: 'var(--border)' }} />
|
|
198
|
+
|
|
199
|
+
<div className="flex flex-col gap-0.5">
|
|
200
|
+
{visibleRecent.map(({ path: filePath, mtime }, idx) => {
|
|
201
|
+
const isCSV = filePath.endsWith('.csv');
|
|
202
|
+
const name = filePath.split('/').pop() || filePath;
|
|
203
|
+
const dir = filePath.split('/').slice(0, -1).join('/');
|
|
204
|
+
return (
|
|
205
|
+
<div key={filePath} className="relative group">
|
|
206
|
+
{/* Timeline dot */}
|
|
207
|
+
<div
|
|
208
|
+
className="absolute -left-4 top-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full transition-all duration-150 group-hover:scale-150"
|
|
209
|
+
style={{
|
|
210
|
+
background: idx === 0 ? 'var(--amber)' : 'var(--border)',
|
|
211
|
+
outline: idx === 0 ? '2px solid var(--amber-dim)' : 'none',
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
<Link
|
|
215
|
+
href={`/view/${encodePath(filePath)}`}
|
|
216
|
+
className="flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-100 group-hover:translate-x-0.5 hover:bg-muted"
|
|
217
|
+
>
|
|
218
|
+
{isCSV
|
|
219
|
+
? <Table size={13} className="shrink-0" style={{ color: '#7aad80' }} />
|
|
220
|
+
: <FileText size={13} className="shrink-0" style={{ color: 'var(--muted-foreground)' }} />
|
|
221
|
+
}
|
|
222
|
+
<div className="flex-1 min-w-0">
|
|
223
|
+
<span className="text-sm font-medium truncate block" style={{ color: 'var(--foreground)' }} suppressHydrationWarning>{name}</span>
|
|
224
|
+
{dir && <span className="text-xs truncate block" style={{ color: 'var(--muted-foreground)', opacity: 0.6 }} suppressHydrationWarning>{dir}</span>}
|
|
225
|
+
</div>
|
|
226
|
+
<span className="text-xs shrink-0 tabular-nums" style={{ color: 'var(--muted-foreground)', opacity: 0.5, fontFamily: "'IBM Plex Mono', monospace" }} suppressHydrationWarning>
|
|
227
|
+
{formatTime(mtime)}
|
|
228
|
+
</span>
|
|
229
|
+
</Link>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Show more / less */}
|
|
236
|
+
{hasMore && (
|
|
237
|
+
<button
|
|
238
|
+
onClick={() => setShowAll(v => !v)}
|
|
239
|
+
className="flex items-center gap-1.5 mt-2 ml-3 text-xs font-medium transition-colors hover:opacity-80 cursor-pointer"
|
|
240
|
+
style={{ color: 'var(--amber)', fontFamily: "'IBM Plex Mono', monospace" }}
|
|
241
|
+
>
|
|
242
|
+
<ChevronDown
|
|
243
|
+
size={12}
|
|
244
|
+
className="transition-transform duration-200"
|
|
245
|
+
style={{ transform: showAll ? 'rotate(180deg)' : undefined }}
|
|
246
|
+
/>
|
|
247
|
+
<span>{showAll ? t.home.showLess : t.home.showMore}</span>
|
|
248
|
+
</button>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
</section>
|
|
252
|
+
);
|
|
253
|
+
})()}
|
|
254
|
+
|
|
255
|
+
{/* Footer */}
|
|
256
|
+
<div className="mt-16 flex items-center gap-1.5 text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.4, fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
257
|
+
<Sparkles size={10} style={{ color: 'var(--amber)' }} />
|
|
258
|
+
<span>{t.app.footer}</span>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
interface JsonViewProps {
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function JsonView({ content }: JsonViewProps) {
|
|
10
|
+
const pretty = useMemo(() => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.stringify(JSON.parse(content), null, 2);
|
|
13
|
+
} catch {
|
|
14
|
+
return content;
|
|
15
|
+
}
|
|
16
|
+
}, [content]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<pre
|
|
20
|
+
className="rounded-xl border border-border bg-card px-4 py-3 overflow-x-auto text-sm leading-relaxed"
|
|
21
|
+
style={{ fontFamily: "'IBM Plex Mono', monospace" }}
|
|
22
|
+
suppressHydrationWarning
|
|
23
|
+
>
|
|
24
|
+
<code>{pretty}</code>
|
|
25
|
+
</pre>
|
|
26
|
+
);
|
|
27
|
+
}
|