├── .cursorignore ├── .gitignore ├── LICENSE ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── preview.md ├── preview ├── preview1.png └── preview2.png ├── public ├── manifest.json └── sw.js ├── src ├── app │ ├── changelog │ │ └── page.tsx │ ├── docker │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── globals.d.ts │ ├── layout.tsx │ ├── page.tsx │ ├── providers.tsx │ ├── settings │ │ ├── layout.tsx │ │ └── page.tsx │ └── statistics │ │ └── page.tsx ├── components │ ├── AITestDialog.tsx │ ├── AIagent.tsx │ ├── BrowserCompatCheck.tsx │ ├── CodeStructureVisualizer.tsx │ ├── DockerComposeView.tsx │ ├── DockerView.tsx │ ├── EnvFileView.tsx │ ├── FolderPicker.tsx │ ├── GitHubDownloader.tsx │ ├── IncrementalScanAlert.tsx │ ├── InstallPwaButton.tsx │ ├── KnowledgeModal.tsx │ ├── LocaleProvider.tsx │ ├── MultiThreadScanAlert.tsx │ ├── PresetPromptModal.tsx │ ├── ProjectAnalysisChart.tsx │ ├── ProjectConfigVisualizer.tsx │ ├── RequirementGeneratorModal.tsx │ ├── ResultDisplay.tsx │ ├── RssFeed.tsx │ ├── ScanControls.tsx │ ├── SettingsModal.tsx │ ├── StatisticsContent.tsx │ ├── ThemeToggle.tsx │ ├── VectorizeModal.tsx │ └── VersionManager.tsx ├── lib │ ├── atomTypes.ts │ ├── codeStructureParser.ts │ ├── commentParser.ts │ ├── dockerService.ts │ ├── fileObserver.ts │ ├── i18n.ts │ ├── knowledgeService.ts │ ├── knowledgeUtils.ts │ ├── modules.d.ts │ ├── publicCutscene.ts │ ├── pwaUtils.ts │ ├── scanService.ts │ ├── scanUtils.ts │ ├── store.ts │ ├── useViewport.ts │ ├── utils.ts │ ├── vectorizeService.ts │ ├── versionService.ts │ ├── viewportScript.ts │ └── workerUtils.ts ├── messages │ ├── en.json │ └── zh.json └── types.ts ├── tailwind.config.ts └── tsconfig.json /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | 3 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | .yarn/install-state.gz 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | /.git/ 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | .cursorrules 41 | /.cursor/ 42 | 43 | /index.html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | /.git/ 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .cursorrules 39 | /.cursor/ 40 | 41 | /index.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Nova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | GitHub stars 4 | GitHub forks 5 | GitHub issues 6 |

7 | 8 |

9 | 10 | Preview 11 | 12 |

13 | 14 |

15 | 简体中文 | English 16 |

17 | 18 | # Folda-Scan: Your Private AI Navigator & Q&A Engine for Codebases 🚀 19 | [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") 20 | [![许可证: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 21 | [![GitHub Repo stars](https://img.shields.io/github/stars/oldjs/web-code-agent?style=social)](https://github.com/oldjs/web-code-agent/stargazers) 22 | 23 | --- 24 | 25 |

26 | Unlock your codebase with AI, all locally, with your privacy intact.xs 27 |

