@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,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
|
|
7
|
+
import FileTree from './FileTree';
|
|
8
|
+
import SearchModal from './SearchModal';
|
|
9
|
+
import AskModal from './AskModal';
|
|
10
|
+
import SettingsModal from './SettingsModal';
|
|
11
|
+
import { FileNode } from '@/lib/types';
|
|
12
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
13
|
+
|
|
14
|
+
interface SidebarProps {
|
|
15
|
+
fileTree: FileNode[];
|
|
16
|
+
collapsed?: boolean;
|
|
17
|
+
onCollapse?: () => void;
|
|
18
|
+
onExpand?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Logo = ({ id }: { id: string }) => (
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40" fill="none" className="w-8 h-4 text-[var(--amber)]" aria-hidden="true">
|
|
23
|
+
<defs>
|
|
24
|
+
<linearGradient id={`grad-human-${id}`} x1="35" y1="20" x2="5" y2="20" gradientUnits="userSpaceOnUse">
|
|
25
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="0.8"/>
|
|
26
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="0.3"/>
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id={`grad-agent-${id}`} x1="35" y1="20" x2="75" y2="20" gradientUnits="userSpaceOnUse">
|
|
29
|
+
<stop offset="0%" stopColor="currentColor" stopOpacity="0.8"/>
|
|
30
|
+
<stop offset="100%" stopColor="currentColor" stopOpacity="1"/>
|
|
31
|
+
</linearGradient>
|
|
32
|
+
</defs>
|
|
33
|
+
<path d="M35,20 C25,35 8,35 8,20 C8,5 25,5 35,20" stroke={`url(#grad-human-${id})`} strokeWidth="3" strokeDasharray="2 4" strokeLinecap="round"/>
|
|
34
|
+
<path d="M35,20 C45,2 75,2 75,20 C75,38 45,38 35,20" stroke={`url(#grad-agent-${id})`} strokeWidth="4.5" strokeLinecap="round"/>
|
|
35
|
+
<path d="M35,17.5 Q35,20 37.5,20 Q35,20 35,22.5 Q35,20 32.5,20 Q35,20 35,17.5 Z" fill="#FEF3C7"/>
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExpand }: SidebarProps) {
|
|
40
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
41
|
+
const [askOpen, setAskOpen] = useState(false);
|
|
42
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
43
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
44
|
+
const { t } = useLocale();
|
|
45
|
+
|
|
46
|
+
const pathname = usePathname();
|
|
47
|
+
const currentFile = pathname.startsWith('/view/')
|
|
48
|
+
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const handler = (e: KeyboardEvent) => {
|
|
53
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(v => !v); }
|
|
54
|
+
if ((e.metaKey || e.ctrlKey) && e.key === '/') { e.preventDefault(); setAskOpen(v => !v); }
|
|
55
|
+
if ((e.metaKey || e.ctrlKey) && e.key === ',') { e.preventDefault(); setSettingsOpen(v => !v); }
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener('keydown', handler);
|
|
58
|
+
return () => window.removeEventListener('keydown', handler);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => { setMobileOpen(false); }, [pathname]);
|
|
62
|
+
|
|
63
|
+
const sidebarContent = (
|
|
64
|
+
<div className="flex flex-col h-full">
|
|
65
|
+
<div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
|
|
66
|
+
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
67
|
+
<Logo id="desktop" />
|
|
68
|
+
<span className="font-semibold text-foreground text-sm tracking-wide" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>MindOS</span>
|
|
69
|
+
</Link>
|
|
70
|
+
{/* Mobile close */}
|
|
71
|
+
<button onClick={() => setMobileOpen(false)} className="md:hidden p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
72
|
+
<X size={16} />
|
|
73
|
+
</button>
|
|
74
|
+
{/* Desktop action buttons — trimmed to 4 */}
|
|
75
|
+
<div className="hidden md:flex items-center gap-1">
|
|
76
|
+
<button onClick={() => setSearchOpen(true)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.searchTitle} aria-label={t.sidebar.searchTitle}>
|
|
77
|
+
<Search size={15} />
|
|
78
|
+
</button>
|
|
79
|
+
<button onClick={() => setSettingsOpen(true)} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.settingsTitle} aria-label={t.sidebar.settingsTitle}>
|
|
80
|
+
<Settings size={15} />
|
|
81
|
+
</button>
|
|
82
|
+
<button onClick={onCollapse} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.sidebar.collapseTitle} aria-label={t.sidebar.collapseTitle}>
|
|
83
|
+
<PanelLeftClose size={15} />
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
|
|
89
|
+
<FileTree nodes={fileTree} onNavigate={() => setMobileOpen(false)} />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<aside className={`sidebar-panel hidden md:flex fixed top-0 left-0 h-screen w-[280px] z-30 bg-card border-r border-border flex-col transition-transform duration-300 ${collapsed ? '-translate-x-full' : 'translate-x-0'}`}>
|
|
97
|
+
{sidebarContent}
|
|
98
|
+
</aside>
|
|
99
|
+
|
|
100
|
+
{collapsed && (
|
|
101
|
+
<button onClick={onExpand} className="hidden md:flex fixed top-4 left-0 z-30 items-center justify-center w-6 h-10 bg-card border border-border rounded-r-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.sidebar.expandTitle}>
|
|
102
|
+
<PanelLeftOpen size={14} />
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Mobile navbar */}
|
|
107
|
+
<header className="md:hidden fixed top-0 left-0 right-0 z-30 bg-card border-b border-border flex items-center justify-between px-3 py-2" style={{ paddingTop: 'env(safe-area-inset-top, 0px)' }}>
|
|
108
|
+
<button onClick={() => setMobileOpen(true)} className="p-2.5 -ml-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label="Open menu">
|
|
109
|
+
<Menu size={20} />
|
|
110
|
+
</button>
|
|
111
|
+
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
112
|
+
<Logo id="mobile" />
|
|
113
|
+
<span className="font-semibold text-foreground text-sm tracking-wide">MindOS</span>
|
|
114
|
+
</Link>
|
|
115
|
+
<div className="flex items-center gap-0.5">
|
|
116
|
+
<button onClick={() => setSearchOpen(true)} className="p-2.5 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.searchTitle}>
|
|
117
|
+
<Search size={20} />
|
|
118
|
+
</button>
|
|
119
|
+
<button onClick={() => setSettingsOpen(true)} className="p-2.5 -mr-1 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors active:bg-accent" aria-label={t.sidebar.settingsTitle}>
|
|
120
|
+
<Settings size={20} />
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
</header>
|
|
124
|
+
|
|
125
|
+
{mobileOpen && <div className="md:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />}
|
|
126
|
+
|
|
127
|
+
<aside className={`md:hidden fixed top-0 left-0 h-screen w-[85vw] max-w-[320px] z-50 bg-card border-r border-border flex flex-col transition-transform duration-300 ease-in-out ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}>
|
|
128
|
+
{sidebarContent}
|
|
129
|
+
</aside>
|
|
130
|
+
|
|
131
|
+
<SearchModal open={searchOpen} onClose={() => setSearchOpen(false)} />
|
|
132
|
+
<AskModal open={askOpen} onClose={() => setAskOpen(false)} currentFile={currentFile} />
|
|
133
|
+
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Sidebar from './Sidebar';
|
|
5
|
+
import AskFab from './AskFab';
|
|
6
|
+
import { FileNode } from '@/lib/types';
|
|
7
|
+
|
|
8
|
+
interface SidebarLayoutProps {
|
|
9
|
+
fileTree: FileNode[];
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps) {
|
|
14
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<Sidebar
|
|
19
|
+
fileTree={fileTree}
|
|
20
|
+
collapsed={collapsed}
|
|
21
|
+
onCollapse={() => setCollapsed(true)}
|
|
22
|
+
onExpand={() => setCollapsed(false)}
|
|
23
|
+
/>
|
|
24
|
+
<main
|
|
25
|
+
className={`min-h-screen transition-all duration-300 pt-[52px] md:pt-0 ${
|
|
26
|
+
collapsed ? 'md:pl-0' : 'md:pl-[280px]'
|
|
27
|
+
}`}
|
|
28
|
+
>
|
|
29
|
+
<div className="min-h-screen bg-background">
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
</main>
|
|
33
|
+
<AskFab />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
|
5
|
+
import GithubSlugger from 'github-slugger';
|
|
6
|
+
|
|
7
|
+
interface Heading {
|
|
8
|
+
id: string;
|
|
9
|
+
text: string;
|
|
10
|
+
level: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseHeadings(content: string): Heading[] {
|
|
14
|
+
const slugger = new GithubSlugger();
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
const headings: Heading[] = [];
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const match = line.match(/^(#{1,4})\s+(.+)/);
|
|
19
|
+
if (match) {
|
|
20
|
+
const level = match[1].length;
|
|
21
|
+
const text = match[2].trim();
|
|
22
|
+
const id = slugger.slug(text);
|
|
23
|
+
headings.push({ id, text, level });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return headings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TOPBAR_H = 44;
|
|
30
|
+
const SCROLL_OFFSET = TOPBAR_H + 12;
|
|
31
|
+
const NAV_W = 212;
|
|
32
|
+
|
|
33
|
+
interface TableOfContentsProps {
|
|
34
|
+
content: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function TableOfContents({ content }: TableOfContentsProps) {
|
|
38
|
+
const headings = parseHeadings(content);
|
|
39
|
+
const [activeId, setActiveId] = useState<string>('');
|
|
40
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
41
|
+
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (headings.length === 0) return;
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
const elements = headings
|
|
47
|
+
.map(h => document.getElementById(h.id))
|
|
48
|
+
.filter(Boolean) as HTMLElement[];
|
|
49
|
+
if (elements.length === 0) return;
|
|
50
|
+
observerRef.current?.disconnect();
|
|
51
|
+
observerRef.current = new IntersectionObserver(
|
|
52
|
+
(entries) => {
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.isIntersecting) { setActiveId(entry.target.id); break; }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{ rootMargin: `-${SCROLL_OFFSET}px 0% -70% 0%`, threshold: 0 }
|
|
58
|
+
);
|
|
59
|
+
elements.forEach(el => observerRef.current?.observe(el));
|
|
60
|
+
}, 150);
|
|
61
|
+
return () => { clearTimeout(timer); observerRef.current?.disconnect(); };
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [content]);
|
|
64
|
+
|
|
65
|
+
if (headings.length < 2) return null;
|
|
66
|
+
|
|
67
|
+
const minLevel = Math.min(...headings.map(h => h.level));
|
|
68
|
+
|
|
69
|
+
const handleClick = (e: React.MouseEvent, id: string) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
const el = document.getElementById(id);
|
|
72
|
+
if (!el) return;
|
|
73
|
+
const top = el.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET;
|
|
74
|
+
window.scrollTo({ top, behavior: 'smooth' });
|
|
75
|
+
setActiveId(id);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<aside
|
|
80
|
+
className="hidden xl:block fixed right-0 z-10"
|
|
81
|
+
style={{
|
|
82
|
+
top: TOPBAR_H,
|
|
83
|
+
height: `calc(100vh - ${TOPBAR_H}px)`,
|
|
84
|
+
// Always reserve full width so content margin doesn't jump
|
|
85
|
+
width: NAV_W,
|
|
86
|
+
// Slide the entire panel off the right edge when collapsed
|
|
87
|
+
transform: collapsed ? `translateX(${NAV_W}px)` : 'translateX(0)',
|
|
88
|
+
transition: 'transform 200ms ease-in-out',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{/* Collapse / expand button — tab attached to left edge of the panel */}
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setCollapsed(v => !v)}
|
|
94
|
+
className="absolute top-6 flex items-center justify-center w-5 h-8 rounded-l-md border border-r-0 border-border hover:bg-muted transition-colors"
|
|
95
|
+
style={{
|
|
96
|
+
left: -20, // sticks out to the left of the panel
|
|
97
|
+
background: 'var(--background)',
|
|
98
|
+
}}
|
|
99
|
+
title={collapsed ? 'Expand TOC' : 'Collapse TOC'}
|
|
100
|
+
>
|
|
101
|
+
<ChevronRight
|
|
102
|
+
size={11}
|
|
103
|
+
className="text-muted-foreground/60 transition-transform duration-200"
|
|
104
|
+
style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
|
105
|
+
/>
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{/* Nav list */}
|
|
109
|
+
<nav
|
|
110
|
+
className="flex flex-col gap-0.5 overflow-y-auto py-5 pl-2 pr-3 h-full border-l border-border"
|
|
111
|
+
style={{ background: 'var(--background)' }}
|
|
112
|
+
>
|
|
113
|
+
<p
|
|
114
|
+
className="text-[10px] font-semibold uppercase tracking-wider px-2 mb-1"
|
|
115
|
+
style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}
|
|
116
|
+
>
|
|
117
|
+
On this page
|
|
118
|
+
</p>
|
|
119
|
+
{headings.map((heading, i) => {
|
|
120
|
+
const indent = (heading.level - minLevel) * 14;
|
|
121
|
+
const isActive = activeId === heading.id;
|
|
122
|
+
const isNested = heading.level > minLevel;
|
|
123
|
+
return (
|
|
124
|
+
<a
|
|
125
|
+
key={`${heading.id}-${i}`}
|
|
126
|
+
href={`#${heading.id}`}
|
|
127
|
+
onClick={(e) => handleClick(e, heading.id)}
|
|
128
|
+
className="block text-xs py-1 rounded transition-colors duration-100 leading-snug"
|
|
129
|
+
style={{
|
|
130
|
+
paddingLeft: `${8 + indent}px`,
|
|
131
|
+
paddingRight: '8px',
|
|
132
|
+
borderLeft: isNested ? '1px solid var(--border)' : 'none',
|
|
133
|
+
marginLeft: isNested ? '8px' : '0',
|
|
134
|
+
whiteSpace: 'nowrap',
|
|
135
|
+
overflow: 'hidden',
|
|
136
|
+
textOverflow: 'ellipsis',
|
|
137
|
+
...(isActive
|
|
138
|
+
? { color: 'var(--amber)', background: 'var(--amber-dim)' }
|
|
139
|
+
: { color: 'var(--muted-foreground)' }
|
|
140
|
+
)
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{heading.text}
|
|
144
|
+
</a>
|
|
145
|
+
);
|
|
146
|
+
})}
|
|
147
|
+
</nav>
|
|
148
|
+
</aside>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { Sun, Moon } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export default function ThemeToggle() {
|
|
7
|
+
const [dark, setDark] = useState(true);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const stored = localStorage.getItem('theme');
|
|
11
|
+
const isDark = stored
|
|
12
|
+
? stored === 'dark'
|
|
13
|
+
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
14
|
+
setDark(isDark);
|
|
15
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const toggle = () => {
|
|
19
|
+
const next = !dark;
|
|
20
|
+
setDark(next);
|
|
21
|
+
document.documentElement.classList.toggle('dark', next);
|
|
22
|
+
localStorage.setItem('theme', next ? 'dark' : 'light');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
onClick={toggle}
|
|
28
|
+
className="p-1.5 rounded-lg hover:bg-zinc-800 text-zinc-500 hover:text-zinc-300 transition-colors"
|
|
29
|
+
title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
30
|
+
>
|
|
31
|
+
{dark ? <Sun size={15} /> : <Moon size={15} />}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
6
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
7
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
8
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
9
|
+
import Link from '@tiptap/extension-link';
|
|
10
|
+
import { Table } from '@tiptap/extension-table';
|
|
11
|
+
import { TableRow } from '@tiptap/extension-table';
|
|
12
|
+
import { TableCell } from '@tiptap/extension-table';
|
|
13
|
+
import { TableHeader } from '@tiptap/extension-table';
|
|
14
|
+
import { Markdown } from 'tiptap-markdown';
|
|
15
|
+
|
|
16
|
+
interface WysiwygEditorProps {
|
|
17
|
+
value: string;
|
|
18
|
+
onChange: (markdown: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function WysiwygEditor({ value, onChange }: WysiwygEditorProps) {
|
|
22
|
+
const onChangeRef = useRef(onChange);
|
|
23
|
+
onChangeRef.current = onChange;
|
|
24
|
+
|
|
25
|
+
const editor = useEditor({
|
|
26
|
+
extensions: [
|
|
27
|
+
StarterKit.configure({
|
|
28
|
+
codeBlock: { HTMLAttributes: { class: 'not-prose' } },
|
|
29
|
+
}),
|
|
30
|
+
Markdown.configure({
|
|
31
|
+
html: true,
|
|
32
|
+
transformPastedText: true,
|
|
33
|
+
transformCopiedText: true,
|
|
34
|
+
}),
|
|
35
|
+
Placeholder.configure({ placeholder: 'Start writing…' }),
|
|
36
|
+
TaskList,
|
|
37
|
+
TaskItem.configure({ nested: true }),
|
|
38
|
+
Link.configure({ openOnClick: false, autolink: true }),
|
|
39
|
+
Table.configure({ resizable: false }),
|
|
40
|
+
TableRow,
|
|
41
|
+
TableHeader,
|
|
42
|
+
TableCell,
|
|
43
|
+
],
|
|
44
|
+
content: value,
|
|
45
|
+
editorProps: {
|
|
46
|
+
attributes: {
|
|
47
|
+
class: 'prose max-w-none focus:outline-none wysiwyg-editor',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
onUpdate({ editor }) {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
const md = (editor.storage as any).markdown?.getMarkdown?.() ?? editor.getText();
|
|
53
|
+
onChangeRef.current(md);
|
|
54
|
+
},
|
|
55
|
+
immediatelyRender: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Sync external value changes (e.g. cancel → revert) without full re-mount
|
|
59
|
+
const lastMd = useRef(value);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!editor || value === lastMd.current) return;
|
|
62
|
+
lastMd.current = value;
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const current = (editor.storage as any).markdown?.getMarkdown?.() ?? '';
|
|
65
|
+
if (current !== value) {
|
|
66
|
+
editor.commands.setContent(value);
|
|
67
|
+
}
|
|
68
|
+
}, [editor, value]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="wysiwyg-wrapper h-full overflow-y-auto px-8 py-6">
|
|
72
|
+
<EditorContent editor={editor} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { X, FileText, Table, Paperclip } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface FileChipProps {
|
|
6
|
+
path: string;
|
|
7
|
+
onRemove: () => void;
|
|
8
|
+
variant?: 'kb' | 'upload';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function FileChip({ path, onRemove, variant = 'kb' }: FileChipProps) {
|
|
12
|
+
const name = path.split('/').pop() ?? path;
|
|
13
|
+
const isCsv = name.endsWith('.csv');
|
|
14
|
+
const Icon = variant === 'upload' ? Paperclip : isCsv ? Table : FileText;
|
|
15
|
+
const iconClass = variant === 'upload' ? 'text-zinc-400' : isCsv ? 'text-emerald-400' : 'text-zinc-400';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs border border-border bg-muted text-foreground max-w-[220px]">
|
|
19
|
+
<Icon size={11} className={`${iconClass} shrink-0`} />
|
|
20
|
+
<span className="truncate" title={path}>{name}</span>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onRemove}
|
|
24
|
+
className="text-muted-foreground hover:text-foreground ml-0.5 shrink-0"
|
|
25
|
+
>
|
|
26
|
+
<X size={10} />
|
|
27
|
+
</button>
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { FileText, Table } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface MentionPopoverProps {
|
|
6
|
+
results: string[];
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
onSelect: (filePath: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function MentionPopover({ results, selectedIndex, onSelect }: MentionPopoverProps) {
|
|
12
|
+
if (results.length === 0) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="mx-4 mb-1 border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
16
|
+
{results.map((f, idx) => {
|
|
17
|
+
const name = f.split('/').pop() ?? f;
|
|
18
|
+
const isCsv = name.endsWith('.csv');
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
key={f}
|
|
22
|
+
type="button"
|
|
23
|
+
onMouseDown={(e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
onSelect(f);
|
|
26
|
+
}}
|
|
27
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
|
|
28
|
+
idx === selectedIndex
|
|
29
|
+
? 'bg-accent text-foreground'
|
|
30
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
31
|
+
}`}
|
|
32
|
+
>
|
|
33
|
+
{isCsv ? (
|
|
34
|
+
<Table size={13} className="text-emerald-400 shrink-0" />
|
|
35
|
+
) : (
|
|
36
|
+
<FileText size={13} className="text-zinc-400 shrink-0" />
|
|
37
|
+
)}
|
|
38
|
+
<span className="truncate flex-1">{name}</span>
|
|
39
|
+
<span className="text-[10px] text-muted-foreground/50 truncate max-w-[140px] shrink-0">
|
|
40
|
+
{f.split('/').slice(0, -1).join('/')}
|
|
41
|
+
</span>
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
<div className="px-3 py-1.5 border-t border-border flex gap-3 text-[10px] text-muted-foreground/50">
|
|
46
|
+
<span>↑↓ navigate</span>
|
|
47
|
+
<span>↵ select</span>
|
|
48
|
+
<span>ESC dismiss</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect } from 'react';
|
|
4
|
+
import { Sparkles, Loader2, AlertCircle } from 'lucide-react';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import type { Message } from '@/lib/types';
|
|
8
|
+
|
|
9
|
+
function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="prose prose-sm dark:prose-invert max-w-none text-foreground
|
|
12
|
+
prose-p:my-1 prose-p:leading-relaxed
|
|
13
|
+
prose-headings:font-semibold prose-headings:my-2
|
|
14
|
+
prose-ul:my-1 prose-li:my-0.5
|
|
15
|
+
prose-ol:my-1
|
|
16
|
+
prose-code:text-[0.8em] prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none
|
|
17
|
+
prose-pre:bg-muted prose-pre:text-foreground prose-pre:text-xs
|
|
18
|
+
prose-blockquote:border-l-amber-400 prose-blockquote:text-muted-foreground
|
|
19
|
+
prose-a:text-amber-500 prose-a:no-underline hover:prose-a:underline
|
|
20
|
+
prose-strong:text-foreground prose-strong:font-semibold
|
|
21
|
+
prose-table:text-xs prose-th:py-1 prose-td:py-1
|
|
22
|
+
">
|
|
23
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
|
24
|
+
{isStreaming && (
|
|
25
|
+
<span className="inline-block w-1.5 h-3.5 bg-amber-400 ml-0.5 align-middle animate-pulse rounded-sm" />
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MessageListProps {
|
|
32
|
+
messages: Message[];
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
loadingPhase: 'connecting' | 'thinking' | 'streaming';
|
|
35
|
+
emptyPrompt: string;
|
|
36
|
+
suggestions: readonly string[];
|
|
37
|
+
onSuggestionClick: (text: string) => void;
|
|
38
|
+
labels: {
|
|
39
|
+
connecting: string;
|
|
40
|
+
thinking: string;
|
|
41
|
+
generating: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function MessageList({
|
|
46
|
+
messages,
|
|
47
|
+
isLoading,
|
|
48
|
+
loadingPhase,
|
|
49
|
+
emptyPrompt,
|
|
50
|
+
suggestions,
|
|
51
|
+
onSuggestionClick,
|
|
52
|
+
labels,
|
|
53
|
+
}: MessageListProps) {
|
|
54
|
+
const endRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
endRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
58
|
+
}, [messages]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4 min-h-0">
|
|
62
|
+
{messages.length === 0 && (
|
|
63
|
+
<div className="mt-6 space-y-3">
|
|
64
|
+
<p className="text-center text-sm text-muted-foreground/60">{emptyPrompt}</p>
|
|
65
|
+
<div className="flex flex-col gap-2 px-2">
|
|
66
|
+
{suggestions.map((s, i) => (
|
|
67
|
+
<button
|
|
68
|
+
key={i}
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={() => onSuggestionClick(s)}
|
|
71
|
+
className="text-left text-xs px-3 py-2 rounded-lg border border-border bg-muted/50 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
72
|
+
>
|
|
73
|
+
{s}
|
|
74
|
+
</button>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
{messages.map((m, i) => (
|
|
80
|
+
<div key={i} className={`flex gap-3 ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
81
|
+
{m.role === 'assistant' && (
|
|
82
|
+
<div
|
|
83
|
+
className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-0.5"
|
|
84
|
+
style={{ background: 'var(--amber-dim)' }}
|
|
85
|
+
>
|
|
86
|
+
<Sparkles size={12} style={{ color: 'var(--amber)' }} />
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{m.role === 'user' ? (
|
|
90
|
+
<div
|
|
91
|
+
className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap"
|
|
92
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
93
|
+
>
|
|
94
|
+
{m.content}
|
|
95
|
+
</div>
|
|
96
|
+
) : m.content.startsWith('__error__') ? (
|
|
97
|
+
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-red-500/20 bg-red-500/8 text-sm">
|
|
98
|
+
<div className="flex items-start gap-2 text-red-400">
|
|
99
|
+
<AlertCircle size={14} className="shrink-0 mt-0.5" />
|
|
100
|
+
<span className="leading-relaxed">{m.content.slice(9)}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
|
|
105
|
+
{m.content ? (
|
|
106
|
+
<AssistantMessage content={m.content} isStreaming={isLoading && i === messages.length - 1} />
|
|
107
|
+
) : isLoading && i === messages.length - 1 ? (
|
|
108
|
+
<div className="flex items-center gap-2 py-1">
|
|
109
|
+
<Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
|
|
110
|
+
<span className="text-xs text-muted-foreground animate-pulse">
|
|
111
|
+
{loadingPhase === 'connecting'
|
|
112
|
+
? labels.connecting
|
|
113
|
+
: loadingPhase === 'thinking'
|
|
114
|
+
? labels.thinking
|
|
115
|
+
: labels.generating}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
<div ref={endRef} />
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|