@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,404 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles } from 'lucide-react';
|
|
5
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
type StepStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
|
|
10
|
+
|
|
11
|
+
interface WorkflowStep {
|
|
12
|
+
index: number;
|
|
13
|
+
heading: string; // full heading text e.g. "Step 1: Gather requirements"
|
|
14
|
+
body: string; // body text below heading
|
|
15
|
+
status: StepStatus;
|
|
16
|
+
output: string; // AI output for this step
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WorkflowMeta {
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function parseWorkflow(content: string): { meta: WorkflowMeta; steps: WorkflowStep[] } {
|
|
27
|
+
const lines = content.split('\n');
|
|
28
|
+
let title = '';
|
|
29
|
+
let description = '';
|
|
30
|
+
const steps: WorkflowStep[] = [];
|
|
31
|
+
let currentStep: { heading: string; bodyLines: string[] } | null = null;
|
|
32
|
+
let inMeta = true;
|
|
33
|
+
const metaLines: string[] = [];
|
|
34
|
+
|
|
35
|
+
const flushStep = () => {
|
|
36
|
+
if (!currentStep) return;
|
|
37
|
+
steps.push({
|
|
38
|
+
index: steps.length,
|
|
39
|
+
heading: currentStep.heading,
|
|
40
|
+
body: currentStep.bodyLines.join('\n').trim(),
|
|
41
|
+
status: 'pending',
|
|
42
|
+
output: '',
|
|
43
|
+
});
|
|
44
|
+
currentStep = null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (/^# /.test(line)) {
|
|
49
|
+
title = line.slice(2).trim();
|
|
50
|
+
inMeta = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// H2 = step
|
|
54
|
+
if (/^## /.test(line)) {
|
|
55
|
+
flushStep();
|
|
56
|
+
inMeta = false;
|
|
57
|
+
currentStep = { heading: line.slice(3).trim(), bodyLines: [] };
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (currentStep) {
|
|
61
|
+
currentStep.bodyLines.push(line);
|
|
62
|
+
} else if (inMeta) {
|
|
63
|
+
metaLines.push(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
flushStep();
|
|
67
|
+
|
|
68
|
+
description = metaLines.filter(l => l.trim() && !/^#/.test(l)).join(' ').trim().slice(0, 200);
|
|
69
|
+
|
|
70
|
+
return { meta: { title, description }, steps };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Inline markdown renderer ─────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function renderInline(text: string): string {
|
|
76
|
+
return text
|
|
77
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
78
|
+
.replace(/`(.+?)`/g, `<code style="font-family:'IBM Plex Mono',monospace;font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>`)
|
|
79
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderBody(body: string): string {
|
|
83
|
+
return body.split('\n').map(line => {
|
|
84
|
+
if (!line.trim()) return '';
|
|
85
|
+
if (/^- /.test(line)) return `<li style="margin:.2em 0;font-size:.82rem;color:var(--muted-foreground)">${renderInline(line.slice(2))}</li>`;
|
|
86
|
+
return `<p style="margin:.3em 0;font-size:.82rem;line-height:1.6;color:var(--muted-foreground)">${renderInline(line)}</p>`;
|
|
87
|
+
}).join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Status icon ─────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function StatusIcon({ status }: { status: StepStatus }) {
|
|
93
|
+
if (status === 'pending') return <Circle size={15} style={{ color: 'var(--border)' }} />;
|
|
94
|
+
if (status === 'running') return <Loader2 size={15} style={{ color: 'var(--amber)', animation: 'spin 1s linear infinite' }} />;
|
|
95
|
+
if (status === 'done') return <CheckCircle2 size={15} style={{ color: '#7aad80' }} />;
|
|
96
|
+
if (status === 'skipped') return <SkipForward size={15} style={{ color: 'var(--muted-foreground)', opacity: .5 }} />;
|
|
97
|
+
return <AlertCircle size={15} style={{ color: '#c85050' }} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const STATUS_BORDER: Record<StepStatus, string> = {
|
|
101
|
+
pending: 'var(--border)',
|
|
102
|
+
running: 'rgba(200,135,58,0.5)',
|
|
103
|
+
done: 'rgba(122,173,128,0.4)',
|
|
104
|
+
skipped: 'var(--border)',
|
|
105
|
+
error: 'rgba(200,80,80,0.4)',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ─── AI execution ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async function runStepWithAI(
|
|
111
|
+
step: WorkflowStep,
|
|
112
|
+
filePath: string,
|
|
113
|
+
allStepsSummary: string,
|
|
114
|
+
onChunk: (chunk: string) => void,
|
|
115
|
+
signal: AbortSignal,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const prompt = `You are executing step ${step.index + 1} of a SOP/Workflow: "${step.heading}".
|
|
118
|
+
|
|
119
|
+
Context of the full workflow:
|
|
120
|
+
${allStepsSummary}
|
|
121
|
+
|
|
122
|
+
Current step instructions:
|
|
123
|
+
${step.body || '(No specific instructions — use common sense for this step.)'}
|
|
124
|
+
|
|
125
|
+
Execute this step concisely. Provide:
|
|
126
|
+
1. What you did / what the output is
|
|
127
|
+
2. Any decisions made
|
|
128
|
+
3. What the next step should watch out for
|
|
129
|
+
|
|
130
|
+
Be specific and actionable. Format in Markdown.`;
|
|
131
|
+
|
|
132
|
+
const res = await fetch('/api/ask', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
messages: [{ role: 'user', content: prompt }],
|
|
137
|
+
currentFile: filePath,
|
|
138
|
+
}),
|
|
139
|
+
signal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
143
|
+
if (!res.body) throw new Error('No response body');
|
|
144
|
+
|
|
145
|
+
const reader = res.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
let acc = '';
|
|
148
|
+
|
|
149
|
+
while (true) {
|
|
150
|
+
const { done, value } = await reader.read();
|
|
151
|
+
if (done) break;
|
|
152
|
+
const raw = decoder.decode(value, { stream: true });
|
|
153
|
+
for (const line of raw.split('\n')) {
|
|
154
|
+
const m = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
155
|
+
if (m) {
|
|
156
|
+
acc += m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
157
|
+
onChunk(acc);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Step card ────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function StepCard({
|
|
166
|
+
step, isActive, onRun, onSkip, canRun,
|
|
167
|
+
}: {
|
|
168
|
+
step: WorkflowStep;
|
|
169
|
+
isActive: boolean;
|
|
170
|
+
onRun: () => void;
|
|
171
|
+
onSkip: () => void;
|
|
172
|
+
canRun: boolean;
|
|
173
|
+
}) {
|
|
174
|
+
const [expanded, setExpanded] = useState(false);
|
|
175
|
+
const hasBody = step.body.trim().length > 0;
|
|
176
|
+
const hasOutput = step.output.length > 0;
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div style={{
|
|
180
|
+
border: `1px solid ${STATUS_BORDER[step.status]}`,
|
|
181
|
+
borderRadius: 10,
|
|
182
|
+
overflow: 'hidden',
|
|
183
|
+
background: 'var(--card)',
|
|
184
|
+
opacity: step.status === 'skipped' ? 0.6 : 1,
|
|
185
|
+
transition: 'border-color .2s, opacity .2s',
|
|
186
|
+
}}>
|
|
187
|
+
{/* header */}
|
|
188
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px' }}>
|
|
189
|
+
<StatusIcon status={step.status} />
|
|
190
|
+
<span
|
|
191
|
+
style={{ flex: 1, fontFamily: "'IBM Plex Sans',sans-serif", fontWeight: 600, fontSize: '.88rem', color: 'var(--foreground)', cursor: hasBody || hasOutput ? 'pointer' : 'default' }}
|
|
192
|
+
onClick={() => (hasBody || hasOutput) && setExpanded(v => !v)}
|
|
193
|
+
>
|
|
194
|
+
{step.heading}
|
|
195
|
+
</span>
|
|
196
|
+
|
|
197
|
+
{/* action buttons */}
|
|
198
|
+
<div style={{ display: 'flex', gap: 5, flexShrink: 0 }}>
|
|
199
|
+
{step.status === 'pending' && (
|
|
200
|
+
<>
|
|
201
|
+
<button
|
|
202
|
+
onClick={onRun}
|
|
203
|
+
disabled={!canRun}
|
|
204
|
+
style={{
|
|
205
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
206
|
+
padding: '3px 10px', borderRadius: 6, fontSize: '0.72rem',
|
|
207
|
+
fontFamily: "'IBM Plex Mono',monospace", cursor: canRun ? 'pointer' : 'not-allowed',
|
|
208
|
+
border: 'none', background: canRun ? 'var(--amber)' : 'var(--muted)',
|
|
209
|
+
color: canRun ? '#131210' : 'var(--muted-foreground)',
|
|
210
|
+
opacity: canRun ? 1 : 0.5,
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<Play size={10} /> Run
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
onClick={onSkip}
|
|
217
|
+
style={{
|
|
218
|
+
padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem',
|
|
219
|
+
fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer',
|
|
220
|
+
border: '1px solid var(--border)', background: 'transparent',
|
|
221
|
+
color: 'var(--muted-foreground)',
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Skip
|
|
225
|
+
</button>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
{step.status === 'running' && (
|
|
229
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--amber)' }}>executing…</span>
|
|
230
|
+
)}
|
|
231
|
+
{(step.status === 'done' || step.status === 'error') && (
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => setExpanded(v => !v)}
|
|
234
|
+
style={{ padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem', fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)' }}
|
|
235
|
+
>
|
|
236
|
+
<ChevronDown size={11} style={{ display: 'inline', transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
|
|
237
|
+
</button>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* body / output */}
|
|
243
|
+
{(expanded || step.status === 'running') && (hasBody || hasOutput) && (
|
|
244
|
+
<div style={{ borderTop: '1px solid var(--border)' }}>
|
|
245
|
+
{hasBody && (
|
|
246
|
+
<div style={{ padding: '10px 14px', borderBottom: hasOutput ? '1px solid var(--border)' : 'none' }}>
|
|
247
|
+
<div dangerouslySetInnerHTML={{ __html: renderBody(step.body) }} />
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
{hasOutput && (
|
|
251
|
+
<div style={{ padding: '10px 14px', background: 'var(--background)', position: 'relative' }}>
|
|
252
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 6 }}>
|
|
253
|
+
<Sparkles size={11} style={{ color: 'var(--amber)' }} />
|
|
254
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '.06em' }}>AI Output</span>
|
|
255
|
+
{step.status === 'running' && <span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--amber)', animation: 'pulse 1.2s ease-in-out infinite', marginLeft: 4 }} />}
|
|
256
|
+
</div>
|
|
257
|
+
<div style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontSize: '.82rem', lineHeight: 1.7, color: 'var(--foreground)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
258
|
+
{step.output}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Main renderer ────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export function WorkflowRenderer({ filePath, content }: RendererContext) {
|
|
271
|
+
const parsed = useMemo(() => parseWorkflow(content), [content]);
|
|
272
|
+
const [steps, setSteps] = useState<WorkflowStep[]>(() => parsed.steps);
|
|
273
|
+
const [running, setRunning] = useState(false);
|
|
274
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
275
|
+
|
|
276
|
+
// Reset when content changes externally
|
|
277
|
+
useMemo(() => { setSteps(parsed.steps.map(s => ({ ...s, status: 'pending' as StepStatus, output: '' }))); }, [parsed]);
|
|
278
|
+
|
|
279
|
+
const allStepsSummary = useMemo(() =>
|
|
280
|
+
parsed.steps.map((s, i) => `${i + 1}. ${s.heading}`).join('\n'),
|
|
281
|
+
[parsed]);
|
|
282
|
+
|
|
283
|
+
const runStep = useCallback(async (idx: number) => {
|
|
284
|
+
if (running) return;
|
|
285
|
+
abortRef.current?.abort();
|
|
286
|
+
const ctrl = new AbortController();
|
|
287
|
+
abortRef.current = ctrl;
|
|
288
|
+
setRunning(true);
|
|
289
|
+
|
|
290
|
+
setSteps(prev => prev.map((s, i) =>
|
|
291
|
+
i === idx ? { ...s, status: 'running', output: '' } : s));
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
await runStepWithAI(
|
|
295
|
+
steps[idx], filePath, allStepsSummary,
|
|
296
|
+
(chunk) => setSteps(prev => prev.map((s, i) =>
|
|
297
|
+
i === idx ? { ...s, output: chunk } : s)),
|
|
298
|
+
ctrl.signal,
|
|
299
|
+
);
|
|
300
|
+
setSteps(prev => prev.map((s, i) =>
|
|
301
|
+
i === idx ? { ...s, status: 'done' } : s));
|
|
302
|
+
} catch (err: unknown) {
|
|
303
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
304
|
+
setSteps(prev => prev.map((s, i) =>
|
|
305
|
+
i === idx ? { ...s, status: 'error', output: (err instanceof Error ? err.message : String(err)) } : s));
|
|
306
|
+
} finally {
|
|
307
|
+
setRunning(false);
|
|
308
|
+
}
|
|
309
|
+
}, [running, steps, filePath, allStepsSummary]);
|
|
310
|
+
|
|
311
|
+
const skipStep = useCallback((idx: number) => {
|
|
312
|
+
setSteps(prev => prev.map((s, i) =>
|
|
313
|
+
i === idx ? { ...s, status: 'skipped' } : s));
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
const reset = useCallback(() => {
|
|
317
|
+
abortRef.current?.abort();
|
|
318
|
+
setRunning(false);
|
|
319
|
+
setSteps(parsed.steps.map(s => ({ ...s, status: 'pending' as StepStatus, output: '' })));
|
|
320
|
+
}, [parsed]);
|
|
321
|
+
|
|
322
|
+
// Next runnable step = first pending step
|
|
323
|
+
const nextPendingIdx = steps.findIndex(s => s.status === 'pending');
|
|
324
|
+
const doneCount = steps.filter(s => s.status === 'done').length;
|
|
325
|
+
const progress = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;
|
|
326
|
+
|
|
327
|
+
if (steps.length === 0) {
|
|
328
|
+
return (
|
|
329
|
+
<div style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12 }}>
|
|
330
|
+
No steps found. Add <code style={{ background: 'var(--muted)', padding: '1px 5px', borderRadius: 4 }}>## Step N: …</code> headings to define workflow steps.
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
|
|
337
|
+
{/* header */}
|
|
338
|
+
<div style={{ marginBottom: '1.2rem' }}>
|
|
339
|
+
{parsed.meta.description && (
|
|
340
|
+
<p style={{ fontFamily: "'IBM Plex Sans',sans-serif", fontSize: '.82rem', color: 'var(--muted-foreground)', lineHeight: 1.6, marginBottom: 12 }}>
|
|
341
|
+
{parsed.meta.description}
|
|
342
|
+
</p>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* progress + actions */}
|
|
346
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
|
347
|
+
{/* progress bar */}
|
|
348
|
+
<div style={{ flex: 1, minWidth: 120, height: 4, borderRadius: 999, background: 'var(--border)', overflow: 'hidden' }}>
|
|
349
|
+
<div style={{ height: '100%', width: `${progress}%`, background: 'var(--amber)', borderRadius: 999, transition: 'width .3s' }} />
|
|
350
|
+
</div>
|
|
351
|
+
<span style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.7rem', color: 'var(--muted-foreground)', flexShrink: 0 }}>
|
|
352
|
+
{doneCount}/{steps.length} done
|
|
353
|
+
</span>
|
|
354
|
+
|
|
355
|
+
{/* run next */}
|
|
356
|
+
{nextPendingIdx >= 0 && (
|
|
357
|
+
<button
|
|
358
|
+
onClick={() => runStep(nextPendingIdx)}
|
|
359
|
+
disabled={running}
|
|
360
|
+
style={{
|
|
361
|
+
display: 'flex', alignItems: 'center', gap: 5,
|
|
362
|
+
padding: '4px 12px', borderRadius: 7, fontSize: '0.75rem',
|
|
363
|
+
fontFamily: "'IBM Plex Mono',monospace", cursor: running ? 'not-allowed' : 'pointer',
|
|
364
|
+
border: 'none', background: running ? 'var(--muted)' : 'var(--amber)',
|
|
365
|
+
color: running ? 'var(--muted-foreground)' : '#131210',
|
|
366
|
+
opacity: running ? 0.7 : 1,
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
{running ? <Loader2 size={11} style={{ animation: 'spin 1s linear infinite' }} /> : <Play size={11} />}
|
|
370
|
+
Run next
|
|
371
|
+
</button>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{/* reset */}
|
|
375
|
+
<button
|
|
376
|
+
onClick={reset}
|
|
377
|
+
style={{ padding: '4px 10px', borderRadius: 7, fontSize: '0.75rem', fontFamily: "'IBM Plex Mono',monospace", cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
378
|
+
>
|
|
379
|
+
<RotateCcw size={11} /> Reset
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
{/* step list */}
|
|
385
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
386
|
+
{steps.map((step, i) => (
|
|
387
|
+
<StepCard
|
|
388
|
+
key={i}
|
|
389
|
+
step={step}
|
|
390
|
+
isActive={i === nextPendingIdx}
|
|
391
|
+
canRun={!running}
|
|
392
|
+
onRun={() => runStep(i)}
|
|
393
|
+
onSkip={() => skipStep(i)}
|
|
394
|
+
/>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<style>{`
|
|
399
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
400
|
+
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
|
|
401
|
+
`}</style>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { Plus } from 'lucide-react';
|
|
5
|
+
import type { BoardConfig } from './types';
|
|
6
|
+
import { serializeCSV, tagColor } from './types';
|
|
7
|
+
|
|
8
|
+
export function BoardView({ headers, rows, cfg, saveAction }: {
|
|
9
|
+
headers: string[];
|
|
10
|
+
rows: string[][];
|
|
11
|
+
cfg: BoardConfig;
|
|
12
|
+
saveAction: (c: string) => Promise<void>;
|
|
13
|
+
}) {
|
|
14
|
+
const [localRows, setLocalRows] = useState(rows);
|
|
15
|
+
const [dragOver, setDragOver] = useState<string | null>(null);
|
|
16
|
+
const [newColInput, setNewColInput] = useState('');
|
|
17
|
+
const [showNewCol, setShowNewCol] = useState(false);
|
|
18
|
+
useEffect(() => { setLocalRows(rows); }, [rows]);
|
|
19
|
+
|
|
20
|
+
const groupIdx = headers.indexOf(cfg.groupField);
|
|
21
|
+
const titleIdx = headers.indexOf(cfg.titleField);
|
|
22
|
+
const descIdx = headers.indexOf(cfg.descField);
|
|
23
|
+
|
|
24
|
+
const { groups, groupKeys } = useMemo(() => {
|
|
25
|
+
const map = new Map<string, { row: string[]; origIdx: number }[]>();
|
|
26
|
+
localRows.forEach((row, i) => {
|
|
27
|
+
const key = (groupIdx >= 0 ? row[groupIdx] : '') || '(empty)';
|
|
28
|
+
if (!map.has(key)) map.set(key, []);
|
|
29
|
+
map.get(key)!.push({ row, origIdx: i });
|
|
30
|
+
});
|
|
31
|
+
return { groups: map, groupKeys: [...map.keys()] };
|
|
32
|
+
}, [localRows, groupIdx]);
|
|
33
|
+
|
|
34
|
+
async function moveCard(origIdx: number, newGroup: string) {
|
|
35
|
+
const updated = localRows.map((r, i) => {
|
|
36
|
+
if (i !== origIdx) return r;
|
|
37
|
+
const next = [...r];
|
|
38
|
+
if (groupIdx >= 0) next[groupIdx] = newGroup;
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
setLocalRows(updated);
|
|
42
|
+
await saveAction(serializeCSV(headers, updated));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Column({ group }: { group: string }) {
|
|
46
|
+
const cards = groups.get(group) ?? [];
|
|
47
|
+
const tc = tagColor(group);
|
|
48
|
+
const isOver = dragOver === group;
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex-shrink-0 w-64 flex flex-col gap-2">
|
|
51
|
+
<div className="flex items-center gap-2 px-1 py-1.5">
|
|
52
|
+
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ background: tc.text }} />
|
|
53
|
+
<span className="text-xs font-semibold uppercase tracking-wider truncate" style={{ color: tc.text, fontFamily: "'IBM Plex Mono',monospace" }}>{group}</span>
|
|
54
|
+
<span className="text-xs ml-auto shrink-0" style={{ color: 'var(--muted-foreground)', opacity: 0.5 }}>{cards.length}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div
|
|
57
|
+
className="flex flex-col gap-2 rounded-xl p-1.5 min-h-[80px] transition-colors"
|
|
58
|
+
style={{ background: isOver ? 'var(--amber-dim)' : 'var(--muted)', border: `1px solid ${isOver ? 'var(--amber)' : 'transparent'}` }}
|
|
59
|
+
onDragOver={e => { e.preventDefault(); setDragOver(group); }}
|
|
60
|
+
onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOver(null); }}
|
|
61
|
+
onDrop={e => {
|
|
62
|
+
setDragOver(null);
|
|
63
|
+
const idx = parseInt(e.dataTransfer.getData('origIdx'));
|
|
64
|
+
if (!isNaN(idx)) moveCard(idx, group);
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{cards.map(({ row, origIdx }) => {
|
|
68
|
+
const title = titleIdx >= 0 ? row[titleIdx] : row[0] ?? '';
|
|
69
|
+
const desc = descIdx >= 0 ? row[descIdx] : '';
|
|
70
|
+
return (
|
|
71
|
+
<div key={origIdx} draggable
|
|
72
|
+
onDragStart={e => { e.dataTransfer.setData('origIdx', String(origIdx)); setDragOver(null); }}
|
|
73
|
+
onDragEnd={() => setDragOver(null)}
|
|
74
|
+
className="rounded-lg border p-3 flex flex-col gap-1.5 cursor-grab active:cursor-grabbing hover:bg-muted/50 transition-colors"
|
|
75
|
+
style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
|
|
76
|
+
>
|
|
77
|
+
<p className="text-sm font-medium leading-snug" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans',sans-serif" }}>{title}</p>
|
|
78
|
+
{desc && <p className="text-xs leading-relaxed line-clamp-2" style={{ color: 'var(--muted-foreground)' }}>{desc}</p>}
|
|
79
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
80
|
+
{headers.map((h, ci) => {
|
|
81
|
+
if (ci === groupIdx || ci === titleIdx || ci === descIdx) return null;
|
|
82
|
+
const v = row[ci]; if (!v) return null;
|
|
83
|
+
return <span key={ci} className="text-[10px] px-1.5 py-0.5 rounded"
|
|
84
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
85
|
+
>{h}: {v}</span>;
|
|
86
|
+
})}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
{cards.length === 0 && (
|
|
92
|
+
<div className="flex items-center justify-center h-12">
|
|
93
|
+
<span className="text-xs" style={{ color: 'var(--muted-foreground)', opacity: 0.4 }}>Drop here</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex gap-3 overflow-x-auto pb-3 items-start">
|
|
103
|
+
{groupKeys.map(group => <Column key={group} group={group} />)}
|
|
104
|
+
|
|
105
|
+
{/* New column */}
|
|
106
|
+
<div className="flex-shrink-0 w-64">
|
|
107
|
+
{showNewCol ? (
|
|
108
|
+
<div className="rounded-xl border p-3 flex flex-col gap-2" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
|
109
|
+
<input autoFocus value={newColInput} onChange={e => setNewColInput(e.target.value)}
|
|
110
|
+
onKeyDown={e => {
|
|
111
|
+
if (e.key === 'Enter' && newColInput.trim()) {
|
|
112
|
+
setNewColInput('');
|
|
113
|
+
setShowNewCol(false);
|
|
114
|
+
}
|
|
115
|
+
if (e.key === 'Escape') { setNewColInput(''); setShowNewCol(false); }
|
|
116
|
+
}}
|
|
117
|
+
placeholder="Column name…"
|
|
118
|
+
className="text-xs bg-transparent outline-none w-full"
|
|
119
|
+
style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--amber)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
120
|
+
/>
|
|
121
|
+
<div className="flex gap-2">
|
|
122
|
+
<button onClick={() => {
|
|
123
|
+
setNewColInput('');
|
|
124
|
+
setShowNewCol(false);
|
|
125
|
+
}}
|
|
126
|
+
className="text-xs px-2 py-1 rounded"
|
|
127
|
+
style={{ background: 'var(--amber)', color: '#131210', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
128
|
+
>Create</button>
|
|
129
|
+
<button onClick={() => { setNewColInput(''); setShowNewCol(false); }}
|
|
130
|
+
className="text-xs px-2 py-1 rounded"
|
|
131
|
+
style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
132
|
+
>Cancel</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
) : (
|
|
136
|
+
<button onClick={() => setShowNewCol(true)}
|
|
137
|
+
className="flex items-center gap-1.5 text-xs px-3 py-2 rounded-xl border border-dashed w-full transition-colors hover:bg-muted"
|
|
138
|
+
style={{ borderColor: 'var(--border)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
139
|
+
>
|
|
140
|
+
<Plus size={12} /> Add column
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import type { CsvConfig, ViewType } from './types';
|
|
5
|
+
|
|
6
|
+
export function ConfigPanel({ headers, cfg, view, onClose, onChange }: {
|
|
7
|
+
headers: string[];
|
|
8
|
+
cfg: CsvConfig;
|
|
9
|
+
view: ViewType;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onChange: (cfg: CsvConfig) => void;
|
|
12
|
+
}) {
|
|
13
|
+
const labelStyle: React.CSSProperties = { color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem' };
|
|
14
|
+
const selectStyle: React.CSSProperties = { background: 'var(--background)', color: 'var(--foreground)', borderColor: 'var(--border)', fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem' };
|
|
15
|
+
|
|
16
|
+
function FieldSelect({ label, value, onChange: onCh }: { label: string; value: string; onChange: (v: string) => void }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center justify-between gap-2">
|
|
19
|
+
<span style={labelStyle}>{label}</span>
|
|
20
|
+
<select value={value} onChange={e => onCh(e.target.value)}
|
|
21
|
+
className="rounded px-2 py-1 outline-none border" style={selectStyle}
|
|
22
|
+
>
|
|
23
|
+
<option value="">— none —</option>
|
|
24
|
+
{headers.map(h => <option key={h} value={h}>{h}</option>)}
|
|
25
|
+
</select>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="absolute right-0 top-10 z-20 w-72 rounded-xl border shadow-xl p-4 flex flex-col gap-3"
|
|
32
|
+
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex items-center justify-between">
|
|
35
|
+
<span className="text-xs font-semibold uppercase tracking-wider" style={labelStyle}>{view} settings</span>
|
|
36
|
+
<button onClick={onClose} style={{ color: 'var(--muted-foreground)' }}><X size={13} /></button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{view === 'table' && (
|
|
40
|
+
<>
|
|
41
|
+
<div className="h-px" style={{ background: 'var(--border)' }} />
|
|
42
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider" style={labelStyle}>Sort</p>
|
|
43
|
+
<FieldSelect label="Sort by" value={cfg.table.sortField}
|
|
44
|
+
onChange={v => onChange({ ...cfg, table: { ...cfg.table, sortField: v } })} />
|
|
45
|
+
<div className="flex items-center justify-between gap-2">
|
|
46
|
+
<span style={labelStyle}>Direction</span>
|
|
47
|
+
<div className="flex rounded overflow-hidden border" style={{ borderColor: 'var(--border)' }}>
|
|
48
|
+
{(['asc', 'desc'] as const).map(d => (
|
|
49
|
+
<button key={d} onClick={() => onChange({ ...cfg, table: { ...cfg.table, sortDir: d } })}
|
|
50
|
+
className="px-3 py-1 text-xs transition-colors"
|
|
51
|
+
style={{
|
|
52
|
+
fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.72rem',
|
|
53
|
+
background: cfg.table.sortDir === d ? 'var(--amber)' : 'var(--background)',
|
|
54
|
+
color: cfg.table.sortDir === d ? '#131210' : 'var(--muted-foreground)',
|
|
55
|
+
}}
|
|
56
|
+
>{d}</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="h-px" style={{ background: 'var(--border)' }} />
|
|
62
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider" style={labelStyle}>Group</p>
|
|
63
|
+
<FieldSelect label="Group by" value={cfg.table.groupField}
|
|
64
|
+
onChange={v => onChange({ ...cfg, table: { ...cfg.table, groupField: v } })} />
|
|
65
|
+
|
|
66
|
+
<div className="h-px" style={{ background: 'var(--border)' }} />
|
|
67
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider" style={labelStyle}>Columns</p>
|
|
68
|
+
<div className="flex flex-col gap-1.5">
|
|
69
|
+
{headers.map(h => {
|
|
70
|
+
const hidden = cfg.table.hiddenFields.includes(h);
|
|
71
|
+
return (
|
|
72
|
+
<div key={h} className="flex items-center justify-between">
|
|
73
|
+
<span style={labelStyle}>{h}</span>
|
|
74
|
+
<button onClick={() => {
|
|
75
|
+
const next = hidden
|
|
76
|
+
? cfg.table.hiddenFields.filter(f => f !== h)
|
|
77
|
+
: [...cfg.table.hiddenFields, h];
|
|
78
|
+
onChange({ ...cfg, table: { ...cfg.table, hiddenFields: next } });
|
|
79
|
+
}}
|
|
80
|
+
className="text-[11px] px-2 py-0.5 rounded transition-colors"
|
|
81
|
+
style={{
|
|
82
|
+
fontFamily: "'IBM Plex Mono',monospace",
|
|
83
|
+
background: hidden ? 'var(--muted)' : 'var(--amber-dim)',
|
|
84
|
+
color: hidden ? 'var(--muted-foreground)' : 'var(--amber)',
|
|
85
|
+
}}
|
|
86
|
+
>{hidden ? 'Hidden' : 'Visible'}</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{view === 'gallery' && (
|
|
95
|
+
<>
|
|
96
|
+
<FieldSelect label="Title" value={cfg.gallery.titleField}
|
|
97
|
+
onChange={v => onChange({ ...cfg, gallery: { ...cfg.gallery, titleField: v } })} />
|
|
98
|
+
<FieldSelect label="Description" value={cfg.gallery.descField}
|
|
99
|
+
onChange={v => onChange({ ...cfg, gallery: { ...cfg.gallery, descField: v } })} />
|
|
100
|
+
<FieldSelect label="Tag / Badge" value={cfg.gallery.tagField}
|
|
101
|
+
onChange={v => onChange({ ...cfg, gallery: { ...cfg.gallery, tagField: v } })} />
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{view === 'board' && (
|
|
106
|
+
<>
|
|
107
|
+
<FieldSelect label="Group by" value={cfg.board.groupField}
|
|
108
|
+
onChange={v => onChange({ ...cfg, board: { ...cfg.board, groupField: v } })} />
|
|
109
|
+
<FieldSelect label="Card title" value={cfg.board.titleField}
|
|
110
|
+
onChange={v => onChange({ ...cfg, board: { ...cfg.board, titleField: v } })} />
|
|
111
|
+
<FieldSelect label="Card desc" value={cfg.board.descField}
|
|
112
|
+
onChange={v => onChange({ ...cfg, board: { ...cfg.board, descField: v } })} />
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|