28 | 29 | --- 30 | 31 | 32 | 33 | **Lost in a code maze? Tired of tedious context prep for AI collaboration? Folda-Scan to the rescue!** 💡 34 | 35 | **Folda-Scan** is a revolutionary intelligent project Q&A tool that **runs entirely locally in your browser**. It transforms your codebase into a conversational partner using advanced semantic vectorization, making code comprehension and AI collaboration unprecedentedly simple and secure. 36 | 37 | Folda-Scan (as part of the [WebFS-Toolkit](https://github.com/oldjs/web-code-agent)) is built with cutting-edge web technologies and AI algorithms to offer you a smooth, efficient, and secure local code interaction experience. 38 | 39 | ### ✨ Key Highlights (Why Folda-Scan?) 40 | 41 | - 🛡️ **Absolute Privacy, Local Execution**: All data processing happens locally in your browser; your code **never leaves** your machine. 42 | - 💬 **"Chat" with Code in Natural Language**: Ask questions about your codebase as if talking to a colleague and get precise answers. 43 | - 🧠 **Deep Semantic Understanding**: Goes beyond keywords to grasp the true intent and complex logic within your code. 44 | - 🎯 **Pinpoint Information Instantly**: Quickly locate relevant code snippets and files, even with vague descriptions. 45 | - 🤖 **LLM Collaboration Accelerator**: Generate context-aware Markdown with one click, perfectly "feeding" AI assistants (ChatGPT, Claude, etc.). 46 | - 💰 **Slash Token Costs**: Optimize LLM interactions, significantly reducing API call expenses and latency. 47 | - 🛠️ **Smart Config Generation**: Assists in creating project configuration files like `Dockerfile`. 48 | - 🚀 **Instant Onboarding**: Clear guidance to kickstart your code exploration journey quickly. 49 | 50 | ### 🚀 How It Works 51 | 52 | Folda-Scan's magic comes from its innovative **semantic vectorization engine**: 53 | 54 | 1. **Local Scanning & Indexing**: Securely scans your selected local project, converting code into high-dimensional vectors via semantic analysis, building a knowledge index locally in your browser. 55 | 2. **Intelligent Natural Language Processing (NLP)**: Understands your natural language questions and converts them into vectors too. 56 | 3. **Precise Semantic Matching**: Efficiently matches question vectors with code content in the vector space to provide the most relevant answers. 57 | _All done efficiently in your browser, with your data privacy fully protected._ 58 | 59 | ### 🛠️ Tech Unveiled (Tech Stack) 60 | 61 | - **Core Framework:** [Next.js 14](https://nextjs.org/) 62 | - **Local File Interaction:** [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) 63 | - **AI & NLP:** Advanced Semantic Vector Analysis, Natural Language Processing Algorithms 64 | - **Main Language:** [JavaScript/TypeScript - please specify] 65 | ### 🏁 Getting Started 66 | 67 | #### Prerequisites 68 | 69 | - Node.js (Recommended v16+ or v18+, refer to `package.json`) 70 | - npm (Version per `package.json`, e.g., npm@10.x.x) / yarn / pnpm 71 | - A modern browser supporting File System Access API (e.g., latest Chrome, Edge) 72 | 73 | ### 💡 Basic Usage 74 | 75 | 1. **Select Folder**: Authorize browser access to your local code project. 76 | 2. **Wait for Indexing**: Folda-Scan will quickly build a semantic index locally. 77 | 3. **Start Asking**: Query your codebase in natural language and unveil its secrets! 78 | 79 | ### 🤝 Contribute Your Prowess (Contributing) 80 | 81 | We enthusiastically welcome contributions of all kinds! Whether it's bug reports, feature suggestions, or code submissions, please refer to our [Contribution Guidelines](CONTRIBUTING.md) (if you have one). Let's build a better Folda-Scan together! 82 | 83 | ### 📄 License 84 | 85 | This project is licensed under the [MIT License](LICENSE). 86 | 87 | --- 88 | 89 |

90 | Back to Top 91 |

92 | --- 93 | 94 | 95 | 96 | ## Chinese 97 | 98 | **代码迷宫中找不到方向?与 AI 协作时上下文准备太繁琐? Folda-Scan 来拯救您!** 💡 99 | 100 | **Folda-Scan** 是一款革命性的智能项目问答工具,它**完全在您的浏览器本地运行**,通过先进的语义向量化技术,将您的代码库转化为可对话的智能伙伴。告别繁琐,拥抱高效,让代码理解和 AI 协作变得前所未有地简单和安全。 101 | 102 | Folda-Scan (作为 [WebFS-Toolkit](https://github.com/oldjs/web-code-agent) 的一部分) 采用尖端 Web 技术和 AI 算法,为您带来流畅、高效且安全的本地代码交互新体验。 103 | 104 | ### ✨ 核心亮点 (Why Folda-Scan?) 105 | 106 | - 🛡️ **绝对隐私,本地运行**:所有数据处理均在浏览器本地完成,代码**永不离开**您的计算机。 107 | - 💬 **自然语言“聊”代码**:像和同事聊天一样向代码库提问,精准获取答案。 108 | - 🧠 **深层语义理解**:超越关键词,理解代码的真实意图和复杂逻辑。 109 | - 🎯 **信息秒级定位**:模糊描述也能快速定位相关代码片段和文件。 110 | - 🤖 **LLM 协作加速器**:一键生成上下文感知的 Markdown,为 AI 助手(ChatGPT, Claude 等)提供完美“食粮”。 111 | - 💰 **Token 成本骤降**:优化 LLM 交互,显著降低 API 调用成本和等待时间。 112 | - 🛠️ **智能配置生成**:辅助生成 `Dockerfile` 等项目配置文件。 113 | - 🚀 **即时上手**:清晰的项目运行指导,快速启动您的代码探索之旅。 114 | 115 | ### 🚀 它是如何工作的? (How It Works) 116 | 117 | Folda-Scan 的魔法源于其创新的**语义向量化引擎**: 118 | 119 | 1. **本地扫描与索引**:安全扫描您选定的本地项目,通过语义分析将代码转化为高维向量,在浏览器本地构建知识索引。 120 | 2. **智能自然语言处理 (NLP)**:理解您的自然语言提问,并将其同样向量化。 121 | 3. **精准语义匹配**:在向量空间中高效匹配问题与代码内容,提供最相关的答案。 122 | _这一切都在保障您数据隐私的前提下,在浏览器中高效完成。_ 123 | 124 | ### 🛠️ 技术栈 (Tech Stack) 125 | 126 | - **核心框架:** [Next.js 14](https://nextjs.org/) 127 | - **本地文件交互:** [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) 128 | - **AI & NLP:** 先进语义向量分析、自然语言处理算法 129 | - **主要语言:** [JavaScript/TypeScript - 请指明] 130 | ### 🏁 快速开始 (Getting Started) 131 | 132 | #### 环境要求 (Prerequisites) 133 | 134 | - Node.js (建议 v16+ 或 v18+, 参照 `package.json`) 135 | - npm (版本参照 `package.json`, 例如 npm@10.x.x) / yarn / pnpm 136 | - 支持 File System Access API 的现代浏览器 (如 Chrome, Edge 最新版) 137 | 138 | ### 💡 基本用法 (Basic Usage) 139 | 140 | 1. **选择文件夹**:授权浏览器访问您的本地代码项目。 141 | 2. **等待索引**:Folda-Scan 将在本地快速构建语义索引。 142 | 3. **开始提问**:用自然语言向您的代码库提问,探索其奥秘! 143 | 144 | ### 🤝 贡献您的力量 (Contributing) 145 | 146 | 我们热烈欢迎各种形式的贡献!无论是 Bug 报告、功能建议还是代码提交,请参考我们的 [贡献指南](CONTRIBUTING.md) (如果您有的话)。期待与您共建更好的 Folda-Scan! 147 | 148 | ### 📄 开源许可 (License) 149 | 150 | 本项目基于 [MIT 许可证](LICENSE) 开源。 151 | 152 | --- 153 | 154 |

155 | 返回顶部 (Back to Top) 156 |

157 | --- 158 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: "export", 4 | reactStrictMode: false, 5 | typescript: { 6 | ignoreBuildErrors: true, 7 | }, 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | compiler: { 12 | removeConsole: 13 | process.env.NODE_ENV === "production" 14 | ? { 15 | exclude: ["error", "warn"], 16 | } 17 | : undefined, 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "folda-scan", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^2.2.3", 13 | "@tailwindcss/typography": "^0.5.16", 14 | "@types/crypto-js": "^4.2.2", 15 | "@types/js-yaml": "^4.0.9", 16 | "@types/xml2js": "^0.4.14", 17 | "crypto-js": "^4.2.0", 18 | "diff": "^8.0.1", 19 | "framer-motion": "^12.11.3", 20 | "highlight.js": "^11.11.1", 21 | "ignore": "^7.0.4", 22 | "jotai": "^2.12.4", 23 | "js-yaml": "^4.1.0", 24 | "jszip": "^3.10.1", 25 | "markdown-to-jsx": "^7.7.6", 26 | "next": "14.2.28", 27 | "next-intl": "^4.1.0", 28 | "next-themes": "^0.4.6", 29 | "next-view-transitions": "^0.3.4", 30 | "react": "^18", 31 | "react-diff-viewer-continued": "^3.4.0", 32 | "react-dom": "^18", 33 | "react-hot-toast": "^2.5.2", 34 | "react-icons": "^5.5.0", 35 | "react-markdown": "^10.1.0", 36 | "react-syntax-highlighter": "^15.6.1", 37 | "react-tooltip": "^5.28.1", 38 | "reactflow": "^11.11.4", 39 | "xml2js": "^0.6.2" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^20", 43 | "@types/react": "^18", 44 | "@types/react-dom": "^18", 45 | "postcss": "^8", 46 | "rimraf": "^5.0.10", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /preview.md: -------------------------------------------------------------------------------- 1 | ![preview](./preview/preview1.png) 2 | 3 | ![preview](./preview/preview2.png) 4 | -------------------------------------------------------------------------------- /preview/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldjs/web-code-agent/068d3aa2b351befe732b1d356ee5e4eb0103b3ff/preview/preview1.png -------------------------------------------------------------------------------- /preview/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldjs/web-code-agent/068d3aa2b351befe732b1d356ee5e4eb0103b3ff/preview/preview2.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Folda-Scan", 3 | "short_name": "Folda-Scan", 4 | "description": "一款基于浏览器的本地文件夹监控工具", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#4f46e5" 9 | } 10 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | // 缓存名称和版本 - 使用时间戳确保每次部署都有新的缓存名称 2 | const CACHE_VERSION = "v3"; 3 | const TIMESTAMP = new Date().toISOString().split("T")[0]; 4 | const CACHE_NAME = `folda-scan-cache-${CACHE_VERSION}-${TIMESTAMP}`; 5 | console.log(`[Service Worker] 缓存名称: ${CACHE_NAME}`); 6 | // 需要缓存的资源列表 7 | let urlsToCache = [ 8 | "/", 9 | "/index.html", 10 | "/manifest.json", 11 | // 添加其他重要静态资源 12 | ]; 13 | 14 | // 安装Service Worker 15 | self.addEventListener("install", (event) => { 16 | console.log(`[Service Worker] 安装新版本 ${CACHE_NAME}`); 17 | 18 | event.waitUntil( 19 | caches 20 | .open(CACHE_NAME) 21 | .then((cache) => { 22 | console.log("[Service Worker] 打开新缓存并预缓存重要资源"); 23 | return cache.addAll(urlsToCache); 24 | }) 25 | .then(() => { 26 | // 强制新安装的 Service Worker 立即激活,不等待旧的关闭 27 | console.log("[Service Worker] 跳过等待,立即激活"); 28 | return self.skipWaiting(); 29 | }) 30 | .catch((error) => { 31 | console.error("[Service Worker] 缓存预加载失败:", error); 32 | }) 33 | ); 34 | }); 35 | 36 | // 激活Service Worker 37 | self.addEventListener("activate", (event) => { 38 | console.log(`[Service Worker] 激活新版本 ${CACHE_NAME}`); 39 | 40 | // 清理旧缓存 41 | event.waitUntil( 42 | caches 43 | .keys() 44 | .then((cacheNames) => { 45 | console.log("[Service Worker] 找到缓存:", cacheNames.join(", ")); 46 | 47 | return Promise.all( 48 | cacheNames.map((cacheName) => { 49 | // 删除不是当前版本的所有缓存 50 | if (cacheName !== CACHE_NAME) { 51 | console.log(`[Service Worker] 删除旧缓存: ${cacheName}`); 52 | return caches.delete(cacheName); 53 | } 54 | }) 55 | ); 56 | }) 57 | .then(() => { 58 | // 立即控制所有客户端 59 | console.log("[Service Worker] 接管所有客户端"); 60 | return self.clients.claim(); 61 | }) 62 | .catch((error) => { 63 | console.error("[Service Worker] 清理缓存失败:", error); 64 | }) 65 | ); 66 | }); 67 | 68 | // 处理资源请求 69 | self.addEventListener("fetch", (event) => { 70 | // 只处理GET请求,其他请求直接通过网络 71 | if (event.request.method !== "GET") { 72 | return; 73 | } 74 | 75 | // 排除一些不应该缓存的请求 76 | const url = new URL(event.request.url); 77 | if ( 78 | url.pathname.startsWith("/api/") || 79 | url.pathname.includes("chrome-extension://") 80 | ) { 81 | return; 82 | } 83 | 84 | // 使用网络优先策略,但对静态资源使用缓存优先 85 | const isStaticAsset = urlsToCache.some( 86 | (staticUrl) => 87 | event.request.url.endsWith(staticUrl) || 88 | event.request.url.includes("/static/") 89 | ); 90 | 91 | if (isStaticAsset) { 92 | // 缓存优先,网络回退策略 (适用于静态资源) 93 | event.respondWith( 94 | caches 95 | .match(event.request) 96 | .then((cachedResponse) => { 97 | if (cachedResponse) { 98 | // console.log(`[Service Worker] 从缓存返回: ${event.request.url}`); 99 | return cachedResponse; 100 | } 101 | 102 | // 缓存中没有,从网络获取 103 | // console.log(`[Service Worker] 从网络获取: ${event.request.url}`); 104 | return fetchAndCache(event.request); 105 | }) 106 | .catch((error) => { 107 | console.error( 108 | `[Service Worker] 获取资源失败: ${event.request.url}`, 109 | error 110 | ); 111 | // 可以在这里返回一个离线页面或默认资源 112 | }) 113 | ); 114 | } else { 115 | // 网络优先,缓存回退策略 (适用于动态内容) 116 | event.respondWith( 117 | fetchAndCache(event.request).catch(() => { 118 | return caches.match(event.request); 119 | }) 120 | ); 121 | } 122 | }); 123 | 124 | // 辅助函数:从网络获取并缓存 125 | async function fetchAndCache(request) { 126 | const response = await fetch(request); 127 | 128 | // 检查响应是否有效且可缓存 129 | if (response && response.status === 200 && response.type === "basic") { 130 | const responseToCache = response.clone(); 131 | const cache = await caches.open(CACHE_NAME); 132 | cache.put(request, responseToCache); 133 | // console.log(`[Service Worker] 缓存新资源: ${request.url}`); 134 | } 135 | 136 | return response; 137 | } 138 | -------------------------------------------------------------------------------- /src/app/docker/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Tab } from "@headlessui/react"; 5 | import { useTranslations } from "@/components/LocaleProvider"; 6 | import { useTransitionRouter } from "next-view-transitions"; 7 | import { slideInOut } from "../../lib/publicCutscene"; 8 | import DockerView from "@/components/DockerView"; 9 | import DockerComposeView from "@/components/DockerComposeView"; 10 | import EnvFileView from "@/components/EnvFileView"; 11 | import { useSearchParams } from "next/navigation"; 12 | 13 | export default function DockerPage() { 14 | const { t } = useTranslations(); 15 | const router = useTransitionRouter(); 16 | const [mounted, setMounted] = useState(false); 17 | const searchParams = useSearchParams(); 18 | const tabParam = searchParams.get("tab"); 19 | const [selectedIndex, setSelectedIndex] = useState(0); 20 | 21 | // 确保组件在客户端挂载后才渲染 22 | useEffect(() => { 23 | setMounted(true); 24 | 25 | // 根据URL参数设置选中的Tab 26 | if (tabParam === "env") { 27 | setSelectedIndex(2); // 环境变量是第三个Tab(索引为2) 28 | } else if (tabParam === "compose") { 29 | setSelectedIndex(1); // Docker Compose是第二个Tab(索引为1) 30 | } 31 | }, [tabParam]); 32 | 33 | // 返回主页 34 | const handleGoBack = () => { 35 | router.push("/", { 36 | onTransitionReady: slideInOut, 37 | }); 38 | }; 39 | 40 | if (!mounted) return null; 41 | 42 | return ( 43 |
44 | {/* 顶部标题栏 */} 45 |
46 |
47 |
48 |
49 | 68 |

69 | {t("docker.title")} 70 |

71 |
72 |
73 |
74 |
75 | 76 | {/* 主要内容区域 */} 77 |
78 |
79 | 80 | 81 | 83 | `w-full py-2.5 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 84 | rounded-lg focus:outline-none focus:ring-2 ring-offset-2 ring-offset-blue-400 ring-white ring-opacity-60 85 | ${ 86 | selected 87 | ? "bg-white dark:bg-gray-700 shadow text-blue-700 dark:text-blue-400" 88 | : "hover:bg-white/[0.12] hover:text-blue-600 dark:hover:text-blue-300" 89 | }` 90 | } 91 | > 92 | {t("docker.title")} 93 | 94 | 96 | `w-full py-2.5 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 97 | rounded-lg focus:outline-none focus:ring-2 ring-offset-2 ring-offset-blue-400 ring-white ring-opacity-60 98 | ${ 99 | selected 100 | ? "bg-white dark:bg-gray-700 shadow text-blue-700 dark:text-blue-400" 101 | : "hover:bg-white/[0.12] hover:text-blue-600 dark:hover:text-blue-300" 102 | }` 103 | } 104 | > 105 | {t("dockerCompose.title")} 106 | 107 | 109 | `w-full py-2.5 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 110 | rounded-lg focus:outline-none focus:ring-2 ring-offset-2 ring-offset-blue-400 ring-white ring-opacity-60 111 | ${ 112 | selected 113 | ? "bg-white dark:bg-gray-700 shadow text-blue-700 dark:text-blue-400" 114 | : "hover:bg-white/[0.12] hover:text-blue-600 dark:hover:text-blue-300" 115 | }` 116 | } 117 | > 118 | {t("envFile.title")} 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 |
135 | 136 | {/* 页脚 */} 137 | 140 |
141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldjs/web-code-agent/068d3aa2b351befe732b1d356ee5e4eb0103b3ff/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldjs/web-code-agent/068d3aa2b351befe732b1d356ee5e4eb0103b3ff/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldjs/web-code-agent/068d3aa2b351befe732b1d356ee5e4eb0103b3ff/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | .dark { 11 | --background: #121212; 12 | --foreground: #f5f5f5; 13 | } 14 | 15 | html, 16 | body { 17 | transition: background-color 0.5s ease, color 0.5s ease; 18 | } 19 | 20 | body { 21 | color: var(--foreground); 22 | background: var(--background); 23 | font-family: Arial, Helvetica, sans-serif; 24 | } 25 | 26 | /* 淡入动画效果 */ 27 | .animate-fade-in { 28 | animation: fadeIn 0.5s ease-in-out; 29 | } 30 | 31 | @keyframes fadeIn { 32 | 0% { 33 | opacity: 0.6; 34 | } 35 | 100% { 36 | opacity: 1; 37 | } 38 | } 39 | 40 | @layer utilities { 41 | .text-balance { 42 | text-wrap: balance; 43 | } 44 | } 45 | 46 | /* 深色模式下的样式覆盖 */ 47 | .dark .bg-white { 48 | background-color: #1e1e1e; 49 | } 50 | 51 | .dark .bg-gray-50 { 52 | background-color: #2d2d2d; 53 | } 54 | 55 | .dark .bg-gray-100 { 56 | background-color: #333333; 57 | } 58 | 59 | .dark .text-gray-600 { 60 | color: #d1d1d1; 61 | } 62 | 63 | .dark .text-gray-700 { 64 | color: #e5e5e5; 65 | } 66 | 67 | .dark .text-gray-800 { 68 | color: #f0f0f0; 69 | } 70 | 71 | .dark .border-gray-200 { 72 | border-color: #3a3a3a; 73 | } 74 | 75 | .dark .shadow { 76 | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/globals.d.ts: -------------------------------------------------------------------------------- 1 | // 定义Web API类型 2 | interface FileSystemDirectoryHandle extends FileSystemHandle { 3 | getDirectoryHandle( 4 | name: string, 5 | options?: { create?: boolean } 6 | ): Promise; 7 | getFileHandle( 8 | name: string, 9 | options?: { create?: boolean } 10 | ): Promise; 11 | entries(): AsyncIterable<[string, FileSystemHandle]>; 12 | } 13 | 14 | interface FileSystemFileHandle extends FileSystemHandle { 15 | getFile(): Promise; 16 | } 17 | 18 | interface FileSystemHandle { 19 | kind: "file" | "directory"; 20 | name: string; 21 | } 22 | 23 | // File System Access API 24 | interface Window { 25 | showDirectoryPicker(options?: { 26 | id?: string; 27 | mode?: "read" | "readwrite"; 28 | }): Promise; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { Providers } from "./providers"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Folda-Scan - 本地文件夹扫描工具", 19 | description: 20 | "扫描和监控本地项目文件夹的变化,支持.gitignore规则,实时生成项目结构和变更报告", 21 | keywords: [ 22 | "文件夹扫描", 23 | "代码监控", 24 | "项目结构", 25 | "文件变更", 26 | "gitignore", 27 | "Web应用", 28 | ], 29 | authors: [{ name: "Folda-Scan Team" }], 30 | creator: "Folda-Scan Team", 31 | publisher: "Folda-Scan", 32 | metadataBase: new URL("https://folda-scan.vercel.app"), 33 | openGraph: { 34 | title: "Folda-Scan - 本地文件夹扫描工具", 35 | description: 36 | "扫描和监控本地项目文件夹的变化,支持.gitignore规则,实时生成项目结构和变更报告", 37 | url: "https://folda-scan.vercel.app", 38 | siteName: "Folda-Scan", 39 | images: [ 40 | { 41 | url: "/og-image.png", 42 | width: 1200, 43 | height: 630, 44 | alt: "Folda-Scan - 本地文件夹扫描工具", 45 | }, 46 | ], 47 | locale: "zh_CN", 48 | type: "website", 49 | }, 50 | twitter: { 51 | card: "summary_large_image", 52 | title: "Folda-Scan - 本地文件夹扫描工具", 53 | description: "扫描和监控本地项目文件夹的变化,支持.gitignore规则", 54 | images: ["/og-image.png"], 55 | creator: "@folda_scan", 56 | }, 57 | manifest: "/manifest.json", 58 | themeColor: "#4f46e5", 59 | appleWebApp: { 60 | capable: true, 61 | statusBarStyle: "default", 62 | title: "Folda-Scan", 63 | }, 64 | }; 65 | 66 | export default function RootLayout({ 67 | children, 68 | }: Readonly<{ 69 | children: React.ReactNode; 70 | }>) { 71 | return ( 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | {children} 90 | 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Provider as JotaiProvider } from "jotai"; 4 | import { ReactNode, useEffect } from "react"; 5 | import { LocaleProvider } from "../components/LocaleProvider"; 6 | import { useViewport } from "../lib/useViewport"; 7 | import { setupGlobalViewport } from "../lib/viewportScript"; 8 | import { registerServiceWorker, captureInstallPrompt } from "../lib/pwaUtils"; 9 | import { ViewTransitions } from "next-view-transitions"; 10 | 11 | export function Providers({ children }: { children: ReactNode }) { 12 | // 直接在Providers组件中调用useViewport hook 13 | useViewport(); 14 | 15 | // 初始化全局viewport脚本 16 | useEffect(() => { 17 | setupGlobalViewport(); 18 | }, []); 19 | 20 | // 注册Service Worker 21 | useEffect(() => { 22 | if (typeof window !== "undefined") { 23 | // 注册Service Worker 24 | registerServiceWorker().then((registered) => { 25 | if (registered) { 26 | console.log("Service Worker已注册"); 27 | } 28 | }); 29 | 30 | // 捕获PWA安装提示 31 | captureInstallPrompt(); 32 | } 33 | }, []); 34 | 35 | return ( 36 | 37 | 38 | {children} 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | import { useViewport } from "@/lib/useViewport"; 5 | 6 | export default function SettingsLayout({ children }: { children: ReactNode }) { 7 | // 在设置页的layout中也调用useViewport 8 | useViewport(); 9 | 10 | return <>{children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/statistics/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import StatisticsContent from "@/components/StatisticsContent"; 6 | import { useTranslations } from "@/components/LocaleProvider"; 7 | 8 | export default function StatisticsPage() { 9 | const router = useRouter(); 10 | const { t } = useTranslations(); 11 | const [mounted, setMounted] = useState(false); 12 | 13 | // 确保组件在客户端挂载后才渲染,避免水合错误 14 | useEffect(() => { 15 | setMounted(true); 16 | }, []); 17 | 18 | // 如果组件未挂载,返回null 19 | if (!mounted) return null; 20 | 21 | return ( 22 |
23 |
24 |

25 | 32 | 38 | 39 | {t("statistics.title")} 40 |

41 | 61 |
62 | 63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/BrowserCompatCheck.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { isServiceWorkerSupported } from "@/lib/pwaUtils"; 5 | import { isFileSystemObserverSupported } from "@/lib/fileObserver"; 6 | 7 | // 检查浏览器是否支持File System Access API 8 | function isFileSystemAccessSupported(): boolean { 9 | return "showDirectoryPicker" in window; 10 | } 11 | 12 | export default function BrowserCompatCheck() { 13 | const [showWarning, setShowWarning] = useState(false); 14 | const [unsupportedFeatures, setUnsupportedFeatures] = useState([]); 15 | 16 | useEffect(() => { 17 | // 只在客户端执行 18 | if (typeof window === "undefined") return; 19 | 20 | const unsupported: string[] = []; 21 | 22 | // 检查File System Access API支持 23 | if (!isFileSystemAccessSupported()) { 24 | unsupported.push("文件系统访问"); 25 | } 26 | 27 | // 检查Service Worker支持 28 | if (!isServiceWorkerSupported()) { 29 | unsupported.push("Service Worker"); 30 | } 31 | 32 | // 若有不支持的功能,显示警告 33 | if (unsupported.length > 0) { 34 | setUnsupportedFeatures(unsupported); 35 | setShowWarning(true); 36 | } 37 | }, []); 38 | 39 | if (!showWarning) return null; 40 | 41 | return ( 42 |
43 |
44 |
45 |
46 | 52 | 57 | 58 |
59 |
60 |

浏览器兼容性警告

61 |
62 |

63 | 您的浏览器不支持以下必要功能: 64 | 65 | {" "} 66 | {unsupportedFeatures.join(", ")} 67 | 68 |

69 |

70 | 建议使用 Chrome、Edge 或 Safari 最新版本,并确保安装了所有更新。 71 |

72 | 78 |
79 |
80 |
81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/DockerView.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useAtom } from "jotai"; 5 | import { 6 | directoryHandleAtom, 7 | dockerfilesAtom, 8 | selectedDockerfileAtom, 9 | dockerfileContentAtom, 10 | parsedDockerfileAtom, 11 | dockerfileErrorsAtom, 12 | } from "../lib/store"; 13 | import { 14 | detectDockerfile, 15 | readDockerfile, 16 | parseDockerfile, 17 | validateDockerfile, 18 | fixDockerfile, 19 | } from "../lib/dockerService"; 20 | import { Dockerfile } from "../types"; 21 | import { useTranslations } from "./LocaleProvider"; 22 | 23 | export default function DockerView() { 24 | const { t } = useTranslations(); 25 | const [directoryHandle] = useAtom(directoryHandleAtom); 26 | const [dockerfiles, setDockerfiles] = useAtom(dockerfilesAtom); 27 | const [selectedDockerfile, setSelectedDockerfile] = useAtom( 28 | selectedDockerfileAtom 29 | ); 30 | const [dockerfileContent, setDockerfileContent] = useAtom( 31 | dockerfileContentAtom 32 | ); 33 | const [parsedDockerfile, setParsedDockerfile] = useAtom(parsedDockerfileAtom); 34 | const [dockerfileErrors, setDockerfileErrors] = useAtom(dockerfileErrorsAtom); 35 | const [isFixing, setIsFixing] = useState(false); 36 | 37 | // 检测Dockerfile 38 | useEffect(() => { 39 | if (!directoryHandle) return; 40 | 41 | async function checkForDockerfiles() { 42 | try { 43 | if (!directoryHandle) return; 44 | 45 | const result = await detectDockerfile(directoryHandle); 46 | setDockerfiles(result); 47 | 48 | // 如果找到Dockerfile并且之前没有选择过,自动选择第一个 49 | if (result.exists && result.paths.length > 0 && !selectedDockerfile) { 50 | setSelectedDockerfile(result.paths[0]); 51 | } 52 | } catch (error) { 53 | console.error("检测Dockerfile时出错:", error); 54 | } 55 | } 56 | 57 | checkForDockerfiles(); 58 | }, [ 59 | directoryHandle, 60 | setDockerfiles, 61 | selectedDockerfile, 62 | setSelectedDockerfile, 63 | ]); 64 | 65 | // 加载选中的Dockerfile内容 66 | useEffect(() => { 67 | if (!directoryHandle || !selectedDockerfile) return; 68 | 69 | async function loadDockerfileContent() { 70 | try { 71 | if (!directoryHandle) return; 72 | 73 | const content = await readDockerfile( 74 | directoryHandle, 75 | selectedDockerfile 76 | ); 77 | setDockerfileContent(content); 78 | 79 | // 解析Dockerfile 80 | const dockerfile = parseDockerfile(content); 81 | setParsedDockerfile(dockerfile); 82 | 83 | // 验证Dockerfile 84 | const errors = validateDockerfile(dockerfile); 85 | setDockerfileErrors(errors); 86 | } catch (error) { 87 | console.error(`读取Dockerfile ${selectedDockerfile} 时出错:`, error); 88 | setDockerfileContent(""); 89 | setParsedDockerfile(null); 90 | setDockerfileErrors([`无法读取Dockerfile: ${error}`]); 91 | } 92 | } 93 | 94 | loadDockerfileContent(); 95 | }, [ 96 | directoryHandle, 97 | selectedDockerfile, 98 | setDockerfileContent, 99 | setParsedDockerfile, 100 | setDockerfileErrors, 101 | ]); 102 | 103 | // 修复Dockerfile 104 | const handleFixDockerfile = async () => { 105 | if (!dockerfileContent) return; 106 | 107 | setIsFixing(true); 108 | try { 109 | // 修复Dockerfile内容 110 | const fixedContent = fixDockerfile(dockerfileContent); 111 | setDockerfileContent(fixedContent); 112 | 113 | // 重新解析和验证 114 | const dockerfile = parseDockerfile(fixedContent); 115 | setParsedDockerfile(dockerfile); 116 | const errors = validateDockerfile(dockerfile); 117 | setDockerfileErrors(errors); 118 | } catch (error) { 119 | console.error("修复Dockerfile时出错:", error); 120 | } finally { 121 | setIsFixing(false); 122 | } 123 | }; 124 | 125 | // 处理Dockerfile选择变化 126 | const handleDockerfileChange = (e: React.ChangeEvent) => { 127 | setSelectedDockerfile(e.target.value); 128 | }; 129 | 130 | if (!directoryHandle) { 131 | return ( 132 |
133 |

134 | {t("docker.selectProject")} 135 |

136 |
137 | ); 138 | } 139 | 140 | if (!dockerfiles.exists) { 141 | return ( 142 |
143 |

144 | {t("docker.noDockerfile")} 145 |

146 |
147 | ); 148 | } 149 | 150 | return ( 151 |
152 | {/* 顶部选择器和工具栏 */} 153 |
154 |
155 | 168 | 169 | 176 |
177 | 178 | {/* 错误状态标签 */} 179 | {parsedDockerfile && ( 180 |
0 183 | ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300" 184 | : "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300" 185 | }`} 186 | > 187 | {parsedDockerfile.hasError || dockerfileErrors.length > 0 188 | ? t("docker.hasErrors") 189 | : t("docker.valid")} 190 |
191 | )} 192 |
193 | 194 | {/* 主要内容区域 */} 195 |
196 | {/* 左侧: Dockerfile内容 */} 197 |
198 |

199 | {t("docker.content")} 200 |

201 |
202 |             {dockerfileContent || t("docker.loading")}
203 |           
204 |
205 | 206 | {/* 右侧: 可视化和错误 */} 207 |
208 |

209 | {t("docker.analysis")} 210 |

211 | 212 | {parsedDockerfile ? ( 213 |
214 | {/* 基本信息 */} 215 |
216 |

217 | {t("docker.baseInfo")} 218 |

219 |
220 |
221 |
222 | 223 | {t("docker.baseImage")}: 224 | {" "} 225 | 226 | {parsedDockerfile.baseImage} 227 | 228 |
229 |
230 | 231 | {t("docker.stages")}: 232 | {" "} 233 | 234 | {parsedDockerfile.stages.length} 235 | 236 |
237 | {parsedDockerfile.workdir && ( 238 |
239 | 240 | {t("docker.workdir")}: 241 | {" "} 242 | 243 | {parsedDockerfile.workdir} 244 | 245 |
246 | )} 247 | {parsedDockerfile.exposedPorts.length > 0 && ( 248 |
249 | 250 | {t("docker.ports")}: 251 | {" "} 252 | 253 | {parsedDockerfile.exposedPorts 254 | .map((p) => `${p.number}/${p.protocol}`) 255 | .join(", ")} 256 | 257 |
258 | )} 259 | {parsedDockerfile.cmd && ( 260 |
261 | 262 | {t("docker.cmd")}: 263 | {" "} 264 | 265 | {parsedDockerfile.cmd} 266 | 267 |
268 | )} 269 |
270 |
271 |
272 | 273 | {/* 环境变量 */} 274 | {Object.keys(parsedDockerfile.env).length > 0 && ( 275 |
276 |

277 | {t("docker.environment")} 278 |

279 |
280 |
281 | {Object.entries(parsedDockerfile.env).map( 282 | ([key, value]) => ( 283 |
284 | 285 | {key}: 286 | {" "} 287 | {value} 288 |
289 | ) 290 | )} 291 |
292 |
293 |
294 | )} 295 | 296 | {/* 错误信息 */} 297 | {(parsedDockerfile.hasError || dockerfileErrors.length > 0) && ( 298 |
299 |

300 | {t("docker.errors")} 301 |

302 |
303 |
    304 | {[...parsedDockerfile.errors, ...dockerfileErrors].map( 305 | (error, index) => ( 306 |
  • {error}
  • 307 | ) 308 | )} 309 |
310 |
311 |
312 | )} 313 | 314 | {/* 构建阶段 */} 315 | {parsedDockerfile.stages.length > 1 && ( 316 |
317 |

318 | {t("docker.buildStages")} 319 |

320 |
321 | {parsedDockerfile.stages.map((stage, index) => ( 322 |
326 |
327 | {stage.name} ({stage.baseImage}) 328 |
329 |
330 | {t("docker.instructions")}:{" "} 331 | {stage.instructions.length} 332 |
333 |
334 | ))} 335 |
336 |
337 | )} 338 |
339 | ) : ( 340 |
341 |
342 | {t("docker.loadingAnalysis")} 343 |
344 |
345 | )} 346 |
347 |
348 |
349 | ); 350 | } 351 | -------------------------------------------------------------------------------- /src/components/EnvFileView.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useAtom } from "jotai"; 5 | import { 6 | directoryHandleAtom, 7 | envFilesAtom, 8 | selectedEnvFileAtom, 9 | envFileContentAtom, 10 | parsedEnvFileAtom, 11 | envFileErrorsAtom, 12 | } from "../lib/store"; 13 | import { 14 | detectEnvFiles, 15 | readEnvFile, 16 | parseEnvFile, 17 | validateEnvFile, 18 | fixEnvFile, 19 | } from "../lib/dockerService"; 20 | import { EnvVariable } from "../types"; 21 | import { useTranslations } from "./LocaleProvider"; 22 | 23 | export default function EnvFileView() { 24 | const { t } = useTranslations(); 25 | const [directoryHandle] = useAtom(directoryHandleAtom); 26 | const [envFiles, setEnvFiles] = useAtom(envFilesAtom); 27 | const [selectedEnvFile, setSelectedEnvFile] = useAtom(selectedEnvFileAtom); 28 | const [envFileContent, setEnvFileContent] = useAtom(envFileContentAtom); 29 | const [parsedEnvFile, setParsedEnvFile] = useAtom(parsedEnvFileAtom); 30 | const [envFileErrors, setEnvFileErrors] = useAtom(envFileErrorsAtom); 31 | const [isFixing, setIsFixing] = useState(false); 32 | const [showSensitive, setShowSensitive] = useState(false); 33 | 34 | // 检测环境变量文件 35 | useEffect(() => { 36 | if (!directoryHandle) return; 37 | 38 | async function checkForEnvFiles() { 39 | try { 40 | if (!directoryHandle) return; 41 | 42 | const result = await detectEnvFiles(directoryHandle); 43 | setEnvFiles(result); 44 | 45 | // 如果找到环境变量文件并且之前没有选择过,自动选择第一个 46 | if (result.exists && result.paths.length > 0 && !selectedEnvFile) { 47 | setSelectedEnvFile(result.paths[0]); 48 | } 49 | } catch (error) { 50 | console.error("检测环境变量文件时出错:", error); 51 | } 52 | } 53 | 54 | checkForEnvFiles(); 55 | }, [directoryHandle, setEnvFiles, selectedEnvFile, setSelectedEnvFile]); 56 | 57 | // 加载选中的环境变量文件内容 58 | useEffect(() => { 59 | if (!directoryHandle || !selectedEnvFile) return; 60 | 61 | async function loadEnvFileContent() { 62 | try { 63 | if (!directoryHandle) return; 64 | 65 | const content = await readEnvFile(directoryHandle, selectedEnvFile); 66 | setEnvFileContent(content); 67 | 68 | // 解析环境变量文件 69 | const envFile = parseEnvFile(content, selectedEnvFile); 70 | setParsedEnvFile(envFile); 71 | 72 | // 验证环境变量文件 73 | const errors = validateEnvFile(envFile); 74 | setEnvFileErrors(errors); 75 | } catch (error) { 76 | console.error(`读取环境变量文件 ${selectedEnvFile} 时出错:`, error); 77 | setEnvFileContent(""); 78 | setParsedEnvFile(null); 79 | setEnvFileErrors([`无法读取环境变量文件: ${error}`]); 80 | } 81 | } 82 | 83 | loadEnvFileContent(); 84 | }, [ 85 | directoryHandle, 86 | selectedEnvFile, 87 | setEnvFileContent, 88 | setParsedEnvFile, 89 | setEnvFileErrors, 90 | ]); 91 | 92 | // 修复环境变量文件 93 | const handleFixEnvFile = async () => { 94 | if (!envFileContent) return; 95 | 96 | setIsFixing(true); 97 | try { 98 | // 修复环境变量文件内容 99 | const fixedContent = fixEnvFile(envFileContent); 100 | setEnvFileContent(fixedContent); 101 | 102 | // 重新解析和验证 103 | const envFile = parseEnvFile(fixedContent, selectedEnvFile); 104 | setParsedEnvFile(envFile); 105 | const errors = validateEnvFile(envFile); 106 | setEnvFileErrors(errors); 107 | } catch (error) { 108 | console.error("修复环境变量文件时出错:", error); 109 | } finally { 110 | setIsFixing(false); 111 | } 112 | }; 113 | 114 | // 处理环境变量文件选择变化 115 | const handleEnvFileChange = (e: React.ChangeEvent) => { 116 | setSelectedEnvFile(e.target.value); 117 | }; 118 | 119 | // 处理显示/隐藏敏感信息 120 | const toggleShowSensitive = () => { 121 | setShowSensitive(!showSensitive); 122 | }; 123 | 124 | // 渲染变量值 125 | const renderVariableValue = (variable: EnvVariable) => { 126 | if (variable.isComment) return variable.value; 127 | if (variable.isSensitive && !showSensitive) return "********"; 128 | return variable.value; 129 | }; 130 | 131 | if (!directoryHandle) { 132 | return ( 133 |
134 |

135 | {t("envFile.selectProject")} 136 |

137 |
138 | ); 139 | } 140 | 141 | if (!envFiles.exists) { 142 | return ( 143 |
144 |

145 | {t("envFile.noEnvFile")} 146 |

147 |
148 | ); 149 | } 150 | 151 | return ( 152 |
153 | {/* 顶部选择器和工具栏 */} 154 |
155 |
156 | 169 | 170 | 177 | 178 | 186 |
187 | 188 | {/* 错误状态标签 */} 189 | {parsedEnvFile && ( 190 |
0 193 | ? "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300" 194 | : "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300" 195 | }`} 196 | > 197 | {parsedEnvFile.hasError || envFileErrors.length > 0 198 | ? t("envFile.hasErrors") 199 | : t("envFile.valid")} 200 |
201 | )} 202 |
203 | 204 | {/* 主要内容区域 */} 205 |
206 | {/* 左侧: 环境变量文件内容 */} 207 |
208 |

209 | {t("envFile.content")} 210 |

211 |
212 |             {envFileContent || t("envFile.loading")}
213 |           
214 |
215 | 216 | {/* 右侧: 变量列表 */} 217 |
218 |

219 | {t("envFile.variables")} 220 |

221 | 222 | {parsedEnvFile?.variables && parsedEnvFile.variables.length > 0 ? ( 223 |
224 | 225 | 226 | 227 | 233 | 239 | 245 | 251 | 252 | 253 | 254 | {parsedEnvFile.variables.map((variable, index) => ( 255 | 256 | 259 | 264 | 267 | 284 | 285 | ))} 286 | 287 |
231 | {t("envFile.line")} 232 | 237 | {t("envFile.key")} 238 | 243 | {t("envFile.value")} 244 | 249 | {t("envFile.type")} 250 |
257 | {variable.line} 258 | 260 | {variable.isComment 261 | ? t("envFile.comment") 262 | : variable.key} 263 | 265 | {renderVariableValue(variable)} 266 | 268 | 277 | {variable.isComment 278 | ? t("envFile.commentType") 279 | : variable.isSensitive 280 | ? t("envFile.sensitiveType") 281 | : t("envFile.normalType")} 282 | 283 |
288 |
289 | ) : ( 290 |
291 |

292 | {t("envFile.loading")} 293 |

294 |
295 | )} 296 | 297 | {/* 错误信息 */} 298 | {envFileErrors && envFileErrors.length > 0 && ( 299 |
300 |

301 | {t("envFile.errors")} 302 |

303 |
    304 | {envFileErrors.map((error, index) => ( 305 |
  • 306 | {error} 307 |
  • 308 | ))} 309 |
310 |
311 | )} 312 |
313 |
314 |
315 | ); 316 | } 317 | -------------------------------------------------------------------------------- /src/components/GitHubDownloader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { IoChevronDown, IoChevronUp } from "react-icons/io5"; 7 | import { 8 | FiGithub, 9 | FiDownload, 10 | FiAlertCircle, 11 | FiCheckCircle, 12 | } from "react-icons/fi"; 13 | 14 | export default function GitHubDownloader() { 15 | const { t } = useTranslations(); 16 | const [isExpanded, setIsExpanded] = useState(false); 17 | const [username, setUsername] = useState(""); 18 | const [repository, setRepository] = useState(""); 19 | const [branch, setBranch] = useState("main"); 20 | const [error, setError] = useState(null); 21 | const [success, setSuccess] = useState(false); 22 | 23 | // 处理展开/折叠 24 | const toggleExpand = () => { 25 | setIsExpanded(!isExpanded); 26 | // 重置状态 27 | if (!isExpanded) { 28 | setError(null); 29 | setSuccess(false); 30 | } 31 | }; 32 | 33 | // 验证表单 34 | const validateForm = (): boolean => { 35 | if (!username.trim()) { 36 | setError(t("github.invalidRepo")); 37 | return false; 38 | } 39 | if (!repository.trim()) { 40 | setError(t("github.invalidRepo")); 41 | return false; 42 | } 43 | return true; 44 | }; 45 | 46 | // 下载仓库 47 | const downloadRepository = () => { 48 | if (!validateForm()) return; 49 | 50 | try { 51 | const downloadUrl = `https://api.github.com/repos/${username}/${repository}/zipball/${branch}`; 52 | // 打开新窗口进行下载 53 | window.open(downloadUrl, "_blank"); 54 | // 显示成功消息 55 | setSuccess(true); 56 | 57 | // 清除表单 58 | setTimeout(() => { 59 | setSuccess(false); 60 | }, 3000); 61 | } catch (err) { 62 | console.error("打开下载链接出错:", err); 63 | setError(t("github.networkError")); 64 | } 65 | }; 66 | 67 | // 渲染标题栏 68 | const renderHeader = () => ( 69 |
73 |

74 | 75 | {t("github.title")} 76 |

77 |
78 | {isExpanded ? ( 79 | 80 | ) : ( 81 | 82 | )} 83 |
84 |
85 | ); 86 | 87 | return ( 88 |
89 | {renderHeader()} 90 | 91 | 92 | {isExpanded && ( 93 | 100 |
101 |

102 | {t("github.description")} 103 |

104 | 105 |
106 | {/* 用户名/组织名输入 */} 107 |
108 | 111 | setUsername(e.target.value)} 115 | placeholder={t("github.placeholder.username")} 116 | className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" 117 | /> 118 |
119 | 120 | {/* 仓库名输入 */} 121 |
122 | 125 | setRepository(e.target.value)} 129 | placeholder={t("github.placeholder.repository")} 130 | className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" 131 | /> 132 |
133 | 134 | {/* 分支名输入 */} 135 |
136 | 142 | setBranch(e.target.value)} 146 | placeholder={t("github.placeholder.branch")} 147 | className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" 148 | /> 149 |
150 | 151 | {/* 下载按钮 */} 152 |
153 | 160 |
161 | 162 | {/* 错误信息 */} 163 | {error && ( 164 |
165 |
166 | 167 | 168 | {error} 169 | 170 |
171 |
172 | )} 173 | 174 | {/* 成功信息 */} 175 | {success && ( 176 |
177 |
178 | 179 | 180 | {t("github.success")} 181 | 182 |
183 |
184 | )} 185 |
186 |
187 |
188 | )} 189 |
190 |
191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /src/components/IncrementalScanAlert.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { isFileSystemObserverSupported } from "../lib/fileObserver"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { motion, AnimatePresence } from "framer-motion"; 7 | 8 | export default function IncrementalScanAlert() { 9 | const [showAlert, setShowAlert] = useState(false); 10 | const [isSupported, setIsSupported] = useState(true); 11 | const { t } = useTranslations(); 12 | 13 | useEffect(() => { 14 | // 只在客户端执行 15 | if (typeof window === "undefined") return; 16 | 17 | // 检查是否支持FileSystemObserver API 18 | const supported = isFileSystemObserverSupported(); 19 | setIsSupported(supported); 20 | 21 | // 无论是否支持,都显示提示 22 | setShowAlert(true); 23 | }, []); 24 | 25 | // 如果用户关闭了提示,则不显示 26 | if (!showAlert) return null; 27 | 28 | return ( 29 | 30 | 40 |
47 | {isSupported ? ( 48 | // 支持增量扫描时显示的图标 49 | 55 | 60 | 61 | ) : ( 62 | // 不支持增量扫描时显示的图标 63 | 69 | 74 | 75 | )} 76 |
77 |
78 |
85 | {isSupported ? ( 86 | // 支持增量扫描时显示的文本 87 | <> 88 |

89 | {t("incrementalScan.supported")} 90 |

91 | 92 | ) : ( 93 | // 不支持增量扫描时显示的文本 94 | <> 95 |

96 | {t("incrementalScan.notSupported")} 97 |

98 | 99 | )} 100 |
101 |
102 | 124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/components/InstallPwaButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { 5 | isPwaInstallable, 6 | isPwaInstalled, 7 | showInstallPrompt, 8 | } from "@/lib/pwaUtils"; 9 | import { useTranslations } from "./LocaleProvider"; 10 | 11 | export default function InstallPwaButton() { 12 | const { t } = useTranslations(); 13 | const [isInstallable, setIsInstallable] = useState(false); 14 | const [isInstalled, setIsInstalled] = useState(false); 15 | 16 | useEffect(() => { 17 | // 只在客户端执行 18 | if (typeof window !== "undefined") { 19 | // 检查是否可安装 20 | setIsInstallable(isPwaInstallable()); 21 | 22 | // 检查是否已安装 23 | setIsInstalled(isPwaInstalled()); 24 | 25 | // 监听显示模式变化(当安装后会从browser变为standalone) 26 | const mediaQuery = window.matchMedia("(display-mode: standalone)"); 27 | 28 | const handleChange = (e: MediaQueryListEvent) => { 29 | setIsInstalled(e.matches); 30 | }; 31 | 32 | mediaQuery.addEventListener("change", handleChange); 33 | 34 | return () => { 35 | mediaQuery.removeEventListener("change", handleChange); 36 | }; 37 | } 38 | }, []); 39 | 40 | // 如果已安装或不可安装,则不显示按钮 41 | if (isInstalled || !isInstallable) { 42 | return null; 43 | } 44 | 45 | const handleInstall = async () => { 46 | const installed = await showInstallPrompt(); 47 | if (installed) { 48 | setIsInstalled(true); 49 | } 50 | }; 51 | 52 | return ( 53 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/LocaleProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ReactNode, 5 | createContext, 6 | useContext, 7 | useEffect, 8 | useState, 9 | } from "react"; 10 | import { useAtom } from "jotai"; 11 | import { localeAtom, Locale, loadMessages } from "../lib/i18n"; 12 | 13 | // 预加载翻译数据 14 | // 这将在应用初始化时立即开始加载翻译 15 | const preloadedTranslationsPromise: Record> = { 16 | en: loadMessages("en"), 17 | zh: loadMessages("zh"), 18 | }; 19 | 20 | // 创建翻译上下文 21 | type TranslationsContextType = { 22 | t: (key: string, params?: Record) => string; 23 | tObject: (key: string) => any; 24 | locale: Locale; 25 | setLocale: (locale: Locale) => void; 26 | }; 27 | 28 | const TranslationsContext = createContext(null); 29 | 30 | // 翻译钩子 31 | export function useTranslations() { 32 | const context = useContext(TranslationsContext); 33 | if (!context) { 34 | throw new Error("useTranslations must be used within a LocaleProvider"); 35 | } 36 | return context; 37 | } 38 | 39 | // 解析翻译键 40 | function getNestedTranslation(obj: any, path: string): string { 41 | const keys = path.split("."); 42 | let result = obj; 43 | 44 | for (const key of keys) { 45 | if (result && typeof result === "object" && key in result) { 46 | result = result[key]; 47 | } else { 48 | return path; // 如果找不到翻译,返回原键 49 | } 50 | } 51 | 52 | return typeof result === "string" ? result : path; 53 | } 54 | 55 | // 获取对象类型的翻译 56 | function getNestedObject(obj: any, path: string): any { 57 | const keys = path.split("."); 58 | let result = obj; 59 | 60 | for (const key of keys) { 61 | if (result && typeof result === "object" && key in result) { 62 | result = result[key]; 63 | } else { 64 | return null; // 如果找不到翻译对象,返回null 65 | } 66 | } 67 | 68 | return result; 69 | } 70 | 71 | // 替换参数 72 | function replaceParams(text: string, params?: Record): string { 73 | if (!params) return text; 74 | 75 | let result = text; 76 | for (const key in params) { 77 | result = result.replace(new RegExp(`{${key}}`, "g"), params[key]); 78 | } 79 | 80 | return result; 81 | } 82 | 83 | // 国际化提供者组件 84 | export function LocaleProvider({ children }: { children: ReactNode }) { 85 | const [locale, setLocale] = useAtom(localeAtom); 86 | const [translations, setTranslations] = useState>({}); 87 | const [loading, setLoading] = useState(true); 88 | const [initialLoadComplete, setInitialLoadComplete] = useState(false); 89 | 90 | // 加载翻译 91 | useEffect(() => { 92 | async function loadTranslations() { 93 | try { 94 | setLoading(true); 95 | 96 | // 使用预加载的翻译数据 97 | const messages = await preloadedTranslationsPromise[locale]; 98 | setTranslations(messages); 99 | 100 | // 标记初始加载已完成 101 | if (!initialLoadComplete) { 102 | setInitialLoadComplete(true); 103 | } 104 | } catch (error) { 105 | console.error("Failed to load translations:", error); 106 | } finally { 107 | setLoading(false); 108 | } 109 | } 110 | 111 | loadTranslations(); 112 | }, [locale, initialLoadComplete]); 113 | 114 | // 翻译函数 115 | const t = (key: string, params?: Record): string => { 116 | // 如果还在加载中,但已经有翻译数据,则尝试使用现有数据 117 | if (loading && Object.keys(translations).length > 0) { 118 | const translation = getNestedTranslation(translations, key); 119 | // 如果能找到翻译,则使用它,否则显示加载占位符 120 | if (translation !== key) { 121 | return replaceParams(translation, params); 122 | } 123 | 124 | // 对于常见的UI元素,提供默认占位符 125 | if (key.includes("title") || key.includes("name")) return "..."; 126 | return ""; // 其他情况返回空字符串而不是键名 127 | } 128 | 129 | // 如果没有翻译数据,但不是第一次加载 130 | if (!translations && initialLoadComplete) return key; 131 | 132 | // 正常情况:有翻译数据且不在加载中 133 | const translation = getNestedTranslation(translations, key); 134 | return replaceParams(translation, params); 135 | }; 136 | 137 | // 对象翻译函数 138 | const tObject = (key: string): any => { 139 | if (!translations) return null; 140 | return getNestedObject(translations, key); 141 | }; 142 | 143 | // 如果是第一次加载且没有翻译数据,显示一个最小的加载状态 144 | if (!initialLoadComplete && loading) { 145 | return null; // 或者返回一个简单的加载指示器 146 | } 147 | 148 | return ( 149 | 150 | {children} 151 | 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/components/MultiThreadScanAlert.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { isMultiThreadScanSupported } from "@/lib/workerUtils"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { motion, AnimatePresence } from "framer-motion"; 7 | 8 | export default function MultiThreadScanAlert() { 9 | const [showAlert, setShowAlert] = useState(false); 10 | const [isSupported, setIsSupported] = useState(true); 11 | const { t } = useTranslations(); 12 | 13 | useEffect(() => { 14 | // 只在客户端执行 15 | if (typeof window === "undefined") return; 16 | 17 | // 检查是否支持多线程扫描 18 | const supported = isMultiThreadScanSupported(); 19 | setIsSupported(supported); 20 | 21 | // 无论是否支持,都显示提示 22 | setShowAlert(true); 23 | }, []); 24 | 25 | // 如果用户关闭了提示,则不显示 26 | if (!showAlert) return null; 27 | 28 | return ( 29 | 30 | 40 |
47 | {isSupported ? ( 48 | // 支持多线程扫描时显示的图标 49 | 55 | 60 | 61 | ) : ( 62 | // 不支持多线程扫描时显示的图标 63 | 69 | 74 | 75 | )} 76 |
77 |
78 |
85 | {isSupported ? ( 86 | // 支持多线程扫描时显示的文本 87 | <> 88 |

89 | {t("changelog.multiThreadSupport.supported")} 90 |

91 | 92 | ) : ( 93 | // 不支持多线程扫描时显示的文本 94 | <> 95 |

96 | {t("changelog.multiThreadSupport.notSupported")} 97 |

98 | 99 | )} 100 |
101 |
102 | 124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/components/PresetPromptModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, Fragment } from "react"; 4 | import { Transition, Dialog } from "@headlessui/react"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { motion } from "framer-motion"; 7 | 8 | interface PresetPromptModalProps { 9 | isOpen: boolean; 10 | onClose: () => void; 11 | onSelectPrompt: (prompt: string) => void; 12 | } 13 | 14 | interface CategoryInfo { 15 | id: string; 16 | name: string; 17 | } 18 | 19 | interface PromptInfo { 20 | key: string; 21 | text: string; 22 | category: string; 23 | categoryName: string; 24 | } 25 | 26 | export default function PresetPromptModal({ 27 | isOpen, 28 | onClose, 29 | onSelectPrompt, 30 | }: PresetPromptModalProps) { 31 | const { t, tObject } = useTranslations(); 32 | const [searchQuery, setSearchQuery] = useState(""); 33 | const [allPrompts, setAllPrompts] = useState([]); 34 | const [filteredPrompts, setFilteredPrompts] = useState([]); 35 | 36 | // 获取所有提示类别 37 | const getCategories = (): CategoryInfo[] => { 38 | // 从翻译文件中动态获取类别 39 | const categories: CategoryInfo[] = []; 40 | const categoriesPath = "presetPrompts.categories"; 41 | 42 | // 使用tObject直接获取类别对象 43 | const categoriesObj = tObject(categoriesPath); 44 | 45 | if (categoriesObj && typeof categoriesObj === "object") { 46 | // 遍历类别 47 | for (const categoryKey in categoriesObj) { 48 | const categoryData = categoriesObj[categoryKey]; 49 | if ( 50 | categoryData && 51 | typeof categoryData === "object" && 52 | categoryData.name 53 | ) { 54 | categories.push({ 55 | id: categoryKey, 56 | name: categoryData.name, 57 | }); 58 | } 59 | } 60 | } 61 | 62 | // 如果没有获取到类别,提供默认类别 63 | if (categories.length === 0) { 64 | return [ 65 | { id: "coreProcesses", name: "核心流程" }, 66 | { id: "components", name: "组件与职责" }, 67 | { id: "dataFlow", name: "数据流路径" }, 68 | { id: "implementation", name: "巧妙实现" }, 69 | { id: "startup", name: "启动与初始化" }, 70 | ]; 71 | } 72 | 73 | return categories; 74 | }; 75 | 76 | // 获取所有提示 77 | const getAllPrompts = (): PromptInfo[] => { 78 | const categories = getCategories(); 79 | const prompts: PromptInfo[] = []; 80 | 81 | for (const category of categories) { 82 | const promptsPath = `presetPrompts.categories.${category.id}.prompts`; 83 | const promptsObj = tObject(promptsPath); 84 | 85 | if (promptsObj && typeof promptsObj === "object") { 86 | // 遍历提示 87 | for (const promptKey in promptsObj) { 88 | if (typeof promptsObj[promptKey] === "string") { 89 | prompts.push({ 90 | key: promptKey, 91 | text: promptsObj[promptKey], 92 | category: category.id, 93 | categoryName: category.name, 94 | }); 95 | } 96 | } 97 | } else { 98 | // 如果没有获取到提示,提供默认提示 99 | const defaultPrompts: Record = 100 | { 101 | coreProcesses: [ 102 | { 103 | key: "mainFlow", 104 | text: "请描述当用户执行主要操作时,从前端到后端的典型处理步骤和关键函数调用顺序。", 105 | }, 106 | ], 107 | components: [ 108 | { 109 | key: "overview", 110 | text: "构成这个项目的主要模块或组件有哪些?请简要描述每个部分的主要功能以及它们如何协作。", 111 | }, 112 | ], 113 | dataFlow: [ 114 | { 115 | key: "tracking", 116 | text: "追踪一个关键数据片段(如用户输入或业务实体)并解释它是如何生成的,哪些函数处理/转换它,以及它最终流向何处。", 117 | }, 118 | ], 119 | implementation: [ 120 | { key: "clever", text: "项目中有哪些巧妙的实现?" }, 121 | ], 122 | startup: [ 123 | { 124 | key: "process", 125 | text: "系统是如何启动和初始化的?请指出首先执行的关键脚本或函数,以及它们主要完成哪些准备工作。", 126 | }, 127 | ], 128 | }; 129 | 130 | if (defaultPrompts[category.id]) { 131 | defaultPrompts[category.id].forEach((prompt) => { 132 | prompts.push({ 133 | ...prompt, 134 | category: category.id, 135 | categoryName: category.name, 136 | }); 137 | }); 138 | } 139 | } 140 | } 141 | 142 | return prompts; 143 | }; 144 | 145 | // 初始化和搜索提示 146 | useEffect(() => { 147 | const prompts = getAllPrompts(); 148 | setAllPrompts(prompts); 149 | 150 | if (searchQuery) { 151 | const filtered = prompts.filter((prompt) => 152 | prompt.text.toLowerCase().includes(searchQuery.toLowerCase()) 153 | ); 154 | setFilteredPrompts(filtered); 155 | } else { 156 | setFilteredPrompts(prompts); 157 | } 158 | }, [searchQuery, isOpen]); 159 | 160 | // 当modal打开时重新加载提示 161 | useEffect(() => { 162 | if (isOpen) { 163 | setSearchQuery(""); 164 | const prompts = getAllPrompts(); 165 | setAllPrompts(prompts); 166 | setFilteredPrompts(prompts); 167 | } 168 | }, [isOpen]); 169 | 170 | // 处理提示点击 171 | const handlePromptClick = (prompt: string) => { 172 | onSelectPrompt(prompt); 173 | onClose(); 174 | }; 175 | 176 | return ( 177 | 178 | 179 | 188 |
189 | 190 | 191 |
192 |
193 | 202 | 203 |
204 | 208 | 215 | 221 | 222 | {t("presetPrompts.title")} 223 | 224 | 244 |
245 | 246 |
247 |

248 | {t("presetPrompts.description")} 249 |

250 | 251 |
252 |
253 | 268 |
269 | setSearchQuery(e.target.value)} 275 | /> 276 |
277 | 278 |
279 |
280 | {filteredPrompts.length > 0 ? ( 281 | filteredPrompts.map((prompt, index) => ( 282 | handlePromptClick(prompt.text)} 292 | > 293 |

294 | {prompt.text} 295 |

296 |

297 | {prompt.categoryName} 298 |

299 |
300 | )) 301 | ) : ( 302 |

303 | 没有找到匹配的提示。 304 |

305 | )} 306 |
307 |
308 |
309 | 310 |
311 | 318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 | ); 326 | } 327 | -------------------------------------------------------------------------------- /src/components/ProjectConfigVisualizer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useMemo } from "react"; 4 | import ReactFlow, { 5 | Node, 6 | Edge, 7 | NodeTypes, 8 | Handle, 9 | Position, 10 | NodeProps, 11 | Background, 12 | Controls, 13 | MiniMap, 14 | } from "reactflow"; 15 | import "reactflow/dist/style.css"; 16 | import { useTranslations } from "./LocaleProvider"; 17 | import { themeAtom } from "../lib/store"; 18 | import { useAtom } from "jotai"; 19 | 20 | // 配置文件类型 21 | interface ConfigFile { 22 | name: string; 23 | path: string; 24 | type: string; 25 | content?: string; 26 | dependencies?: string[]; 27 | } 28 | 29 | // 获取文件类型 30 | const getFileType = (fileName: string): string => { 31 | if (fileName.includes("config")) return "config"; 32 | if (fileName.endsWith(".json")) return "config"; 33 | if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) return "config"; 34 | if (fileName.endsWith(".toml") || fileName.endsWith(".ini")) return "config"; 35 | if (fileName.startsWith(".")) return "config"; 36 | return "default"; 37 | }; 38 | 39 | // 节点数据类型 40 | interface NodeData { 41 | label: string; 42 | type: string; 43 | path?: string; 44 | content?: string; 45 | isExpanded: boolean; 46 | onToggleExpand: (id: string) => void; 47 | } 48 | 49 | // 主题类型 50 | type ThemeType = "light" | "dark"; 51 | 52 | // 主题配置 53 | const themeColors = { 54 | light: { 55 | config: "#3182ce", // 蓝色 56 | component: "#38a169", // 绿色 57 | page: "#d69e2e", // 黄色 58 | api: "#805ad5", // 紫色 59 | lib: "#dd6b20", // 橙色 60 | public: "#e53e3e", // 红色 61 | background: "#f7fafc", 62 | text: "#1a202c", 63 | line: "#a0aec0", 64 | }, 65 | dark: { 66 | config: "#4299e1", // 蓝色 67 | component: "#48bb78", // 绿色 68 | page: "#ecc94b", // 黄色 69 | api: "#9f7aea", // 紫色 70 | lib: "#ed8936", // 橙色 71 | public: "#fc8181", // 红色 72 | background: "#1a202c", 73 | text: "#f7fafc", 74 | line: "#4a5568", 75 | }, 76 | }; 77 | 78 | // 文件类型图标 79 | const fileIcons: Record = { 80 | config: "⚙️", 81 | component: "🧩", 82 | page: "📄", 83 | api: "🔌", 84 | lib: "📚", 85 | public: "🌐", 86 | default: "📁", 87 | }; 88 | 89 | // 自定义节点组件 90 | const ConfigNode = ({ data }: NodeProps) => { 91 | const [theme] = useAtom(themeAtom); 92 | const colors = themeColors[(theme as ThemeType) || "light"]; 93 | 94 | // 获取节点类型的颜色 95 | const getColor = (type: string) => { 96 | return colors[type as keyof typeof colors] || colors.config; 97 | }; 98 | 99 | const backgroundColor = getColor(data.type); 100 | const isExpanded = data.isExpanded; 101 | 102 | return ( 103 |
104 | 105 | 106 | {/* 节点标题 */} 107 |
111 |
112 | 113 | {fileIcons[data.type] || fileIcons.default} 114 | 115 |
{data.label}
116 |
117 | 118 | {data.content && ( 119 | 125 | )} 126 |
127 | 128 | {/* 节点内容 */} 129 | {isExpanded && data.content && ( 130 |
131 |
132 |
133 |               {data.content}
134 |             
135 |
136 |
137 | )} 138 | 139 | 140 |
141 | ); 142 | }; 143 | 144 | // 节点类型定义 145 | const nodeTypes: NodeTypes = { 146 | configNode: ConfigNode, 147 | }; 148 | 149 | // 主组件 150 | export default function ProjectConfigVisualizer({ 151 | entries, 152 | }: { 153 | entries: FileSystemEntry[]; 154 | }) { 155 | const { t } = useTranslations(); 156 | const [theme] = useAtom(themeAtom); 157 | const [nodes, setNodes] = useState([]); 158 | const [edges, setEdges] = useState([]); 159 | const [expandedNodes, setExpandedNodes] = useState>( 160 | {} 161 | ); 162 | 163 | // 配置文件列表 164 | const configFiles = useMemo(() => { 165 | const configs: ConfigFile[] = []; 166 | 167 | // 扫描配置文件 168 | entries.forEach((entry) => { 169 | if (entry.kind === "file") { 170 | const fileName = entry.name.toLowerCase(); 171 | 172 | // 检查是否为配置文件 173 | if ( 174 | fileName.includes("config") || 175 | fileName.endsWith(".json") || 176 | fileName.endsWith(".yaml") || 177 | fileName.endsWith(".yml") || 178 | fileName.endsWith(".toml") || 179 | fileName.endsWith(".ini") || 180 | fileName === ".env" || 181 | fileName === ".gitignore" || 182 | fileName.startsWith(".") // 大多数配置文件都是以点开头的隐藏文件 183 | ) { 184 | const fileType = getFileType(fileName); 185 | configs.push({ 186 | name: entry.name, 187 | path: entry.path, 188 | type: fileType, 189 | content: entry.content, 190 | }); 191 | } 192 | } 193 | }); 194 | 195 | return configs; 196 | }, [entries]); 197 | 198 | // 切换节点展开状态 199 | const toggleNodeExpand = (nodeId: string) => { 200 | setExpandedNodes((prev) => ({ 201 | ...prev, 202 | [nodeId]: !prev[nodeId], 203 | })); 204 | }; 205 | 206 | // 创建节点和边 207 | useEffect(() => { 208 | if (configFiles.length === 0) return; 209 | 210 | const newNodes: Node[] = []; 211 | const newEdges: Edge[] = []; 212 | 213 | // 创建根节点 214 | newNodes.push({ 215 | id: "root", 216 | type: "configNode", 217 | position: { x: 0, y: 0 }, 218 | data: { 219 | label: "项目配置", 220 | type: "config", 221 | isExpanded: true, 222 | onToggleExpand: toggleNodeExpand, 223 | }, 224 | }); 225 | 226 | // 创建配置文件节点 227 | configFiles.forEach((file, index) => { 228 | const nodeId = `config-${index}`; 229 | 230 | // 添加节点 231 | newNodes.push({ 232 | id: nodeId, 233 | type: "configNode", 234 | position: { 235 | x: (index % 3) * 350 - 350, 236 | y: Math.floor(index / 3) * 300 + 150, 237 | }, 238 | data: { 239 | label: file.name, 240 | type: file.type, 241 | path: file.path, 242 | content: file.content, 243 | isExpanded: expandedNodes[file.path] || false, 244 | onToggleExpand: toggleNodeExpand, 245 | }, 246 | }); 247 | 248 | // 添加边 249 | newEdges.push({ 250 | id: `edge-root-${nodeId}`, 251 | source: "root", 252 | target: nodeId, 253 | animated: true, 254 | }); 255 | }); 256 | 257 | // 添加项目架构节点 258 | const architectureNodes = [ 259 | { id: "arch-pages", label: "页面", type: "page", x: 350, y: 100 }, 260 | { 261 | id: "arch-components", 262 | label: "组件", 263 | type: "component", 264 | x: 350, 265 | y: 300, 266 | }, 267 | { id: "arch-lib", label: "库函数", type: "lib", x: 350, y: 500 }, 268 | { id: "arch-public", label: "静态资源", type: "public", x: 700, y: 200 }, 269 | { id: "arch-api", label: "API", type: "api", x: 700, y: 400 }, 270 | ]; 271 | 272 | // 添加架构节点 273 | architectureNodes.forEach((node) => { 274 | newNodes.push({ 275 | id: node.id, 276 | type: "configNode", 277 | position: { x: node.x, y: node.y }, 278 | data: { 279 | label: node.label, 280 | type: node.type, 281 | isExpanded: false, 282 | onToggleExpand: toggleNodeExpand, 283 | }, 284 | }); 285 | 286 | // 添加边 287 | newEdges.push({ 288 | id: `edge-${node.id}`, 289 | source: "root", 290 | target: node.id, 291 | animated: false, 292 | }); 293 | }); 294 | 295 | // 设置架构节点之间的关系 296 | newEdges.push( 297 | { 298 | id: "edge-pages-components", 299 | source: "arch-pages", 300 | target: "arch-components", 301 | }, 302 | { 303 | id: "edge-components-lib", 304 | source: "arch-components", 305 | target: "arch-lib", 306 | }, 307 | { id: "edge-pages-api", source: "arch-pages", target: "arch-api" }, 308 | { id: "edge-api-lib", source: "arch-api", target: "arch-lib" }, 309 | { id: "edge-pages-public", source: "arch-pages", target: "arch-public" } 310 | ); 311 | 312 | setNodes(newNodes); 313 | setEdges(newEdges); 314 | }, [configFiles, expandedNodes]); 315 | 316 | // 如果没有配置文件 317 | if (configFiles.length === 0) { 318 | return ( 319 |
320 |

321 | {t("projectConfig.noConfigFiles")} 322 |

323 |
324 | ); 325 | } 326 | 327 | return ( 328 |
329 | 336 | 341 | 342 | { 344 | const type = (n.data?.type || "default") as string; 345 | return ( 346 | themeColors[(theme as ThemeType) || "light"][ 347 | type as keyof typeof themeColors.light 348 | ] || "#ddd" 349 | ); 350 | }} 351 | nodeColor={(n) => { 352 | const type = (n.data?.type || "default") as string; 353 | return ( 354 | themeColors[(theme as ThemeType) || "light"][ 355 | type as keyof typeof themeColors.light 356 | ] || "#ddd" 357 | ); 358 | }} 359 | maskColor={ 360 | themeColors[(theme as ThemeType) || "light"].background + "80" 361 | } 362 | /> 363 | 364 |
365 | ); 366 | } 367 | 368 | // 声明 FileSystemEntry 类型 369 | interface FileSystemEntry { 370 | name: string; 371 | kind: "file" | "directory"; 372 | path: string; 373 | lastModified?: number; 374 | size?: number; 375 | content?: string; 376 | } 377 | -------------------------------------------------------------------------------- /src/components/RequirementGeneratorModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef } from "react"; 4 | import { motion } from "framer-motion"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { useTheme } from "next-themes"; 7 | 8 | interface RequirementGeneratorModalProps { 9 | context: string; 10 | onClose: () => void; 11 | } 12 | 13 | export default function RequirementGeneratorModal({ 14 | context, 15 | onClose, 16 | }: RequirementGeneratorModalProps) { 17 | const { t, locale } = useTranslations(); 18 | const { resolvedTheme } = useTheme(); 19 | const [requirement, setRequirement] = useState(""); 20 | const [isGenerating, setIsGenerating] = useState(false); 21 | const [isCopied, setIsCopied] = useState(false); 22 | const [error, setError] = useState(""); 23 | const contentRef = useRef(null); 24 | 25 | useEffect(() => { 26 | // 生成需求 27 | generateRequirement(); 28 | }, []); 29 | 30 | // 自动滚动到底部 31 | useEffect(() => { 32 | if (contentRef.current && isGenerating) { 33 | contentRef.current.scrollTop = contentRef.current.scrollHeight; 34 | } 35 | }, [requirement, isGenerating]); 36 | 37 | const generateRequirement = async () => { 38 | if (isGenerating) return; 39 | 40 | setIsGenerating(true); 41 | setRequirement(""); 42 | setError(""); 43 | 44 | try { 45 | // 构建提示词,根据当前语言设置调整 46 | const promptLanguage = locale === "zh" ? "Chinese" : "English"; 47 | 48 | const prompt = ` 49 | Based on the following conversation, please generate a clear, specific requirement description. This requirement should: 50 | 1. Be written in the first person from the user's perspective ("I need...", "I want..." etc.) 51 | 2. Clearly describe functional requirements, technical requirements, and expected results 52 | 3. Be structured for easy understanding by developers 53 | 4. Avoid overly complex technical terms, keeping it concise and clear 54 | 5. Be suitable for direct pasting into code assistant tools (such as Cursor, GitHub Copilot, etc.) 55 | 6. Be written in ${promptLanguage} 56 | 57 | Conversation content: 58 | ${context} 59 | 60 | Please start directly with "My requirement is:" or the equivalent in ${promptLanguage}, without adding any prefix explanation or introduction. 61 | `; 62 | 63 | // 调用API获取流式响应 64 | const response = await fetch("https://text.pollinations.ai/openai", { 65 | method: "POST", 66 | headers: { 67 | "Content-Type": "application/json", 68 | }, 69 | body: JSON.stringify({ 70 | model: "", 71 | messages: [ 72 | { 73 | role: "system", 74 | content: `You are a professional requirements analyst who specializes in converting conversation content into structured development requirements. Please respond in ${promptLanguage}.`, 75 | }, 76 | { role: "user", content: prompt }, 77 | ], 78 | stream: true, 79 | referrer: "FoldaScan", 80 | }), 81 | }); 82 | 83 | if (!response.ok) { 84 | throw new Error(`HTTP error! status: ${response.status}`); 85 | } 86 | 87 | const reader = response.body?.getReader(); 88 | if (!reader) throw new Error("无法获取响应流"); 89 | 90 | // 用于解析SSE数据的函数 91 | const decoder = new TextDecoder(); 92 | let buffer = ""; 93 | 94 | while (true) { 95 | const { done, value } = await reader.read(); 96 | if (done) break; 97 | 98 | // 解码当前块 99 | buffer += decoder.decode(value, { stream: true }); 100 | 101 | // 处理SSE数据 102 | const lines = buffer.split("\n"); 103 | buffer = lines.pop() || ""; 104 | 105 | for (const line of lines) { 106 | if (line.startsWith("data: ")) { 107 | const data = line.slice(5).trim(); 108 | if (data === "[DONE]") break; 109 | 110 | try { 111 | const parsed = JSON.parse(data); 112 | const content = parsed.choices?.[0]?.delta?.content || ""; 113 | if (content) { 114 | setRequirement((prev) => prev + content); 115 | } 116 | } catch (e) { 117 | console.error("解析SSE数据出错:", e); 118 | } 119 | } 120 | } 121 | } 122 | } catch (error) { 123 | console.error("生成需求出错:", error); 124 | setError(t("requirementGenerator.error")); 125 | } finally { 126 | setIsGenerating(false); 127 | } 128 | }; 129 | 130 | const handleCopy = () => { 131 | navigator.clipboard.writeText(requirement); 132 | setIsCopied(true); 133 | setTimeout(() => setIsCopied(false), 2000); 134 | }; 135 | 136 | return ( 137 |
138 | 144 | {/* 标题栏 */} 145 |
146 |

