@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,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export function EditableCell({ value, onCommit }: { value: string; onCommit: (v: string) => void }) {
|
|
6
|
+
const [editing, setEditing] = useState(false);
|
|
7
|
+
const [draft, setDraft] = useState(value);
|
|
8
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
9
|
+
useEffect(() => { setDraft(value); }, [value]);
|
|
10
|
+
useEffect(() => { if (editing) ref.current?.select(); }, [editing]);
|
|
11
|
+
function commit() { setEditing(false); if (draft !== value) onCommit(draft); else setDraft(value); }
|
|
12
|
+
if (editing) return (
|
|
13
|
+
<input ref={ref} value={draft} onChange={e => setDraft(e.target.value)}
|
|
14
|
+
onBlur={commit} onKeyDown={e => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') { setDraft(value); setEditing(false); } }}
|
|
15
|
+
className="w-full bg-transparent outline-none text-sm" onClick={e => e.stopPropagation()}
|
|
16
|
+
style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--amber)', minWidth: 60 }}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
return (
|
|
20
|
+
<div className="truncate text-sm cursor-text" style={{ color: 'var(--foreground)', minWidth: 60 }}
|
|
21
|
+
onClick={() => setEditing(true)} title={value}
|
|
22
|
+
>{value || <span style={{ color: 'var(--muted-foreground)', opacity: 0.3 }}>—</span>}</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AddRowTr({ headers, visibleIndices, onAdd, onCancel }: { headers: string[]; visibleIndices: number[]; onAdd: (r: string[]) => void; onCancel: () => void }) {
|
|
27
|
+
const [vals, setVals] = useState(() => Array(headers.length).fill(''));
|
|
28
|
+
function set(i: number, v: string) { setVals(prev => { const n = [...prev]; n[i] = v; return n; }); }
|
|
29
|
+
return (
|
|
30
|
+
<tr style={{ background: 'color-mix(in srgb, var(--amber) 6%, transparent)', borderTop: '1px solid var(--amber)' }}>
|
|
31
|
+
{visibleIndices.map((ci, pos) => (
|
|
32
|
+
<td key={ci} className="px-3 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
|
|
33
|
+
<input autoFocus={pos === 0} value={vals[ci]} onChange={e => set(ci, e.target.value)}
|
|
34
|
+
onKeyDown={e => { if (e.key === 'Enter') onAdd(vals); if (e.key === 'Escape') onCancel(); }}
|
|
35
|
+
placeholder={headers[ci]} className="w-full bg-transparent outline-none text-sm placeholder:opacity-30"
|
|
36
|
+
style={{ color: 'var(--foreground)', borderBottom: '1px solid var(--border)' }}
|
|
37
|
+
/>
|
|
38
|
+
</td>
|
|
39
|
+
))}
|
|
40
|
+
<td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }} />
|
|
41
|
+
</tr>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { GalleryConfig } from './types';
|
|
4
|
+
import { tagColor } from './types';
|
|
5
|
+
|
|
6
|
+
export function GalleryView({ headers, rows, cfg }: { headers: string[]; rows: string[][]; cfg: GalleryConfig }) {
|
|
7
|
+
const titleIdx = headers.indexOf(cfg.titleField);
|
|
8
|
+
const descIdx = headers.indexOf(cfg.descField);
|
|
9
|
+
const tagIdx = headers.indexOf(cfg.tagField);
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
12
|
+
{rows.map((row, i) => {
|
|
13
|
+
const title = titleIdx >= 0 ? row[titleIdx] : row[0] ?? '';
|
|
14
|
+
const desc = descIdx >= 0 ? row[descIdx] : '';
|
|
15
|
+
const tag = tagIdx >= 0 ? row[tagIdx] : '';
|
|
16
|
+
const tc = tag ? tagColor(tag) : null;
|
|
17
|
+
return (
|
|
18
|
+
<div key={i} className="rounded-xl border p-4 flex flex-col gap-2 hover:bg-muted/50 transition-colors"
|
|
19
|
+
style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
|
|
20
|
+
>
|
|
21
|
+
{tag && tc && <span className="self-start text-[11px] px-2 py-0.5 rounded-full font-medium"
|
|
22
|
+
style={{ background: tc.bg, color: tc.text, fontFamily: "'IBM Plex Mono',monospace" }}>{tag}</span>}
|
|
23
|
+
<p className="text-sm font-semibold leading-snug" style={{ color: 'var(--foreground)', fontFamily: "'IBM Plex Sans',sans-serif" }}>{title}</p>
|
|
24
|
+
{desc && <p className="text-xs leading-relaxed line-clamp-3" style={{ color: 'var(--muted-foreground)' }}>{desc}</p>}
|
|
25
|
+
<div className="mt-1 flex flex-col gap-0.5">
|
|
26
|
+
{headers.map((h, ci) => {
|
|
27
|
+
if (ci === titleIdx || ci === descIdx || ci === tagIdx) return null;
|
|
28
|
+
const v = row[ci]; if (!v) return null;
|
|
29
|
+
return <div key={ci} className="flex items-baseline gap-1.5 text-xs">
|
|
30
|
+
<span style={{ color: 'var(--muted-foreground)', opacity: 0.6, fontFamily: "'IBM Plex Mono',monospace", fontSize: '0.68rem' }}>{h}</span>
|
|
31
|
+
<span className="truncate" style={{ color: 'var(--muted-foreground)' }}>{v}</span>
|
|
32
|
+
</div>;
|
|
33
|
+
})}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
|
+
import { ChevronUp, ChevronDown, Plus, Trash2 } from 'lucide-react';
|
|
5
|
+
import type { TableConfig } from './types';
|
|
6
|
+
import { serializeCSV } from './types';
|
|
7
|
+
import { EditableCell, AddRowTr } from './EditableCell';
|
|
8
|
+
|
|
9
|
+
export function TableView({ headers, rows, cfg, saveAction }: {
|
|
10
|
+
headers: string[];
|
|
11
|
+
rows: string[][];
|
|
12
|
+
cfg: TableConfig;
|
|
13
|
+
saveAction: (content: string) => Promise<void>;
|
|
14
|
+
}) {
|
|
15
|
+
const [localRows, setLocalRows] = useState(rows);
|
|
16
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
17
|
+
useEffect(() => { setLocalRows(rows); }, [rows]);
|
|
18
|
+
|
|
19
|
+
const visibleIndices = useMemo(
|
|
20
|
+
() => headers.map((_, i) => i).filter(i => !cfg.hiddenFields.includes(headers[i])),
|
|
21
|
+
[headers, cfg.hiddenFields],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const sortIdx = headers.indexOf(cfg.sortField);
|
|
25
|
+
|
|
26
|
+
const processedRows = useMemo(() => {
|
|
27
|
+
let result = [...localRows];
|
|
28
|
+
if (sortIdx >= 0) {
|
|
29
|
+
result.sort((a, b) => {
|
|
30
|
+
const va = a[sortIdx] ?? '', vb = b[sortIdx] ?? '';
|
|
31
|
+
const na = parseFloat(va), nb = parseFloat(vb);
|
|
32
|
+
const cmp = (!isNaN(na) && !isNaN(nb)) ? na - nb : va.localeCompare(vb);
|
|
33
|
+
return cfg.sortDir === 'asc' ? cmp : -cmp;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}, [localRows, sortIdx, cfg.sortDir]);
|
|
38
|
+
|
|
39
|
+
const groupIdx = headers.indexOf(cfg.groupField);
|
|
40
|
+
|
|
41
|
+
type Section = { key: string | null; rows: { row: string[]; orig: string[] }[] };
|
|
42
|
+
const sections = useMemo((): Section[] => {
|
|
43
|
+
if (groupIdx < 0) return [{ key: null, rows: processedRows.map(r => ({ row: r, orig: r })) }];
|
|
44
|
+
const map = new Map<string, string[][]>();
|
|
45
|
+
for (const row of processedRows) {
|
|
46
|
+
const k = row[groupIdx] || '(empty)';
|
|
47
|
+
if (!map.has(k)) map.set(k, []);
|
|
48
|
+
map.get(k)!.push(row);
|
|
49
|
+
}
|
|
50
|
+
return [...map.entries()].map(([key, rs]) => ({ key, rows: rs.map(r => ({ row: r, orig: r })) }));
|
|
51
|
+
}, [processedRows, groupIdx]);
|
|
52
|
+
|
|
53
|
+
async function commitCell(origRow: string[], colIdx: number, val: string) {
|
|
54
|
+
const updated = localRows.map(r => r === origRow ? r.map((c, ci) => ci === colIdx ? val : c) : r);
|
|
55
|
+
setLocalRows(updated);
|
|
56
|
+
await saveAction(serializeCSV(headers, updated));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function deleteRow(origRow: string[]) {
|
|
60
|
+
const updated = localRows.filter(r => r !== origRow);
|
|
61
|
+
setLocalRows(updated);
|
|
62
|
+
await saveAction(serializeCSV(headers, updated));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function addRow(newRow: string[]) {
|
|
66
|
+
const updated = [...localRows, newRow];
|
|
67
|
+
setLocalRows(updated);
|
|
68
|
+
setShowAdd(false);
|
|
69
|
+
await saveAction(serializeCSV(headers, updated));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const thStyle: React.CSSProperties = {
|
|
73
|
+
borderBottom: '1px solid var(--border)',
|
|
74
|
+
fontFamily: "'IBM Plex Sans',sans-serif",
|
|
75
|
+
fontSize: '0.72rem',
|
|
76
|
+
letterSpacing: '0.05em',
|
|
77
|
+
textTransform: 'uppercase',
|
|
78
|
+
color: 'var(--muted-foreground)',
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let rowCounter = 0;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="rounded-xl overflow-hidden border" style={{ borderColor: 'var(--border)' }}>
|
|
86
|
+
<div className="overflow-x-auto">
|
|
87
|
+
<table className="min-w-full text-sm border-collapse">
|
|
88
|
+
<thead>
|
|
89
|
+
<tr style={{ background: 'var(--muted)' }}>
|
|
90
|
+
{visibleIndices.map(ci => (
|
|
91
|
+
<th key={ci} className="px-4 py-2.5 text-left whitespace-nowrap" style={thStyle}>
|
|
92
|
+
<div className="flex items-center gap-1">
|
|
93
|
+
{headers[ci]}
|
|
94
|
+
{cfg.sortField === headers[ci] && (
|
|
95
|
+
cfg.sortDir === 'asc'
|
|
96
|
+
? <ChevronUp size={10} style={{ color: 'var(--amber)' }} />
|
|
97
|
+
: <ChevronDown size={10} style={{ color: 'var(--amber)' }} />
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</th>
|
|
101
|
+
))}
|
|
102
|
+
<th className="w-8" style={{ ...thStyle, background: 'var(--muted)' }} />
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
{sections.map(section => (
|
|
107
|
+
<>
|
|
108
|
+
{section.key !== null && (
|
|
109
|
+
<tr key={`grp-${section.key}`}>
|
|
110
|
+
<td colSpan={visibleIndices.length + 1} className="px-4 py-1.5"
|
|
111
|
+
style={{ background: 'var(--accent)', borderBottom: '1px solid var(--border)', borderTop: '1px solid var(--border)' }}
|
|
112
|
+
>
|
|
113
|
+
<span className="text-xs font-semibold" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}>
|
|
114
|
+
{section.key} · {section.rows.length}
|
|
115
|
+
</span>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
)}
|
|
119
|
+
{section.rows.map(({ row, orig }) => {
|
|
120
|
+
const ri = rowCounter++;
|
|
121
|
+
return (
|
|
122
|
+
<tr key={ri} className="group transition-colors"
|
|
123
|
+
style={{ background: ri % 2 === 0 ? 'var(--background)' : 'var(--card)' }}
|
|
124
|
+
onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
|
|
125
|
+
onMouseLeave={e => (e.currentTarget.style.background = ri % 2 === 0 ? 'var(--background)' : 'var(--card)')}
|
|
126
|
+
>
|
|
127
|
+
{visibleIndices.map(ci => (
|
|
128
|
+
<td key={ci} className="px-4 py-2 max-w-xs" style={{ borderBottom: '1px solid var(--border)' }}>
|
|
129
|
+
<EditableCell value={row[ci] ?? ''} onCommit={v => commitCell(orig, ci, v)} />
|
|
130
|
+
</td>
|
|
131
|
+
))}
|
|
132
|
+
<td className="px-2 py-2" style={{ borderBottom: '1px solid var(--border)' }}>
|
|
133
|
+
<button onClick={() => deleteRow(orig)}
|
|
134
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-destructive/10"
|
|
135
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
136
|
+
><Trash2 size={12} /></button>
|
|
137
|
+
</td>
|
|
138
|
+
</tr>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</>
|
|
142
|
+
))}
|
|
143
|
+
{showAdd && (
|
|
144
|
+
<AddRowTr headers={headers} visibleIndices={visibleIndices} onAdd={addRow} onCancel={() => setShowAdd(false)} />
|
|
145
|
+
)}
|
|
146
|
+
</tbody>
|
|
147
|
+
</table>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="px-4 py-2 flex items-center justify-between" style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}>
|
|
150
|
+
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}>
|
|
151
|
+
{localRows.length} rows · {headers.length} cols
|
|
152
|
+
</span>
|
|
153
|
+
{!showAdd
|
|
154
|
+
? <button onClick={() => setShowAdd(true)} className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md"
|
|
155
|
+
style={{ color: 'var(--amber)', background: 'var(--amber-dim)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
156
|
+
><Plus size={12} /> Add row</button>
|
|
157
|
+
: <button onClick={() => setShowAdd(false)} className="text-xs px-2.5 py-1 rounded-md"
|
|
158
|
+
style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono',monospace" }}
|
|
159
|
+
>Cancel</button>
|
|
160
|
+
}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Papa from 'papaparse';
|
|
2
|
+
|
|
3
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type ViewType = 'table' | 'gallery' | 'board';
|
|
6
|
+
|
|
7
|
+
export interface TableConfig {
|
|
8
|
+
sortField: string;
|
|
9
|
+
sortDir: 'asc' | 'desc';
|
|
10
|
+
groupField: string;
|
|
11
|
+
hiddenFields: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GalleryConfig {
|
|
15
|
+
titleField: string;
|
|
16
|
+
descField: string;
|
|
17
|
+
tagField: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BoardConfig {
|
|
21
|
+
groupField: string;
|
|
22
|
+
titleField: string;
|
|
23
|
+
descField: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CsvConfig {
|
|
27
|
+
activeView: ViewType;
|
|
28
|
+
table: TableConfig;
|
|
29
|
+
gallery: GalleryConfig;
|
|
30
|
+
board: BoardConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function defaultConfig(headers: string[]): CsvConfig {
|
|
34
|
+
return {
|
|
35
|
+
activeView: 'table',
|
|
36
|
+
table: { sortField: '', sortDir: 'asc', groupField: '', hiddenFields: [] },
|
|
37
|
+
gallery: { titleField: headers[0] ?? '', descField: headers[1] ?? '', tagField: headers[2] ?? '' },
|
|
38
|
+
board: { groupField: headers[headers.length - 1] ?? '', titleField: headers[0] ?? '', descField: headers[1] ?? '' },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function configKey(filePath: string) { return `mindos-csv-config:${filePath}`; }
|
|
43
|
+
|
|
44
|
+
export function loadConfig(filePath: string, headers: string[]): CsvConfig {
|
|
45
|
+
try {
|
|
46
|
+
const raw = localStorage.getItem(configKey(filePath));
|
|
47
|
+
if (raw) {
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
const def = defaultConfig(headers);
|
|
50
|
+
return { ...def, ...parsed, table: { ...def.table, ...parsed.table }, gallery: { ...def.gallery, ...parsed.gallery }, board: { ...def.board, ...parsed.board } };
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
return defaultConfig(headers);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function saveConfig(filePath: string, cfg: CsvConfig) {
|
|
57
|
+
try { localStorage.setItem(configKey(filePath), JSON.stringify(cfg)); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Parse / serialize ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export function parseCSV(content: string) {
|
|
63
|
+
const result = Papa.parse<string[]>(content, { skipEmptyLines: true });
|
|
64
|
+
const data = result.data as string[][];
|
|
65
|
+
return { headers: data[0] ?? [], rows: data.slice(1) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function serializeCSV(headers: string[], rows: string[][]) {
|
|
69
|
+
return Papa.unparse([headers, ...rows]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Tag color ────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const TAG_COLORS = [
|
|
75
|
+
{ bg: 'rgba(200,135,58,0.12)', text: 'var(--amber)' },
|
|
76
|
+
{ bg: 'rgba(122,173,128,0.12)', text: '#7aad80' },
|
|
77
|
+
{ bg: 'rgba(138,180,216,0.12)', text: '#8ab4d8' },
|
|
78
|
+
{ bg: 'rgba(200,160,216,0.12)', text: '#c8a0d8' },
|
|
79
|
+
{ bg: 'rgba(200,96,96,0.12)', text: '#c86060' },
|
|
80
|
+
{ bg: 'rgba(150,150,150,0.12)', text: 'var(--muted-foreground)' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function tagColor(val: string) {
|
|
84
|
+
let h = 0;
|
|
85
|
+
for (let i = 0; i < val.length; i++) h = (h * 31 + val.charCodeAt(i)) & 0xffff;
|
|
86
|
+
return TAG_COLORS[h % TAG_COLORS.length];
|
|
87
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle } from 'lucide-react';
|
|
4
|
+
import type { AiSettings, ProviderConfig, SettingsData } from './types';
|
|
5
|
+
import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
|
|
6
|
+
|
|
7
|
+
interface AiTabProps {
|
|
8
|
+
data: SettingsData;
|
|
9
|
+
updateAi: (patch: Partial<AiSettings>) => void;
|
|
10
|
+
t: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AiTab({ data, updateAi, t }: AiTabProps) {
|
|
14
|
+
const env = data.envOverrides ?? {};
|
|
15
|
+
const envVal = data.envValues ?? {};
|
|
16
|
+
const provider = data.ai.provider;
|
|
17
|
+
|
|
18
|
+
function patchProvider(name: 'anthropic' | 'openai', patch: Partial<ProviderConfig>) {
|
|
19
|
+
updateAi({
|
|
20
|
+
providers: {
|
|
21
|
+
...data.ai.providers,
|
|
22
|
+
[name]: { ...data.ai.providers?.[name], ...patch },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const anthropic = data.ai.providers?.anthropic ?? { apiKey: '', model: '' };
|
|
28
|
+
const openai = data.ai.providers?.openai ?? { apiKey: '', model: '', baseUrl: '' };
|
|
29
|
+
|
|
30
|
+
const activeApiKey = provider === 'anthropic' ? anthropic.apiKey : openai.apiKey;
|
|
31
|
+
const activeEnvKey = provider === 'anthropic' ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY;
|
|
32
|
+
const missingApiKey = !activeApiKey && !activeEnvKey;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-5">
|
|
36
|
+
<Field label={<>{t.settings.ai.provider} <EnvBadge overridden={env.AI_PROVIDER} /></>}>
|
|
37
|
+
<Select
|
|
38
|
+
value={provider}
|
|
39
|
+
onChange={e => updateAi({ provider: e.target.value as 'anthropic' | 'openai' })}
|
|
40
|
+
>
|
|
41
|
+
<option value="anthropic">Anthropic (Claude)</option>
|
|
42
|
+
<option value="openai">OpenAI / compatible</option>
|
|
43
|
+
</Select>
|
|
44
|
+
</Field>
|
|
45
|
+
|
|
46
|
+
{provider === 'anthropic' ? (
|
|
47
|
+
<>
|
|
48
|
+
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.ANTHROPIC_MODEL} /></>}>
|
|
49
|
+
<Input
|
|
50
|
+
value={anthropic.model}
|
|
51
|
+
onChange={e => patchProvider('anthropic', { model: e.target.value })}
|
|
52
|
+
placeholder={envVal.ANTHROPIC_MODEL || 'claude-sonnet-4-6'}
|
|
53
|
+
/>
|
|
54
|
+
</Field>
|
|
55
|
+
<Field
|
|
56
|
+
label={<>{t.settings.ai.apiKey} <EnvBadge overridden={env.ANTHROPIC_API_KEY} /></>}
|
|
57
|
+
hint={env.ANTHROPIC_API_KEY ? t.settings.ai.envFieldNote('ANTHROPIC_API_KEY') : t.settings.ai.keyHint}
|
|
58
|
+
>
|
|
59
|
+
<ApiKeyInput
|
|
60
|
+
value={anthropic.apiKey}
|
|
61
|
+
onChange={v => patchProvider('anthropic', { apiKey: v })}
|
|
62
|
+
/>
|
|
63
|
+
</Field>
|
|
64
|
+
</>
|
|
65
|
+
) : (
|
|
66
|
+
<>
|
|
67
|
+
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.OPENAI_MODEL} /></>}>
|
|
68
|
+
<Input
|
|
69
|
+
value={openai.model}
|
|
70
|
+
onChange={e => patchProvider('openai', { model: e.target.value })}
|
|
71
|
+
placeholder={envVal.OPENAI_MODEL || 'gpt-5.4'}
|
|
72
|
+
/>
|
|
73
|
+
</Field>
|
|
74
|
+
<Field
|
|
75
|
+
label={<>{t.settings.ai.apiKey} <EnvBadge overridden={env.OPENAI_API_KEY} /></>}
|
|
76
|
+
hint={env.OPENAI_API_KEY ? t.settings.ai.envFieldNote('OPENAI_API_KEY') : t.settings.ai.keyHint}
|
|
77
|
+
>
|
|
78
|
+
<ApiKeyInput
|
|
79
|
+
value={openai.apiKey}
|
|
80
|
+
onChange={v => patchProvider('openai', { apiKey: v })}
|
|
81
|
+
/>
|
|
82
|
+
</Field>
|
|
83
|
+
<Field
|
|
84
|
+
label={<>{t.settings.ai.baseUrl} <EnvBadge overridden={env.OPENAI_BASE_URL} /></>}
|
|
85
|
+
hint={t.settings.ai.baseUrlHint}
|
|
86
|
+
>
|
|
87
|
+
<Input
|
|
88
|
+
value={openai.baseUrl ?? ''}
|
|
89
|
+
onChange={e => patchProvider('openai', { baseUrl: e.target.value })}
|
|
90
|
+
placeholder={envVal.OPENAI_BASE_URL || 'https://api.openai.com/v1'}
|
|
91
|
+
/>
|
|
92
|
+
</Field>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{missingApiKey && (
|
|
97
|
+
<div className="flex items-start gap-2 text-xs text-destructive/80 bg-destructive/8 border border-destructive/20 rounded-lg px-3 py-2.5">
|
|
98
|
+
<AlertCircle size={13} className="shrink-0 mt-0.5" />
|
|
99
|
+
<span>{t.settings.ai.noApiKey}</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{Object.values(env).some(Boolean) && (
|
|
104
|
+
<div className="flex items-start gap-2 text-xs text-amber-500/80 bg-amber-500/8 border border-amber-500/20 rounded-lg px-3 py-2.5">
|
|
105
|
+
<AlertCircle size={13} className="shrink-0 mt-0.5" />
|
|
106
|
+
<span>{t.settings.ai.envHint}</span>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Locale } from '@/lib/i18n';
|
|
4
|
+
import { CONTENT_WIDTHS, FONTS } from './types';
|
|
5
|
+
import { Field, Select } from './Primitives';
|
|
6
|
+
|
|
7
|
+
interface AppearanceTabProps {
|
|
8
|
+
font: string;
|
|
9
|
+
setFont: (v: string) => void;
|
|
10
|
+
contentWidth: string;
|
|
11
|
+
setContentWidth: (v: string) => void;
|
|
12
|
+
dark: boolean;
|
|
13
|
+
setDark: (v: boolean) => void;
|
|
14
|
+
locale: Locale;
|
|
15
|
+
setLocale: (v: Locale) => void;
|
|
16
|
+
t: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-5">
|
|
22
|
+
<Field label={t.settings.appearance.readingFont}>
|
|
23
|
+
<Select value={font} onChange={e => setFont(e.target.value)}>
|
|
24
|
+
{FONTS.map(f => (
|
|
25
|
+
<option key={f.value} value={f.value}>{f.label}</option>
|
|
26
|
+
))}
|
|
27
|
+
</Select>
|
|
28
|
+
<p className="text-xs text-muted-foreground mt-1.5 px-0.5" style={{ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily }}>
|
|
29
|
+
{t.settings.appearance.fontPreview}
|
|
30
|
+
</p>
|
|
31
|
+
</Field>
|
|
32
|
+
|
|
33
|
+
<Field label={t.settings.appearance.contentWidth}>
|
|
34
|
+
<div className="grid grid-cols-2 gap-2">
|
|
35
|
+
{CONTENT_WIDTHS.map(w => (
|
|
36
|
+
<button
|
|
37
|
+
key={w.value}
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => setContentWidth(w.value)}
|
|
40
|
+
className={`px-3 py-2 text-sm rounded-lg border transition-colors text-left ${
|
|
41
|
+
contentWidth === w.value
|
|
42
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground'
|
|
43
|
+
: 'border-border text-muted-foreground hover:border-border/80 hover:bg-muted'
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{w.label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</Field>
|
|
51
|
+
|
|
52
|
+
<Field label={t.settings.appearance.colorTheme}>
|
|
53
|
+
<div className="grid grid-cols-2 gap-2">
|
|
54
|
+
{[
|
|
55
|
+
{ value: 'dark', label: t.settings.appearance.dark },
|
|
56
|
+
{ value: 'light', label: t.settings.appearance.light },
|
|
57
|
+
].map(opt => (
|
|
58
|
+
<button
|
|
59
|
+
key={opt.value}
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => {
|
|
62
|
+
const isDark = opt.value === 'dark';
|
|
63
|
+
setDark(isDark);
|
|
64
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
65
|
+
localStorage.setItem('theme', opt.value);
|
|
66
|
+
}}
|
|
67
|
+
className={`px-3 py-2 text-sm rounded-lg border transition-colors text-left ${
|
|
68
|
+
(opt.value === 'dark') === dark
|
|
69
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground'
|
|
70
|
+
: 'border-border text-muted-foreground hover:border-border/80 hover:bg-muted'
|
|
71
|
+
}`}
|
|
72
|
+
>
|
|
73
|
+
{opt.label}
|
|
74
|
+
</button>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</Field>
|
|
78
|
+
|
|
79
|
+
<Field label={t.settings.appearance.language}>
|
|
80
|
+
<div className="grid grid-cols-2 gap-2">
|
|
81
|
+
{([['en', 'English'], ['zh', '中文']] as [Locale, string][]).map(([code, label]) => (
|
|
82
|
+
<button
|
|
83
|
+
key={code}
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={() => setLocale(code)}
|
|
86
|
+
className={`px-3 py-2 text-sm rounded-lg border transition-colors text-left ${
|
|
87
|
+
locale === code
|
|
88
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground'
|
|
89
|
+
: 'border-border text-muted-foreground hover:border-border/80 hover:bg-muted'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
{label}
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</Field>
|
|
97
|
+
|
|
98
|
+
<p className="text-xs text-muted-foreground">{t.settings.appearance.browserNote}</p>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|