@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,236 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { Check, Loader2, Plus, X } from 'lucide-react';
|
|
5
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
+
|
|
7
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | { [k: string]: JsonValue };
|
|
8
|
+
|
|
9
|
+
type UiSection = {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
fields: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type KeySpec = {
|
|
17
|
+
type?: string;
|
|
18
|
+
control?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
constraints?: { min?: number; max?: number };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ConfigSchema = {
|
|
25
|
+
uiSchema?: { sections?: UiSection[] };
|
|
26
|
+
keySpecs?: Record<string, KeySpec>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getByPath(obj: unknown, path: string): JsonValue {
|
|
30
|
+
const parts = path.split('.');
|
|
31
|
+
let cur: unknown = obj;
|
|
32
|
+
for (const p of parts) {
|
|
33
|
+
if (!cur || typeof cur !== 'object' || !(p in (cur as Record<string, unknown>))) return null;
|
|
34
|
+
cur = (cur as Record<string, unknown>)[p];
|
|
35
|
+
}
|
|
36
|
+
return cur as JsonValue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setByPath(obj: Record<string, unknown>, path: string, value: JsonValue): Record<string, unknown> {
|
|
40
|
+
const parts = path.split('.');
|
|
41
|
+
const next = structuredClone(obj) as Record<string, unknown>;
|
|
42
|
+
let cur: Record<string, unknown> = next;
|
|
43
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
44
|
+
const k = parts[i];
|
|
45
|
+
const v = cur[k];
|
|
46
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) cur[k] = {};
|
|
47
|
+
cur = cur[k] as Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
cur[parts[parts.length - 1]] = value;
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ConfigRenderer({ content, saveAction }: RendererContext) {
|
|
54
|
+
const parsed = useMemo(() => {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}, [content]);
|
|
61
|
+
|
|
62
|
+
const [data, setData] = useState<Record<string, unknown> | null>(parsed);
|
|
63
|
+
const [saving, setSaving] = useState(false);
|
|
64
|
+
const [saved, setSaved] = useState(false);
|
|
65
|
+
const [error, setError] = useState('');
|
|
66
|
+
const [tagInput, setTagInput] = useState<Record<string, string>>({});
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setData(parsed);
|
|
70
|
+
setError('');
|
|
71
|
+
}, [parsed]);
|
|
72
|
+
|
|
73
|
+
const schema = (data ?? {}) as ConfigSchema;
|
|
74
|
+
const sections = schema.uiSchema?.sections ?? [];
|
|
75
|
+
const keySpecs = schema.keySpecs ?? {};
|
|
76
|
+
|
|
77
|
+
const persist = useCallback(async (next: Record<string, unknown>) => {
|
|
78
|
+
setSaving(true);
|
|
79
|
+
setSaved(false);
|
|
80
|
+
setError('');
|
|
81
|
+
try {
|
|
82
|
+
await saveAction(`${JSON.stringify(next, null, 2)}\n`);
|
|
83
|
+
setSaved(true);
|
|
84
|
+
setTimeout(() => setSaved(false), 1200);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
setError(e instanceof Error ? e.message : 'Failed to save');
|
|
87
|
+
} finally {
|
|
88
|
+
setSaving(false);
|
|
89
|
+
}
|
|
90
|
+
}, [saveAction]);
|
|
91
|
+
|
|
92
|
+
const updateValue = useCallback(async (path: string, value: JsonValue) => {
|
|
93
|
+
if (!data) return;
|
|
94
|
+
const next = setByPath(data, path, value);
|
|
95
|
+
setData(next);
|
|
96
|
+
await persist(next);
|
|
97
|
+
}, [data, persist]);
|
|
98
|
+
|
|
99
|
+
if (!parsed || !data) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="rounded-xl border border-border p-4 text-sm text-red-400">
|
|
102
|
+
CONFIG.json parse failed. Please check JSON format.
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="max-w-3xl mx-auto flex flex-col gap-3">
|
|
109
|
+
<div className="flex items-center justify-between rounded-xl border border-border bg-card px-4 py-2.5">
|
|
110
|
+
<div className="text-xs text-muted-foreground">CONFIG Control Panel</div>
|
|
111
|
+
<div className="text-xs flex items-center gap-2">
|
|
112
|
+
{saving && <span className="inline-flex items-center gap-1 text-muted-foreground"><Loader2 size={12} className="animate-spin" />Saving</span>}
|
|
113
|
+
{!saving && saved && <span className="inline-flex items-center gap-1" style={{ color: '#7aad80' }}><Check size={12} />Saved</span>}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{error && <div className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">{error}</div>}
|
|
118
|
+
|
|
119
|
+
{sections.map((section) => (
|
|
120
|
+
<div key={section.id} className="rounded-xl border border-border bg-card p-4">
|
|
121
|
+
<div className="mb-3">
|
|
122
|
+
<h3 className="text-sm font-semibold text-foreground">{section.title}</h3>
|
|
123
|
+
{section.description && <p className="text-xs text-muted-foreground mt-1">{section.description}</p>}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="space-y-3">
|
|
127
|
+
{section.fields.map((fieldPath) => {
|
|
128
|
+
const spec = keySpecs[fieldPath] ?? {};
|
|
129
|
+
const value = getByPath(data, fieldPath);
|
|
130
|
+
const label = spec.label || fieldPath;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div key={fieldPath} className="rounded-lg border border-border/80 bg-background px-3 py-2.5">
|
|
134
|
+
<div className="flex items-center justify-between gap-3 mb-1.5">
|
|
135
|
+
<div>
|
|
136
|
+
<div className="text-sm text-foreground">{label}</div>
|
|
137
|
+
{spec.description && <div className="text-xs text-muted-foreground mt-0.5">{spec.description}</div>}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{spec.control === 'switch' && typeof value === 'boolean' && (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => updateValue(fieldPath, !value)}
|
|
144
|
+
className="px-2.5 py-1 rounded-md text-xs font-medium"
|
|
145
|
+
style={{
|
|
146
|
+
background: value ? 'var(--amber)' : 'var(--muted)',
|
|
147
|
+
color: value ? '#131210' : 'var(--muted-foreground)',
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{value ? 'ON' : 'OFF'}
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{spec.control === 'text' && typeof value === 'string' && (
|
|
156
|
+
<input
|
|
157
|
+
defaultValue={value}
|
|
158
|
+
onBlur={(e) => updateValue(fieldPath, e.target.value)}
|
|
159
|
+
className="w-full px-2 py-1.5 text-sm bg-card border border-border rounded text-foreground"
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{spec.control === 'number' && typeof value === 'number' && (
|
|
164
|
+
<input
|
|
165
|
+
type="number"
|
|
166
|
+
defaultValue={value}
|
|
167
|
+
min={spec.constraints?.min}
|
|
168
|
+
max={spec.constraints?.max}
|
|
169
|
+
onBlur={(e) => {
|
|
170
|
+
const raw = Number(e.target.value);
|
|
171
|
+
const min = spec.constraints?.min;
|
|
172
|
+
const max = spec.constraints?.max;
|
|
173
|
+
let next = Number.isFinite(raw) ? raw : value;
|
|
174
|
+
if (typeof min === 'number' && next < min) next = min;
|
|
175
|
+
if (typeof max === 'number' && next > max) next = max;
|
|
176
|
+
updateValue(fieldPath, next);
|
|
177
|
+
}}
|
|
178
|
+
className="w-full px-2 py-1.5 text-sm bg-card border border-border rounded text-foreground"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{spec.control === 'tag-list' && Array.isArray(value) && (
|
|
183
|
+
<div className="flex flex-col gap-2">
|
|
184
|
+
<div className="flex flex-wrap gap-1.5">
|
|
185
|
+
{value.map((tag, idx) => (
|
|
186
|
+
<span key={`${String(tag)}-${idx}`} className="inline-flex items-center gap-1 px-2 py-0.5 rounded border border-border text-xs bg-muted text-foreground">
|
|
187
|
+
{String(tag)}
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onClick={() => updateValue(fieldPath, value.filter((_, i) => i !== idx))}
|
|
191
|
+
className="text-muted-foreground hover:text-foreground"
|
|
192
|
+
>
|
|
193
|
+
<X size={11} />
|
|
194
|
+
</button>
|
|
195
|
+
</span>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<input
|
|
200
|
+
value={tagInput[fieldPath] ?? ''}
|
|
201
|
+
onChange={(e) => setTagInput((prev) => ({ ...prev, [fieldPath]: e.target.value }))}
|
|
202
|
+
onKeyDown={(e) => {
|
|
203
|
+
if (e.key !== 'Enter') return;
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
const nextTag = (tagInput[fieldPath] ?? '').trim();
|
|
206
|
+
if (!nextTag) return;
|
|
207
|
+
updateValue(fieldPath, [...value, nextTag]);
|
|
208
|
+
setTagInput((prev) => ({ ...prev, [fieldPath]: '' }));
|
|
209
|
+
}}
|
|
210
|
+
className="flex-1 px-2 py-1.5 text-sm bg-card border border-border rounded text-foreground"
|
|
211
|
+
placeholder="Add item"
|
|
212
|
+
/>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
onClick={() => {
|
|
216
|
+
const nextTag = (tagInput[fieldPath] ?? '').trim();
|
|
217
|
+
if (!nextTag) return;
|
|
218
|
+
updateValue(fieldPath, [...value, nextTag]);
|
|
219
|
+
setTagInput((prev) => ({ ...prev, [fieldPath]: '' }));
|
|
220
|
+
}}
|
|
221
|
+
className="px-2.5 py-1.5 rounded-md text-xs bg-muted text-foreground inline-flex items-center gap-1"
|
|
222
|
+
>
|
|
223
|
+
<Plus size={12} /> Add
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
4
|
+
import { LayoutGrid, Columns, Table2, Settings2 } from 'lucide-react';
|
|
5
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
+
import type { ViewType, CsvConfig } from './csv/types';
|
|
7
|
+
import { defaultConfig, loadConfig, saveConfig, parseCSV } from './csv/types';
|
|
8
|
+
import { TableView } from './csv/TableView';
|
|
9
|
+
import { GalleryView } from './csv/GalleryView';
|
|
10
|
+
import { BoardView } from './csv/BoardView';
|
|
11
|
+
import { ConfigPanel } from './csv/ConfigPanel';
|
|
12
|
+
|
|
13
|
+
const VIEW_TABS: { id: ViewType; icon: React.ReactNode; label: string }[] = [
|
|
14
|
+
{ id: 'table', icon: <Table2 size={13} />, label: 'Table' },
|
|
15
|
+
{ id: 'gallery', icon: <LayoutGrid size={13} />, label: 'Gallery' },
|
|
16
|
+
{ id: 'board', icon: <Columns size={13} />, label: 'Board' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function CsvRenderer({ filePath, content, saveAction }: RendererContext) {
|
|
20
|
+
const { headers, rows } = useMemo(() => parseCSV(content), [content]);
|
|
21
|
+
const [cfg, setCfg] = useState<CsvConfig>(() => defaultConfig(headers));
|
|
22
|
+
const [configLoaded, setConfigLoaded] = useState(false);
|
|
23
|
+
const [showConfig, setShowConfig] = useState(false);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setCfg(loadConfig(filePath, headers));
|
|
27
|
+
setConfigLoaded(true);
|
|
28
|
+
}, [filePath]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
29
|
+
|
|
30
|
+
const updateConfig = useCallback((next: CsvConfig) => {
|
|
31
|
+
setCfg(next);
|
|
32
|
+
saveConfig(filePath, next);
|
|
33
|
+
}, [filePath]);
|
|
34
|
+
|
|
35
|
+
if (!configLoaded) return null;
|
|
36
|
+
const view = cfg.activeView;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="max-w-[1100px] mx-auto px-0 py-2">
|
|
40
|
+
{/* Toolbar */}
|
|
41
|
+
<div className="flex items-center gap-2 mb-4 relative">
|
|
42
|
+
<div className="flex items-center gap-0.5 p-1 rounded-lg" style={{ background: 'var(--muted)' }}>
|
|
43
|
+
{VIEW_TABS.map(tab => (
|
|
44
|
+
<button key={tab.id} onClick={() => updateConfig({ ...cfg, activeView: tab.id })}
|
|
45
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors"
|
|
46
|
+
style={{
|
|
47
|
+
fontFamily: "'IBM Plex Mono',monospace",
|
|
48
|
+
background: view === tab.id ? 'var(--card)' : 'transparent',
|
|
49
|
+
color: view === tab.id ? 'var(--foreground)' : 'var(--muted-foreground)',
|
|
50
|
+
boxShadow: view === tab.id ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
|
|
51
|
+
}}
|
|
52
|
+
>{tab.icon}{tab.label}</button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex-1" />
|
|
56
|
+
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", opacity: 0.5 }}>
|
|
57
|
+
{rows.length} rows
|
|
58
|
+
</span>
|
|
59
|
+
<div className="relative">
|
|
60
|
+
<button onClick={() => setShowConfig(v => !v)}
|
|
61
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs transition-colors"
|
|
62
|
+
style={{ background: showConfig ? 'var(--accent)' : 'var(--muted)', color: showConfig ? 'var(--foreground)' : 'var(--muted-foreground)' }}
|
|
63
|
+
title="View settings"
|
|
64
|
+
><Settings2 size={13} /></button>
|
|
65
|
+
{showConfig && (
|
|
66
|
+
<ConfigPanel headers={headers} cfg={cfg} view={view}
|
|
67
|
+
onClose={() => setShowConfig(false)} onChange={updateConfig} />
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{view === 'table' && <TableView headers={headers} rows={rows} cfg={cfg.table} saveAction={saveAction} />}
|
|
73
|
+
{view === 'gallery' && <GalleryView headers={headers} rows={rows} cfg={cfg.gallery} />}
|
|
74
|
+
{view === 'board' && <BoardView headers={headers} rows={rows} cfg={cfg.board} saveAction={saveAction} />}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { GitCompare, CheckCircle2, XCircle, FileEdit, ChevronDown } from 'lucide-react';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
8
|
+
|
|
9
|
+
// ─── Diff entry format ────────────────────────────────────────────────────────
|
|
10
|
+
// Agent writes diff entries as fenced blocks:
|
|
11
|
+
//
|
|
12
|
+
// ```agent-diff
|
|
13
|
+
// { "ts": "2025-01-15T10:30:00Z", "path": "Profile/Identity.md",
|
|
14
|
+
// "tool": "mindos_write_file",
|
|
15
|
+
// "before": "...full old content...",
|
|
16
|
+
// "after": "...full new content..." }
|
|
17
|
+
// ```
|
|
18
|
+
|
|
19
|
+
interface DiffEntry {
|
|
20
|
+
ts: string;
|
|
21
|
+
path: string;
|
|
22
|
+
tool: string;
|
|
23
|
+
before: string;
|
|
24
|
+
after: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function parseDiffs(content: string): DiffEntry[] {
|
|
30
|
+
const entries: DiffEntry[] = [];
|
|
31
|
+
const re = /```agent-diff\n([\s\S]*?)```/g;
|
|
32
|
+
let m: RegExpExecArray | null;
|
|
33
|
+
while ((m = re.exec(content)) !== null) {
|
|
34
|
+
try {
|
|
35
|
+
const entry = JSON.parse(m[1].trim()) as DiffEntry;
|
|
36
|
+
if (entry.path && entry.ts) entries.push(entry);
|
|
37
|
+
} catch { /* skip */ }
|
|
38
|
+
}
|
|
39
|
+
return entries.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Diff algorithm (line-level Myers-light) ──────────────────────────────────
|
|
43
|
+
|
|
44
|
+
type LineChange = { type: 'equal' | 'insert' | 'delete'; text: string };
|
|
45
|
+
|
|
46
|
+
function diffLines(oldText: string, newText: string): LineChange[] {
|
|
47
|
+
const oldLines = oldText.split('\n');
|
|
48
|
+
const newLines = newText.split('\n');
|
|
49
|
+
|
|
50
|
+
// LCS-based diff (simple patience-like for short files)
|
|
51
|
+
const result: LineChange[] = [];
|
|
52
|
+
|
|
53
|
+
// Build LCS table
|
|
54
|
+
const m = oldLines.length, n = newLines.length;
|
|
55
|
+
// For large files, limit context
|
|
56
|
+
if (m > 500 || n > 500) {
|
|
57
|
+
// Truncate and just show a summary
|
|
58
|
+
const added = newLines.filter(l => !oldLines.includes(l)).length;
|
|
59
|
+
const removed = oldLines.filter(l => !newLines.includes(l)).length;
|
|
60
|
+
result.push({ type: 'delete', text: `[... ${removed} lines removed, ${added} lines added — file too large for line diff ...]` });
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
65
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
66
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
67
|
+
if (oldLines[i] === newLines[j]) {
|
|
68
|
+
dp[i][j] = 1 + dp[i + 1][j + 1];
|
|
69
|
+
} else {
|
|
70
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let i = 0, j = 0;
|
|
76
|
+
while (i < m || j < n) {
|
|
77
|
+
if (i < m && j < n && oldLines[i] === newLines[j]) {
|
|
78
|
+
result.push({ type: 'equal', text: oldLines[i] });
|
|
79
|
+
i++; j++;
|
|
80
|
+
} else if (j < n && (i >= m || dp[i][j + 1] >= dp[i + 1][j])) {
|
|
81
|
+
result.push({ type: 'insert', text: newLines[j] });
|
|
82
|
+
j++;
|
|
83
|
+
} else {
|
|
84
|
+
result.push({ type: 'delete', text: oldLines[i] });
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Collapse long equal runs — show 3 context lines around changes
|
|
93
|
+
function collapseContext(changes: LineChange[], ctx = 3): Array<LineChange | { type: 'collapse'; count: number }> {
|
|
94
|
+
type AnyLine = LineChange | { type: 'collapse'; count: number };
|
|
95
|
+
const result: AnyLine[] = [];
|
|
96
|
+
const changed = new Set<number>();
|
|
97
|
+
|
|
98
|
+
changes.forEach((c, i) => { if (c.type !== 'equal') { for (let k = Math.max(0, i - ctx); k <= Math.min(changes.length - 1, i + ctx); k++) changed.add(k); } });
|
|
99
|
+
|
|
100
|
+
let skipStart = -1;
|
|
101
|
+
for (let i = 0; i < changes.length; i++) {
|
|
102
|
+
if (changed.has(i)) {
|
|
103
|
+
if (skipStart !== -1) {
|
|
104
|
+
result.push({ type: 'collapse', count: i - skipStart });
|
|
105
|
+
skipStart = -1;
|
|
106
|
+
}
|
|
107
|
+
result.push(changes[i]);
|
|
108
|
+
} else {
|
|
109
|
+
if (skipStart === -1) skipStart = i;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (skipStart !== -1) result.push({ type: 'collapse', count: changes.length - skipStart });
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function relativeTs(ts: string): string {
|
|
120
|
+
const diff = Date.now() - new Date(ts).getTime();
|
|
121
|
+
const m = Math.floor(diff / 60000);
|
|
122
|
+
const h = Math.floor(diff / 3600000);
|
|
123
|
+
const d = Math.floor(diff / 86400000);
|
|
124
|
+
if (m < 1) return 'just now';
|
|
125
|
+
if (m < 60) return `${m}m ago`;
|
|
126
|
+
if (h < 24) return `${h}h ago`;
|
|
127
|
+
return `${d}d ago`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stats(changes: LineChange[]): { added: number; removed: number } {
|
|
131
|
+
return {
|
|
132
|
+
added: changes.filter(c => c.type === 'insert').length,
|
|
133
|
+
removed: changes.filter(c => c.type === 'delete').length,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Diff card ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
function DiffCard({ entry, saveAction, fullContent }: {
|
|
140
|
+
entry: DiffEntry;
|
|
141
|
+
saveAction: (c: string) => Promise<void>;
|
|
142
|
+
fullContent: string;
|
|
143
|
+
}) {
|
|
144
|
+
const router = useRouter();
|
|
145
|
+
const [expanded, setExpanded] = useState(false);
|
|
146
|
+
const [approved, setApproved] = useState<boolean | null>(null);
|
|
147
|
+
|
|
148
|
+
const changes = useMemo(() => diffLines(entry.before, entry.after), [entry]);
|
|
149
|
+
const collapsed = useMemo(() => collapseContext(changes), [changes]);
|
|
150
|
+
const { added, removed } = stats(changes);
|
|
151
|
+
|
|
152
|
+
const toolShort = entry.tool.replace('mindos_', '');
|
|
153
|
+
|
|
154
|
+
async function handleApprove() {
|
|
155
|
+
setApproved(true);
|
|
156
|
+
// Mark this diff as approved by updating the block in the source file
|
|
157
|
+
const updated = fullContent.replace(
|
|
158
|
+
`"ts": "${entry.ts}", "path": "${entry.path}"`,
|
|
159
|
+
`"ts": "${entry.ts}", "path": "${entry.path}", "approved": true`,
|
|
160
|
+
);
|
|
161
|
+
await saveAction(updated);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleReject() {
|
|
165
|
+
setApproved(false);
|
|
166
|
+
// Revert: write the "before" content back to the target file
|
|
167
|
+
await apiFetch('/api/file', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({ op: 'save_file', path: entry.path, content: entry.before }),
|
|
171
|
+
});
|
|
172
|
+
const updated = fullContent.replace(
|
|
173
|
+
`"ts": "${entry.ts}", "path": "${entry.path}"`,
|
|
174
|
+
`"ts": "${entry.ts}", "path": "${entry.path}", "approved": false, "reverted": true`,
|
|
175
|
+
);
|
|
176
|
+
await saveAction(updated);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden', background: 'var(--card)', marginBottom: 10 }}>
|
|
181
|
+
{/* header */}
|
|
182
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '10px 14px', borderBottom: expanded ? '1px solid var(--border)' : 'none' }}>
|
|
183
|
+
<FileEdit size={13} style={{ color: 'var(--amber)', flexShrink: 0 }} />
|
|
184
|
+
<span
|
|
185
|
+
style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.78rem', color: 'var(--amber)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }}
|
|
186
|
+
onClick={() => router.push('/view/' + entry.path.split('/').map(encodeURIComponent).join('/'))}
|
|
187
|
+
title={entry.path}
|
|
188
|
+
>
|
|
189
|
+
{entry.path}
|
|
190
|
+
</span>
|
|
191
|
+
|
|
192
|
+
{/* diff stats */}
|
|
193
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: '#7aad80', flexShrink: 0 }}>+{added}</span>
|
|
194
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: '#c85050', flexShrink: 0 }}>−{removed}</span>
|
|
195
|
+
|
|
196
|
+
{/* tool badge */}
|
|
197
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.65rem', padding: '1px 7px', borderRadius: 999, background: 'var(--muted)', color: 'var(--muted-foreground)', flexShrink: 0 }}>
|
|
198
|
+
{toolShort}
|
|
199
|
+
</span>
|
|
200
|
+
|
|
201
|
+
{/* timestamp */}
|
|
202
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.65rem', color: 'var(--muted-foreground)', opacity: 0.6, flexShrink: 0 }}>
|
|
203
|
+
{relativeTs(entry.ts)}
|
|
204
|
+
</span>
|
|
205
|
+
|
|
206
|
+
{/* approve/reject — only if not yet decided */}
|
|
207
|
+
{approved === null ? (
|
|
208
|
+
<>
|
|
209
|
+
<button
|
|
210
|
+
onClick={handleApprove}
|
|
211
|
+
title="Approve this change"
|
|
212
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: '#7aad80', display: 'flex', alignItems: 'center' }}
|
|
213
|
+
>
|
|
214
|
+
<CheckCircle2 size={15} />
|
|
215
|
+
</button>
|
|
216
|
+
<button
|
|
217
|
+
onClick={handleReject}
|
|
218
|
+
title="Reject & revert this change"
|
|
219
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: '#c85050', display: 'flex', alignItems: 'center' }}
|
|
220
|
+
>
|
|
221
|
+
<XCircle size={15} />
|
|
222
|
+
</button>
|
|
223
|
+
</>
|
|
224
|
+
) : (
|
|
225
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: approved ? '#7aad80' : '#c85050' }}>
|
|
226
|
+
{approved ? '✓ approved' : '✕ reverted'}
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<button
|
|
231
|
+
onClick={() => setExpanded(v => !v)}
|
|
232
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center' }}
|
|
233
|
+
>
|
|
234
|
+
<ChevronDown size={13} style={{ transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
{/* diff view */}
|
|
239
|
+
{expanded && (
|
|
240
|
+
<div style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem', lineHeight: 1.5, overflowX: 'auto' }}>
|
|
241
|
+
{collapsed.map((line, i) => {
|
|
242
|
+
if (line.type === 'collapse') {
|
|
243
|
+
return (
|
|
244
|
+
<div key={i} style={{ padding: '2px 14px', background: 'var(--muted)', color: 'var(--muted-foreground)', opacity: 0.6, fontSize: '0.65rem' }}>
|
|
245
|
+
··· {line.count} unchanged lines ···
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const bg =
|
|
250
|
+
line.type === 'insert' ? 'rgba(122,173,128,0.12)' :
|
|
251
|
+
line.type === 'delete' ? 'rgba(200,80,80,0.10)' :
|
|
252
|
+
'transparent';
|
|
253
|
+
const color =
|
|
254
|
+
line.type === 'insert' ? '#7aad80' :
|
|
255
|
+
line.type === 'delete' ? '#c85050' :
|
|
256
|
+
'var(--muted-foreground)';
|
|
257
|
+
const prefix =
|
|
258
|
+
line.type === 'insert' ? '+' :
|
|
259
|
+
line.type === 'delete' ? '−' :
|
|
260
|
+
' ';
|
|
261
|
+
return (
|
|
262
|
+
<div key={i} style={{ display: 'flex', background: bg, borderLeft: line.type !== 'equal' ? `2px solid ${color}` : '2px solid transparent' }}>
|
|
263
|
+
<span style={{ width: 20, textAlign: 'center', color, opacity: 0.8, flexShrink: 0, userSelect: 'none' }}>{prefix}</span>
|
|
264
|
+
<span style={{ padding: '1px 8px 1px 0', color: line.type === 'equal' ? 'var(--muted-foreground)' : color, whiteSpace: 'pre', flex: 1 }}>
|
|
265
|
+
{line.text || ' '}
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
})}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Main renderer ────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
export function DiffRenderer({ content, saveAction }: RendererContext) {
|
|
279
|
+
const entries = useMemo(() => parseDiffs(content), [content]);
|
|
280
|
+
|
|
281
|
+
if (entries.length === 0) {
|
|
282
|
+
return (
|
|
283
|
+
<div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
|
|
284
|
+
<GitCompare size={28} style={{ margin: '0 auto 10px', opacity: 0.3 }} />
|
|
285
|
+
<p>No agent diffs logged yet.</p>
|
|
286
|
+
<p style={{ marginTop: 6, opacity: 0.6, fontSize: 11 }}>
|
|
287
|
+
Agent writes appear here as <code style={{ background: 'var(--muted)', padding: '1px 5px', borderRadius: 4 }}>```agent-diff</code> blocks.
|
|
288
|
+
</p>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const totalAdded = entries.reduce((acc, e) => acc + diffLines(e.before, e.after).filter(c => c.type === 'insert').length, 0);
|
|
294
|
+
const totalRemoved = entries.reduce((acc, e) => acc + diffLines(e.before, e.after).filter(c => c.type === 'delete').length, 0);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div style={{ maxWidth: 800, margin: '0 auto', padding: '1.5rem 0' }}>
|
|
298
|
+
{/* stats bar */}
|
|
299
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: '1.2rem', fontFamily: "'IBM Plex Mono',monospace", fontSize: 11, color: 'var(--muted-foreground)' }}>
|
|
300
|
+
<span>{entries.length} change{entries.length !== 1 ? 's' : ''}</span>
|
|
301
|
+
<span style={{ color: '#7aad80' }}>+{totalAdded}</span>
|
|
302
|
+
<span style={{ color: '#c85050' }}>−{totalRemoved}</span>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{entries.map((entry, i) => (
|
|
306
|
+
<DiffCard key={i} entry={entry} saveAction={saveAction} fullContent={content} />
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|