147 | 154 | 160 | 161 | {t("requirementGenerator.title")} 162 |

163 | 182 |
183 | 184 | {/* 内容区域 */} 185 |
186 | {error ? ( 187 |
188 | {error} 189 |
190 | ) : isGenerating ? ( 191 | <> 192 |
193 | {t("requirementGenerator.generating")} 194 |
195 |
196 |
197 | {requirement} 198 | 207 |
208 |
209 | 210 | ) : ( 211 | <> 212 |
213 | {t("requirementGenerator.generated")} 214 |
215 |
216 |
217 | {requirement} 218 |
219 |
220 | 221 | )} 222 |
223 | 224 | {/* 底部按钮区域 */} 225 |
226 |
227 | {t("requirementGenerator.footer")} 228 |
229 |
230 | 237 | {t("requirementGenerator.close")} 238 | 239 | 246 | {isCopied ? ( 247 | <> 248 | 254 | 259 | 260 | {t("requirementGenerator.copied")} 261 | 262 | ) : ( 263 | <> 264 | 271 | 277 | 278 | {t("requirementGenerator.copy")} 279 | 280 | )} 281 | 282 |
283 |
284 |
285 |
286 | ); 287 | } 288 | -------------------------------------------------------------------------------- /src/components/ResultDisplay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useAtom } from "jotai"; 5 | import { 6 | changeReportAtom, 7 | currentScanAtom, 8 | scanStatusAtom, 9 | errorMessageAtom, 10 | showAllFilesAtom, 11 | themeAtom, 12 | readmeContentAtom, 13 | } from "../lib/store"; 14 | import dynamic from "next/dynamic"; 15 | import { useTranslations } from "./LocaleProvider"; 16 | import ReactMarkdown from "react-markdown"; 17 | 18 | // 动态导入差异查看器组件 19 | const DiffViewer = dynamic(() => import("react-diff-viewer-continued"), { 20 | ssr: false, 21 | }); 22 | 23 | export default function ResultDisplay() { 24 | const { t } = useTranslations(); 25 | const [changeReport] = useAtom(changeReportAtom); 26 | const [currentScan] = useAtom(currentScanAtom); 27 | const [scanStatus] = useAtom(scanStatusAtom); 28 | const [errorMessage] = useAtom(errorMessageAtom); 29 | const [showAllFiles] = useAtom(showAllFilesAtom); 30 | const [theme] = useAtom(themeAtom); 31 | const [readmeContent] = useAtom(readmeContentAtom); 32 | 33 | const [activeTab, setActiveTab] = useState< 34 | "documentation" | "structure" | "changes" | "details" | "files" 35 | >(readmeContent ? "documentation" : "structure"); 36 | const [selectedFile, setSelectedFile] = useState(null); 37 | 38 | // 调试用:监控状态变化 39 | useEffect(() => { 40 | console.log("当前选中文件:", selectedFile); 41 | console.log("当前活动标签:", activeTab); 42 | 43 | if (selectedFile && changeReport) { 44 | const file = changeReport.modifiedFiles.find( 45 | (f) => f.path === selectedFile 46 | ); 47 | console.log("找到的文件:", file); 48 | } 49 | }, [selectedFile, activeTab, changeReport]); 50 | 51 | // 当无结果时的提示 52 | if (!currentScan) { 53 | return ( 54 |
55 | {scanStatus === "scanning" ? ( 56 |

57 | {t("resultDisplay.waitingForScan")} 58 |

59 | ) : ( 60 |

61 | {t("resultDisplay.selectFolderPrompt")} 62 |

63 | )} 64 | 65 | {errorMessage && ( 66 |

{errorMessage}

67 | )} 68 |
69 | ); 70 | } 71 | 72 | // 渲染项目结构 73 | const renderStructure = () => { 74 | if (!changeReport) 75 | return ( 76 |

