@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,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const toggleVariants = cva(
|
|
9
|
+
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-transparent",
|
|
14
|
+
outline: "border border-input bg-transparent hover:bg-muted",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: "h-8 min-w-8 px-2",
|
|
18
|
+
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]",
|
|
19
|
+
lg: "h-9 min-w-9 px-2.5",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
size: "default",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
function Toggle({
|
|
30
|
+
className,
|
|
31
|
+
variant = "default",
|
|
32
|
+
size = "default",
|
|
33
|
+
...props
|
|
34
|
+
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
|
35
|
+
return (
|
|
36
|
+
<TogglePrimitive
|
|
37
|
+
data-slot="toggle"
|
|
38
|
+
className={cn(toggleVariants({ variant, size, className }))}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { Toggle, toggleVariants }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function TooltipProvider({
|
|
8
|
+
delay = 0,
|
|
9
|
+
...props
|
|
10
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
11
|
+
return (
|
|
12
|
+
<TooltipPrimitive.Provider
|
|
13
|
+
data-slot="tooltip-provider"
|
|
14
|
+
delay={delay}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
21
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
25
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TooltipContent({
|
|
29
|
+
className,
|
|
30
|
+
side = "top",
|
|
31
|
+
sideOffset = 4,
|
|
32
|
+
align = "center",
|
|
33
|
+
alignOffset = 0,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: TooltipPrimitive.Popup.Props &
|
|
37
|
+
Pick<
|
|
38
|
+
TooltipPrimitive.Positioner.Props,
|
|
39
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
40
|
+
>) {
|
|
41
|
+
return (
|
|
42
|
+
<TooltipPrimitive.Portal>
|
|
43
|
+
<TooltipPrimitive.Positioner
|
|
44
|
+
align={align}
|
|
45
|
+
alignOffset={alignOffset}
|
|
46
|
+
side={side}
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className="isolate z-50"
|
|
49
|
+
>
|
|
50
|
+
<TooltipPrimitive.Popup
|
|
51
|
+
data-slot="tooltip-content"
|
|
52
|
+
className={cn(
|
|
53
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
60
|
+
</TooltipPrimitive.Popup>
|
|
61
|
+
</TooltipPrimitive.Positioner>
|
|
62
|
+
</TooltipPrimitive.Portal>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "base-nova",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "@/components",
|
|
17
|
+
"utils": "@/lib/utils",
|
|
18
|
+
"ui": "@/components/ui",
|
|
19
|
+
"lib": "@/lib",
|
|
20
|
+
"hooks": "@/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import type { Message, ChatSession } from '@/lib/types';
|
|
5
|
+
import { apiFetch } from '@/lib/api';
|
|
6
|
+
|
|
7
|
+
const MAX_SESSIONS = 30;
|
|
8
|
+
|
|
9
|
+
function createSession(currentFile?: string): ChatSession {
|
|
10
|
+
const ts = Date.now();
|
|
11
|
+
return {
|
|
12
|
+
id: `${ts}-${Math.random().toString(36).slice(2, 8)}`,
|
|
13
|
+
currentFile,
|
|
14
|
+
createdAt: ts,
|
|
15
|
+
updatedAt: ts,
|
|
16
|
+
messages: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sessionTitle(s: ChatSession): string {
|
|
21
|
+
const firstUser = s.messages.find((m) => m.role === 'user');
|
|
22
|
+
if (!firstUser) return '(empty session)';
|
|
23
|
+
const line = firstUser.content.replace(/\s+/g, ' ').trim();
|
|
24
|
+
return line.length > 42 ? `${line.slice(0, 42)}...` : line;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchSessions(): Promise<ChatSession[]> {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch('/api/ask-sessions', { cache: 'no-store' });
|
|
30
|
+
if (!res.ok) return [];
|
|
31
|
+
const data = (await res.json()) as ChatSession[];
|
|
32
|
+
if (!Array.isArray(data)) return [];
|
|
33
|
+
return data.slice(0, MAX_SESSIONS);
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function upsertSession(session: ChatSession): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await fetch('/api/ask-sessions', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ session }),
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore persistence errors
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function removeSession(id: string): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
await fetch('/api/ask-sessions', {
|
|
54
|
+
method: 'DELETE',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ id }),
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore persistence errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useAskSession(currentFile?: string) {
|
|
64
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
65
|
+
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
66
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
67
|
+
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
68
|
+
|
|
69
|
+
/** Load sessions from server, pick the matching one or create fresh. */
|
|
70
|
+
const initSessions = useCallback(async () => {
|
|
71
|
+
const sorted = (await fetchSessions())
|
|
72
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
73
|
+
.slice(0, MAX_SESSIONS);
|
|
74
|
+
setSessions(sorted);
|
|
75
|
+
|
|
76
|
+
const matched = currentFile
|
|
77
|
+
? sorted.find((sess) => sess.currentFile === currentFile)
|
|
78
|
+
: sorted[0];
|
|
79
|
+
|
|
80
|
+
if (matched) {
|
|
81
|
+
setActiveSessionId(matched.id);
|
|
82
|
+
setMessages(matched.messages);
|
|
83
|
+
} else {
|
|
84
|
+
const fresh = createSession(currentFile);
|
|
85
|
+
setActiveSessionId(fresh.id);
|
|
86
|
+
setMessages([]);
|
|
87
|
+
const next = [fresh, ...sorted].slice(0, MAX_SESSIONS);
|
|
88
|
+
setSessions(next);
|
|
89
|
+
await upsertSession(fresh);
|
|
90
|
+
}
|
|
91
|
+
}, [currentFile]);
|
|
92
|
+
|
|
93
|
+
/** Persist current session (debounced). */
|
|
94
|
+
const persistSession = useCallback(
|
|
95
|
+
(msgs: Message[], sessionId: string | null) => {
|
|
96
|
+
if (!sessionId) return;
|
|
97
|
+
let sessionToPersist: ChatSession | null = null;
|
|
98
|
+
setSessions((prev) => {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const existing = prev.find((s) => s.id === sessionId);
|
|
101
|
+
sessionToPersist = existing
|
|
102
|
+
? { ...existing, currentFile, updatedAt: now, messages: msgs }
|
|
103
|
+
: { id: sessionId, currentFile, createdAt: now, updatedAt: now, messages: msgs };
|
|
104
|
+
|
|
105
|
+
const rest = prev.filter((s) => s.id !== sessionId);
|
|
106
|
+
return [sessionToPersist!, ...rest]
|
|
107
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
108
|
+
.slice(0, MAX_SESSIONS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
|
|
112
|
+
persistTimerRef.current = setTimeout(() => {
|
|
113
|
+
if (sessionToPersist) void upsertSession(sessionToPersist);
|
|
114
|
+
}, 600);
|
|
115
|
+
},
|
|
116
|
+
[currentFile],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const clearPersistTimer = useCallback(() => {
|
|
120
|
+
if (persistTimerRef.current) {
|
|
121
|
+
clearTimeout(persistTimerRef.current);
|
|
122
|
+
persistTimerRef.current = null;
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/** Create a brand-new session. */
|
|
127
|
+
const resetSession = useCallback(() => {
|
|
128
|
+
const fresh = createSession(currentFile);
|
|
129
|
+
setActiveSessionId(fresh.id);
|
|
130
|
+
setMessages([]);
|
|
131
|
+
setSessions((prev) => {
|
|
132
|
+
const next = [fresh, ...prev]
|
|
133
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
134
|
+
.slice(0, MAX_SESSIONS);
|
|
135
|
+
void upsertSession(fresh);
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
}, [currentFile]);
|
|
139
|
+
|
|
140
|
+
/** Switch to an existing session. */
|
|
141
|
+
const loadSession = useCallback(
|
|
142
|
+
(id: string) => {
|
|
143
|
+
const target = sessions.find((s) => s.id === id);
|
|
144
|
+
if (!target) return;
|
|
145
|
+
setActiveSessionId(target.id);
|
|
146
|
+
setMessages(target.messages);
|
|
147
|
+
},
|
|
148
|
+
[sessions],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
/** Delete a session. If it's the active one, create fresh. */
|
|
152
|
+
const deleteSession = useCallback(
|
|
153
|
+
(id: string) => {
|
|
154
|
+
void removeSession(id);
|
|
155
|
+
const remaining = sessions.filter((s) => s.id !== id);
|
|
156
|
+
setSessions(remaining);
|
|
157
|
+
|
|
158
|
+
if (activeSessionId === id) {
|
|
159
|
+
const fresh = createSession(currentFile);
|
|
160
|
+
setActiveSessionId(fresh.id);
|
|
161
|
+
setMessages([]);
|
|
162
|
+
setSessions([fresh, ...remaining].slice(0, MAX_SESSIONS));
|
|
163
|
+
void upsertSession(fresh);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[activeSessionId, currentFile, sessions],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
messages,
|
|
171
|
+
setMessages,
|
|
172
|
+
sessions,
|
|
173
|
+
activeSessionId,
|
|
174
|
+
initSessions,
|
|
175
|
+
persistSession,
|
|
176
|
+
clearPersistTimer,
|
|
177
|
+
resetSession,
|
|
178
|
+
loadSession,
|
|
179
|
+
deleteSession,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import type { LocalAttachment } from '@/lib/types';
|
|
5
|
+
|
|
6
|
+
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
|
7
|
+
'.txt', '.md', '.markdown', '.csv', '.json', '.yaml', '.yml', '.xml', '.html', '.htm', '.pdf',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function getExt(name: string): string {
|
|
11
|
+
const idx = name.lastIndexOf('.');
|
|
12
|
+
return idx >= 0 ? name.slice(idx).toLowerCase() : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function uint8ToBase64(bytes: Uint8Array): string {
|
|
16
|
+
let binary = '';
|
|
17
|
+
const chunkSize = 0x8000;
|
|
18
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
19
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
20
|
+
binary += String.fromCharCode(...chunk);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function extractPdfText(file: File): Promise<string> {
|
|
26
|
+
const buffer = await file.arrayBuffer();
|
|
27
|
+
const dataBase64 = uint8ToBase64(new Uint8Array(buffer));
|
|
28
|
+
|
|
29
|
+
const res = await fetch('/api/extract-pdf', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ name: file.name, dataBase64 }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let payload: { text?: string; extracted?: boolean; error?: string } = {};
|
|
36
|
+
try {
|
|
37
|
+
payload = await res.json();
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore JSON parse error
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(payload.error || `PDF extraction failed (${res.status})`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return payload.extracted ? (payload.text || '') : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useFileUpload() {
|
|
50
|
+
const [localAttachments, setLocalAttachments] = useState<LocalAttachment[]>([]);
|
|
51
|
+
const [uploadError, setUploadError] = useState('');
|
|
52
|
+
const uploadInputRef = useRef<HTMLInputElement>(null);
|
|
53
|
+
|
|
54
|
+
const pickFiles = useCallback(async (files: FileList | null) => {
|
|
55
|
+
if (!files || files.length === 0) return;
|
|
56
|
+
|
|
57
|
+
const picked = Array.from(files).slice(0, 8);
|
|
58
|
+
const accepted: File[] = [];
|
|
59
|
+
const rejected: string[] = [];
|
|
60
|
+
|
|
61
|
+
for (const f of picked) {
|
|
62
|
+
const ext = getExt(f.name);
|
|
63
|
+
if (!ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
|
64
|
+
rejected.push(f.name);
|
|
65
|
+
} else {
|
|
66
|
+
accepted.push(f);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (rejected.length > 0) {
|
|
71
|
+
setUploadError(`Unsupported file type: ${rejected.join(', ')}`);
|
|
72
|
+
} else {
|
|
73
|
+
setUploadError('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const loaded = await Promise.all(
|
|
77
|
+
accepted.map(async (f) => {
|
|
78
|
+
const ext = getExt(f.name);
|
|
79
|
+
if (ext === '.pdf') {
|
|
80
|
+
try {
|
|
81
|
+
const extracted = await extractPdfText(f);
|
|
82
|
+
return {
|
|
83
|
+
name: f.name,
|
|
84
|
+
content: extracted
|
|
85
|
+
? `[PDF TEXT EXTRACTED: ${f.name}]\n\n${extracted}`
|
|
86
|
+
: `[PDF: ${f.name}] Could not extract readable text (possibly scanned/image PDF).`,
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
name: f.name,
|
|
91
|
+
content: `[PDF: ${f.name}] Failed to extract text from this PDF.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { name: f.name, content: await f.text() };
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
setLocalAttachments((prev) => {
|
|
100
|
+
const merged = [...prev];
|
|
101
|
+
for (const item of loaded) {
|
|
102
|
+
if (!merged.some((m) => m.name === item.name && m.content === item.content))
|
|
103
|
+
merged.push(item);
|
|
104
|
+
}
|
|
105
|
+
return merged;
|
|
106
|
+
});
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const removeAttachment = useCallback((idx: number) => {
|
|
110
|
+
setLocalAttachments((prev) => prev.filter((_, i) => i !== idx));
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const clearAttachments = useCallback(() => {
|
|
114
|
+
setLocalAttachments([]);
|
|
115
|
+
setUploadError('');
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
localAttachments,
|
|
120
|
+
uploadError,
|
|
121
|
+
uploadInputRef,
|
|
122
|
+
pickFiles,
|
|
123
|
+
removeAttachment,
|
|
124
|
+
clearAttachments,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export function useMention() {
|
|
6
|
+
const [allFiles, setAllFiles] = useState<string[]>([]);
|
|
7
|
+
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
|
8
|
+
const [mentionResults, setMentionResults] = useState<string[]>([]);
|
|
9
|
+
const [mentionIndex, setMentionIndex] = useState(0);
|
|
10
|
+
|
|
11
|
+
// Load file list once
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch('/api/files')
|
|
14
|
+
.then((r) => r.json())
|
|
15
|
+
.then(setAllFiles)
|
|
16
|
+
.catch(() => {});
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
/** Parse @-mention from input text; returns updated input if a mention was selected. */
|
|
20
|
+
const updateMentionFromInput = useCallback(
|
|
21
|
+
(val: string) => {
|
|
22
|
+
const atIdx = val.lastIndexOf('@');
|
|
23
|
+
if (atIdx === -1) {
|
|
24
|
+
setMentionQuery(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const before = val[atIdx - 1];
|
|
28
|
+
if (atIdx > 0 && before !== ' ') {
|
|
29
|
+
setMentionQuery(null);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const query = val.slice(atIdx + 1).toLowerCase();
|
|
33
|
+
setMentionQuery(query);
|
|
34
|
+
setMentionResults(allFiles.filter((f) => f.toLowerCase().includes(query)).slice(0, 8));
|
|
35
|
+
setMentionIndex(0);
|
|
36
|
+
},
|
|
37
|
+
[allFiles],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const navigateMention = useCallback(
|
|
41
|
+
(direction: 'up' | 'down') => {
|
|
42
|
+
if (direction === 'down') {
|
|
43
|
+
setMentionIndex((i) => Math.min(i + 1, mentionResults.length - 1));
|
|
44
|
+
} else {
|
|
45
|
+
setMentionIndex((i) => Math.max(i - 1, 0));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[mentionResults.length],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const resetMention = useCallback(() => {
|
|
52
|
+
setMentionQuery(null);
|
|
53
|
+
setMentionResults([]);
|
|
54
|
+
setMentionIndex(0);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
mentionQuery,
|
|
59
|
+
mentionResults,
|
|
60
|
+
mentionIndex,
|
|
61
|
+
updateMentionFromInput,
|
|
62
|
+
navigateMention,
|
|
63
|
+
resetMention,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
4
|
+
import { Locale, messages, Messages } from './i18n';
|
|
5
|
+
|
|
6
|
+
interface LocaleContextValue {
|
|
7
|
+
locale: Locale;
|
|
8
|
+
setLocale: (l: Locale) => void;
|
|
9
|
+
t: Messages;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LocaleContext = createContext<LocaleContextValue>({
|
|
13
|
+
locale: 'en',
|
|
14
|
+
setLocale: () => {},
|
|
15
|
+
t: messages['en'],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function LocaleProvider({ children }: { children: ReactNode }) {
|
|
19
|
+
const [locale, setLocaleState] = useState<Locale>('en');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const saved = localStorage.getItem('locale') as Locale | null;
|
|
23
|
+
if (saved === 'zh' || saved === 'en') setLocaleState(saved);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const setLocale = (l: Locale) => {
|
|
27
|
+
setLocaleState(l);
|
|
28
|
+
localStorage.setItem('locale', l);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<LocaleContext.Provider value={{ locale, setLocale, t: messages[locale] as unknown as Messages }}>
|
|
33
|
+
{children}
|
|
34
|
+
</LocaleContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useLocale() {
|
|
39
|
+
return useContext(LocaleContext);
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createFile, deleteFile, renameFile } from '@/lib/fs';
|
|
4
|
+
import { revalidatePath } from 'next/cache';
|
|
5
|
+
|
|
6
|
+
export async function createFileAction(dirPath: string, fileName: string): Promise<{ success: boolean; filePath?: string; error?: string }> {
|
|
7
|
+
try {
|
|
8
|
+
const name = fileName.trim();
|
|
9
|
+
if (!name) return { success: false, error: 'File name is required' };
|
|
10
|
+
// Ensure extension
|
|
11
|
+
const hasExt = name.endsWith('.md') || name.endsWith('.csv');
|
|
12
|
+
const finalName = hasExt ? name : `${name}.md`;
|
|
13
|
+
const filePath = dirPath ? `${dirPath}/${finalName}` : finalName;
|
|
14
|
+
createFile(filePath);
|
|
15
|
+
revalidatePath('/', 'layout');
|
|
16
|
+
return { success: true, filePath };
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to create file' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function deleteFileAction(filePath: string): Promise<{ success: boolean; error?: string }> {
|
|
23
|
+
try {
|
|
24
|
+
deleteFile(filePath);
|
|
25
|
+
revalidatePath('/', 'layout');
|
|
26
|
+
return { success: true };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to delete file' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function renameFileAction(oldPath: string, newName: string): Promise<{ success: boolean; newPath?: string; error?: string }> {
|
|
33
|
+
try {
|
|
34
|
+
const newPath = renameFile(oldPath, newName);
|
|
35
|
+
revalidatePath('/', 'layout');
|
|
36
|
+
return { success: true, newPath };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { success: false, error: err instanceof Error ? err.message : 'Failed to rename file' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
import { effectiveAiConfig } from '@/lib/settings';
|
|
4
|
+
|
|
5
|
+
export function getModel() {
|
|
6
|
+
const cfg = effectiveAiConfig();
|
|
7
|
+
|
|
8
|
+
if (cfg.provider === 'openai') {
|
|
9
|
+
const openai = createOpenAI({
|
|
10
|
+
apiKey: cfg.openaiApiKey,
|
|
11
|
+
baseURL: cfg.openaiBaseUrl || undefined,
|
|
12
|
+
});
|
|
13
|
+
return openai.chat(cfg.openaiModel);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const anthropic = createAnthropic({ apiKey: cfg.anthropicApiKey });
|
|
17
|
+
return anthropic(cfg.anthropicModel);
|
|
18
|
+
}
|