@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,428 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo, memo, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import {
|
|
6
|
+
ReactFlow,
|
|
7
|
+
Background,
|
|
8
|
+
BackgroundVariant,
|
|
9
|
+
Controls,
|
|
10
|
+
MiniMap,
|
|
11
|
+
Handle,
|
|
12
|
+
Position,
|
|
13
|
+
type NodeProps,
|
|
14
|
+
} from '@xyflow/react';
|
|
15
|
+
import '@xyflow/react/dist/style.css';
|
|
16
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
17
|
+
import type { GraphData, GraphNode, GraphEdge } from '@/app/api/graph/route';
|
|
18
|
+
import { apiFetch } from '@/lib/api';
|
|
19
|
+
|
|
20
|
+
// ─── Force Layout ──────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface Pos { x: number; y: number }
|
|
23
|
+
|
|
24
|
+
function forceLayout(
|
|
25
|
+
nodeIds: string[],
|
|
26
|
+
edges: { source: string; target: string }[],
|
|
27
|
+
iterations?: number,
|
|
28
|
+
): Record<string, Pos> {
|
|
29
|
+
const n = nodeIds.length;
|
|
30
|
+
if (n === 0) return {};
|
|
31
|
+
|
|
32
|
+
const iters = iterations ?? (n > 100 ? 80 : 150);
|
|
33
|
+
const width = 1200;
|
|
34
|
+
const height = 900;
|
|
35
|
+
// k is the "ideal" distance between nodes
|
|
36
|
+
const k = Math.sqrt((width * height) / Math.max(n, 1)) * 0.6;
|
|
37
|
+
const pos: Record<string, Pos> = {};
|
|
38
|
+
|
|
39
|
+
// Initialize in a more spread-out circle or random
|
|
40
|
+
nodeIds.forEach((id, i) => {
|
|
41
|
+
const angle = (2 * Math.PI * i) / n;
|
|
42
|
+
const radius = Math.min(width, height) * 0.4 * Math.random();
|
|
43
|
+
pos[id] = {
|
|
44
|
+
x: width / 2 + Math.cos(angle) * radius,
|
|
45
|
+
y: height / 2 + Math.sin(angle) * radius,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const disp: Record<string, Pos> = {};
|
|
50
|
+
const initTemp = width * 0.1;
|
|
51
|
+
|
|
52
|
+
for (let iter = 0; iter < iters; iter++) {
|
|
53
|
+
const temp = initTemp * (1 - iter / iters);
|
|
54
|
+
|
|
55
|
+
for (const id of nodeIds) disp[id] = { x: 0, y: 0 };
|
|
56
|
+
|
|
57
|
+
// Repulsion (nodes push each other away)
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
for (let j = i + 1; j < n; j++) {
|
|
60
|
+
const u = nodeIds[i], v = nodeIds[j];
|
|
61
|
+
const dx = pos[u].x - pos[v].x;
|
|
62
|
+
const dy = pos[u].y - pos[v].y;
|
|
63
|
+
const distSq = dx * dx + dy * dy || 0.01;
|
|
64
|
+
const dist = Math.sqrt(distSq);
|
|
65
|
+
const force = (k * k) / dist;
|
|
66
|
+
disp[u].x += (dx / dist) * force;
|
|
67
|
+
disp[u].y += (dy / dist) * force;
|
|
68
|
+
disp[v].x -= (dx / dist) * force;
|
|
69
|
+
disp[v].y -= (dy / dist) * force;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Attraction (edges pull nodes together)
|
|
74
|
+
for (const e of edges) {
|
|
75
|
+
const u = e.source, v = e.target;
|
|
76
|
+
if (!pos[u] || !pos[v]) continue;
|
|
77
|
+
const dx = pos[u].x - pos[v].x;
|
|
78
|
+
const dy = pos[u].y - pos[v].y;
|
|
79
|
+
const distSq = dx * dx + dy * dy || 0.01;
|
|
80
|
+
const dist = Math.sqrt(distSq);
|
|
81
|
+
const force = (dist * dist) / k;
|
|
82
|
+
disp[u].x -= (dx / dist) * force;
|
|
83
|
+
disp[u].y -= (dy / dist) * force;
|
|
84
|
+
disp[v].x += (dx / dist) * force;
|
|
85
|
+
disp[v].y += (dy / dist) * force;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Gravity (pull towards center to avoid drifting)
|
|
89
|
+
for (const id of nodeIds) {
|
|
90
|
+
const dx = pos[id].x - width / 2;
|
|
91
|
+
const dy = pos[id].y - height / 2;
|
|
92
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
|
|
93
|
+
const force = 0.05 * dist; // Gentle pull
|
|
94
|
+
disp[id].x -= (dx / dist) * force;
|
|
95
|
+
disp[id].y -= (dy / dist) * force;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply displacements
|
|
99
|
+
for (const id of nodeIds) {
|
|
100
|
+
const d = disp[id];
|
|
101
|
+
const dlen = Math.sqrt(d.x * d.x + d.y * d.y) || 0.01;
|
|
102
|
+
pos[id].x += (d.x / dlen) * Math.min(dlen, temp);
|
|
103
|
+
pos[id].y += (d.y / dlen) * Math.min(dlen, temp);
|
|
104
|
+
|
|
105
|
+
// Softer clamping
|
|
106
|
+
pos[id].x = Math.max(0, Math.min(width, pos[id].x));
|
|
107
|
+
pos[id].y = Math.max(0, Math.min(height, pos[id].y));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return pos;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── WikiNode ──────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
interface WikiNodeData {
|
|
117
|
+
label: string;
|
|
118
|
+
id: string;
|
|
119
|
+
isCurrent: boolean;
|
|
120
|
+
isOrphan: boolean;
|
|
121
|
+
[key: string]: unknown;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const WikiNode = memo(function WikiNode({ data }: NodeProps) {
|
|
125
|
+
const router = useRouter();
|
|
126
|
+
const { label, id, isCurrent, isOrphan, size = 1 } = data as WikiNodeData & { size?: number };
|
|
127
|
+
|
|
128
|
+
const handleClick = useCallback(() => {
|
|
129
|
+
const encoded = (id as string).split('/').map(encodeURIComponent).join('/');
|
|
130
|
+
router.push('/view/' + encoded);
|
|
131
|
+
}, [id, router]);
|
|
132
|
+
|
|
133
|
+
const scale = 0.8 + Math.min(size * 0.1, 1.2);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
onClick={handleClick}
|
|
138
|
+
title={id as string}
|
|
139
|
+
className="group"
|
|
140
|
+
style={{
|
|
141
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
142
|
+
fontSize: 10 * scale,
|
|
143
|
+
padding: `${4 * scale}px ${12 * scale}px`,
|
|
144
|
+
borderRadius: 999, // Pill shape
|
|
145
|
+
cursor: 'pointer',
|
|
146
|
+
userSelect: 'none',
|
|
147
|
+
whiteSpace: 'nowrap',
|
|
148
|
+
maxWidth: 240,
|
|
149
|
+
overflow: 'hidden',
|
|
150
|
+
textOverflow: 'ellipsis',
|
|
151
|
+
opacity: isOrphan ? 0.4 : 1,
|
|
152
|
+
background: isCurrent ? 'var(--amber)' : 'var(--card)',
|
|
153
|
+
color: isCurrent ? '#131210' : 'var(--foreground)',
|
|
154
|
+
border: `1.5px solid ${isCurrent ? 'var(--amber)' : 'var(--border)'}`,
|
|
155
|
+
boxShadow: isCurrent
|
|
156
|
+
? '0 0 20px var(--amber-dim), 0 0 0 2px var(--amber-dim)'
|
|
157
|
+
: '0 2px 4px rgba(0,0,0,0.1)',
|
|
158
|
+
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
159
|
+
zIndex: isCurrent ? 10 : 1,
|
|
160
|
+
}}
|
|
161
|
+
onMouseEnter={e => {
|
|
162
|
+
if (!isCurrent) {
|
|
163
|
+
e.currentTarget.style.borderColor = 'var(--amber)';
|
|
164
|
+
e.currentTarget.style.background = 'var(--accent)';
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
onMouseLeave={e => {
|
|
168
|
+
if (!isCurrent) {
|
|
169
|
+
e.currentTarget.style.borderColor = 'var(--border)';
|
|
170
|
+
e.currentTarget.style.background = 'var(--card)';
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
|
|
175
|
+
{label as string}
|
|
176
|
+
<Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── GraphRenderer ─────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
type Scope = 'global' | 'local';
|
|
184
|
+
|
|
185
|
+
export function GraphRenderer({ filePath }: RendererContext) {
|
|
186
|
+
const [graphData, setGraphData] = useState<GraphData | null>(null);
|
|
187
|
+
const [loading, setLoading] = useState(true);
|
|
188
|
+
const [scope, setScope] = useState<Scope>('local');
|
|
189
|
+
const [mounted, setMounted] = useState(false);
|
|
190
|
+
|
|
191
|
+
useEffect(() => { setMounted(true); }, []);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
apiFetch<GraphData>('/api/graph')
|
|
195
|
+
.then((data) => { setGraphData(data); setLoading(false); })
|
|
196
|
+
.catch(() => setLoading(false));
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
// Degree calculation (for node sizing)
|
|
200
|
+
const degrees = useMemo(() => {
|
|
201
|
+
if (!graphData) return new Map<string, number>();
|
|
202
|
+
const d = new Map<string, number>();
|
|
203
|
+
for (const e of graphData.edges) {
|
|
204
|
+
d.set(e.source, (d.get(e.source) || 0) + 1);
|
|
205
|
+
d.set(e.target, (d.get(e.target) || 0) + 1);
|
|
206
|
+
}
|
|
207
|
+
return d;
|
|
208
|
+
}, [graphData]);
|
|
209
|
+
|
|
210
|
+
// Build adjacency for BFS (local scope)
|
|
211
|
+
const adjacency = useMemo(() => {
|
|
212
|
+
if (!graphData) return null;
|
|
213
|
+
const adj = new Map<string, Set<string>>();
|
|
214
|
+
for (const e of graphData.edges) {
|
|
215
|
+
if (!adj.has(e.source)) adj.set(e.source, new Set());
|
|
216
|
+
if (!adj.has(e.target)) adj.set(e.target, new Set());
|
|
217
|
+
adj.get(e.source)!.add(e.target);
|
|
218
|
+
adj.get(e.target)!.add(e.source);
|
|
219
|
+
}
|
|
220
|
+
return adj;
|
|
221
|
+
}, [graphData]);
|
|
222
|
+
|
|
223
|
+
// Scope filter
|
|
224
|
+
const { filteredNodes, filteredEdges } = useMemo(() => {
|
|
225
|
+
if (!graphData) return { filteredNodes: [], filteredEdges: [] };
|
|
226
|
+
|
|
227
|
+
let nodeSubset: GraphNode[];
|
|
228
|
+
let edgeSubset: GraphEdge[];
|
|
229
|
+
|
|
230
|
+
if (scope === 'global') {
|
|
231
|
+
nodeSubset = graphData.nodes;
|
|
232
|
+
edgeSubset = graphData.edges;
|
|
233
|
+
} else {
|
|
234
|
+
// local: BFS 2 hops
|
|
235
|
+
const visited = new Set<string>();
|
|
236
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: filePath, depth: 0 }];
|
|
237
|
+
visited.add(filePath);
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const { id, depth } = queue.shift()!;
|
|
240
|
+
if (depth >= 2) continue;
|
|
241
|
+
const neighbors = adjacency?.get(id) ?? new Set<string>();
|
|
242
|
+
for (const nb of neighbors) {
|
|
243
|
+
if (!visited.has(nb)) {
|
|
244
|
+
visited.add(nb);
|
|
245
|
+
queue.push({ id: nb, depth: depth + 1 });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
nodeSubset = graphData.nodes.filter(n => visited.has(n.id));
|
|
250
|
+
const nodeIds = new Set(nodeSubset.map(n => n.id));
|
|
251
|
+
edgeSubset = graphData.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { filteredNodes: nodeSubset, filteredEdges: edgeSubset };
|
|
255
|
+
}, [graphData, scope, filePath, adjacency]);
|
|
256
|
+
|
|
257
|
+
// Orphan detection (nodes with no edges in the current subset)
|
|
258
|
+
const connectedIds = useMemo(() => {
|
|
259
|
+
const s = new Set<string>();
|
|
260
|
+
for (const e of filteredEdges) {
|
|
261
|
+
s.add(e.source);
|
|
262
|
+
s.add(e.target);
|
|
263
|
+
}
|
|
264
|
+
return s;
|
|
265
|
+
}, [filteredEdges]);
|
|
266
|
+
|
|
267
|
+
// Compute layout + build RF nodes/edges
|
|
268
|
+
const { rfNodes, rfEdges } = useMemo(() => {
|
|
269
|
+
if (filteredNodes.length === 0) return { rfNodes: [], rfEdges: [] };
|
|
270
|
+
|
|
271
|
+
const nodeIds = filteredNodes.map(n => n.id);
|
|
272
|
+
const layout = forceLayout(nodeIds, filteredEdges);
|
|
273
|
+
|
|
274
|
+
const rfNodes = filteredNodes.map(n => ({
|
|
275
|
+
id: n.id,
|
|
276
|
+
type: 'wiki' as const,
|
|
277
|
+
position: layout[n.id] ?? { x: 0, y: 0 },
|
|
278
|
+
data: {
|
|
279
|
+
label: n.label,
|
|
280
|
+
id: n.id,
|
|
281
|
+
isCurrent: n.id === filePath,
|
|
282
|
+
isOrphan: !connectedIds.has(n.id),
|
|
283
|
+
size: degrees.get(n.id) || 1,
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
|
|
287
|
+
const rfEdges = filteredEdges.map((e, i) => {
|
|
288
|
+
const isRelatedToCurrent = e.source === filePath || e.target === filePath;
|
|
289
|
+
return {
|
|
290
|
+
id: `e-${i}`,
|
|
291
|
+
source: e.source,
|
|
292
|
+
target: e.target,
|
|
293
|
+
type: 'default' as const, // Curved default
|
|
294
|
+
markerEnd: { type: 'arrowclosed' as const, color: isRelatedToCurrent ? 'var(--amber)' : 'var(--border)' },
|
|
295
|
+
style: {
|
|
296
|
+
stroke: isRelatedToCurrent ? 'var(--amber)' : 'var(--border)',
|
|
297
|
+
strokeWidth: isRelatedToCurrent ? 1.5 : 1,
|
|
298
|
+
opacity: isRelatedToCurrent ? 0.8 : 0.4,
|
|
299
|
+
},
|
|
300
|
+
animated: isRelatedToCurrent,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return { rfNodes, rfEdges };
|
|
305
|
+
}, [filteredNodes, filteredEdges, filePath, connectedIds, degrees]);
|
|
306
|
+
|
|
307
|
+
const nodeTypes = useMemo(() => ({ wiki: WikiNode }), []);
|
|
308
|
+
|
|
309
|
+
const scopeButtons: { id: Scope; label: string }[] = [
|
|
310
|
+
{ id: 'local', label: 'Local' },
|
|
311
|
+
{ id: 'global', label: 'Global' },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
if (!mounted || loading) {
|
|
315
|
+
return (
|
|
316
|
+
<div
|
|
317
|
+
style={{
|
|
318
|
+
width: '100%',
|
|
319
|
+
height: 'calc(100vh - 160px)',
|
|
320
|
+
minHeight: 400,
|
|
321
|
+
borderRadius: 12,
|
|
322
|
+
background: 'var(--muted)',
|
|
323
|
+
border: '1px solid var(--border)',
|
|
324
|
+
display: 'flex',
|
|
325
|
+
alignItems: 'center',
|
|
326
|
+
justifyContent: 'center',
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<span style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace", fontSize: 12 }}>
|
|
330
|
+
{loading ? 'Building graph…' : 'Loading…'}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div style={{ width: '100%', position: 'relative', zIndex: 0 }}>
|
|
338
|
+
{/* Toolbar */}
|
|
339
|
+
<div
|
|
340
|
+
style={{
|
|
341
|
+
display: 'flex',
|
|
342
|
+
alignItems: 'center',
|
|
343
|
+
gap: 12,
|
|
344
|
+
marginBottom: 10,
|
|
345
|
+
flexWrap: 'wrap',
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<span
|
|
349
|
+
style={{
|
|
350
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
351
|
+
fontSize: 11,
|
|
352
|
+
color: 'var(--muted-foreground)',
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{filteredNodes.length} nodes · {filteredEdges.length} edges
|
|
356
|
+
</span>
|
|
357
|
+
|
|
358
|
+
<div
|
|
359
|
+
style={{
|
|
360
|
+
display: 'flex',
|
|
361
|
+
gap: 2,
|
|
362
|
+
padding: 3,
|
|
363
|
+
borderRadius: 8,
|
|
364
|
+
background: 'var(--muted)',
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
{scopeButtons.map(btn => (
|
|
368
|
+
<button
|
|
369
|
+
key={btn.id}
|
|
370
|
+
onClick={() => setScope(btn.id)}
|
|
371
|
+
style={{
|
|
372
|
+
padding: '3px 12px',
|
|
373
|
+
borderRadius: 5,
|
|
374
|
+
fontSize: 11,
|
|
375
|
+
fontFamily: "'IBM Plex Mono', monospace",
|
|
376
|
+
cursor: 'pointer',
|
|
377
|
+
border: 'none',
|
|
378
|
+
outline: 'none',
|
|
379
|
+
background: scope === btn.id ? 'var(--card)' : 'transparent',
|
|
380
|
+
color: scope === btn.id ? 'var(--foreground)' : 'var(--muted-foreground)',
|
|
381
|
+
boxShadow: scope === btn.id ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
|
|
382
|
+
transition: 'all 0.1s',
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
{btn.label}
|
|
386
|
+
</button>
|
|
387
|
+
))}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{/* React Flow */}
|
|
392
|
+
<div
|
|
393
|
+
style={{
|
|
394
|
+
width: '100%',
|
|
395
|
+
height: 'calc(100vh - 160px)',
|
|
396
|
+
minHeight: 400,
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
<ReactFlow
|
|
400
|
+
nodes={rfNodes}
|
|
401
|
+
edges={rfEdges}
|
|
402
|
+
nodeTypes={nodeTypes}
|
|
403
|
+
fitView
|
|
404
|
+
fitViewOptions={{ padding: 0.15 }}
|
|
405
|
+
proOptions={{ hideAttribution: true }}
|
|
406
|
+
style={{
|
|
407
|
+
background: 'var(--background)',
|
|
408
|
+
borderRadius: 12,
|
|
409
|
+
border: '1px solid var(--border)',
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
<Background
|
|
413
|
+
color="var(--border)"
|
|
414
|
+
gap={24}
|
|
415
|
+
size={1}
|
|
416
|
+
variant={BackgroundVariant.Dots}
|
|
417
|
+
/>
|
|
418
|
+
<Controls showInteractive={false} />
|
|
419
|
+
<MiniMap
|
|
420
|
+
nodeColor={(n) =>
|
|
421
|
+
(n.data as WikiNodeData)?.isCurrent ? 'var(--amber)' : 'var(--muted-foreground)'
|
|
422
|
+
}
|
|
423
|
+
/>
|
|
424
|
+
</ReactFlow>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { Sparkles, RefreshCw, Clock, FileText } from 'lucide-react';
|
|
5
|
+
import { encodePath } from '@/lib/utils';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
8
|
+
|
|
9
|
+
interface RecentFile {
|
|
10
|
+
path: string;
|
|
11
|
+
mtime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function relativeTime(mtime: number): string {
|
|
15
|
+
const diff = Date.now() - mtime;
|
|
16
|
+
const m = Math.floor(diff / 60000);
|
|
17
|
+
const h = Math.floor(diff / 3600000);
|
|
18
|
+
const d = Math.floor(diff / 86400000);
|
|
19
|
+
if (m < 1) return 'just now';
|
|
20
|
+
if (m < 60) return `${m}m ago`;
|
|
21
|
+
if (h < 24) return `${h}h ago`;
|
|
22
|
+
return `${d}d ago`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function basename(p: string) {
|
|
26
|
+
return p.split('/').pop()?.replace(/\.(md|csv)$/, '') ?? p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Minimal markdown→HTML for the streamed summary
|
|
30
|
+
function renderMarkdown(md: string): string {
|
|
31
|
+
return md
|
|
32
|
+
.replace(/^### (.+)$/gm, '<h3 style="font-size:.8rem;font-weight:600;color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em;margin:1em 0 .3em">$1</h3>')
|
|
33
|
+
.replace(/^## (.+)$/gm, '<h2 style="font-size:.9rem;font-weight:700;color:var(--foreground);margin:1.2em 0 .4em">$1</h2>')
|
|
34
|
+
.replace(/^# (.+)$/gm, '<h1 style="font-size:1rem;font-weight:700;color:var(--foreground);margin:1.2em 0 .4em">$1</h1>')
|
|
35
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
36
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
37
|
+
.replace(/`(.+?)`/g, '<code style="font-family:\'IBM Plex Mono\',monospace;font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>')
|
|
38
|
+
.replace(/^[-*] (.+)$/gm, '<li style="margin:.2em 0;padding-left:.3em">$1</li>')
|
|
39
|
+
.replace(/(<li[^>]*>.*<\/li>\n?)+/g, s => `<ul style="margin:.4em 0;padding-left:1.4em;list-style:disc">${s}</ul>`)
|
|
40
|
+
.replace(/\n{2,}/g, '</p><p style="margin:.5em 0;font-size:.85rem;line-height:1.7;color:var(--foreground)">')
|
|
41
|
+
.replace(/^(?!<[hulo])(.+)$/gm, '<p style="margin:.5em 0;font-size:.85rem;line-height:1.7;color:var(--foreground)">$1</p>');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LIMIT = 8;
|
|
45
|
+
|
|
46
|
+
export function SummaryRenderer({ filePath }: RendererContext) {
|
|
47
|
+
const [recentFiles, setRecentFiles] = useState<RecentFile[]>([]);
|
|
48
|
+
const [summary, setSummary] = useState('');
|
|
49
|
+
const [streaming, setStreaming] = useState(false);
|
|
50
|
+
const [error, setError] = useState('');
|
|
51
|
+
const [generated, setGenerated] = useState(false);
|
|
52
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
53
|
+
|
|
54
|
+
// Fetch recent files once
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
apiFetch<RecentFile[]>(`/api/recent-files?limit=${LIMIT}`)
|
|
57
|
+
.then((data) => setRecentFiles(data.filter(f => f.path.endsWith('.md'))))
|
|
58
|
+
.catch(() => {});
|
|
59
|
+
}, [filePath]);
|
|
60
|
+
|
|
61
|
+
async function generate() {
|
|
62
|
+
if (recentFiles.length === 0) return;
|
|
63
|
+
abortRef.current?.abort();
|
|
64
|
+
const ctrl = new AbortController();
|
|
65
|
+
abortRef.current = ctrl;
|
|
66
|
+
|
|
67
|
+
setSummary('');
|
|
68
|
+
setError('');
|
|
69
|
+
setStreaming(true);
|
|
70
|
+
setGenerated(false);
|
|
71
|
+
|
|
72
|
+
const attachedFiles = recentFiles.map(f => f.path);
|
|
73
|
+
const fileListMd = recentFiles
|
|
74
|
+
.map(f => `- **${basename(f.path)}** (${f.path}, modified ${relativeTime(f.mtime)})`)
|
|
75
|
+
.join('\n');
|
|
76
|
+
|
|
77
|
+
const prompt = `You are summarizing recent changes in a personal knowledge base (MindOS).
|
|
78
|
+
|
|
79
|
+
The following files were recently modified:
|
|
80
|
+
${fileListMd}
|
|
81
|
+
|
|
82
|
+
Please provide a concise daily briefing in this format:
|
|
83
|
+
1. **Key changes**: What was added or updated in each file (1–2 sentences per file)
|
|
84
|
+
2. **Themes**: Any patterns or recurring topics across the changes
|
|
85
|
+
3. **Suggested next actions**: 2–3 actionable follow-ups based on the content
|
|
86
|
+
|
|
87
|
+
Be specific. Reference actual content from the files. Keep the total response under 300 words.`;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch('/api/ask', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
messages: [{ role: 'user', content: prompt }],
|
|
95
|
+
attachedFiles,
|
|
96
|
+
}),
|
|
97
|
+
signal: ctrl.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
101
|
+
if (!res.body) throw new Error('No response body');
|
|
102
|
+
|
|
103
|
+
const reader = res.body.getReader();
|
|
104
|
+
const decoder = new TextDecoder();
|
|
105
|
+
let acc = '';
|
|
106
|
+
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
// Vercel AI SDK text stream: each chunk may have "0:..." prefix
|
|
111
|
+
const raw = decoder.decode(value, { stream: true });
|
|
112
|
+
for (const line of raw.split('\n')) {
|
|
113
|
+
const m = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
114
|
+
if (m) {
|
|
115
|
+
acc += m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
116
|
+
} else if (line && !line.startsWith('d:') && !line.startsWith('e:') && !line.startsWith('0:')) {
|
|
117
|
+
// plain text stream fallback
|
|
118
|
+
acc += line;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setSummary(acc);
|
|
122
|
+
}
|
|
123
|
+
setGenerated(true);
|
|
124
|
+
} catch (err: unknown) {
|
|
125
|
+
if (err instanceof Error && err.name !== 'AbortError') {
|
|
126
|
+
setError(err.message || 'Failed to generate summary');
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
setStreaming(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
useEffect(() => () => { abortRef.current?.abort(); }, []);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
|
|
137
|
+
{/* header row */}
|
|
138
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
|
139
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
|
|
140
|
+
{recentFiles.length > 0
|
|
141
|
+
? `${recentFiles.length} recently modified files`
|
|
142
|
+
: 'Loading recent files…'}
|
|
143
|
+
</span>
|
|
144
|
+
<button
|
|
145
|
+
onClick={generate}
|
|
146
|
+
disabled={streaming || recentFiles.length === 0}
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
gap: 6,
|
|
151
|
+
padding: '5px 14px',
|
|
152
|
+
borderRadius: 7,
|
|
153
|
+
fontSize: 12,
|
|
154
|
+
fontFamily: "'IBM Plex Mono',monospace",
|
|
155
|
+
cursor: streaming || recentFiles.length === 0 ? 'not-allowed' : 'pointer',
|
|
156
|
+
border: 'none',
|
|
157
|
+
background: streaming ? 'var(--muted)' : 'var(--amber)',
|
|
158
|
+
color: streaming ? 'var(--muted-foreground)' : '#131210',
|
|
159
|
+
opacity: recentFiles.length === 0 ? 0.5 : 1,
|
|
160
|
+
transition: 'opacity .15s',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{streaming ? (
|
|
164
|
+
<RefreshCw size={12} style={{ animation: 'spin 1s linear infinite' }} />
|
|
165
|
+
) : (
|
|
166
|
+
<Sparkles size={12} />
|
|
167
|
+
)}
|
|
168
|
+
{streaming ? 'Generating…' : generated ? 'Regenerate' : 'Generate briefing'}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* source files */}
|
|
173
|
+
{recentFiles.length > 0 && (
|
|
174
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: '1.5rem' }}>
|
|
175
|
+
{recentFiles.map(f => (
|
|
176
|
+
<a
|
|
177
|
+
key={f.path}
|
|
178
|
+
href={`/view/${encodePath(f.path)}`}
|
|
179
|
+
style={{
|
|
180
|
+
display: 'inline-flex',
|
|
181
|
+
alignItems: 'center',
|
|
182
|
+
gap: 5,
|
|
183
|
+
padding: '3px 10px',
|
|
184
|
+
borderRadius: 999,
|
|
185
|
+
fontSize: '0.7rem',
|
|
186
|
+
fontFamily: "'IBM Plex Mono',monospace",
|
|
187
|
+
background: 'var(--muted)',
|
|
188
|
+
color: 'var(--muted-foreground)',
|
|
189
|
+
textDecoration: 'none',
|
|
190
|
+
border: '1px solid var(--border)',
|
|
191
|
+
transition: 'color .15s',
|
|
192
|
+
}}
|
|
193
|
+
onMouseEnter={e => (e.currentTarget.style.color = 'var(--foreground)')}
|
|
194
|
+
onMouseLeave={e => (e.currentTarget.style.color = 'var(--muted-foreground)')}
|
|
195
|
+
title={f.path}
|
|
196
|
+
>
|
|
197
|
+
<FileText size={10} />
|
|
198
|
+
{basename(f.path)}
|
|
199
|
+
<span style={{ opacity: 0.5 }}>
|
|
200
|
+
<Clock size={9} style={{ display: 'inline', marginLeft: 2 }} />
|
|
201
|
+
{' '}{relativeTime(f.mtime)}
|
|
202
|
+
</span>
|
|
203
|
+
</a>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* error */}
|
|
209
|
+
{error && (
|
|
210
|
+
<div style={{ padding: '10px 14px', borderRadius: 8, background: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12, marginBottom: '1rem' }}>
|
|
211
|
+
{error}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* summary output */}
|
|
216
|
+
{summary ? (
|
|
217
|
+
<div style={{
|
|
218
|
+
background: 'var(--card)',
|
|
219
|
+
border: '1px solid var(--border)',
|
|
220
|
+
borderRadius: 10,
|
|
221
|
+
padding: '18px 20px',
|
|
222
|
+
position: 'relative',
|
|
223
|
+
}}>
|
|
224
|
+
{streaming && (
|
|
225
|
+
<div style={{ position: 'absolute', top: 12, right: 14, width: 6, height: 6, borderRadius: '50%', background: 'var(--amber)', animation: 'pulse 1.2s ease-in-out infinite' }} />
|
|
226
|
+
)}
|
|
227
|
+
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(summary) }} />
|
|
228
|
+
</div>
|
|
229
|
+
) : !streaming && !generated && recentFiles.length > 0 ? (
|
|
230
|
+
<div style={{
|
|
231
|
+
border: '1px dashed var(--border)',
|
|
232
|
+
borderRadius: 10,
|
|
233
|
+
padding: '2.5rem 1.5rem',
|
|
234
|
+
textAlign: 'center',
|
|
235
|
+
color: 'var(--muted-foreground)',
|
|
236
|
+
}}>
|
|
237
|
+
<Sparkles size={28} style={{ margin: '0 auto 10px', opacity: 0.3, color: 'var(--amber)' }} />
|
|
238
|
+
<p style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
|
|
239
|
+
Click <strong style={{ color: 'var(--foreground)' }}>Generate briefing</strong> to summarize recent changes with AI.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
|
|
244
|
+
{/* CSS keyframes injected inline */}
|
|
245
|
+
<style>{`
|
|
246
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
247
|
+
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
|
|
248
|
+
`}</style>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|