还没有项目结构信息

77 | ); 78 | 79 | return ( 80 |
 81 |         {changeReport.projectStructure}
 82 |       
83 | ); 84 | }; 85 | 86 | // 处理文件点击 87 | const handleFileClick = (filePath: string) => { 88 | console.log("文件被点击:", filePath); 89 | setSelectedFile(filePath); 90 | setActiveTab("details"); 91 | }; 92 | 93 | // 文件夹图标 94 | const FolderIcon = () => ( 95 | 101 | 106 | 107 | 108 | ); 109 | 110 | // 文件图标 111 | const FileIcon = () => ( 112 | 118 | 123 | 124 | ); 125 | 126 | // 渲染变动列表 127 | const renderChanges = () => { 128 | if (!changeReport) 129 | return

还没有变动信息

; 130 | 131 | const hasChanges = 132 | changeReport.addedFiles.length > 0 || 133 | changeReport.deletedFiles.length > 0 || 134 | changeReport.modifiedFiles.length > 0; 135 | 136 | if (!hasChanges) { 137 | return ( 138 |

139 | {t("resultDisplay.noChanges")} 140 |

141 | ); 142 | } 143 | 144 | return ( 145 |
146 | {changeReport.addedFiles.length > 0 && ( 147 |
148 |

149 | {t("resultDisplay.addedFiles")} ({changeReport.addedFiles.length}) 150 |

151 |
    152 | {changeReport.addedFiles.map((file) => { 153 | // 检查是否有对应的修改文件记录(包含diff) 154 | const hasFileDiff = changeReport.modifiedFiles.some( 155 | (diff) => diff.path === file.path && diff.type === "added" 156 | ); 157 | const isDirectory = file.type === "directory"; 158 | 159 | return ( 160 |
  • handleFileClick(file.path) 170 | : undefined 171 | } 172 | > 173 | {isDirectory ? : } 174 | {file.path} 175 | {isDirectory ? "/" : ""} 176 |
  • 177 | ); 178 | })} 179 |
180 |
181 | )} 182 | 183 | {changeReport.deletedFiles.length > 0 && ( 184 |
185 |

186 | {t("resultDisplay.deletedFiles")} ( 187 | {changeReport.deletedFiles.length}) 188 |

189 |
    190 | {changeReport.deletedFiles.map((file) => { 191 | const isDirectory = file.type === "directory"; 192 | return ( 193 |
  • 197 | {isDirectory ? : } 198 | {file.path} 199 | {isDirectory ? "/" : ""} 200 |
  • 201 | ); 202 | })} 203 |
204 |
205 | )} 206 | 207 | {changeReport.modifiedFiles.length > 0 && ( 208 |
209 |

210 | {t("resultDisplay.modifiedFiles")} ( 211 | {changeReport.modifiedFiles.length}) 212 |

213 |
    214 | {changeReport.modifiedFiles.map((file) => ( 215 |
  • handleFileClick(file.path)} 223 | > 224 | 225 | {file.path} 226 | 235 | {file.type === "added" ? "新增" : "修改"} 236 | 237 |
  • 238 | ))} 239 |
240 |
241 | )} 242 |
243 | ); 244 | }; 245 | 246 | // 渲染文件详情 247 | const renderDetails = () => { 248 | if (!changeReport || !selectedFile) { 249 | return ( 250 |

