@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,93 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
|
+
import { getFileContent, saveFileContent, isDirectory, getDirEntries, createFile, getFileTree } from '@/lib/fs';
|
|
3
|
+
import type { FileNode } from '@/lib/types';
|
|
4
|
+
import ViewPageClient from './ViewPageClient';
|
|
5
|
+
import DirView from '@/components/DirView';
|
|
6
|
+
import Papa from 'papaparse';
|
|
7
|
+
|
|
8
|
+
interface PageProps {
|
|
9
|
+
params: Promise<{ path: string[] }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function collectDirectories(nodes: FileNode[]): string[] {
|
|
13
|
+
const dirs: string[] = [];
|
|
14
|
+
for (const n of nodes) {
|
|
15
|
+
if (n.type === 'directory') {
|
|
16
|
+
dirs.push(n.path);
|
|
17
|
+
if (n.children) dirs.push(...collectDirectories(n.children));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return dirs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function ViewPage({ params }: PageProps) {
|
|
24
|
+
const { path: segments } = await params;
|
|
25
|
+
const filePath = segments.map(decodeURIComponent).join('/');
|
|
26
|
+
|
|
27
|
+
// Directory: show folder listing
|
|
28
|
+
if (isDirectory(filePath)) {
|
|
29
|
+
const entries = getDirEntries(filePath);
|
|
30
|
+
return <DirView dirPath={filePath} entries={entries} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const extension = filePath.split('.').pop()?.toLowerCase() || '';
|
|
34
|
+
|
|
35
|
+
async function saveAction(newContent: string) {
|
|
36
|
+
'use server';
|
|
37
|
+
saveFileContent(filePath, newContent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function appendRowAction(newRow: string[]): Promise<{ newContent: string }> {
|
|
41
|
+
'use server';
|
|
42
|
+
const current = getFileContent(filePath);
|
|
43
|
+
const parsed = Papa.parse<string[]>(current, { skipEmptyLines: true });
|
|
44
|
+
const rows = parsed.data as string[][];
|
|
45
|
+
rows.push(newRow);
|
|
46
|
+
const newContent = Papa.unparse(rows);
|
|
47
|
+
saveFileContent(filePath, newContent);
|
|
48
|
+
return { newContent };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function createDraftAction(targetPath: string, draftContent: string) {
|
|
52
|
+
'use server';
|
|
53
|
+
createFile(targetPath, draftContent);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let content = '';
|
|
57
|
+
let exists = true;
|
|
58
|
+
try {
|
|
59
|
+
content = getFileContent(filePath);
|
|
60
|
+
} catch {
|
|
61
|
+
exists = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!exists) {
|
|
65
|
+
// Special draft entry used by homepage "New Notes"
|
|
66
|
+
if (filePath === 'Untitled.md') {
|
|
67
|
+
const draftDirectories = collectDirectories(getFileTree());
|
|
68
|
+
return (
|
|
69
|
+
<ViewPageClient
|
|
70
|
+
filePath={filePath}
|
|
71
|
+
content=""
|
|
72
|
+
extension="md"
|
|
73
|
+
saveAction={saveAction}
|
|
74
|
+
initialEditing
|
|
75
|
+
isDraft
|
|
76
|
+
draftDirectories={draftDirectories}
|
|
77
|
+
createDraftAction={createDraftAction}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
notFound();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ViewPageClient
|
|
86
|
+
filePath={filePath}
|
|
87
|
+
content={content}
|
|
88
|
+
extension={extension}
|
|
89
|
+
saveAction={saveAction}
|
|
90
|
+
appendRowAction={extension === 'csv' ? appendRowAction : undefined}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { Sparkles } from 'lucide-react';
|
|
6
|
+
import AskModal from './AskModal';
|
|
7
|
+
|
|
8
|
+
export default function AskFab() {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
const currentFile = pathname.startsWith('/view/')
|
|
12
|
+
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
13
|
+
: undefined;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<button
|
|
18
|
+
onClick={() => setOpen(true)}
|
|
19
|
+
className="
|
|
20
|
+
group
|
|
21
|
+
fixed z-40
|
|
22
|
+
bottom-5 right-5
|
|
23
|
+
md:bottom-5 md:right-5
|
|
24
|
+
flex items-center justify-center
|
|
25
|
+
gap-0 hover:gap-2
|
|
26
|
+
p-3 md:p-[11px] rounded-xl
|
|
27
|
+
text-white font-medium text-[13px]
|
|
28
|
+
shadow-md shadow-amber-900/15
|
|
29
|
+
transition-all duration-200 ease-out
|
|
30
|
+
hover:shadow-lg hover:shadow-amber-800/25
|
|
31
|
+
active:scale-95
|
|
32
|
+
cursor-pointer
|
|
33
|
+
overflow-hidden
|
|
34
|
+
"
|
|
35
|
+
style={{
|
|
36
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
37
|
+
background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
|
|
38
|
+
marginBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
39
|
+
}}
|
|
40
|
+
title="MindOS Agent (⌘/)"
|
|
41
|
+
aria-label="MindOS Agent"
|
|
42
|
+
>
|
|
43
|
+
<Sparkles size={16} className="relative z-10 shrink-0" />
|
|
44
|
+
|
|
45
|
+
<span className="
|
|
46
|
+
relative z-10
|
|
47
|
+
max-w-0 group-hover:max-w-[120px]
|
|
48
|
+
opacity-0 group-hover:opacity-100
|
|
49
|
+
transition-all duration-200 ease-out
|
|
50
|
+
whitespace-nowrap overflow-hidden
|
|
51
|
+
">
|
|
52
|
+
MindOS Agent
|
|
53
|
+
</span>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
<AskModal open={open} onClose={() => setOpen(false)} currentFile={currentFile} />
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
+
import { X, Sparkles, Send, AtSign, Paperclip, StopCircle, RotateCcw, History } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import type { Message } from '@/lib/types';
|
|
7
|
+
import { useAskSession } from '@/hooks/useAskSession';
|
|
8
|
+
import { useFileUpload } from '@/hooks/useFileUpload';
|
|
9
|
+
import { useMention } from '@/hooks/useMention';
|
|
10
|
+
import MessageList from '@/components/ask/MessageList';
|
|
11
|
+
import MentionPopover from '@/components/ask/MentionPopover';
|
|
12
|
+
import SessionHistory from '@/components/ask/SessionHistory';
|
|
13
|
+
import FileChip from '@/components/ask/FileChip';
|
|
14
|
+
|
|
15
|
+
interface AskModalProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
currentFile?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function AskModal({ open, onClose, currentFile }: AskModalProps) {
|
|
22
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
24
|
+
const { t } = useLocale();
|
|
25
|
+
|
|
26
|
+
const [input, setInput] = useState('');
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
|
|
29
|
+
const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
|
|
30
|
+
const [maxSteps, setMaxSteps] = useState(20);
|
|
31
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
32
|
+
|
|
33
|
+
const session = useAskSession(currentFile);
|
|
34
|
+
const upload = useFileUpload();
|
|
35
|
+
const mention = useMention();
|
|
36
|
+
|
|
37
|
+
// Focus and reset on open
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
if (open) {
|
|
41
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
42
|
+
void (async () => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
await session.initSessions();
|
|
45
|
+
})();
|
|
46
|
+
setInput('');
|
|
47
|
+
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
48
|
+
upload.clearAttachments();
|
|
49
|
+
mention.resetMention();
|
|
50
|
+
setShowHistory(false);
|
|
51
|
+
} else {
|
|
52
|
+
abortRef.current?.abort();
|
|
53
|
+
}
|
|
54
|
+
return () => { cancelled = true; };
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, [open, currentFile]);
|
|
57
|
+
|
|
58
|
+
// Persist session on message changes
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open || !session.activeSessionId) return;
|
|
61
|
+
session.persistSession(session.messages, session.activeSessionId);
|
|
62
|
+
return () => session.clearPersistTimer();
|
|
63
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
|
+
}, [open, session.messages, session.activeSessionId]);
|
|
65
|
+
|
|
66
|
+
// Esc to close (or dismiss mention)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open) return;
|
|
69
|
+
const handler = (e: KeyboardEvent) => {
|
|
70
|
+
if (e.key === 'Escape') {
|
|
71
|
+
if (mention.mentionQuery !== null) { mention.resetMention(); return; }
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
window.addEventListener('keydown', handler);
|
|
76
|
+
return () => window.removeEventListener('keydown', handler);
|
|
77
|
+
}, [open, onClose, mention]);
|
|
78
|
+
|
|
79
|
+
const handleInputChange = useCallback((val: string) => {
|
|
80
|
+
setInput(val);
|
|
81
|
+
mention.updateMentionFromInput(val);
|
|
82
|
+
}, [mention]);
|
|
83
|
+
|
|
84
|
+
const selectMention = useCallback((filePath: string) => {
|
|
85
|
+
const atIdx = input.lastIndexOf('@');
|
|
86
|
+
setInput(input.slice(0, atIdx));
|
|
87
|
+
mention.resetMention();
|
|
88
|
+
if (!attachedFiles.includes(filePath)) {
|
|
89
|
+
setAttachedFiles(prev => [...prev, filePath]);
|
|
90
|
+
}
|
|
91
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
92
|
+
}, [input, attachedFiles, mention]);
|
|
93
|
+
|
|
94
|
+
const handleInputKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
95
|
+
if (mention.mentionQuery === null) return;
|
|
96
|
+
if (e.key === 'ArrowDown') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
mention.navigateMention('down');
|
|
99
|
+
} else if (e.key === 'ArrowUp') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
mention.navigateMention('up');
|
|
102
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
103
|
+
if (mention.mentionResults.length > 0) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
selectMention(mention.mentionResults[mention.mentionIndex]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, [mention, selectMention]);
|
|
109
|
+
|
|
110
|
+
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
111
|
+
|
|
112
|
+
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
if (mention.mentionQuery !== null) return;
|
|
115
|
+
const text = input.trim();
|
|
116
|
+
if (!text || isLoading) return;
|
|
117
|
+
|
|
118
|
+
const userMsg: Message = { role: 'user', content: text };
|
|
119
|
+
const requestMessages = [...session.messages, userMsg];
|
|
120
|
+
session.setMessages([...requestMessages, { role: 'assistant', content: '' }]);
|
|
121
|
+
setInput('');
|
|
122
|
+
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
123
|
+
setIsLoading(true);
|
|
124
|
+
setLoadingPhase('connecting');
|
|
125
|
+
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
abortRef.current = controller;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch('/api/ask', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
messages: requestMessages,
|
|
135
|
+
currentFile,
|
|
136
|
+
attachedFiles,
|
|
137
|
+
uploadedFiles: upload.localAttachments,
|
|
138
|
+
maxSteps,
|
|
139
|
+
}),
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
let errorMsg = `Request failed (${res.status})`;
|
|
145
|
+
try {
|
|
146
|
+
const errBody = await res.json();
|
|
147
|
+
if (errBody.error) errorMsg = errBody.error;
|
|
148
|
+
} catch {}
|
|
149
|
+
throw new Error(errorMsg);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!res.body) throw new Error('No response body');
|
|
153
|
+
|
|
154
|
+
const reader = res.body.getReader();
|
|
155
|
+
const decoder = new TextDecoder();
|
|
156
|
+
let assistantContent = '';
|
|
157
|
+
setLoadingPhase('thinking');
|
|
158
|
+
|
|
159
|
+
while (true) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done) break;
|
|
162
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
163
|
+
if (chunk) setLoadingPhase('streaming');
|
|
164
|
+
assistantContent += chunk;
|
|
165
|
+
session.setMessages(prev => {
|
|
166
|
+
const updated = [...prev];
|
|
167
|
+
updated[updated.length - 1] = { role: 'assistant', content: assistantContent };
|
|
168
|
+
return updated;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!assistantContent.trim()) {
|
|
173
|
+
session.setMessages(prev => {
|
|
174
|
+
const updated = [...prev];
|
|
175
|
+
updated[updated.length - 1] = { role: 'assistant', content: `__error__${t.ask.errorNoResponse}` };
|
|
176
|
+
return updated;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if ((err as Error).name === 'AbortError') {
|
|
181
|
+
session.setMessages(prev => {
|
|
182
|
+
const updated = [...prev];
|
|
183
|
+
const lastIdx = updated.length - 1;
|
|
184
|
+
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant' && !updated[lastIdx].content.trim()) {
|
|
185
|
+
updated[lastIdx] = { role: 'assistant', content: `__error__${t.ask.stopped}` };
|
|
186
|
+
}
|
|
187
|
+
return updated;
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
const errMsg = err instanceof Error ? err.message : 'Something went wrong';
|
|
191
|
+
session.setMessages(prev => {
|
|
192
|
+
const updated = [...prev];
|
|
193
|
+
const lastIdx = updated.length - 1;
|
|
194
|
+
if (lastIdx >= 0 && updated[lastIdx].role === 'assistant' && !updated[lastIdx].content.trim()) {
|
|
195
|
+
updated[lastIdx] = { role: 'assistant', content: `__error__${errMsg}` };
|
|
196
|
+
return updated;
|
|
197
|
+
}
|
|
198
|
+
return [...updated, { role: 'assistant', content: `__error__${errMsg}` }];
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
setIsLoading(false);
|
|
203
|
+
abortRef.current = null;
|
|
204
|
+
}
|
|
205
|
+
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, maxSteps, t.ask.errorNoResponse, t.ask.stopped]);
|
|
206
|
+
|
|
207
|
+
const handleResetSession = useCallback(() => {
|
|
208
|
+
if (isLoading) return;
|
|
209
|
+
session.resetSession();
|
|
210
|
+
setInput('');
|
|
211
|
+
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
212
|
+
upload.clearAttachments();
|
|
213
|
+
mention.resetMention();
|
|
214
|
+
setShowHistory(false);
|
|
215
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
216
|
+
}, [isLoading, currentFile, session, upload, mention]);
|
|
217
|
+
|
|
218
|
+
const handleLoadSession = useCallback((id: string) => {
|
|
219
|
+
session.loadSession(id);
|
|
220
|
+
setShowHistory(false);
|
|
221
|
+
setInput('');
|
|
222
|
+
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
223
|
+
upload.clearAttachments();
|
|
224
|
+
mention.resetMention();
|
|
225
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
226
|
+
}, [session, currentFile, upload, mention]);
|
|
227
|
+
|
|
228
|
+
if (!open) return null;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div
|
|
232
|
+
className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[10vh] modal-backdrop"
|
|
233
|
+
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
234
|
+
>
|
|
235
|
+
<div
|
|
236
|
+
role="dialog"
|
|
237
|
+
aria-modal="true"
|
|
238
|
+
aria-label={t.ask.title}
|
|
239
|
+
className="w-full md:max-w-2xl md:mx-4 bg-card border-t md:border border-border rounded-t-2xl md:rounded-xl shadow-2xl flex flex-col h-[92vh] md:h-auto md:max-h-[75vh]"
|
|
240
|
+
>
|
|
241
|
+
{/* Header */}
|
|
242
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
243
|
+
{/* Mobile drag indicator */}
|
|
244
|
+
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
|
|
245
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
246
|
+
<Sparkles size={15} style={{ color: 'var(--amber)' }} />
|
|
247
|
+
<span style={{ fontFamily: "'IBM Plex Mono', monospace" }}>{t.ask.title}</span>
|
|
248
|
+
{currentFile && (
|
|
249
|
+
<span className="text-xs text-muted-foreground font-normal truncate max-w-[200px]">
|
|
250
|
+
— {currentFile.split('/').pop()}
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<div className="flex items-center gap-1">
|
|
255
|
+
<button type="button" onClick={() => setShowHistory(v => !v)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title="Session history">
|
|
256
|
+
<History size={14} />
|
|
257
|
+
</button>
|
|
258
|
+
<button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
|
|
259
|
+
<RotateCcw size={14} />
|
|
260
|
+
</button>
|
|
261
|
+
<button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
262
|
+
<X size={15} />
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{showHistory && (
|
|
268
|
+
<SessionHistory
|
|
269
|
+
sessions={session.sessions}
|
|
270
|
+
activeSessionId={session.activeSessionId}
|
|
271
|
+
onLoad={handleLoadSession}
|
|
272
|
+
onDelete={session.deleteSession}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Messages */}
|
|
277
|
+
<MessageList
|
|
278
|
+
messages={session.messages}
|
|
279
|
+
isLoading={isLoading}
|
|
280
|
+
loadingPhase={loadingPhase}
|
|
281
|
+
emptyPrompt={t.ask.emptyPrompt}
|
|
282
|
+
suggestions={t.ask.suggestions}
|
|
283
|
+
onSuggestionClick={setInput}
|
|
284
|
+
labels={{ connecting: t.ask.connecting, thinking: t.ask.thinking, generating: t.ask.generating }}
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
{/* Input area */}
|
|
288
|
+
<div className="border-t border-border shrink-0">
|
|
289
|
+
{/* Attached file chips */}
|
|
290
|
+
{attachedFiles.length > 0 && (
|
|
291
|
+
<div className="px-4 pt-2.5 pb-1">
|
|
292
|
+
<div className="text-[11px] text-muted-foreground/70 mb-1.5">Knowledge Base Context</div>
|
|
293
|
+
<div className="flex flex-wrap gap-1.5">
|
|
294
|
+
{attachedFiles.map(f => (
|
|
295
|
+
<FileChip key={f} path={f} onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{upload.localAttachments.length > 0 && (
|
|
302
|
+
<div className="px-4 pb-1">
|
|
303
|
+
<div className="text-[11px] text-muted-foreground/70 mb-1.5">Uploaded Files</div>
|
|
304
|
+
<div className="flex flex-wrap gap-1.5">
|
|
305
|
+
{upload.localAttachments.map((f, idx) => (
|
|
306
|
+
<FileChip key={`${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
{upload.uploadError && (
|
|
313
|
+
<div className="px-4 pb-1 text-xs text-red-400">{upload.uploadError}</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* @-mention dropdown */}
|
|
317
|
+
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
318
|
+
<MentionPopover
|
|
319
|
+
results={mention.mentionResults}
|
|
320
|
+
selectedIndex={mention.mentionIndex}
|
|
321
|
+
onSelect={selectMention}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
<form onSubmit={handleSubmit} className="flex items-center gap-2 px-3 py-3">
|
|
326
|
+
<button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title="Attach local file">
|
|
327
|
+
<Paperclip size={15} />
|
|
328
|
+
</button>
|
|
329
|
+
|
|
330
|
+
<input
|
|
331
|
+
ref={upload.uploadInputRef}
|
|
332
|
+
type="file"
|
|
333
|
+
className="hidden"
|
|
334
|
+
multiple
|
|
335
|
+
accept=".txt,.md,.markdown,.csv,.json,.yaml,.yml,.xml,.html,.htm,.pdf,text/plain,text/markdown,text/csv,application/json,application/pdf"
|
|
336
|
+
onChange={async (e) => {
|
|
337
|
+
const inputEl = e.currentTarget;
|
|
338
|
+
await upload.pickFiles(inputEl.files);
|
|
339
|
+
inputEl.value = '';
|
|
340
|
+
}}
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
onClick={() => {
|
|
346
|
+
const el = inputRef.current;
|
|
347
|
+
if (!el) return;
|
|
348
|
+
const pos = el.selectionStart ?? input.length;
|
|
349
|
+
const newVal = input.slice(0, pos) + '@' + input.slice(pos);
|
|
350
|
+
handleInputChange(newVal);
|
|
351
|
+
setTimeout(() => { el.focus(); el.setSelectionRange(pos + 1, pos + 1); }, 0);
|
|
352
|
+
}}
|
|
353
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
354
|
+
title="@ mention file"
|
|
355
|
+
>
|
|
356
|
+
<AtSign size={15} />
|
|
357
|
+
</button>
|
|
358
|
+
|
|
359
|
+
<input
|
|
360
|
+
ref={inputRef}
|
|
361
|
+
value={input}
|
|
362
|
+
onChange={e => handleInputChange(e.target.value)}
|
|
363
|
+
onKeyDown={handleInputKeyDown}
|
|
364
|
+
placeholder={t.ask.placeholder}
|
|
365
|
+
disabled={isLoading}
|
|
366
|
+
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none disabled:opacity-50"
|
|
367
|
+
/>
|
|
368
|
+
|
|
369
|
+
{isLoading ? (
|
|
370
|
+
<button type="button" onClick={handleStop} className="p-1.5 rounded-md transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted" title={t.ask.stopTitle}>
|
|
371
|
+
<StopCircle size={15} />
|
|
372
|
+
</button>
|
|
373
|
+
) : (
|
|
374
|
+
<button type="submit" disabled={!input.trim()} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0" style={{ background: 'var(--amber)', color: '#131210' }}>
|
|
375
|
+
<Send size={14} />
|
|
376
|
+
</button>
|
|
377
|
+
)}
|
|
378
|
+
</form>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Footer hint — desktop only */}
|
|
382
|
+
<div className="hidden md:flex px-4 pb-2 items-center gap-3 text-xs text-muted-foreground/50 shrink-0">
|
|
383
|
+
<span><kbd className="font-mono">↵</kbd> {t.ask.send}</span>
|
|
384
|
+
<span><kbd className="font-mono">@</kbd> {t.ask.attachFile}</span>
|
|
385
|
+
<span className="inline-flex items-center gap-1">
|
|
386
|
+
<span>Agent steps</span>
|
|
387
|
+
<select value={maxSteps} onChange={(e) => setMaxSteps(Number(e.target.value))} disabled={isLoading} className="bg-transparent border border-border rounded px-1.5 py-0.5 text-[11px] text-foreground">
|
|
388
|
+
<option value={10}>10</option>
|
|
389
|
+
<option value={20}>20</option>
|
|
390
|
+
<option value={30}>30</option>
|
|
391
|
+
</select>
|
|
392
|
+
</span>
|
|
393
|
+
<span><kbd className="font-mono">ESC</kbd> {t.search.close}</span>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Link as LinkIcon, FileText } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { apiFetch } from '@/lib/api';
|
|
8
|
+
import type { BacklinkItem } from '@/lib/types';
|
|
9
|
+
|
|
10
|
+
export default function Backlinks({ filePath }: { filePath: string }) {
|
|
11
|
+
const [backlinks, setBacklinks] = useState<BacklinkItem[]>([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const { t } = useLocale();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
apiFetch<BacklinkItem[]>(`/api/backlinks?path=${encodeURIComponent(filePath)}`)
|
|
18
|
+
.then(data => {
|
|
19
|
+
setBacklinks(Array.isArray(data) ? data : []);
|
|
20
|
+
setLoading(false);
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
setBacklinks([]);
|
|
24
|
+
setLoading(false);
|
|
25
|
+
});
|
|
26
|
+
}, [filePath]);
|
|
27
|
+
|
|
28
|
+
if (!loading && backlinks.length === 0) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="mt-12 pt-8 border-t border-border">
|
|
32
|
+
<div className="flex items-center gap-2 mb-6 text-muted-foreground">
|
|
33
|
+
<LinkIcon size={16} className="text-amber-500/70" />
|
|
34
|
+
<h3 className="text-sm font-semibold tracking-wider uppercase" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
35
|
+
{t.common?.relatedFiles || 'Related Files'}
|
|
36
|
+
</h3>
|
|
37
|
+
<span className="text-xs bg-muted px-1.5 py-0.5 rounded-full font-mono">
|
|
38
|
+
{backlinks.length}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div className="grid gap-3">
|
|
43
|
+
{loading ? (
|
|
44
|
+
<div className="space-y-3">
|
|
45
|
+
{[1, 2].map(i => (
|
|
46
|
+
<div key={i} className="h-20 bg-muted/30 rounded-lg animate-pulse" />
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
backlinks.map((link) => (
|
|
51
|
+
<Link
|
|
52
|
+
key={link.filePath}
|
|
53
|
+
href={`/view/${link.filePath.split('/').map(encodeURIComponent).join('/')}`}
|
|
54
|
+
className="group block p-4 rounded-xl border border-border/50 bg-card/30 hover:bg-muted/30 hover:border-amber-500/30 transition-all duration-200"
|
|
55
|
+
>
|
|
56
|
+
<div className="flex items-start gap-3">
|
|
57
|
+
<div className="mt-1 p-1.5 rounded-md bg-muted group-hover:bg-amber-500/10 transition-colors">
|
|
58
|
+
<FileText size={14} className="text-muted-foreground group-hover:text-amber-500" />
|
|
59
|
+
</div>
|
|
60
|
+
<div className="min-w-0 flex-1">
|
|
61
|
+
<div className="font-medium text-sm text-foreground group-hover:text-amber-500 transition-colors truncate mb-1">
|
|
62
|
+
{link.filePath}
|
|
63
|
+
</div>
|
|
64
|
+
<div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed italic opacity-80 group-hover:opacity-100 transition-opacity">
|
|
65
|
+
{link.snippets[0] || ''}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</Link>
|
|
70
|
+
))
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { ChevronRight, Home } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export default function Breadcrumb({ filePath }: { filePath: string }) {
|
|
7
|
+
const parts = filePath.split('/');
|
|
8
|
+
return (
|
|
9
|
+
<nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
|
|
10
|
+
<Link href="/" className="hover:text-foreground transition-colors">
|
|
11
|
+
<Home size={14} />
|
|
12
|
+
</Link>
|
|
13
|
+
{parts.map((part, i) => {
|
|
14
|
+
const isLast = i === parts.length - 1;
|
|
15
|
+
const href = '/view/' + parts.slice(0, i + 1).map(encodeURIComponent).join('/');
|
|
16
|
+
return (
|
|
17
|
+
<span key={i} className="flex items-center gap-1">
|
|
18
|
+
<ChevronRight size={12} className="text-muted-foreground/50" />
|
|
19
|
+
{isLast ? (
|
|
20
|
+
<span className="text-foreground font-medium" suppressHydrationWarning>{part}</span>
|
|
21
|
+
) : (
|
|
22
|
+
<Link href={href} className="hover:text-foreground transition-colors truncate max-w-[200px]" suppressHydrationWarning>
|
|
23
|
+
{part}
|
|
24
|
+
</Link>
|
|
25
|
+
)}
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
28
|
+
})}
|
|
29
|
+
</nav>
|
|
30
|
+
);
|
|
31
|
+
}
|