├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 简体中文 | English
16 |
17 |
18 | # Folda-Scan: Your Private AI Navigator & Q&A Engine for Codebases 🚀
19 | [](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
20 | [](https://opensource.org/licenses/MIT)
21 | [](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 | 
2 |
3 | 
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 |
53 |
60 |
66 |
67 |
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 |
router.back()}
43 | className="px-4 py-2 bg-gray-100 dark:bg-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 transition-colors duration-300 flex items-center"
44 | >
45 |
52 |
58 |
59 | {t("settings.back")}
60 |
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 |
59 |
60 |
浏览器兼容性警告
61 |
62 |
63 | 您的浏览器不支持以下必要功能:
64 |
65 | {" "}
66 | {unsupportedFeatures.join(", ")}
67 |
68 |
69 |
70 | 建议使用 Chrome、Edge 或 Safari 最新版本,并确保安装了所有更新。
71 |
72 |
setShowWarning(false)}
74 | className="mt-2 bg-yellow-200 hover:bg-yellow-300 text-yellow-800 py-1 px-2 rounded text-xs"
75 | >
76 | 暂时忽略
77 |
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 |
160 | {dockerfiles &&
161 | dockerfiles.paths &&
162 | dockerfiles.paths.map((path) => (
163 |
164 | {path}
165 |
166 | ))}
167 |
168 |
169 |
174 | {isFixing ? t("docker.fixing") : t("docker.fix")}
175 |
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 |
161 | {envFiles &&
162 | envFiles.paths &&
163 | envFiles.paths.map((path) => (
164 |
165 | {path}
166 |
167 | ))}
168 |
169 |
170 |
175 | {isFixing ? t("envFile.fixing") : t("envFile.fix")}
176 |
177 |
178 |
182 | {showSensitive
183 | ? t("envFile.hideSensitive")
184 | : t("envFile.showSensitive")}
185 |
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 |
231 | {t("envFile.line")}
232 |
233 |
237 | {t("envFile.key")}
238 |
239 |
243 | {t("envFile.value")}
244 |
245 |
249 | {t("envFile.type")}
250 |
251 |
252 |
253 |
254 | {parsedEnvFile.variables.map((variable, index) => (
255 |
256 |
257 | {variable.line}
258 |
259 |
260 | {variable.isComment
261 | ? t("envFile.comment")
262 | : variable.key}
263 |
264 |
265 | {renderVariableValue(variable)}
266 |
267 |
268 |
277 | {variable.isComment
278 | ? t("envFile.commentType")
279 | : variable.isSensitive
280 | ? t("envFile.sensitiveType")
281 | : t("envFile.normalType")}
282 |
283 |
284 |
285 | ))}
286 |
287 |
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 |
109 | {t("github.username")}
110 |
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 |
123 | {t("github.repository")}
124 |
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 |
137 | {t("github.branch")}
138 |
139 | {t("github.defaultBranch")}
140 |
141 |
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 |
157 |
158 | {t("github.download")}
159 |
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 | setShowAlert(false)}
104 | className={`ml-auto flex-shrink-0 ${
105 | isSupported
106 | ? "text-green-500 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
107 | : "text-yellow-500 hover:text-yellow-700 dark:text-yellow-400 dark:hover:text-yellow-300"
108 | } focus:outline-none`}
109 | aria-label="关闭提示"
110 | >
111 |
117 |
122 |
123 |
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 |
57 |
65 |
70 |
71 | {t("pwa.install") || "将网页安装为应用"}
72 |
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 | setShowAlert(false)}
104 | className={`ml-auto flex-shrink-0 ${
105 | isSupported
106 | ? "text-green-500 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
107 | : "text-yellow-500 hover:text-yellow-700 dark:text-yellow-400 dark:hover:text-yellow-300"
108 | } focus:outline-none`}
109 | aria-label="关闭提示"
110 | >
111 |
117 |
122 |
123 |
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 |
229 | Close
230 |
236 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | {t("presetPrompts.description")}
249 |
250 |
251 |
252 |
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 |
316 | {t("presetPrompts.close")}
317 |
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 |
data.onToggleExpand(data.path || "")}
121 | className="ml-2 text-xs px-1 py-0.5 bg-white bg-opacity-30 rounded hover:bg-opacity-50"
122 | >
123 | {isExpanded ? "−" : "+"}
124 |
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 |
167 |
174 |
180 |
181 |
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 |
389 | {readmeContent && (
390 | setActiveTab("documentation")}
397 | >
398 | 文档
399 |
400 | )}
401 | setActiveTab("structure")}
408 | >
409 | {t("resultDisplay.structure")}
410 |
411 | setActiveTab("changes")}
418 | >
419 | {t("resultDisplay.changes")}
420 |
421 | setActiveTab("details")}
428 | disabled={!selectedFile}
429 | >
430 | {t("resultDisplay.details")}
431 |
432 | {showAllFiles && (
433 | setActiveTab("files")}
440 | >
441 | {t("resultDisplay.files")}
442 |
443 | )}
444 |
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 |
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 | {
268 | e.stopPropagation(); // 阻止事件冒泡到父元素
269 | handleRefresh();
270 | }}
271 | disabled={refreshing || loading}
272 | className="p-2 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors mr-2 hover:text-red-600 dark:hover:text-red-400"
273 | aria-label="刷新"
274 | >
275 |
280 |
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 |
42 | {theme === "light" ? (
43 | // 月亮图标 - 深色模式
44 |
52 |
57 |
58 | ) : (
59 | // 太阳图标 - 浅色模式
60 |
68 |
73 |
74 | )}
75 |
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 |
--------------------------------------------------------------------------------