251 | 请从变动列表中选择一个文件查看详情 252 |

253 | ); 254 | } 255 | 256 | const file = changeReport.modifiedFiles.find( 257 | (f) => f.path === selectedFile 258 | ); 259 | 260 | if (!file) { 261 | return ( 262 |

找不到所选文件的详情

263 | ); 264 | } 265 | 266 | return ( 267 |
268 |

{file.path}

269 |
270 |

271 | 文件类型:{" "} 272 | 273 | {file.type === "added" 274 | ? "新增" 275 | : file.type === "deleted" 276 | ? "删除" 277 | : "修改"} 278 | 279 |

280 | {file.oldContent !== undefined && file.newContent !== undefined && ( 281 |

282 | 内容状态: 有文本内容 283 |

284 | )} 285 | {file.diff && ( 286 |

287 | 差异: 已生成 288 |

289 | )} 290 |
291 | 292 | {file.oldContent !== undefined && file.newContent !== undefined ? ( 293 |
294 | 302 |
303 | ) : ( 304 |

305 | 无文本内容可显示差异 306 |

307 | )} 308 |
309 | ); 310 | }; 311 | 312 | // 渲染所有文件内容 313 | const renderAllFiles = () => { 314 | if (!changeReport || !changeReport.allFiles) { 315 | return ( 316 |

317 | 没有文件内容可显示。请确保已勾选"显示所有文件内容"选项并完成扫描。 318 |

319 | ); 320 | } 321 | 322 | if (changeReport.allFiles.length === 0) { 323 | return ( 324 |

没有找到任何文件

325 | ); 326 | } 327 | 328 | return ( 329 |
330 |

331 | {t("resultDisplay.filesAndFoldersCount").replace( 332 | "{count}", 333 | changeReport.allFiles.length.toString() 334 | )} 335 |

336 | 337 | {changeReport.allFiles.map((file) => ( 338 |
342 |

343 | {file.type === "directory" ? : } 344 | {file.path} 345 | {file.type === "directory" ? "/" : ""} 346 |

347 | {file.type === "file" && file.content ? ( 348 |
349 |                 {file.content}
350 |               
351 | ) : file.type === "directory" ? ( 352 |

353 | ({t("resultDisplay.directoryContent")}) 354 |

355 | ) : ( 356 |

357 | ({t("resultDisplay.noContent")}) 358 |

359 | )} 360 |
361 | ))} 362 |
363 | ); 364 | }; 365 | 366 | // 渲染README文档内容 367 | const renderDocumentation = () => { 368 | if (!readmeContent) { 369 | return ( 370 |

371 | 没有找到README.md文件或文件无法读取 372 |

373 | ); 374 | } 375 | 376 | return ( 377 |
378 |
379 | {readmeContent} 380 |
381 |
382 | ); 383 | }; 384 | 385 | return ( 386 |
387 |
388 | 445 |
446 | 447 |
448 | {activeTab === "documentation" && renderDocumentation()} 449 | {activeTab === "structure" && renderStructure()} 450 | {activeTab === "changes" && renderChanges()} 451 | {activeTab === "details" && renderDetails()} 452 | {activeTab === "files" && renderAllFiles()} 453 |
454 |
455 | ); 456 | } 457 | -------------------------------------------------------------------------------- /src/components/RssFeed.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { useTranslations } from "./LocaleProvider"; 6 | import { parseStringPromise } from "xml2js"; 7 | import { IoMdRefresh } from "react-icons/io"; 8 | import { IoChevronDown, IoChevronUp } from "react-icons/io5"; 9 | import { FiClock, FiUser, FiTag, FiExternalLink } from "react-icons/fi"; 10 | 11 | interface RssItem { 12 | title: string; 13 | link: string; 14 | description: string; 15 | category?: string; 16 | pubDate: string; 17 | creator?: string; 18 | thumbnail?: string; 19 | } 20 | 21 | // 现代化的骨架屏组件 22 | const SkeletonItem = () => ( 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ); 36 | 37 | export default function RssFeed() { 38 | const { t, locale } = useTranslations(); 39 | const [items, setItems] = useState([]); 40 | const [loading, setLoading] = useState(false); 41 | const [error, setError] = useState(null); 42 | const [currentFeedTitle, setCurrentFeedTitle] = useState(""); 43 | const [refreshing, setRefreshing] = useState(false); 44 | const [isExpanded, setIsExpanded] = useState(false); // 默认折叠状态 45 | 46 | // RSS源列表 47 | const chineseRssSources = [ 48 | { url: "https://www.oschina.net/news/rss", title: "开源中国" }, 49 | { 50 | url: "https://www.ithome.com/rss/", 51 | title: "IT之家", 52 | }, 53 | { 54 | url: "https://sspai.com/feed", 55 | title: "少数派", 56 | }, 57 | { 58 | url: "https://www.gcores.com/rss", 59 | title: "机核", 60 | }, 61 | { 62 | url: "https://www.solidot.org/index.rss", 63 | title: "Solidot", 64 | }, 65 | { 66 | url: "https://feeds.appinn.com/appinns/", 67 | title: "Appinn", 68 | }, 69 | { 70 | url: "https://www.geekpark.net/rss", 71 | title: "GeekPark", 72 | }, 73 | ]; 74 | 75 | const englishRssSources = [ 76 | { url: "https://news.ycombinator.com/rss", title: "Hacker News" }, 77 | { 78 | url: "http://feeds.arstechnica.com/arstechnica/index/", 79 | title: "Ars Technica", 80 | }, 81 | { url: "https://techcrunch.com/feed/", title: "TechCrunch" }, 82 | { url: "https://lobste.rs/rss", title: "Lobsters" }, 83 | { url: "https://dev.to/feed", title: "DEV Community" }, 84 | { url: "https://stackoverflow.blog/feed/", title: "Stack Overflow Blog" }, 85 | ]; 86 | 87 | // 判断是否为中文环境 88 | const isChineseLocale = locale === "zh"; 89 | 90 | // 根据语言选择对应的RSS源列表 91 | const rssSources = isChineseLocale ? chineseRssSources : englishRssSources; 92 | 93 | // 随机选择一个RSS源 94 | const getRandomRssSource = () => { 95 | const randomIndex = Math.floor(Math.random() * rssSources.length); 96 | return rssSources[randomIndex]; 97 | }; 98 | 99 | const fetchRss = async () => { 100 | try { 101 | setLoading(true); 102 | setError(null); 103 | 104 | // 随机选择一个RSS源 105 | const selectedSource = getRandomRssSource(); 106 | setCurrentFeedTitle(selectedSource.title); 107 | 108 | const rssToJsonUrl = "https://api.rss2json.com/v1/api.json?rss_url="; 109 | const response = await fetch( 110 | `${rssToJsonUrl}${encodeURIComponent(selectedSource.url)}` + 111 | "&seed=" + 112 | Math.random(), 113 | { 114 | cache: "no-store", 115 | } 116 | ); 117 | 118 | if (!response.ok) { 119 | throw new Error(`${t("rssFeed.error")}: ${response.status}`); 120 | } 121 | 122 | const data = await response.json(); 123 | 124 | // RSS2JSON 返回的是已解析好的JSON格式,不需要进一步解析XML 125 | if (data.status === "ok" && data.items && data.items.length > 0) { 126 | const parsedItems = data.items 127 | .map((item: any) => ({ 128 | title: item.title, 129 | link: item.link, 130 | description: item.description || "", 131 | category: 132 | item.categories && item.categories.length > 0 133 | ? item.categories[0] 134 | : "Technology", 135 | pubDate: item.pubDate, 136 | creator: item.author, 137 | thumbnail: item.thumbnail || "", 138 | })) 139 | .slice(0, 10); // 只显示前10条 140 | 141 | setItems(parsedItems); 142 | } else { 143 | throw new Error("RSS 源返回数据格式不正确"); 144 | } 145 | } catch (err) { 146 | console.error("获取RSS:", err); 147 | setError(err instanceof Error ? err.message : t("rssFeed.error")); 148 | } finally { 149 | setLoading(false); 150 | setRefreshing(false); 151 | } 152 | }; 153 | 154 | // 点击刷新按钮 155 | const handleRefresh = () => { 156 | if (refreshing) return; 157 | setRefreshing(true); 158 | fetchRss(); 159 | }; 160 | 161 | // 处理展开/折叠 162 | const toggleExpand = () => { 163 | // 如果是首次展开且没有数据,则加载数据 164 | if (!isExpanded && items.length === 0 && !loading && !error) { 165 | fetchRss(); 166 | } 167 | setIsExpanded(!isExpanded); 168 | }; 169 | 170 | useEffect(() => { 171 | // 当语言变化且面板已展开时,重新获取RSS 172 | if (isExpanded) { 173 | fetchRss(); 174 | } 175 | }, [locale, isExpanded]); // 当语言变化或面板展开状态变化时触发 176 | 177 | // 格式化发布日期 178 | const formatDate = (dateString: string) => { 179 | try { 180 | const date = new Date(dateString); 181 | return new Intl.DateTimeFormat(isChineseLocale ? "zh-CN" : "en-US", { 182 | year: "numeric", 183 | month: "short", 184 | day: "numeric", 185 | hour: "2-digit", 186 | minute: "2-digit", 187 | }).format(date); 188 | } catch (e) { 189 | return dateString; 190 | } 191 | }; 192 | 193 | // 处理CDATA内容 194 | const extractCdata = (text: string | undefined): string => { 195 | if (!text) return ""; 196 | // 处理CDATA标签 197 | const cdataMatch = text.match(//); 198 | if (cdataMatch && cdataMatch[1]) { 199 | return cdataMatch[1].trim(); 200 | } 201 | return text.trim(); 202 | }; 203 | 204 | // 提取描述中的第一段文本 205 | const extractFirstParagraph = (html: string) => { 206 | // 首先处理CDATA 207 | const content = extractCdata(html); 208 | // 然后移除HTML标签 209 | const text = content.replace(/<[^>]*>/g, " ").trim(); 210 | return ( 211 | text.split(/\s+/).slice(0, 15).join(" ") + 212 | (text.split(/\s+/).length > 15 ? "..." : "") 213 | ); 214 | }; 215 | 216 | // 从文章描述中提取第一张图片的URL 217 | const extractImageFromDescription = (description: string): string | null => { 218 | if (!description) return null; 219 | 220 | // 处理CDATA 221 | const content = extractCdata(description); 222 | // 尝试从HTML中匹配图片 223 | const imgMatch = content.match(/]+src="([^"]+)"/i); 224 | return imgMatch ? imgMatch[1] : null; 225 | }; 226 | 227 | // 处理图片加载错误 228 | const handleImageError = ( 229 | event: React.SyntheticEvent 230 | ) => { 231 | event.currentTarget.style.display = "none"; 232 | }; 233 | 234 | // 现代化标题栏 235 | const renderHeader = () => ( 236 |
240 |

241 | 248 | 254 | 255 | 256 | {t("rssFeed.title")} 257 | {currentFeedTitle && ( 258 | 259 | {currentFeedTitle} 260 | 261 | )} 262 | 263 |

264 |
265 | {isExpanded && ( 266 | 281 | )} 282 | {isExpanded ? ( 283 | 284 | ) : ( 285 | 286 | )} 287 |
288 |
289 | ); 290 | 291 | return ( 292 |
293 | {renderHeader()} 294 | 295 | 296 | {isExpanded && ( 297 | 304 | {loading && ( 305 |
306 | {Array(4) 307 | .fill(0) 308 | .map((_, index) => ( 309 | 310 | ))} 311 |
312 | )} 313 | 314 | {error && !loading && ( 315 |
316 |
317 |

318 | 324 | 329 | 330 | {error} 331 |

332 |
333 |
334 | )} 335 | 336 | {!loading && !error && items.length > 0 && ( 337 | 411 | )} 412 |
413 | )} 414 |
415 |
416 | ); 417 | } 418 | -------------------------------------------------------------------------------- /src/components/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Link, useTransitionRouter } from "next-view-transitions"; 4 | import { useTranslations } from "./LocaleProvider"; 5 | import { slideInOut } from "../lib/publicCutscene"; 6 | export default function SettingsButton() { 7 | const { t } = useTranslations(); 8 | const router = useTransitionRouter(); 9 | 10 | return ( 11 | { 13 | e.preventDefault(); 14 | router.push("/settings", { 15 | onTransitionReady: slideInOut, 16 | }); 17 | }} 18 | className="flex items-center gap-1 px-3 py-2 text-sm font-medium bg-gray-200 dark:bg-gray-700 rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors" 19 | aria-label={t("settings.title")} 20 | > 21 | 29 | 34 | 39 | 40 | {t("settings.title")} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useAtom } from "jotai"; 5 | import { themeAtom } from "../lib/store"; 6 | import { useTranslations } from "./LocaleProvider"; 7 | 8 | export default function ThemeToggle() { 9 | const [theme, setTheme] = useAtom(themeAtom); 10 | const { t } = useTranslations(); 11 | 12 | // 切换主题 13 | const toggleTheme = () => { 14 | setTheme(theme === "light" ? "dark" : "light"); 15 | }; 16 | 17 | // 监听主题变化,应用到文档 18 | useEffect(() => { 19 | // 移除之前的动画类 20 | document.documentElement.classList.remove("animate-fade-in"); 21 | 22 | // 添加动画类 23 | setTimeout(() => { 24 | document.documentElement.classList.add("animate-fade-in"); 25 | }, 0); 26 | 27 | if (theme === "dark") { 28 | document.documentElement.classList.add("dark"); 29 | } else { 30 | document.documentElement.classList.remove("dark"); 31 | } 32 | }, [theme]); 33 | 34 | return ( 35 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/atomTypes.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | // 我们不需要自定义PrimitiveAtom类型,直接使用jotai提供的类型 4 | // 移除错误的类型定义,改为导出工厂函数 5 | 6 | // 创建一个简单状态的atom 7 | export function createAtom(initialValue: T) { 8 | return atom(initialValue); 9 | } 10 | 11 | // 创建派生的atom 12 | export function createDerivedAtom( 13 | baseAtom: ReturnType>, 14 | read: (value: T) => U 15 | ) { 16 | return atom((get) => read(get(baseAtom))); 17 | } 18 | 19 | // 创建一个写入派生atom的函数 20 | export function createWritableAtom( 21 | readAtom: ReturnType>, 22 | write: ( 23 | get: (atom: any) => any, 24 | set: (atom: any, value: any) => void, 25 | ...args: Args 26 | ) => Result 27 | ) { 28 | return atom((get) => get(readAtom), write); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/commentParser.ts: -------------------------------------------------------------------------------- 1 | // 注释分析结果类型 2 | interface CommentAnalysisResult { 3 | isComment: boolean; // 是否为注释行 4 | isStartBlockComment: boolean; // 是否是块注释的开始 5 | isEndBlockComment: boolean; // 是否是块注释的结束 6 | commentText: string; // 提取的注释文本内容 7 | } 8 | 9 | // 语言配置类型 10 | interface LanguageConfig { 11 | lineComment?: string[]; // 行注释标记 12 | blockCommentStart?: string[]; // 块注释开始标记 13 | blockCommentEnd?: string[]; // 块注释结束标记 14 | } 15 | 16 | // 各种语言的注释标记配置 17 | const languageConfigs: Record = { 18 | // JavaScript/TypeScript 19 | ".js": { 20 | lineComment: ["//"], 21 | blockCommentStart: ["/*"], 22 | blockCommentEnd: ["*/"], 23 | }, 24 | ".jsx": { 25 | lineComment: ["//"], 26 | blockCommentStart: ["/*"], 27 | blockCommentEnd: ["*/"], 28 | }, 29 | ".ts": { 30 | lineComment: ["//"], 31 | blockCommentStart: ["/*"], 32 | blockCommentEnd: ["*/"], 33 | }, 34 | ".tsx": { 35 | lineComment: ["//"], 36 | blockCommentStart: ["/*"], 37 | blockCommentEnd: ["*/"], 38 | }, 39 | 40 | // HTML/XML 41 | ".html": { blockCommentStart: [""] }, 42 | ".xml": { blockCommentStart: [""] }, 43 | ".svg": { blockCommentStart: [""] }, 44 | 45 | // CSS/SCSS 46 | ".css": { blockCommentStart: ["/*"], blockCommentEnd: ["*/"] }, 47 | ".scss": { 48 | lineComment: ["//"], 49 | blockCommentStart: ["/*"], 50 | blockCommentEnd: ["*/"], 51 | }, 52 | 53 | // Python 54 | ".py": { 55 | lineComment: ["#"], 56 | blockCommentStart: ['"""', "'''"], 57 | blockCommentEnd: ['"""', "'''"], 58 | }, 59 | 60 | // Ruby 61 | ".rb": { 62 | lineComment: ["#"], 63 | blockCommentStart: ["=begin"], 64 | blockCommentEnd: ["=end"], 65 | }, 66 | 67 | // PHP 68 | ".php": { 69 | lineComment: ["//"], 70 | blockCommentStart: ["/*"], 71 | blockCommentEnd: ["*/"], 72 | }, 73 | 74 | // Shell 75 | ".sh": { lineComment: ["#"] }, 76 | ".bash": { lineComment: ["#"] }, 77 | 78 | // C/C++/C#/Java 79 | ".c": { 80 | lineComment: ["//"], 81 | blockCommentStart: ["/*"], 82 | blockCommentEnd: ["*/"], 83 | }, 84 | ".cpp": { 85 | lineComment: ["//"], 86 | blockCommentStart: ["/*"], 87 | blockCommentEnd: ["*/"], 88 | }, 89 | ".cs": { 90 | lineComment: ["//"], 91 | blockCommentStart: ["/*"], 92 | blockCommentEnd: ["*/"], 93 | }, 94 | ".java": { 95 | lineComment: ["//"], 96 | blockCommentStart: ["/*"], 97 | blockCommentEnd: ["*/"], 98 | }, 99 | 100 | // Go 101 | ".go": { 102 | lineComment: ["//"], 103 | blockCommentStart: ["/*"], 104 | blockCommentEnd: ["*/"], 105 | }, 106 | 107 | // Rust 108 | ".rs": { 109 | lineComment: ["//"], 110 | blockCommentStart: ["/*"], 111 | blockCommentEnd: ["*/"], 112 | }, 113 | 114 | // Swift 115 | ".swift": { 116 | lineComment: ["//"], 117 | blockCommentStart: ["/*"], 118 | blockCommentEnd: ["*/"], 119 | }, 120 | 121 | // Kotlin 122 | ".kt": { 123 | lineComment: ["//"], 124 | blockCommentStart: ["/*"], 125 | blockCommentEnd: ["*/"], 126 | }, 127 | 128 | // Vue 129 | ".vue": { 130 | lineComment: ["//"], 131 | blockCommentStart: ["", "*/"], 133 | }, 134 | 135 | // JSON (虽然JSON不支持注释,但有些环境允许) 136 | ".json": { 137 | lineComment: ["//"], 138 | blockCommentStart: ["/*"], 139 | blockCommentEnd: ["*/"], 140 | }, 141 | 142 | // Markdown 143 | ".md": { blockCommentStart: [""] }, 144 | 145 | // 其他文件类型的默认配置 146 | default: { 147 | lineComment: ["//"], 148 | blockCommentStart: ["/*"], 149 | blockCommentEnd: ["*/"], 150 | }, 151 | }; 152 | 153 | /** 154 | * 解析一行代码中的注释 155 | * @param line 要分析的代码行 156 | * @param fileExtension 文件扩展名(用于确定注释格式) 157 | * @param inBlockComment 当前是否在块注释内 158 | * @returns 注释分析结果 159 | */ 160 | export function parseComments( 161 | line: string, 162 | fileExtension: string, 163 | inBlockComment: boolean = false 164 | ): CommentAnalysisResult { 165 | // 默认结果 166 | const result: CommentAnalysisResult = { 167 | isComment: false, 168 | isStartBlockComment: false, 169 | isEndBlockComment: false, 170 | commentText: "", 171 | }; 172 | 173 | // 获取语言配置,如果没有特定配置则使用默认配置 174 | const config = languageConfigs[fileExtension] || languageConfigs["default"]; 175 | 176 | // 如果已经在块注释中 177 | if (inBlockComment) { 178 | result.isComment = true; 179 | 180 | // 检查是否为块注释结束 181 | if (config.blockCommentEnd) { 182 | for (const endMark of config.blockCommentEnd) { 183 | if (line.trim().endsWith(endMark) || line.includes(endMark)) { 184 | result.isEndBlockComment = true; 185 | 186 | // 提取注释内容(去除结束标记) 187 | const endIndex = line.indexOf(endMark); 188 | result.commentText = line.substring(0, endIndex).trim(); 189 | break; 190 | } 191 | } 192 | 193 | // 如果没有找到结束标记,整行都是注释 194 | if (!result.isEndBlockComment) { 195 | result.commentText = line.trim(); 196 | } 197 | } 198 | 199 | return result; 200 | } 201 | 202 | // 检查行注释 203 | if (config.lineComment) { 204 | for (const commentMark of config.lineComment) { 205 | if (line.trim().startsWith(commentMark)) { 206 | result.isComment = true; 207 | result.commentText = line 208 | .substring(line.indexOf(commentMark) + commentMark.length) 209 | .trim(); 210 | return result; 211 | } 212 | } 213 | } 214 | 215 | // 检查块注释开始 216 | if (config.blockCommentStart) { 217 | for (const startMark of config.blockCommentStart) { 218 | if (line.trim().startsWith(startMark)) { 219 | result.isComment = true; 220 | result.isStartBlockComment = true; 221 | 222 | // 检查是否在同一行结束 223 | if (config.blockCommentEnd) { 224 | for (const endMark of config.blockCommentEnd) { 225 | if (line.trim().endsWith(endMark)) { 226 | result.isEndBlockComment = true; 227 | 228 | // 提取注释内容(去除开始和结束标记) 229 | const startIndex = line.indexOf(startMark) + startMark.length; 230 | const endIndex = line.lastIndexOf(endMark); 231 | result.commentText = line.substring(startIndex, endIndex).trim(); 232 | return result; 233 | } 234 | } 235 | } 236 | 237 | // 如果没有在同一行结束,提取开始部分 238 | result.commentText = line 239 | .substring(line.indexOf(startMark) + startMark.length) 240 | .trim(); 241 | return result; 242 | } 243 | } 244 | } 245 | 246 | return result; 247 | } 248 | -------------------------------------------------------------------------------- /src/lib/fileObserver.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemObserverCallback } from "../types"; 2 | 3 | // 检查浏览器是否支持FileSystemObserver API 4 | export function isFileSystemObserverSupported(): boolean { 5 | return "FileSystemObserver" in window; 6 | } 7 | 8 | // 用于跟踪已观察的目录路径,避免重复观察 9 | const observedPaths = new Set(); 10 | 11 | /** 12 | * 创建文件系统观察器 13 | * 如果浏览器支持FileSystemObserver,则使用原生API 14 | * 否则返回一个空的观察器,所有方法都是空操作 15 | */ 16 | export function createFileObserver(callback: FileSystemObserverCallback) { 17 | // 检查浏览器是否支持FileSystemObserver 18 | if (isFileSystemObserverSupported()) { 19 | try { 20 | // 重置已观察路径记录 21 | observedPaths.clear(); 22 | 23 | // @ts-ignore - TypeScript可能不认识这个新的API 24 | return new window.FileSystemObserver(callback); 25 | } catch (error) { 26 | console.error("创建FileSystemObserver失败:", error); 27 | } 28 | } 29 | 30 | console.warn("当前浏览器不支持FileSystemObserver API,将使用备用轮询机制"); 31 | 32 | // 返回一个空实现,这样调用代码不需要特殊处理 33 | return { 34 | async observe(handle: FileSystemHandle): Promise { 35 | // 不输出日志,避免轮询时过多的控制台输出 36 | return Promise.resolve(); 37 | }, 38 | disconnect(): void { 39 | // 不输出日志,避免轮询时过多的控制台输出 40 | }, 41 | }; 42 | } 43 | 44 | /** 45 | * 获取目录的唯一标识,用于避免重复观察 46 | */ 47 | async function getDirectoryId( 48 | dirHandle: FileSystemDirectoryHandle 49 | ): Promise { 50 | try { 51 | // 尝试使用浏览器原生API获取唯一ID 52 | // @ts-ignore - 这是实验性API 53 | if (dirHandle.isSameEntry && window.crypto && window.crypto.subtle) { 54 | // 使用更可靠的方式生成ID,结合目录名和创建时间 55 | const encoder = new TextEncoder(); 56 | const data = encoder.encode(dirHandle.name); 57 | const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); 58 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 59 | const hashHex = hashArray 60 | .map((b) => b.toString(16).padStart(2, "0")) 61 | .join(""); 62 | return `${dirHandle.name}-${hashHex.substring(0, 8)}`; 63 | } 64 | } catch (e) { 65 | // 忽略错误,使用备用方法 66 | } 67 | 68 | // 备用方法:使用名称加时间戳作为唯一标识 69 | return `${dirHandle.name}-${Date.now()}`; 70 | } 71 | 72 | /** 73 | * 递归获取目录中的所有子目录句柄 74 | * 优化版本:使用更可靠的方式获取所有子目录 75 | */ 76 | async function getAllSubdirectories( 77 | dirHandle: FileSystemDirectoryHandle, 78 | maxDepth: number = 1000 // 增加最大深度限制,允许更深层次的递归 79 | ): Promise { 80 | const directories: FileSystemDirectoryHandle[] = []; 81 | const queue: Array<{ handle: FileSystemDirectoryHandle; depth: number }> = [ 82 | { handle: dirHandle, depth: 0 }, 83 | ]; 84 | 85 | // 使用广度优先搜索确保能获取所有子目录 86 | while (queue.length > 0) { 87 | const { handle: currentDir, depth } = queue.shift()!; 88 | 89 | // 如果达到最大深度,则跳过继续处理 90 | if (depth >= maxDepth) { 91 | console.warn(`已达到最大递归深度 ${maxDepth},跳过更深层的目录`); 92 | continue; 93 | } 94 | 95 | try { 96 | // 遍历当前目录的所有条目 97 | for await (const [name, handle] of currentDir.entries()) { 98 | // 跳过 .fe 版本管理目录 99 | if ( 100 | name === ".fe" || 101 | name.startsWith(".fe/") || 102 | name === ".git" || 103 | name.startsWith(".git/") 104 | ) { 105 | continue; 106 | } 107 | 108 | // 如果是目录,添加到列表中并加入队列以便后续处理 109 | if (handle.kind === "directory") { 110 | const subdirHandle = handle as FileSystemDirectoryHandle; 111 | directories.push(subdirHandle); 112 | queue.push({ handle: subdirHandle, depth: depth + 1 }); 113 | 114 | // // 输出调试信息,显示目录深度 115 | // if (process.env.NODE_ENV === "development") { 116 | // console.log(`发现目录: ${name}, 深度: ${depth + 1}`); 117 | // } 118 | } 119 | } 120 | } catch (error) { 121 | console.error(`获取目录 ${currentDir.name} 的子目录时出错:`, error); 122 | } 123 | } 124 | 125 | return directories; 126 | } 127 | 128 | /** 129 | * 观察单个目录及其所有子目录 130 | * 这个函数用于在检测到新目录时调用 131 | */ 132 | export async function observeSingleDirectoryWithSubdirs( 133 | dirHandle: FileSystemDirectoryHandle, 134 | observer: any 135 | ): Promise { 136 | try { 137 | // 获取目录ID 138 | const dirId = await getDirectoryId(dirHandle); 139 | 140 | // 如果这个目录已经被观察,则跳过 141 | if (observedPaths.has(dirId)) { 142 | return 0; 143 | } 144 | 145 | // 观察当前目录 146 | await observer.observe(dirHandle); 147 | observedPaths.add(dirId); 148 | 149 | // 获取并观察所有子目录 150 | const subDirectories = await getAllSubdirectories(dirHandle); 151 | console.log( 152 | `正在观察新目录 ${dirHandle.name} 及其 ${subDirectories.length} 个子目录` 153 | ); 154 | 155 | // 计数新观察的目录 156 | let newObservedCount = 1; // 包括当前目录 157 | 158 | // 为每个子目录设置观察 159 | for (const subDir of subDirectories) { 160 | const subDirId = await getDirectoryId(subDir); 161 | 162 | // 如果这个子目录还没有被观察,则观察它 163 | if (!observedPaths.has(subDirId)) { 164 | await observer.observe(subDir); 165 | observedPaths.add(subDirId); 166 | newObservedCount++; 167 | } 168 | } 169 | 170 | return newObservedCount; 171 | } catch (error) { 172 | console.error(`观察目录 ${dirHandle.name} 及其子目录时出错:`, error); 173 | return 0; 174 | } 175 | } 176 | 177 | /** 178 | * 尝试观察目录及其所有子目录的变化 179 | * 返回一个布尔值,表示是否成功启用了FileSystemObserver 180 | */ 181 | export async function observeDirectoryChanges( 182 | dirHandle: FileSystemDirectoryHandle, 183 | callback: FileSystemObserverCallback 184 | ): Promise { 185 | // 检查浏览器是否支持FileSystemObserver 186 | if (!isFileSystemObserverSupported()) { 187 | return false; 188 | } 189 | 190 | try { 191 | const observer = createFileObserver(callback); 192 | 193 | // 每次调用时重置已观察路径,确保能捕获到新创建的文件夹 194 | observedPaths.clear(); 195 | 196 | // 首先观察根目录 197 | const rootId = await getDirectoryId(dirHandle); 198 | await observer.observe(dirHandle); 199 | observedPaths.add(rootId); 200 | console.log(`已开始观察根目录: ${dirHandle.name}`); 201 | 202 | const subDirectories = await getAllSubdirectories(dirHandle); 203 | 204 | if (subDirectories.length > 0) { 205 | console.log(`正在观察根目录及${subDirectories.length}个子目录的变化`); 206 | 207 | // 计数新观察的目录 208 | let newObservedCount = 0; 209 | let batchSize = 10; // 每批处理的目录数量 210 | 211 | // 分批处理子目录,避免一次性处理太多导致性能问题 212 | for (let i = 0; i < subDirectories.length; i += batchSize) { 213 | const batch = subDirectories.slice(i, i + batchSize); 214 | 215 | // 为每个子目录设置观察,使用Promise.all并行处理当前批次 216 | await Promise.all( 217 | batch.map(async (subDir) => { 218 | try { 219 | const dirId = await getDirectoryId(subDir); 220 | 221 | // 如果这个目录还没有被观察,则观察它 222 | if (!observedPaths.has(dirId)) { 223 | await observer.observe(subDir); 224 | observedPaths.add(dirId); 225 | newObservedCount++; 226 | } 227 | } catch (subError) { 228 | // 仅在调试模式下记录错误 229 | if (process.env.NODE_ENV === "development") { 230 | console.warn(`无法观察子目录 ${subDir.name}:`, subError); 231 | } 232 | } 233 | }) 234 | ); 235 | } 236 | 237 | // 如果有新观察的目录,输出日志 238 | if (newObservedCount > 0) { 239 | console.log(`新增观察了${newObservedCount}个子目录`); 240 | } 241 | } else { 242 | console.log("未找到子目录,仅观察根目录"); 243 | } 244 | 245 | return true; 246 | } catch (error) { 247 | console.error("观察目录变化失败:", error); 248 | return false; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | // 定义支持的语言列表 4 | export type Locale = "en" | "zh"; 5 | export const locales: Locale[] = ["en", "zh"]; 6 | 7 | // 翻译缓存 8 | const translationsCache: Record = { 9 | en: null, 10 | zh: null, 11 | }; 12 | 13 | // 获取默认语言,优先使用浏览器语言 14 | export function getDefaultLocale(): Locale { 15 | if (typeof window !== "undefined") { 16 | const browserLang = navigator.language.split("-")[0] as Locale; 17 | return locales.includes(browserLang) ? browserLang : "en"; 18 | } 19 | return "en"; 20 | } 21 | 22 | // 创建语言原子状态 23 | export const localeAtom = atom(getDefaultLocale()); 24 | 25 | // 加载特定语言的翻译 26 | export async function loadMessages(locale: Locale) { 27 | // 如果缓存中已有翻译,直接返回 28 | if (translationsCache[locale]) { 29 | return translationsCache[locale]; 30 | } 31 | 32 | try { 33 | // 加载翻译 34 | const messages = (await import(`../messages/${locale}.json`)).default; 35 | 36 | // 缓存翻译 37 | translationsCache[locale] = messages; 38 | 39 | return messages; 40 | } catch (error) { 41 | console.error(`Failed to load translations for ${locale}:`, error); 42 | 43 | // 如果加载失败,尝试返回英文翻译作为后备 44 | if (locale !== "en") { 45 | return loadMessages("en"); 46 | } 47 | 48 | // 如果英文翻译也加载失败,返回空对象 49 | return {}; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/knowledgeService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 知识库服务 3 | * 使用IndexedDB实现本地知识库存储 4 | */ 5 | 6 | // 知识条目类型定义 7 | export interface KnowledgeEntry { 8 | id: string; // 唯一标识符 9 | title: string; // 标题 10 | content: string; // Markdown格式内容 11 | createdAt: string; // 创建时间 12 | updatedAt: string; // 更新时间 13 | } 14 | 15 | // 知识库文件格式(.kn)定义 16 | export interface KnowledgeLibraryFile { 17 | version: string; // 文件版本号 18 | entries: KnowledgeEntry[]; // 知识条目数组 19 | exportedAt: string; // 导出时间 20 | } 21 | 22 | // 数据库配置 23 | const DB_NAME = "folda-scan-knowledge"; 24 | const DB_VERSION = 1; 25 | const STORE_NAME = "knowledge-entries"; 26 | 27 | /** 28 | * 初始化数据库 29 | */ 30 | export async function initKnowledgeDB(): Promise { 31 | return new Promise((resolve, reject) => { 32 | const request = indexedDB.open(DB_NAME, DB_VERSION); 33 | 34 | // 创建/升级数据库结构 35 | request.onupgradeneeded = (event) => { 36 | const db = (event.target as IDBOpenDBRequest).result; 37 | 38 | // 如果知识条目存储不存在,则创建 39 | if (!db.objectStoreNames.contains(STORE_NAME)) { 40 | const store = db.createObjectStore(STORE_NAME, { keyPath: "id" }); 41 | 42 | // 创建索引 43 | store.createIndex("title", "title", { unique: false }); 44 | store.createIndex("updatedAt", "updatedAt", { unique: false }); 45 | } 46 | }; 47 | 48 | request.onsuccess = (event) => { 49 | resolve((event.target as IDBOpenDBRequest).result); 50 | }; 51 | 52 | request.onerror = (event) => { 53 | console.error( 54 | "知识库数据库初始化失败:", 55 | (event.target as IDBOpenDBRequest).error 56 | ); 57 | reject((event.target as IDBOpenDBRequest).error); 58 | }; 59 | }); 60 | } 61 | 62 | /** 63 | * 添加知识条目 64 | */ 65 | export async function addKnowledgeEntry( 66 | entry: Omit 67 | ): Promise { 68 | const db = await initKnowledgeDB(); 69 | 70 | // 生成唯一ID和时间戳 71 | const now = new Date().toISOString(); 72 | const newEntry: KnowledgeEntry = { 73 | id: `knowledge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, 74 | title: entry.title, 75 | content: entry.content, 76 | createdAt: now, 77 | updatedAt: now, 78 | }; 79 | 80 | return new Promise((resolve, reject) => { 81 | const transaction = db.transaction([STORE_NAME], "readwrite"); 82 | const store = transaction.objectStore(STORE_NAME); 83 | 84 | const request = store.add(newEntry); 85 | 86 | request.onsuccess = () => { 87 | resolve(newEntry); 88 | }; 89 | 90 | request.onerror = (event) => { 91 | console.error("添加知识条目失败:", (event.target as IDBRequest).error); 92 | reject((event.target as IDBRequest).error); 93 | }; 94 | 95 | transaction.oncomplete = () => { 96 | db.close(); 97 | }; 98 | }); 99 | } 100 | 101 | /** 102 | * 获取所有知识条目 103 | */ 104 | export async function getAllKnowledgeEntries(): Promise { 105 | const db = await initKnowledgeDB(); 106 | 107 | return new Promise((resolve, reject) => { 108 | const transaction = db.transaction([STORE_NAME], "readonly"); 109 | const store = transaction.objectStore(STORE_NAME); 110 | const index = store.index("updatedAt"); // 使用更新时间排序 111 | 112 | const request = index.getAll(); 113 | 114 | request.onsuccess = (event) => { 115 | const entries = (event.target as IDBRequest).result; 116 | // 按更新时间降序排序 117 | resolve( 118 | entries.sort( 119 | (a, b) => 120 | new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() 121 | ) 122 | ); 123 | }; 124 | 125 | request.onerror = (event) => { 126 | console.error("获取知识条目失败:", (event.target as IDBRequest).error); 127 | reject((event.target as IDBRequest).error); 128 | }; 129 | 130 | transaction.oncomplete = () => { 131 | db.close(); 132 | }; 133 | }); 134 | } 135 | 136 | /** 137 | * 获取单个知识条目 138 | */ 139 | export async function getKnowledgeEntry( 140 | id: string 141 | ): Promise { 142 | const db = await initKnowledgeDB(); 143 | 144 | return new Promise((resolve, reject) => { 145 | const transaction = db.transaction([STORE_NAME], "readonly"); 146 | const store = transaction.objectStore(STORE_NAME); 147 | 148 | const request = store.get(id); 149 | 150 | request.onsuccess = (event) => { 151 | resolve((event.target as IDBRequest).result || null); 152 | }; 153 | 154 | request.onerror = (event) => { 155 | console.error("获取知识条目失败:", (event.target as IDBRequest).error); 156 | reject((event.target as IDBRequest).error); 157 | }; 158 | 159 | transaction.oncomplete = () => { 160 | db.close(); 161 | }; 162 | }); 163 | } 164 | 165 | /** 166 | * 更新知识条目 167 | */ 168 | export async function updateKnowledgeEntry( 169 | id: string, 170 | updates: Partial 171 | ): Promise { 172 | const db = await initKnowledgeDB(); 173 | 174 | // 先获取现有条目 175 | const existingEntry = await getKnowledgeEntry(id); 176 | if (!existingEntry) { 177 | throw new Error(`找不到ID为 ${id} 的知识条目`); 178 | } 179 | 180 | // 创建更新后的条目 181 | const updatedEntry: KnowledgeEntry = { 182 | ...existingEntry, 183 | ...updates, 184 | updatedAt: new Date().toISOString(), 185 | id: existingEntry.id, // 确保ID不变 186 | createdAt: existingEntry.createdAt, // 确保创建时间不变 187 | }; 188 | 189 | return new Promise((resolve, reject) => { 190 | const transaction = db.transaction([STORE_NAME], "readwrite"); 191 | const store = transaction.objectStore(STORE_NAME); 192 | 193 | const request = store.put(updatedEntry); 194 | 195 | request.onsuccess = () => { 196 | resolve(updatedEntry); 197 | }; 198 | 199 | request.onerror = (event) => { 200 | console.error("更新知识条目失败:", (event.target as IDBRequest).error); 201 | reject((event.target as IDBRequest).error); 202 | }; 203 | 204 | transaction.oncomplete = () => { 205 | db.close(); 206 | }; 207 | }); 208 | } 209 | 210 | /** 211 | * 删除知识条目 212 | */ 213 | export async function deleteKnowledgeEntry(id: string): Promise { 214 | const db = await initKnowledgeDB(); 215 | 216 | return new Promise((resolve, reject) => { 217 | const transaction = db.transaction([STORE_NAME], "readwrite"); 218 | const store = transaction.objectStore(STORE_NAME); 219 | 220 | const request = store.delete(id); 221 | 222 | request.onsuccess = () => { 223 | resolve(); 224 | }; 225 | 226 | request.onerror = (event) => { 227 | console.error("删除知识条目失败:", (event.target as IDBRequest).error); 228 | reject((event.target as IDBRequest).error); 229 | }; 230 | 231 | transaction.oncomplete = () => { 232 | db.close(); 233 | }; 234 | }); 235 | } 236 | 237 | /** 238 | * 导出知识库为.kn格式 239 | */ 240 | export async function exportKnowledgeLibrary(): Promise { 241 | const entries = await getAllKnowledgeEntries(); 242 | 243 | const libraryFile: KnowledgeLibraryFile = { 244 | version: "1.0", 245 | entries, 246 | exportedAt: new Date().toISOString(), 247 | }; 248 | 249 | return libraryFile; 250 | } 251 | 252 | /** 253 | * 导入知识库 254 | * 采用合并策略处理冲突 255 | */ 256 | export async function importKnowledgeLibrary( 257 | libraryFile: KnowledgeLibraryFile 258 | ): Promise<{ total: number; added: number; updated: number }> { 259 | // 验证导入文件格式 260 | if (!libraryFile.version || !Array.isArray(libraryFile.entries)) { 261 | throw new Error("知识库文件格式无效"); 262 | } 263 | 264 | const db = await initKnowledgeDB(); 265 | let added = 0; 266 | let updated = 0; 267 | 268 | // 获取现有条目的ID 269 | const existingEntries = await getAllKnowledgeEntries(); 270 | const existingIds = new Set(existingEntries.map((entry) => entry.id)); 271 | 272 | // 处理每个导入的条目 273 | for (const entry of libraryFile.entries) { 274 | if (existingIds.has(entry.id)) { 275 | // 如果条目已存在,获取现有条目 276 | const existingEntry = await getKnowledgeEntry(entry.id); 277 | 278 | // 如果导入条目的更新时间比现有条目新,则更新 279 | if ( 280 | existingEntry && 281 | new Date(entry.updatedAt) > new Date(existingEntry.updatedAt) 282 | ) { 283 | await updateKnowledgeEntry(entry.id, { 284 | title: entry.title, 285 | content: entry.content, 286 | updatedAt: entry.updatedAt, 287 | }); 288 | updated++; 289 | } 290 | } else { 291 | // 添加新条目,保留原始ID和时间戳 292 | const transaction = db.transaction([STORE_NAME], "readwrite"); 293 | const store = transaction.objectStore(STORE_NAME); 294 | await new Promise((resolve, reject) => { 295 | const request = store.add(entry); 296 | request.onsuccess = () => resolve(); 297 | request.onerror = (e) => reject((e.target as IDBRequest).error); 298 | }); 299 | added++; 300 | } 301 | } 302 | 303 | db.close(); 304 | 305 | return { 306 | total: libraryFile.entries.length, 307 | added, 308 | updated, 309 | }; 310 | } 311 | 312 | /** 313 | * 搜索知识条目 (按标题) 314 | */ 315 | export async function searchKnowledgeEntries( 316 | query: string 317 | ): Promise { 318 | const entries = await getAllKnowledgeEntries(); 319 | 320 | if (!query.trim()) { 321 | return entries; 322 | } 323 | 324 | const searchTerms = query.toLowerCase().split(" ").filter(Boolean); 325 | 326 | return entries.filter((entry) => { 327 | const titleLower = entry.title.toLowerCase(); 328 | const contentLower = entry.content.toLowerCase(); 329 | 330 | // 检查每个搜索词是否出现在标题或内容中 331 | return searchTerms.every( 332 | (term) => titleLower.includes(term) || contentLower.includes(term) 333 | ); 334 | }); 335 | } 336 | 337 | /** 338 | * 清空知识库 339 | */ 340 | export async function clearKnowledgeLibrary(): Promise { 341 | const db = await initKnowledgeDB(); 342 | 343 | return new Promise((resolve, reject) => { 344 | const transaction = db.transaction([STORE_NAME], "readwrite"); 345 | const store = transaction.objectStore(STORE_NAME); 346 | 347 | const request = store.clear(); 348 | 349 | request.onsuccess = () => { 350 | resolve(); 351 | }; 352 | 353 | request.onerror = (event) => { 354 | console.error("清空知识库失败:", (event.target as IDBRequest).error); 355 | reject((event.target as IDBRequest).error); 356 | }; 357 | 358 | transaction.oncomplete = () => { 359 | db.close(); 360 | }; 361 | }); 362 | } 363 | -------------------------------------------------------------------------------- /src/lib/knowledgeUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 知识库工具函数 3 | */ 4 | import { 5 | exportKnowledgeLibrary, 6 | importKnowledgeLibrary, 7 | KnowledgeLibraryFile, 8 | addKnowledgeEntry, 9 | } from "./knowledgeService"; 10 | import JSZip from "jszip"; 11 | 12 | /** 13 | * 文件下载处理程序 14 | * @param blob 文件数据 15 | * @param filename 文件名 16 | */ 17 | export function downloadBlob(blob: Blob, filename: string): void { 18 | const url = URL.createObjectURL(blob); 19 | const a = document.createElement("a"); 20 | a.href = url; 21 | a.download = filename; 22 | a.style.display = "none"; 23 | document.body.appendChild(a); 24 | a.click(); 25 | 26 | // 清理 27 | setTimeout(() => { 28 | document.body.removeChild(a); 29 | URL.revokeObjectURL(url); 30 | }, 100); 31 | } 32 | 33 | /** 34 | * 导出知识库为.kn文件 35 | */ 36 | export async function downloadKnowledgeLibrary(): Promise { 37 | try { 38 | const libraryData = await exportKnowledgeLibrary(); 39 | const blob = new Blob([JSON.stringify(libraryData, null, 2)], { 40 | type: "application/json", 41 | }); 42 | 43 | // 使用日期生成文件名 44 | const date = new Date().toISOString().split("T")[0]; 45 | downloadBlob(blob, `folda-scan-knowledge-${date}.kn`); 46 | } catch (error) { 47 | console.error("导出知识库失败:", error); 48 | throw new Error("导出知识库失败"); 49 | } 50 | } 51 | 52 | /** 53 | * 从.md文件创建知识条目内容 54 | */ 55 | export function parseMarkdownFile( 56 | file: File 57 | ): Promise<{ title: string; content: string }> { 58 | return new Promise((resolve, reject) => { 59 | const reader = new FileReader(); 60 | 61 | reader.onload = (event) => { 62 | try { 63 | const content = event.target?.result as string; 64 | 65 | if (!content) { 66 | throw new Error("无法读取文件内容"); 67 | } 68 | 69 | // 尝试从文件名或Markdown内容中提取标题 70 | const filename = file.name.replace(/\.md$/, ""); 71 | 72 | // 从Markdown内容的第一个#标题中提取标题 73 | let title = filename; 74 | const headerMatch = content.match(/^#\s+(.+)$/m); 75 | if (headerMatch && headerMatch[1]) { 76 | title = headerMatch[1].trim(); 77 | } 78 | 79 | resolve({ title, content }); 80 | } catch (error) { 81 | reject(error); 82 | } 83 | }; 84 | 85 | reader.onerror = () => reject(new Error("读取文件失败")); 86 | reader.readAsText(file); 87 | }); 88 | } 89 | 90 | /** 91 | * 解析导入的.kn文件 92 | */ 93 | export function parseKnowledgeFile(file: File): Promise { 94 | return new Promise((resolve, reject) => { 95 | const reader = new FileReader(); 96 | 97 | reader.onload = (event) => { 98 | try { 99 | const content = event.target?.result as string; 100 | 101 | if (!content) { 102 | throw new Error("无法读取文件内容"); 103 | } 104 | 105 | const libraryData = JSON.parse(content) as KnowledgeLibraryFile; 106 | 107 | // 验证格式 108 | if (!libraryData.version || !Array.isArray(libraryData.entries)) { 109 | throw new Error("文件格式无效"); 110 | } 111 | 112 | resolve(libraryData); 113 | } catch (error) { 114 | reject(error); 115 | } 116 | }; 117 | 118 | reader.onerror = () => reject(new Error("读取文件失败")); 119 | reader.readAsText(file); 120 | }); 121 | } 122 | 123 | /** 124 | * 导入知识库文件 125 | */ 126 | export async function importKnowledgeFile( 127 | file: File 128 | ): Promise<{ total: number; added: number; updated: number }> { 129 | try { 130 | // 验证文件扩展名 131 | if (!file.name.endsWith(".kn")) { 132 | throw new Error("请选择.kn格式的知识库文件"); 133 | } 134 | 135 | const libraryData = await parseKnowledgeFile(file); 136 | return await importKnowledgeLibrary(libraryData); 137 | } catch (error) { 138 | console.error("导入知识库失败:", error); 139 | throw error; 140 | } 141 | } 142 | 143 | /** 144 | * 从ZIP文件导入多个Markdown文件作为知识条目 145 | * @param file ZIP文件 146 | */ 147 | export async function importZipFile( 148 | file: File 149 | ): Promise<{ total: number; imported: number }> { 150 | try { 151 | // 验证文件扩展名 152 | if (!file.name.toLowerCase().endsWith(".zip")) { 153 | throw new Error("请选择.zip格式的文件"); 154 | } 155 | 156 | const zip = new JSZip(); 157 | const zipContent = await zip.loadAsync(file); 158 | 159 | let total = 0; 160 | let imported = 0; 161 | 162 | // 创建处理单个文件的函数 163 | const processMarkdownFile = async (filename: string, content: string) => { 164 | // 只处理.md文件 165 | if (!filename.toLowerCase().endsWith(".md")) { 166 | return false; 167 | } 168 | 169 | try { 170 | // 尝试从文件名或Markdown内容中提取标题 171 | const filenameWithoutExt = 172 | filename.replace(/\.md$/i, "").split("/").pop() || ""; 173 | 174 | // 从Markdown内容的第一个#标题中提取标题 175 | let title = filenameWithoutExt; 176 | const headerMatch = content.match(/^#\s+(.+)$/m); 177 | if (headerMatch && headerMatch[1]) { 178 | title = headerMatch[1].trim(); 179 | } 180 | 181 | // 添加为知识条目 182 | await addKnowledgeEntry({ 183 | title, 184 | content, 185 | }); 186 | 187 | return true; 188 | } catch (error) { 189 | console.error(`处理文件 ${filename} 时出错:`, error); 190 | return false; 191 | } 192 | }; 193 | 194 | // 处理所有文件 195 | const promises: Promise[] = []; 196 | 197 | zipContent.forEach((relativePath, zipEntry) => { 198 | // 跳过目录 199 | if (!zipEntry.dir) { 200 | total++; 201 | const promise = zipEntry.async("string").then((content) => { 202 | return processMarkdownFile(relativePath, content); 203 | }); 204 | promises.push(promise); 205 | } 206 | }); 207 | 208 | // 等待所有文件处理完成 209 | const results = await Promise.all(promises); 210 | imported = results.filter(Boolean).length; 211 | 212 | return { total, imported }; 213 | } catch (error) { 214 | console.error("导入ZIP文件失败:", error); 215 | throw error; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/lib/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-diff-viewer-continued" { 2 | import { ComponentType } from "react"; 3 | 4 | export interface DiffViewerProps { 5 | oldValue: string; 6 | newValue: string; 7 | splitView?: boolean; 8 | useDarkTheme?: boolean; 9 | leftTitle?: string; 10 | rightTitle?: string; 11 | [key: string]: any; 12 | } 13 | 14 | const DiffViewer: ComponentType; 15 | export default DiffViewer; 16 | } 17 | 18 | declare module "ignore"; 19 | declare module "diff"; 20 | -------------------------------------------------------------------------------- /src/lib/publicCutscene.ts: -------------------------------------------------------------------------------- 1 | export async function slideInOut(direction = "forward") { 2 | const outTransform = 3 | direction === "forward" ? "translateX(-100%)" : "translateX(100%)"; 4 | const inTransformStart = 5 | direction === "forward" ? "translateX(100%)" : "translateX(-100%)"; 6 | 7 | const easing = "cubic-bezier(0.4, 0.0, 0.2, 1)"; 8 | const duration = 250; 9 | 10 | document.documentElement.animate( 11 | [{ transform: "translateX(0)" }, { transform: outTransform }], 12 | { 13 | duration: duration, 14 | easing: easing, 15 | fill: "forwards", 16 | pseudoElement: "::view-transition-old(root)", 17 | } 18 | ); 19 | 20 | document.documentElement.animate( 21 | [{ transform: inTransformStart }, { transform: "translateX(0)" }], 22 | { 23 | duration: duration, 24 | easing: easing, 25 | fill: "forwards", 26 | pseudoElement: "::view-transition-new(root)", 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/pwaUtils.ts: -------------------------------------------------------------------------------- 1 | // 检查是否支持Service Worker 2 | export function isServiceWorkerSupported(): boolean { 3 | return "serviceWorker" in navigator; 4 | } 5 | 6 | // 检查是否支持安装PWA 7 | export function isPwaInstallable(): boolean { 8 | // 检查是否在浏览器模式下运行 9 | const isBrowserMode = window.matchMedia("(display-mode: browser)").matches; 10 | 11 | // 判断是否支持PWA安装 12 | // 注意:Edge浏览器可能没有BeforeInstallPromptEvent,但仍然支持PWA安装 13 | // 使用更通用的方法判断安装能力 14 | const hasInstallCapability = 15 | "BeforeInstallPromptEvent" in window || 16 | (navigator.userAgent.includes("Edg") && 17 | "serviceWorker" in navigator && 18 | "caches" in window); 19 | 20 | return isBrowserMode && hasInstallCapability; 21 | } 22 | 23 | // 注册Service Worker 24 | export async function registerServiceWorker(): Promise { 25 | if (!isServiceWorkerSupported()) { 26 | console.log("当前浏览器不支持Service Worker"); 27 | return false; 28 | } 29 | if (process.env.NODE_ENV === "development") { 30 | console.log("开发环境下不注册Service Worker"); 31 | return false; 32 | } 33 | return false; 34 | 35 | try { 36 | const registration = await navigator.serviceWorker.register("/sw.js"); 37 | console.log("Service Worker注册成功:", registration.scope); 38 | return true; 39 | } catch (error) { 40 | console.error("Service Worker注册失败:", error); 41 | return false; 42 | } 43 | } 44 | 45 | // PWA安装事件处理 46 | let deferredPrompt: any; 47 | 48 | // 捕获安装提示事件 49 | export function captureInstallPrompt(): void { 50 | window.addEventListener("beforeinstallprompt", (e) => { 51 | // 阻止Chrome 67及更早版本自动显示安装提示 52 | e.preventDefault(); 53 | // 保存事件,以便稍后触发 54 | deferredPrompt = e; 55 | console.log("捕获到安装提示事件"); 56 | }); 57 | } 58 | 59 | // 显示安装提示 60 | export async function showInstallPrompt(): Promise { 61 | if (!deferredPrompt) { 62 | console.log("没有可用的安装提示"); 63 | return false; 64 | } 65 | 66 | try { 67 | // 显示安装提示 68 | deferredPrompt.prompt(); 69 | 70 | // 等待用户响应 71 | const choiceResult = await deferredPrompt.userChoice; 72 | 73 | // 重置deferredPrompt 74 | deferredPrompt = null; 75 | 76 | return choiceResult.outcome === "accepted"; 77 | } catch (error) { 78 | console.error("显示安装提示时出错:", error); 79 | return false; 80 | } 81 | } 82 | 83 | // 检查PWA是否已安装 84 | export function isPwaInstalled(): boolean { 85 | return ( 86 | window.matchMedia("(display-mode: standalone)").matches || 87 | (navigator as any).standalone === true 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { 3 | FileSystemEntry, 4 | ScanResult, 5 | ChangeReport, 6 | ScanStatus, 7 | VersionHistoryItem, 8 | OperationStatus, 9 | Dockerfile, 10 | EnvFile, 11 | DockerComposeConfig, 12 | KnowledgeStoreState, 13 | } from "../types"; 14 | import { Locale, getDefaultLocale } from "./i18n"; 15 | 16 | // 文件夹句柄 17 | export const directoryHandleAtom = atom(null); 18 | 19 | // 当前扫描结果 20 | export const currentScanAtom = atom(null); 21 | 22 | //需要重新索引的文件路径 23 | export const needReindexAtom = atom([]); 24 | 25 | // 上一次扫描结果 26 | export const previousScanAtom = atom(null); 27 | 28 | // 当前变动报告 29 | export const changeReportAtom = atom(null); 30 | 31 | // 是否有gitignore 32 | export const hasGitignoreAtom = atom(false); 33 | 34 | // gitignore内容 35 | export const gitignoreContentAtom = atom(null); 36 | 37 | // 扫描状态 38 | export const scanStatusAtom = atom<"idle" | "scanning" | "preparing" | "error">( 39 | "idle" 40 | ); 41 | 42 | // 错误信息 43 | export const errorMessageAtom = atom(null); 44 | 45 | // 是否正在监控 46 | export const isMonitoringAtom = atom(false); 47 | 48 | // 监控间隔(毫秒) 49 | export const monitorIntervalAtom = atom(5000); 50 | 51 | // 上次扫描时间 52 | export const lastScanTimeAtom = atom(null); 53 | 54 | // 是否显示所有文件内容 55 | export const showAllFilesAtom = atom(false); 56 | 57 | // 主题模式 58 | export const themeAtom = atom<"light" | "dark">("light"); 59 | 60 | // 应用语言 61 | export const localeAtom = atom(getDefaultLocale()); 62 | 63 | // 版本管理相关状态 64 | export const versionHistoryAtom = atom([]); 65 | 66 | // 是否显示版本管理模态窗 67 | export const showVersionModalAtom = atom(false); 68 | 69 | // 是否显示设置模态窗 70 | export const showSettingsModalAtom = atom(false); 71 | 72 | // 版本管理操作状态 73 | export const versionOperationStatusAtom = atom< 74 | "idle" | "backing-up" | "restoring" | "error" 75 | >("idle"); 76 | 77 | // 版本管理操作信息 78 | export const versionOperationMessageAtom = atom(null); 79 | 80 | // 版本备份信息输入 81 | export const versionBackupInfoAtom = atom(""); 82 | 83 | // 备份进度 84 | export const backupProgressAtom = atom(0); 85 | 86 | // 恢复进度 87 | export const restoreProgressAtom = atom(0); 88 | 89 | // README文件内容 90 | export const readmeContentAtom = atom(null); 91 | 92 | // Docker相关状态 93 | export const dockerfilesAtom = atom<{ exists: boolean; paths: string[] }>({ 94 | exists: false, 95 | paths: [], 96 | }); 97 | 98 | export const selectedDockerfileAtom = atom(""); 99 | export const dockerfileContentAtom = atom(""); 100 | export const parsedDockerfileAtom = atom(null); 101 | export const dockerfileErrorsAtom = atom([]); 102 | 103 | // Docker Compose相关状态 104 | export const dockerComposeFilesAtom = atom<{ 105 | exists: boolean; 106 | paths: string[]; 107 | }>({ 108 | exists: false, 109 | paths: [], 110 | }); 111 | 112 | export const selectedDockerComposeAtom = atom(""); 113 | export const dockerComposeContentAtom = atom(""); 114 | export const parsedDockerComposeAtom = atom(null); 115 | export const dockerComposeErrorsAtom = atom([]); 116 | 117 | // 环境变量文件相关状态 118 | export const envFilesAtom = atom<{ exists: boolean; paths: string[] }>({ 119 | exists: false, 120 | paths: [], 121 | }); 122 | 123 | export const selectedEnvFileAtom = atom(""); 124 | export const envFileContentAtom = atom(""); 125 | export const parsedEnvFileAtom = atom(null); 126 | export const envFileErrorsAtom = atom([]); 127 | 128 | // 知识库状态 129 | export const knowledgeStoreAtom = atom({ 130 | entries: [], 131 | isLoading: false, 132 | error: null, 133 | currentEntry: null, 134 | searchQuery: "", 135 | }); 136 | 137 | // 知识库模态窗口显示状态 138 | export const knowledgeModalOpenAtom = atom(false); 139 | 140 | // 知识库条目编辑状态 141 | export const knowledgeEditingAtom = atom<{ 142 | isEditing: boolean; 143 | entryId: string | null; 144 | title: string; 145 | content: string; 146 | }>({ 147 | isEditing: false, 148 | entryId: null, 149 | title: "", 150 | content: "", 151 | }); 152 | -------------------------------------------------------------------------------- /src/lib/useViewport.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | /** 4 | * 设置移动端viewport的自定义Hook 5 | * 设置缩放比例为0.91并禁用用户缩放 6 | */ 7 | export function useViewport() { 8 | useEffect(() => { 9 | // 使用更强的初始化策略 10 | const initViewport = () => { 11 | // 先尝试查找已存在的viewport meta 12 | let viewportMeta = document.querySelector( 13 | 'meta[name="viewport"]' 14 | ) as HTMLMetaElement | null; 15 | 16 | // 如果没找到,创建一个新的 17 | if (!viewportMeta) { 18 | console.log("创建新的viewport meta标签"); 19 | viewportMeta = document.createElement("meta"); 20 | viewportMeta.name = "viewport"; 21 | document.head.appendChild(viewportMeta); 22 | } else { 23 | console.log("找到已存在的viewport meta标签"); 24 | } 25 | 26 | // 无论是新创建的还是已存在的,都设置content 27 | const viewportContent = 28 | "width=device-width, initial-scale=0.91, maximum-scale=0.91, user-scalable=no"; 29 | 30 | // 只有当内容不同时才更新,避免不必要的重绘 31 | if (viewportMeta.content !== viewportContent) { 32 | viewportMeta.content = viewportContent; 33 | console.log("viewport已设置为:", viewportContent); 34 | } 35 | }; 36 | 37 | // 立即执行一次 38 | initViewport(); 39 | 40 | // 设置一系列定时器确保在各种时机都能正确设置viewport 41 | const timeoutIds = [ 42 | setTimeout(() => initViewport(), 100), // 页面加载后立即设置 43 | setTimeout(() => initViewport(), 500), // 页面内容加载后设置 44 | setTimeout(() => initViewport(), 1000), // 确保在路由变化后设置 45 | ]; 46 | 47 | // 监听页面可见性变化 48 | const handleVisibilityChange = () => { 49 | if (document.visibilityState === "visible") { 50 | initViewport(); 51 | } 52 | }; 53 | document.addEventListener("visibilitychange", handleVisibilityChange); 54 | 55 | // 监听DOM变化,检测是否有人移除了viewport标签 56 | let observer: MutationObserver | null = null; 57 | try { 58 | observer = new MutationObserver((mutations) => { 59 | for (const mutation of mutations) { 60 | if ( 61 | mutation.type === "childList" && 62 | mutation.removedNodes.length > 0 63 | ) { 64 | // 检查是否有viewport meta被移除 65 | const viewportExists = document.querySelector( 66 | 'meta[name="viewport"]' 67 | ); 68 | if (!viewportExists) { 69 | console.log("检测到viewport meta被移除,重新创建"); 70 | initViewport(); 71 | } 72 | } 73 | } 74 | }); 75 | 76 | // 监视head元素的子节点变化 77 | observer.observe(document.head, { childList: true }); 78 | } catch (err) { 79 | console.warn("MutationObserver不可用,无法监控viewport变化:", err); 80 | } 81 | 82 | // 保存当前的pushState和replaceState函数 83 | const originalPushState = window.history.pushState; 84 | const originalReplaceState = window.history.replaceState; 85 | 86 | // 覆盖pushState和replaceState,以便在路由变化后重新设置viewport 87 | window.history.pushState = function () { 88 | originalPushState.apply(this, arguments as any); 89 | setTimeout(initViewport, 10); 90 | }; 91 | 92 | window.history.replaceState = function () { 93 | originalReplaceState.apply(this, arguments as any); 94 | setTimeout(initViewport, 10); 95 | }; 96 | 97 | // 监听popstate事件(浏览器的前进/后退按钮) 98 | const handlePopState = () => { 99 | setTimeout(initViewport, 10); 100 | }; 101 | window.addEventListener("popstate", handlePopState); 102 | 103 | // 清理函数 104 | return () => { 105 | timeoutIds.forEach(clearTimeout); 106 | document.removeEventListener("visibilitychange", handleVisibilityChange); 107 | observer?.disconnect(); 108 | window.history.pushState = originalPushState; 109 | window.history.replaceState = originalReplaceState; 110 | window.removeEventListener("popstate", handlePopState); 111 | }; 112 | }, []); 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // 判断是否为文本文件 2 | export function isTextFile(filename: string): boolean { 3 | // 文本文件的扩展名列表 4 | const textExtensions = [ 5 | ".txt", 6 | ".md", 7 | ".js", 8 | ".jsx", 9 | ".ts", 10 | ".tsx", 11 | ".html", 12 | ".css", 13 | ".scss", 14 | ".json", 15 | ".xml", 16 | ".yml", 17 | ".yaml", 18 | ".toml", 19 | ".ini", 20 | ".conf", 21 | ".py", 22 | ".rb", 23 | ".java", 24 | ".c", 25 | ".cpp", 26 | ".cs", 27 | ".go", 28 | ".rs", 29 | ".php", 30 | ".swift", 31 | ".kt", 32 | ".sh", 33 | ".bat", 34 | ".ps1", 35 | ".svg", 36 | ".vue", 37 | ]; 38 | 39 | // 获取文件扩展名并转小写 40 | const extension = getFileExtension(filename).toLowerCase(); 41 | 42 | // 检查是否在文本文件扩展名列表中 43 | return textExtensions.includes(extension); 44 | } 45 | 46 | // 获取文件扩展名 47 | export function getFileExtension(filename: string): string { 48 | const lastDotIndex = filename.lastIndexOf("."); 49 | if (lastDotIndex === -1) { 50 | return ""; // 没有扩展名 51 | } 52 | return filename.slice(lastDotIndex); 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/viewportScript.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这个文件包含在页面加载时强制设置viewport的全局脚本 3 | * 不依赖于React生命周期 4 | */ 5 | 6 | // 避免重复执行 7 | let isSetup = false; 8 | 9 | export function setupGlobalViewport() { 10 | // 如果已经设置过,直接返回 11 | if (isSetup) return; 12 | isSetup = true; 13 | 14 | // 确保在客户端环境中执行 15 | if (typeof window === "undefined") return; 16 | 17 | // 立即运行一次设置 18 | setViewport(); 19 | 20 | // 设置定时器定期检查viewport 21 | setInterval(setViewport, 1000); 22 | 23 | // 监听页面加载完成事件 24 | window.addEventListener("load", setViewport); 25 | 26 | // 监听历史状态变化 27 | window.addEventListener("popstate", setViewport); 28 | 29 | // 监听页面可见性变化 30 | document.addEventListener("visibilitychange", () => { 31 | if (document.visibilityState === "visible") { 32 | setViewport(); 33 | } 34 | }); 35 | 36 | // 保存原始的History方法 37 | const originalPushState = window.history.pushState; 38 | const originalReplaceState = window.history.replaceState; 39 | 40 | // 覆盖pushState方法 41 | window.history.pushState = function () { 42 | // 调用原始方法 43 | originalPushState.apply(this, arguments as any); 44 | // 路由变化后设置viewport 45 | setTimeout(setViewport, 10); 46 | }; 47 | 48 | // 覆盖replaceState方法 49 | window.history.replaceState = function () { 50 | // 调用原始方法 51 | originalReplaceState.apply(this, arguments as any); 52 | // 路由变化后设置viewport 53 | setTimeout(setViewport, 10); 54 | }; 55 | 56 | console.log("[viewportScript] 全局viewport管理已初始化"); 57 | } 58 | 59 | /** 60 | * 设置viewport 61 | */ 62 | function setViewport() { 63 | let viewportMeta = document.querySelector( 64 | 'meta[name="viewport"]' 65 | ) as HTMLMetaElement | null; 66 | 67 | if (!viewportMeta) { 68 | viewportMeta = document.createElement("meta"); 69 | viewportMeta.name = "viewport"; 70 | document.head.appendChild(viewportMeta); 71 | } 72 | 73 | const viewportContent = 74 | "width=device-width, initial-scale=0.91, maximum-scale=0.91, user-scalable=no"; 75 | 76 | if (viewportMeta.content !== viewportContent) { 77 | viewportMeta.content = viewportContent; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/workerUtils.ts: -------------------------------------------------------------------------------- 1 | // 检查浏览器是否支持Web Worker 2 | export function isWebWorkerSupported(): boolean { 3 | return typeof Worker !== 'undefined'; 4 | } 5 | 6 | // 检查浏览器是否支持多线程扫描 7 | export function isMultiThreadScanSupported(): boolean { 8 | // 检查是否支持Web Worker 9 | if (!isWebWorkerSupported()) { 10 | return false; 11 | } 12 | 13 | // 检查是否支持其他必要的API 14 | // 例如:SharedArrayBuffer、Atomics等,这些在某些浏览器中可能被禁用 15 | // 目前只检查基本的Web Worker支持 16 | return true; 17 | } 18 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // 声明全局类型来避免TypeScript错误 2 | declare interface FileSystemSyncAccessHandle {} 3 | 4 | // 文件系统条目类型 5 | export interface FileSystemEntry { 6 | name: string; // 文件/文件夹名称 7 | path: string; // 相对路径 8 | type: "file" | "directory"; // 类型:文件或目录 9 | lastModified?: number; // 最后修改时间 10 | size?: number; // 文件大小(字节) 11 | content?: string; // 文件内容(仅用于文本文件的变更检测) 12 | } 13 | 14 | // 扫描结果类型 15 | export interface ScanResult { 16 | entries: FileSystemEntry[]; // 所有条目,包括文件和目录 17 | timestamp: number; // 扫描时间戳 18 | codeStructure?: { 19 | functions: FunctionInfo[]; // 函数和方法信息 20 | modules: ModuleInfo[]; // 模块导入信息 21 | variables: VariableInfo[]; // 变量信息 22 | comments: CommentInfo[]; // 注释信息 23 | totalFiles?: number; // 总文件数 24 | totalFunctions?: number; // 总函数数 25 | totalMethods?: number; // 总方法数 26 | totalClasses?: number; // 总类数 27 | totalLines?: number; // 总代码行数 28 | totalModules?: number; // 总模块导入数 29 | totalVariables?: number; // 总变量数 30 | totalComments?: number; // 总注释行数 31 | }; 32 | } 33 | 34 | // 文件变化记录类型 35 | export interface FileSystemChangeRecord { 36 | type: "added" | "deleted" | "modified" | "renamed" | "appeared"; 37 | changedHandle: FileSystemHandle; 38 | oldName?: string; // 用于重命名操作 39 | } 40 | 41 | // 文件系统观察者类型 42 | export interface FileSystemObserver { 43 | observe(handle: FileSystemHandle | FileSystemSyncAccessHandle): Promise; 44 | disconnect(): void; 45 | } 46 | 47 | // 文件系统观察者回调函数类型 48 | export type FileSystemObserverCallback = ( 49 | records: FileSystemChangeRecord[], 50 | observer: FileSystemObserver 51 | ) => void; 52 | 53 | // 差异类型 54 | export interface FileDiff { 55 | path: string; 56 | type: "added" | "deleted" | "modified"; 57 | diff?: string; 58 | oldContent?: string; 59 | newContent?: string; 60 | } 61 | 62 | // 函数或方法信息 63 | export interface FunctionInfo { 64 | name: string; 65 | type: "函数" | "方法" | "类" | "箭头函数"; 66 | lines: [number, number]; // 开始行和结束行 67 | filePath: string; 68 | calls: string[]; // 调用的其他函数名称 69 | } 70 | 71 | // 模块导入信息 72 | export interface ModuleInfo { 73 | name: string; // 模块名称 74 | path?: string; // 导入路径 75 | isExternal: boolean; // 是否为外部模块 76 | importedItems?: string[]; // 导入的项目(如具名导入) 77 | filePath: string; // 在哪个文件中导入 78 | line: number; // 在文件中的行号 79 | } 80 | 81 | // 变量信息 82 | export interface VariableInfo { 83 | name: string; // 变量名称 84 | type?: string; // 变量类型(如果可以推断) 85 | value?: string; // 初始值(如果有) 86 | isConst: boolean; // 是否为常量 87 | filePath: string; // 在哪个文件中定义 88 | line: number; // 在文件中的行号 89 | } 90 | 91 | // 注释信息 92 | export interface CommentInfo { 93 | content: string; // 注释内容 94 | type: "单行" | "多行" | "文档"; // 注释类型 95 | filePath: string; // 在哪个文件中 96 | line: number; // 在文件中的行号 97 | isImportant: boolean; // 是否包含重要标记(如TODO, FIXME, NOTE等) 98 | } 99 | 100 | // 变动报告类型 101 | export interface ChangeReport { 102 | timestamp: number; // 报告时间戳 103 | fileChanges: FileChange[]; // 文件变更 104 | dirChanges: DirectoryChange[]; // 目录变更 105 | gitignoreRules: string[]; // 应用的 .gitignore 规则 106 | 107 | // 添加缺少的字段 108 | addedFiles: FileSystemEntry[]; // 新增的文件和文件夹 109 | deletedFiles: FileSystemEntry[]; // 删除的文件和文件夹 110 | modifiedFiles: FileDiff[]; // 修改的文件及其差异 111 | projectStructure: string; // 项目结构字符串表示 112 | codeStructure: any; // 代码结构信息 113 | allFiles?: FileSystemEntry[]; // 所有文件(可选,仅在showAllFiles=true时存在) 114 | } 115 | 116 | // 版本信息类型 117 | export interface VersionInfo { 118 | backupTime: string; // ISO 8601 格式的备份时间戳 119 | versionTitle: string; // 用户提供的版本标题或自动生成的时间戳 120 | } 121 | 122 | // 版本历史记录类型 123 | export interface VersionHistoryItem { 124 | versionTitle: string; // 版本标题 125 | backupTime: string; // 版本备份时间 126 | folderName: string; // 版本文件夹名称 127 | } 128 | 129 | // Docker相关类型 130 | export interface Port { 131 | number: number; 132 | protocol: "tcp" | "udp"; 133 | } 134 | 135 | export interface Instruction { 136 | type: string; 137 | value: string; 138 | } 139 | 140 | export interface Stage { 141 | name: string; 142 | baseImage: string; 143 | instructions: Instruction[]; 144 | } 145 | 146 | export interface Dockerfile { 147 | baseImage?: string; 148 | stages?: Stage[]; 149 | workdir?: string; 150 | exposedPorts?: Port[]; 151 | entrypoint?: string; 152 | cmd?: string; 153 | env?: Record; 154 | labels?: Record; 155 | hasError?: boolean; 156 | errors?: string[]; 157 | exists?: boolean; // 是否存在 158 | path?: string; // 路径 159 | content?: string; // 内容 160 | } 161 | 162 | // 环境变量文件类型 163 | export interface EnvVariable { 164 | key: string; 165 | value: string; 166 | description?: string; // 描述或注释 167 | line: number; // 在文件中的行号 168 | isComment: boolean; // 是否为注释行 169 | isSensitive: boolean; // 是否包含敏感信息 170 | } 171 | 172 | export interface EnvFile { 173 | exists?: boolean; // 是否存在 174 | path?: string; // 路径 175 | content?: string; // 内容 176 | parsedEnv?: Record; // 解析后的环境变量 177 | name?: string; // 文件名 178 | variables?: EnvVariable[]; // 解析后的环境变量列表 179 | hasError?: boolean; // 是否有错误 180 | errors?: string[]; // 错误信息 181 | } 182 | 183 | // Docker Compose 相关类型 184 | export interface DockerComposeService { 185 | name: string; 186 | image?: string; 187 | build?: { 188 | context: string; 189 | dockerfile?: string; 190 | }; 191 | ports?: string[]; 192 | volumes?: string[]; 193 | environment?: Record; 194 | env_file?: string[]; 195 | depends_on?: string[]; 196 | networks?: string[]; 197 | } 198 | 199 | export interface DockerComposeConfig { 200 | exists: boolean; // 是否存在 201 | path: string; // 路径 202 | content: string; // 内容 203 | parsedConfig?: any; // 解析后的配置(JSON) 204 | services?: string[]; // 服务列表 205 | } 206 | 207 | // 扫描状态类型 208 | export type ScanStatus = "idle" | "scanning" | "preparing" | "error"; 209 | 210 | // 操作状态类型 211 | export type OperationStatus = "idle" | "backing-up" | "restoring" | "error"; 212 | 213 | // 文件变更类型 214 | export interface FileChange { 215 | path: string; // 文件路径 216 | type: "added" | "deleted" | "modified"; // 变更类型 217 | beforeContent?: string; // 修改前内容 218 | afterContent?: string; // 修改后内容 219 | diff?: string; // 差异文本 220 | } 221 | 222 | // 目录变更类型 223 | export interface DirectoryChange { 224 | path: string; // 目录路径 225 | type: "added" | "deleted"; // 变更类型 226 | } 227 | 228 | // 版本管理配置类型 229 | export interface VersionConfig { 230 | backupInterval: number; // 自动备份间隔(分钟,0表示禁用) 231 | maxBackups: number; // 最大备份数量 232 | backupPath: string; // 备份路径 233 | } 234 | 235 | // 配置类型 236 | export interface AppConfig { 237 | theme: "light" | "dark" | "system"; // 主题 238 | language: string; // 语言 239 | autoScan: boolean; // 自动扫描 240 | scanInterval: number; // 扫描间隔(秒) 241 | versionConfig: VersionConfig; // 版本管理配置 242 | } 243 | 244 | // 用户设置类型 245 | export interface UserSettings { 246 | theme: "light" | "dark" | "system"; // 主题 247 | language: string; // 语言 248 | autoScan: boolean; // 自动扫描 249 | scanInterval: number; // 扫描间隔(秒) 250 | enableNotifications: boolean; // 启用通知 251 | enableAutoBackup: boolean; // 启用自动备份 252 | backupInterval: number; // 备份间隔(分钟) 253 | } 254 | 255 | // Docker镜像标签类型 256 | export interface DockerImageTag { 257 | name: string; // 标签名称 258 | description: string; // 标签描述 259 | isRecommended: boolean; // 是否推荐 260 | } 261 | 262 | // Docker容器基本配置类型 263 | export interface DockerContainerConfig { 264 | name: string; // 容器名称 265 | image: string; // 镜像 266 | ports: string[]; // 端口映射 267 | environment: Record; // 环境变量 268 | volumes: string[]; // 卷挂载 269 | } 270 | 271 | // 知识条目类型 272 | export interface KnowledgeEntry { 273 | id: string; // 唯一标识符 274 | title: string; // 标题 275 | content: string; // Markdown格式内容 276 | createdAt: string; // 创建时间 277 | updatedAt: string; // 更新时间 278 | } 279 | 280 | // 知识库存储状态 281 | export interface KnowledgeStoreState { 282 | entries: KnowledgeEntry[]; // 知识条目列表 283 | isLoading: boolean; // 加载状态 284 | error: string | null; // 错误信息 285 | currentEntry: KnowledgeEntry | null; // 当前选中的条目 286 | searchQuery: string; // 搜索查询 287 | } 288 | 289 | // 知识库文件格式 290 | export interface KnowledgeLibraryFile { 291 | version: string; // 文件版本号 292 | entries: KnowledgeEntry[]; // 知识条目数组 293 | exportedAt: string; // 导出时间 294 | } 295 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | darkMode: "class", 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: "var(--background)", 14 | foreground: "var(--foreground)", 15 | }, 16 | animation: { 17 | "fade-in": "fadeIn 0.5s ease-in-out", 18 | }, 19 | keyframes: { 20 | fadeIn: { 21 | "0%": { opacity: "0" }, 22 | "100%": { opacity: "1" }, 23 | }, 24 | }, 25 | }, 26 | }, 27 | plugins: [require("@tailwindcss/typography")], 28 | }; 29 | export default config; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------