├── docs ├── .nojekyll ├── robots.txt ├── sitemap.xml └── assets │ ├── site.js │ └── site.css ├── npm_version.txt ├── .DS_Store ├── .gitattributes ├── public ├── icon.png ├── favicon.ico ├── favicon-16.png ├── favicon-32.png ├── favicon.svg ├── desktop-index.html └── desktop-main.html ├── .gitignore ├── next-env.d.ts ├── tsconfig.json ├── electron-preload.js ├── setup-problems.js ├── cleanup-test-problem.js ├── pages ├── api │ ├── problems.ts │ ├── ai-providers.ts │ ├── add-problem.ts │ ├── delete-problems.ts │ ├── update-problem.ts │ ├── import-problems.ts │ └── import-from-folder.ts ├── add-problem.tsx ├── _app.tsx ├── generator.tsx └── markdown-test.tsx ├── .github └── workflows │ ├── deploy-pages.yml │ └── release.yml ├── jest.config.js ├── next.config.js ├── jest.setup.js ├── src ├── components │ ├── CodeWithCopy.tsx │ ├── LanguageThemeControls.tsx │ └── MarkdownRenderer.tsx ├── types │ └── wasm.d.ts └── contexts │ ├── ThemeContext.tsx │ └── I18nContext.tsx ├── styles └── globals.css ├── test-dynamic-problems.js ├── .env.example ├── CHANGELOG.md ├── DESKTOP-APP-GUIDE-zh.md ├── package.json ├── DESKTOP-APP-GUIDE.md ├── scripts ├── generate-icons.js ├── release.sh └── test-runner.js ├── README-zh.md ├── AI_PROVIDER_GUIDE.md ├── electron-builder.config.js ├── MODIFY-PROBLEMS-GUIDE.md ├── README.md ├── start-local.bat ├── start-local.sh ├── tests ├── README.md └── api │ └── run-direct.test.js └── locales └── zh.json /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /npm_version.txt: -------------------------------------------------------------------------------- 1 | 11.3.0 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxypro1/OfflineCodePractice/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxypro1/OfflineCodePractice/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxypro1/OfflineCodePractice/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxypro1/OfflineCodePractice/HEAD/public/favicon-16.png -------------------------------------------------------------------------------- /public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zxypro1/OfflineCodePractice/HEAD/public/favicon-32.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .env 4 | temp 5 | dist 6 | 7 | # Debug logs 8 | *.log 9 | .cursor/debug.log -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://zxypro1.github.io/OfflineCodePractice/sitemap.xml 5 | 6 | 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /docs/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://zxypro1.github.io/OfflineCodePractice/ 5 | 6 | 7 | https://zxypro1.github.io/OfflineCodePractice/zh/ 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/assets/site.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | const observer = new IntersectionObserver( 3 | (entries) => { 4 | entries.forEach((entry) => { 5 | if (entry.isIntersecting) { 6 | entry.target.classList.add('visible'); 7 | } 8 | }); 9 | }, 10 | { 11 | threshold: 0.1, 12 | rootMargin: '0px 0px -50px 0px', 13 | } 14 | ); 15 | 16 | document.querySelectorAll('.fade-in').forEach((el) => observer.observe(el)); 17 | }); 18 | 19 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules", "dist", ".next"] 20 | } 21 | -------------------------------------------------------------------------------- /electron-preload.js: -------------------------------------------------------------------------------- 1 | // electron-preload.js 2 | const { contextBridge, ipcRenderer } = require('electron'); 3 | 4 | // Expose protected methods that allow the renderer process to use 5 | // the ipcRenderer without exposing the entire object 6 | contextBridge.exposeInMainWorld('electronAPI', { 7 | saveConfiguration: (configData) => ipcRenderer.invoke('save-config', configData), 8 | loadConfiguration: () => ipcRenderer.invoke('load-config'), 9 | setLanguage: (language) => ipcRenderer.invoke('set-language', language), 10 | setTheme: (theme) => ipcRenderer.invoke('set-theme', theme), 11 | // Add other IPC methods as needed 12 | }); -------------------------------------------------------------------------------- /setup-problems.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Copy problems.json to public folder for runtime access 5 | const sourceFile = path.join(__dirname, 'problems', 'problems.json'); 6 | const targetFile = path.join(__dirname, 'public', 'problems.json'); 7 | 8 | try { 9 | // Ensure public directory exists 10 | const publicDir = path.join(__dirname, 'public'); 11 | if (!fs.existsSync(publicDir)) { 12 | fs.mkdirSync(publicDir, { recursive: true }); 13 | } 14 | 15 | // Copy the file 16 | fs.copyFileSync(sourceFile, targetFile); 17 | console.log('✅ Copied problems.json to public folder for runtime access'); 18 | } catch (error) { 19 | console.error('❌ Failed to copy problems.json:', error); 20 | process.exit(1); 21 | } -------------------------------------------------------------------------------- /cleanup-test-problem.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | console.log('🧹 Cleaning up test problem...\n'); 5 | 6 | // Read the current problems.json 7 | const problemsPath = path.join(__dirname, 'public', 'problems.json'); 8 | const originalData = fs.readFileSync(problemsPath, 'utf8'); 9 | const problems = JSON.parse(originalData); 10 | 11 | console.log(`📊 Current number of problems: ${problems.length}`); 12 | 13 | // Remove the test problem 14 | const filteredProblems = problems.filter(p => p.id !== 'test-problem'); 15 | 16 | // Write back to file 17 | fs.writeFileSync(problemsPath, JSON.stringify(filteredProblems, null, 2)); 18 | 19 | console.log(`✅ Removed test problem. New count: ${filteredProblems.length}`); 20 | console.log('🔗 Visit http://localhost:3002 to verify the test problem is gone!'); -------------------------------------------------------------------------------- /pages/api/problems.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | // 优先使用 Electron 主进程注入的 APP_ROOT,保证打包后路径正确 8 | const appRoot = process.env.APP_ROOT || process.cwd(); 9 | console.log('appRoot', appRoot); 10 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 11 | console.log('problemsPath', problemsPath); 12 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 13 | console.log('problemsData', problemsData); 14 | const problems = JSON.parse(problemsData); 15 | console.log('problems', problems); 16 | res.status(200).json(problems); 17 | } catch (error) { 18 | console.error('Error reading problems.json:', error); 19 | res.status(500).json({ error: 'Failed to load problems: ' + (error as Error).message }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages (docs/) 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths: 7 | - "docs/**" 8 | - ".github/workflows/deploy-pages.yml" 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | deploy: 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v5 32 | 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: 36 | path: docs 37 | 38 | - name: Deploy to GitHub Pages 39 | id: deployment 40 | uses: actions/deploy-pages@v4 41 | 42 | 43 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files 5 | dir: './', 6 | }) 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | setupFilesAfterEnv: ['/jest.setup.js'], 11 | testEnvironment: 'node', 12 | testMatch: [ 13 | '/tests/**/*.test.js', 14 | '/tests/**/*.test.ts' 15 | ], 16 | collectCoverageFrom: [ 17 | 'pages/api/**/*.{js,ts}', 18 | 'src/**/*.{js,ts,tsx}', 19 | '!**/*.d.ts', 20 | '!**/node_modules/**', 21 | ], 22 | coverageReporters: ['text', 'lcov', 'html'], 23 | testTimeout: 30000, // 30 seconds timeout for API tests 24 | maxWorkers: 1, // Run tests sequentially to avoid conflicts 25 | } 26 | 27 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 28 | module.exports = createJestConfig(customJestConfig) -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | // Support both server and static output 5 | output: process.env.NEXT_OUTPUT === 'export' ? 'export' : undefined, 6 | // Disable image optimization for Electron compatibility 7 | images: { 8 | unoptimized: true 9 | }, 10 | // i18n 国际化配置 11 | i18n: { 12 | locales: ['zh', 'en'], 13 | defaultLocale: 'zh', 14 | localeDetection: false 15 | }, 16 | // Webpack configuration for WASM support 17 | webpack: (config, { isServer }) => { 18 | // Enable WebAssembly 19 | config.experiments = { 20 | ...config.experiments, 21 | asyncWebAssembly: true, 22 | }; 23 | 24 | // Handle .wasm files 25 | config.module.rules.push({ 26 | test: /\.wasm$/, 27 | type: 'webassembly/async', 28 | }); 29 | 30 | return config; 31 | }, 32 | // Transpile packages that need it 33 | transpilePackages: [], 34 | // Disable telemetry for desktop app 35 | env: { 36 | NEXT_TELEMETRY_DISABLED: '1' 37 | } 38 | }; 39 | 40 | module.exports = nextConfig; 41 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Global test timeout 5 | jest.setTimeout(30000); 6 | 7 | // Mock console methods to reduce noise in tests (but keep log for debugging) 8 | global.console = { 9 | ...console, 10 | // Keep console.log and console.error for debugging 11 | log: console.log, // Keep for debugging 12 | debug: jest.fn(), 13 | info: jest.fn(), 14 | warn: jest.fn(), 15 | error: console.error, // Keep error for important messages 16 | }; 17 | 18 | // Global test utilities 19 | global.testUtils = { 20 | // Delay utility for async tests 21 | delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)), 22 | 23 | // Common API response structure validation 24 | validateAPIResponse: (response) => { 25 | expect(response).toHaveProperty('status'); 26 | expect(response).toHaveProperty('total'); 27 | expect(response).toHaveProperty('passed'); 28 | expect(response).toHaveProperty('results'); 29 | expect(Array.isArray(response.results)).toBe(true); 30 | } 31 | }; -------------------------------------------------------------------------------- /pages/api/ai-providers.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | if (req.method !== 'GET') { 5 | return res.status(405).json({ error: 'Method not allowed' }); 6 | } 7 | 8 | try { 9 | // Check what providers are configured 10 | const isOllamaConfigured = !!process.env.OLLAMA_ENDPOINT || !!process.env.OLLAMA_MODEL; 11 | const isDeepSeekConfigured = !!process.env.DEEPSEEK_API_KEY; 12 | const isOpenAIConfigured = !!process.env.OPENAI_API_KEY; 13 | const isQwenConfigured = !!process.env.QWEN_API_KEY; 14 | const isClaudeConfigured = !!process.env.CLAUDE_API_KEY; 15 | 16 | return res.status(200).json({ 17 | providers: { 18 | ollama: isOllamaConfigured, 19 | deepseek: isDeepSeekConfigured, 20 | openai: isOpenAIConfigured, 21 | qwen: isQwenConfigured, 22 | claude: isClaudeConfigured 23 | } 24 | }); 25 | } catch (error) { 26 | console.error('Error checking AI provider configuration:', error); 27 | return res.status(500).json({ error: 'Failed to check AI provider configuration' }); 28 | } 29 | } -------------------------------------------------------------------------------- /src/components/CodeWithCopy.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Code, Button, Group, Text } from '@mantine/core'; 3 | import { IconCopy, IconCheck } from '@tabler/icons-react'; 4 | import { useTranslation } from '../../src/contexts/I18nContext'; 5 | 6 | interface CodeWithCopyProps { 7 | code: string; 8 | language?: string; 9 | } 10 | 11 | export function CodeWithCopy({ code, language = 'javascript' }: CodeWithCopyProps) { 12 | const { t } = useTranslation(); 13 | const [copied, setCopied] = useState(false); 14 | 15 | const copyToClipboard = async () => { 16 | try { 17 | await navigator.clipboard.writeText(code); 18 | setCopied(true); 19 | setTimeout(() => setCopied(false), 2000); 20 | } catch (err) { 21 | console.error('Failed to copy code to clipboard:', err); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | 28 | {code} 29 | 30 | 39 |
40 | ); 41 | } -------------------------------------------------------------------------------- /pages/add-problem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import Link from 'next/link'; 4 | import { Container, AppShell, Title, Button, Group } from '@mantine/core'; 5 | import { useTranslation } from '../src/contexts/I18nContext'; 6 | import ProblemForm from '../src/components/ProblemForm'; 7 | 8 | const AddProblem: React.FC = () => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 | 13 | 14 | {t('addProblem.title')} - {t('header.title')} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {t('header.title')} 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default AddProblem; -------------------------------------------------------------------------------- /src/types/wasm.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type declarations for WASM libraries and global objects 3 | */ 4 | 5 | // Pyodide types (loaded from CDN) 6 | interface PyodideInterface { 7 | runPythonAsync(code: string): Promise; 8 | runPython(code: string): any; 9 | loadPackage(packages: string | string[]): Promise; 10 | globals: any; 11 | } 12 | 13 | interface LoadPyodideOptions { 14 | indexURL?: string; 15 | fullStdLib?: boolean; 16 | stdin?: () => string; 17 | stdout?: (text: string) => void; 18 | stderr?: (text: string) => void; 19 | } 20 | 21 | // TypeScript types (loaded from CDN) 22 | interface TypeScriptTranspileOutput { 23 | outputText: string; 24 | diagnostics?: any[]; 25 | sourceMapText?: string; 26 | } 27 | 28 | interface TypeScriptCompilerOptions { 29 | module?: number; 30 | target?: number; 31 | strict?: boolean; 32 | esModuleInterop?: boolean; 33 | skipLibCheck?: boolean; 34 | forceConsistentCasingInFileNames?: boolean; 35 | } 36 | 37 | interface TypeScriptModule { 38 | transpileModule(input: string, options: { compilerOptions: TypeScriptCompilerOptions }): TypeScriptTranspileOutput; 39 | ModuleKind: { CommonJS: number; ESNext: number; ES2020: number }; 40 | ScriptTarget: { ES5: number; ES2015: number; ES2020: number; Latest: number }; 41 | } 42 | 43 | // 扩展 Window 接口 44 | declare global { 45 | interface Window { 46 | loadPyodide?: (options?: LoadPyodideOptions) => Promise; 47 | ts?: TypeScriptModule; 48 | } 49 | } 50 | 51 | export {}; 52 | 53 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | /* 全局样式 - 确保暗色主题正确工作 */ 2 | 3 | /* 亮色主题样式 */ 4 | [data-mantine-color-scheme="light"] { 5 | --mantine-color-body: #ffffff; 6 | --mantine-color-text: #000000; 7 | } 8 | 9 | /* 暗色主题样式 */ 10 | [data-mantine-color-scheme="dark"] { 11 | --mantine-color-body: #1a1b1e; 12 | --mantine-color-text: #c1c2c5; 13 | } 14 | 15 | /* 确保body元素使用正确的背景色 */ 16 | html[data-mantine-color-scheme="dark"] body { 17 | background-color: #1a1b1e; 18 | color: #c1c2c5; 19 | } 20 | 21 | html[data-mantine-color-scheme="light"] body { 22 | background-color: #ffffff; 23 | color: #000000; 24 | } 25 | 26 | /* 确保页面根元素的背景色 */ 27 | html[data-mantine-color-scheme="dark"] { 28 | background-color: #1a1b1e; 29 | } 30 | 31 | html[data-mantine-color-scheme="light"] { 32 | background-color: #ffffff; 33 | } 34 | 35 | /* 自定义滚动条样式 */ 36 | ::-webkit-scrollbar { 37 | width: 8px; 38 | height: 8px; 39 | } 40 | 41 | [data-mantine-color-scheme="dark"] ::-webkit-scrollbar-track { 42 | background: #25262b; 43 | } 44 | 45 | [data-mantine-color-scheme="dark"] ::-webkit-scrollbar-thumb { 46 | background: #373a40; 47 | border-radius: 4px; 48 | } 49 | 50 | [data-mantine-color-scheme="dark"] ::-webkit-scrollbar-thumb:hover { 51 | background: #5c5f66; 52 | } 53 | 54 | [data-mantine-color-scheme="light"] ::-webkit-scrollbar-track { 55 | background: #f1f3f4; 56 | } 57 | 58 | [data-mantine-color-scheme="light"] ::-webkit-scrollbar-thumb { 59 | background: #c1c2c5; 60 | border-radius: 4px; 61 | } 62 | 63 | [data-mantine-color-scheme="light"] ::-webkit-scrollbar-thumb:hover { 64 | background: #a6a7ab; 65 | } -------------------------------------------------------------------------------- /test-dynamic-problems.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | console.log('🧪 Testing dynamic problems.json modification...\n'); 5 | 6 | // Read the current problems.json 7 | const problemsPath = path.join(__dirname, 'public', 'problems.json'); 8 | const originalData = fs.readFileSync(problemsPath, 'utf8'); 9 | const problems = JSON.parse(originalData); 10 | 11 | console.log(`📊 Current number of problems: ${problems.length}`); 12 | 13 | // Add a test problem 14 | const testProblem = { 15 | "id": "test-problem", 16 | "title": { "en": "Test Problem", "zh": "测试问题" }, 17 | "difficulty": "Easy", 18 | "tags": ["test"], 19 | "description": { 20 | "en": "This is a test problem to verify dynamic loading.", 21 | "zh": "这是一个测试问题,用于验证动态加载。" 22 | }, 23 | "examples": [ 24 | { "input": "1", "output": "1" } 25 | ], 26 | "template": { 27 | "js": "function testFunction(x) {\n // Write your code here\n return x;\n}\nmodule.exports = testFunction;" 28 | }, 29 | "solution": { 30 | "js": "function testFunction(x) {\n return x;\n}\nmodule.exports = testFunction;" 31 | }, 32 | "tests": [ 33 | { "input": "1", "output": "1" }, 34 | { "input": "42", "output": "42" } 35 | ] 36 | }; 37 | 38 | // Add the test problem 39 | problems.push(testProblem); 40 | 41 | // Write back to file 42 | fs.writeFileSync(problemsPath, JSON.stringify(problems, null, 2)); 43 | 44 | console.log(`✅ Added test problem. New count: ${problems.length}`); 45 | console.log('🔗 Visit http://localhost:3002 to see the new problem!'); 46 | console.log('🔗 Direct link: http://localhost:3002/problems/test-problem'); 47 | console.log('\n💡 The test problem should appear immediately without rebuilding!'); 48 | 49 | // Provide cleanup instructions 50 | console.log('\n🧹 To remove the test problem, run: npm run test:cleanup'); -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 35 | 36 | 37 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # AI Problem Generator Configuration 2 | # Copy this file to .env.local and add your actual API keys 3 | 4 | # === DeepSeek Configuration (Optional) === 5 | # DeepSeek API Key for AI problem generation 6 | # Get your API key from: https://platform.deepseek.com/ 7 | # DEEPSEEK_API_KEY=your_deepseek_api_key_here 8 | 9 | # Optional: Set DeepSeek model (default: deepseek-chat) 10 | # DEEPSEEK_MODEL=deepseek-chat 11 | 12 | # Optional: Set API timeout (default: 30000ms) 13 | # DEEPSEEK_API_TIMEOUT=30000 14 | 15 | # Optional: Set maximum tokens for generation (default: 4000) 16 | # DEEPSEEK_MAX_TOKENS=4000 17 | 18 | # === OpenAI Configuration (Optional) === 19 | # OpenAI API Key for AI problem generation 20 | # Get your API key from: https://platform.openai.com/ 21 | # OPENAI_API_KEY=your_openai_api_key_here 22 | 23 | # Optional: Set OpenAI model (default: gpt-4-turbo) 24 | # OPENAI_MODEL=gpt-4-turbo 25 | 26 | # === Qwen (通义千问) Configuration (Optional) === 27 | # Qwen API Key for AI problem generation 28 | # Get your API key from: https://dashscope.console.aliyun.com/ 29 | # QWEN_API_KEY=your_qwen_api_key_here 30 | 31 | # Optional: Set Qwen model (default: qwen-turbo) 32 | # QWEN_MODEL=qwen-turbo 33 | 34 | # === Claude Configuration (Optional) === 35 | # Claude API Key for AI problem generation 36 | # Get your API key from: https://console.anthropic.com/ 37 | # CLAUDE_API_KEY=your_claude_api_key_here 38 | 39 | # Optional: Set Claude model (default: claude-3-haiku-20240307) 40 | # CLAUDE_MODEL=claude-3-haiku-20240307 41 | 42 | # === Ollama Configuration (Optional) === 43 | # Ollama endpoint (default: http://localhost:11434) 44 | # OLLAMA_ENDPOINT=http://localhost:11434 45 | 46 | # Ollama model (default: llama3) 47 | # OLLAMA_MODEL=llama3 48 | 49 | # The system will automatically detect which AI providers are configured. 50 | # If multiple providers are configured, the system will use them in this order of preference: 51 | # 1. Ollama (local) 52 | # 2. OpenAI 53 | # 3. Claude 54 | # 4. Qwen 55 | # 5. DeepSeek 56 | # You can manually select a provider in the UI if multiple are configured. 57 | # The frontend will fetch this configuration from the server via /api/ai-providers endpoint. -------------------------------------------------------------------------------- /src/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, ReactNode, useState, useEffect } from 'react'; 2 | import { MantineColorScheme } from '@mantine/core'; 3 | 4 | interface ThemeContextType { 5 | colorScheme: MantineColorScheme; 6 | toggleColorScheme: () => void; 7 | setColorScheme: (scheme: MantineColorScheme) => void; 8 | } 9 | 10 | const ThemeContext = createContext(undefined); 11 | 12 | export function ThemeProvider({ children }: { children: ReactNode }) { 13 | const [colorScheme, setColorSchemeState] = useState('light'); 14 | 15 | // 从localStorage加载主题设置 16 | useEffect(() => { 17 | const savedScheme = localStorage.getItem('mantine-color-scheme') as MantineColorScheme; 18 | if (savedScheme === 'light' || savedScheme === 'dark') { 19 | setColorSchemeState(savedScheme); 20 | document.documentElement.setAttribute('data-mantine-color-scheme', savedScheme); 21 | } else { 22 | // 检测系统主题偏好 23 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 24 | const initialScheme = prefersDark ? 'dark' : 'light'; 25 | setColorSchemeState(initialScheme); 26 | document.documentElement.setAttribute('data-mantine-color-scheme', initialScheme); 27 | } 28 | }, []); 29 | 30 | // 保存主题设置到localStorage 31 | const setColorScheme = (scheme: MantineColorScheme) => { 32 | setColorSchemeState(scheme); 33 | localStorage.setItem('mantine-color-scheme', scheme); 34 | // 同时更新HTML元素的data属性 35 | document.documentElement.setAttribute('data-mantine-color-scheme', scheme); 36 | }; 37 | 38 | const toggleColorScheme = () => { 39 | const newScheme = colorScheme === 'dark' ? 'light' : 'dark'; 40 | setColorScheme(newScheme); 41 | }; 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | } 49 | 50 | export function useTheme() { 51 | const context = useContext(ThemeContext); 52 | if (context === undefined) { 53 | throw new Error('useTheme must be used within a ThemeProvider'); 54 | } 55 | return context; 56 | } -------------------------------------------------------------------------------- /pages/api/add-problem.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { problem } = req.body; 12 | 13 | if (!problem) { 14 | return res.status(400).json({ error: 'Problem data is required' }); 15 | } 16 | 17 | // Validate required fields 18 | const requiredFields = ['id', 'title', 'difficulty', 'description', 'template', 'tests']; 19 | for (const field of requiredFields) { 20 | if (!problem[field]) { 21 | return res.status(400).json({ error: `Field '${field}' is required` }); 22 | } 23 | } 24 | 25 | // Validate id format 26 | if (!/^[a-z0-9-]+$/.test(problem.id)) { 27 | return res.status(400).json({ error: 'ID must contain only lowercase letters, numbers, and hyphens' }); 28 | } 29 | 30 | const appRoot = process.env.APP_ROOT || process.cwd(); 31 | // Read current problems 32 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 33 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 34 | const problems = JSON.parse(problemsData); 35 | 36 | // Check if problem ID already exists 37 | if (problems.find((p: any) => p.id === problem.id)) { 38 | return res.status(409).json({ error: 'Problem with this ID already exists' }); 39 | } 40 | 41 | // Add new problem 42 | problems.push(problem); 43 | 44 | // Write to public/problems.json 45 | fs.writeFileSync(problemsPath, JSON.stringify(problems, null, 2)); 46 | 47 | // Also sync to problems/problems.json 48 | const sourceProblemsPath = path.join(appRoot, 'problems', 'problems.json'); 49 | fs.writeFileSync(sourceProblemsPath, JSON.stringify(problems, null, 2)); 50 | 51 | res.status(201).json({ message: 'Problem added successfully', id: problem.id }); 52 | } catch (error) { 53 | console.error('Error adding problem:', error); 54 | res.status(500).json({ error: 'Failed to add problem' }); 55 | } 56 | } -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | import { MantineProvider, createTheme, ColorSchemeScript } from '@mantine/core'; 4 | import '@mantine/core/styles.css'; 5 | import '../styles/globals.css'; 6 | import { I18nProvider, useI18n } from '../src/contexts/I18nContext'; 7 | import { ThemeProvider, useTheme } from '../src/contexts/ThemeContext'; 8 | 9 | // 创建主题配置 10 | const lightTheme = createTheme({ 11 | primaryColor: 'blue', 12 | fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 13 | }); 14 | 15 | const darkTheme = createTheme({ 16 | primaryColor: 'blue', 17 | fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 18 | colors: { 19 | dark: [ 20 | '#C1C2C5', 21 | '#A6A7AB', 22 | '#909296', 23 | '#5c5f66', 24 | '#373A40', 25 | '#2C2E33', 26 | '#25262b', 27 | '#1A1B1E', 28 | '#141517', 29 | '#101113', 30 | ], 31 | }, 32 | }); 33 | 34 | function AppContent({ Component, pageProps }: AppProps) { 35 | const { colorScheme } = useTheme(); 36 | const theme = colorScheme === 'dark' ? darkTheme : lightTheme; 37 | const forceScheme = (colorScheme === 'dark' || colorScheme === 'light') ? colorScheme : 'light'; 38 | 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function AppHead() { 47 | const { t } = useI18n(); 48 | const title = t('header.title'); 49 | 50 | return ( 51 | 52 | {title} 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export default function App(props: AppProps) { 62 | return ( 63 | <> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | } -------------------------------------------------------------------------------- /src/components/LanguageThemeControls.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Button, Menu } from '@mantine/core'; 2 | import { useI18n } from '../contexts/I18nContext'; 3 | import { useTheme } from '../contexts/ThemeContext'; 4 | 5 | export function LanguageThemeControls() { 6 | const { locale, switchLocale, t } = useI18n(); 7 | const { colorScheme, toggleColorScheme } = useTheme(); 8 | 9 | return ( 10 | 11 | {/* 语言切换 */} 12 | 13 | 14 | 17 | 18 | 19 | 20 | {t('common.language')} 21 | { 23 | switchLocale('zh'); 24 | // Notify Electron of language change 25 | if (typeof window !== 'undefined' && (window as any).electronAPI) { 26 | (window as any).electronAPI.setLanguage('zh'); 27 | } 28 | }} 29 | color={locale === 'zh' ? 'blue' : undefined} 30 | > 31 | 中文 32 | 33 | { 35 | switchLocale('en'); 36 | // Notify Electron of language change 37 | if (typeof window !== 'undefined' && (window as any).electronAPI) { 38 | (window as any).electronAPI.setLanguage('en'); 39 | } 40 | }} 41 | color={locale === 'en' ? 'blue' : undefined} 42 | > 43 | English 44 | 45 | 46 | 47 | 48 | {/* 主题切换 */} 49 | 63 | 64 | ); 65 | } -------------------------------------------------------------------------------- /public/desktop-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Offline Leet Practice 7 | 51 | 52 | 53 |
54 | 55 |

Offline Leet Practice

56 |

Starting the application... Please wait while we prepare your coding environment.

57 |

Loading

58 |
59 | 66 | 67 | -------------------------------------------------------------------------------- /pages/api/delete-problems.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { ids } = req.body; 12 | 13 | if (!ids || !Array.isArray(ids) || ids.length === 0) { 14 | return res.status(400).json({ error: 'Problem IDs array is required' }); 15 | } 16 | 17 | // Validate all IDs are strings 18 | if (!ids.every(id => typeof id === 'string')) { 19 | return res.status(400).json({ error: 'All IDs must be strings' }); 20 | } 21 | 22 | const appRoot = process.env.APP_ROOT || process.cwd(); 23 | 24 | // Read current problems 25 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 26 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 27 | const problems: any[] = JSON.parse(problemsData); 28 | 29 | // Create a set of IDs to delete for efficient lookup 30 | const idsToDelete = new Set(ids); 31 | 32 | // Filter out problems to delete 33 | const remainingProblems = problems.filter(p => !idsToDelete.has(p.id)); 34 | 35 | // Calculate how many were actually deleted 36 | const deletedCount = problems.length - remainingProblems.length; 37 | 38 | if (deletedCount === 0) { 39 | return res.status(404).json({ error: 'No matching problems found to delete' }); 40 | } 41 | 42 | // Save updated problems 43 | fs.writeFileSync(problemsPath, JSON.stringify(remainingProblems, null, 2)); 44 | 45 | // Also sync to problems/problems.json 46 | const sourceProblemsPath = path.join(appRoot, 'problems', 'problems.json'); 47 | fs.writeFileSync(sourceProblemsPath, JSON.stringify(remainingProblems, null, 2)); 48 | 49 | return res.status(200).json({ 50 | message: 'Problems deleted successfully', 51 | deletedCount, 52 | remainingCount: remainingProblems.length, 53 | }); 54 | } catch (error) { 55 | console.error('Error deleting problems:', error); 56 | return res.status(500).json({ 57 | error: error instanceof Error ? error.message : 'Failed to delete problems' 58 | }); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [v0.0.8] - 2025-12-08 8 | ### :sparkles: New Features 9 | - [`67f3808`](https://github.com/zxypro1/OfflineCodePractice/commit/67f380881f27cf6e866c8c8e9b455d03fac38905) - **desktop**: add Electron-based desktop app support for Offline Leet Practice *(commit by [@zxypro1](https://github.com/zxypro1))* 10 | - [`3645fc3`](https://github.com/zxypro1/OfflineCodePractice/commit/3645fc30352b076374220a3835ec2c7e55df0e60) - refactor and enhance Algorithm Practice desktop application 11 | 12 | ### :bug: Bug Fixes 13 | - [`500047e`](https://github.com/zxypro1/OfflineCodePractice/commit/500047e80cf7152540ef3a157fbd9b1a75f42c83) - **menu**: update documentation link to correct GitHub repository *(commit by [@zxypro1](https://github.com/zxypro1))* 14 | 15 | ### :wrench: Chores 16 | - [`c256daa`](https://github.com/zxypro1/OfflineCodePractice/commit/c256daa0e650261f062288bf95d651b3ed0f69b2) - **start-local**: rewrite and improve Windows startup script *(commit by [@zxypro1](https://github.com/zxypro1))* 17 | 18 | 19 | ## [v0.0.7] - 2025-08-26 20 | ### :bug: Bug Fixes 21 | - [`da72634`](https://github.com/zxypro1/OfflineLeetPractice/commit/da72634a44b05cf02707f76117ae59f1fe2fb6be) - 修正打包过程中获取标签的方式,确保使用正确的标签名称 *(commit by [@zxypro1](https://github.com/zxypro1))* 22 | 23 | 24 | ## [v0.0.6] - 2025-08-26 25 | ### :bug: Bug Fixes 26 | - [`4c6af69`](https://github.com/zxypro1/OfflineLeetPractice/commit/4c6af694b1f43e932b482bca6af12f85cea8949b) - 修正发布资产文件名以使用正确的标签引用 *(commit by [@zxypro1](https://github.com/zxypro1))* 27 | 28 | 29 | ## [v0.0.5] - 2025-08-26 30 | ### :wrench: Chores 31 | - [`e22d12a`](https://github.com/zxypro1/OfflineLeetPractice/commit/e22d12ae51de40e57872554dea10d440c4456c48) - 优化发布流程,移除依赖文件以减小打包体积 *(commit by [@zxypro1](https://github.com/zxypro1))* 32 | 33 | 34 | ## [v0.0.4] - 2025-08-26 35 | ### :bug: Bug Fixes 36 | - [`0f3745f`](https://github.com/zxypro1/OfflineLeetPractice/commit/0f3745f4cdfbfa264c889df49d3d5436d0b84dc8) - 将 CHANGELOG.md 提交分支从 master 更新为 main *(commit by [@zxypro1](https://github.com/zxypro1))* 37 | 38 | [v0.0.4]: https://github.com/zxypro1/OfflineLeetPractice/compare/v0.0.3...v0.0.4 39 | [v0.0.5]: https://github.com/zxypro1/OfflineLeetPractice/compare/v0.0.4...v0.0.5 40 | [v0.0.6]: https://github.com/zxypro1/OfflineLeetPractice/compare/v0.0.5...v0.0.6 41 | [v0.0.7]: https://github.com/zxypro1/OfflineLeetPractice/compare/v0.0.6...v0.0.7 42 | [v0.0.8]: https://github.com/zxypro1/OfflineCodePractice/compare/v0.0.7...v0.0.8 43 | -------------------------------------------------------------------------------- /pages/api/update-problem.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'POST') { 7 | return res.status(405).json({ error: 'Method not allowed' }); 8 | } 9 | 10 | try { 11 | const { problem, originalId } = req.body; 12 | 13 | if (!problem) { 14 | return res.status(400).json({ error: 'Problem data is required' }); 15 | } 16 | 17 | if (!originalId) { 18 | return res.status(400).json({ error: 'Original problem ID is required' }); 19 | } 20 | 21 | // Validate required fields 22 | const requiredFields = ['id', 'title', 'difficulty', 'description', 'template', 'tests']; 23 | for (const field of requiredFields) { 24 | if (!problem[field]) { 25 | return res.status(400).json({ error: `Field '${field}' is required` }); 26 | } 27 | } 28 | 29 | // Validate id format 30 | if (!/^[a-z0-9-]+$/.test(problem.id)) { 31 | return res.status(400).json({ error: 'ID must contain only lowercase letters, numbers, and hyphens' }); 32 | } 33 | 34 | const appRoot = process.env.APP_ROOT || process.cwd(); 35 | // Read current problems 36 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 37 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 38 | const problems = JSON.parse(problemsData); 39 | 40 | // Find the problem to update 41 | const problemIndex = problems.findIndex((p: any) => p.id === originalId); 42 | if (problemIndex === -1) { 43 | return res.status(404).json({ error: 'Problem not found' }); 44 | } 45 | 46 | // If ID changed, check if new ID already exists 47 | if (problem.id !== originalId) { 48 | const existingProblem = problems.find((p: any) => p.id === problem.id); 49 | if (existingProblem) { 50 | return res.status(409).json({ error: 'Problem with this ID already exists' }); 51 | } 52 | } 53 | 54 | // Update the problem 55 | problems[problemIndex] = problem; 56 | 57 | // Write to public/problems.json 58 | fs.writeFileSync(problemsPath, JSON.stringify(problems, null, 2)); 59 | 60 | // Also sync to problems/problems.json 61 | const sourceProblemsPath = path.join(appRoot, 'problems', 'problems.json'); 62 | if (fs.existsSync(path.dirname(sourceProblemsPath))) { 63 | fs.writeFileSync(sourceProblemsPath, JSON.stringify(problems, null, 2)); 64 | } 65 | 66 | res.status(200).json({ message: 'Problem updated successfully', id: problem.id }); 67 | } catch (error) { 68 | console.error('Error updating problem:', error); 69 | res.status(500).json({ error: 'Failed to update problem' }); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /DESKTOP-APP-GUIDE-zh.md: -------------------------------------------------------------------------------- 1 | # 桌面应用构建指南 2 | 3 | 本指南介绍如何将离线算法练习构建为跨平台桌面应用,支持 Windows、macOS 和 Linux。 4 | 5 | ## 概述 6 | 7 | 桌面应用是一个独立的、自包含的软件包,无需任何外部依赖。用户可以直接下载运行,无需安装 Node.js、Python 或任何开发工具。 8 | 9 | ## 技术架构 10 | 11 | - **前端框架**:Next.js + React 12 | - **桌面框架**:Electron 13 | - **代码执行**:浏览器端 WASM 14 | - JavaScript:浏览器原生执行 15 | - TypeScript:TypeScript 编译器转译 16 | - Python:Pyodide (CPython WASM) 17 | 18 | ## 前置要求(仅限构建) 19 | 20 | 以下要求仅适用于从源码构建的开发者: 21 | 22 | - Node.js >= 18.x 23 | - npm >= 8.x 24 | - 平台特定要求: 25 | - macOS 构建:macOS 系统 26 | - Windows 构建:Windows 系统或 Wine 27 | - Linux 构建:Linux 系统或 Docker 28 | 29 | ## 构建应用 30 | 31 | ### 开发模式 32 | 33 | ```bash 34 | npm install 35 | npm run build 36 | npm run electron:start 37 | ``` 38 | 39 | ### 生产构建 40 | 41 | #### macOS 42 | 43 | ```bash 44 | ./build-mac.sh 45 | # 或 46 | npm run dist:mac 47 | ``` 48 | 49 | 输出文件: 50 | - `dist/Algorithm Practice-x.x.x-macOS-x64.dmg`(Intel) 51 | - `dist/Algorithm Practice-x.x.x-macOS-arm64.dmg`(Apple Silicon) 52 | 53 | #### Windows 54 | 55 | PowerShell: 56 | ```powershell 57 | .\build-windows.ps1 58 | # 或 59 | npm run dist:win 60 | ``` 61 | 62 | 命令提示符: 63 | ```cmd 64 | build-windows.bat 65 | ``` 66 | 67 | 输出文件: 68 | - `dist/Algorithm Practice-x.x.x-Windows-x64.exe`(安装版) 69 | - `dist/Algorithm Practice-x.x.x-Windows-Portable.exe`(便携版) 70 | 71 | #### Linux 72 | 73 | ```bash 74 | npm run dist:linux 75 | ``` 76 | 77 | 输出文件: 78 | - `dist/Algorithm Practice-x.x.x-Linux.AppImage` 79 | - `dist/Algorithm Practice-x.x.x-Linux.deb` 80 | - `dist/Algorithm Practice-x.x.x-Linux.rpm` 81 | 82 | #### 所有平台 83 | 84 | ```bash 85 | npm run dist:all 86 | ``` 87 | 88 | ## 支持的语言 89 | 90 | | 语言 | 状态 | 实现方式 | 91 | |------|------|---------| 92 | | JavaScript | 支持 | 浏览器原生 | 93 | | TypeScript | 支持 | TypeScript 编译器 | 94 | | Python | 支持 | Pyodide (WASM) | 95 | 96 | 注意:需要原生编译器的语言(Java、C、C++、Go)在浏览器端 WASM 环境中不支持。 97 | 98 | ## 项目结构 99 | 100 | ``` 101 | . 102 | ├── electron-main.js # Electron 主进程 103 | ├── electron-preload.js # Electron 预加载脚本 104 | ├── electron-builder.config.js # 构建配置 105 | ├── build/ 106 | │ └── entitlements.mac.plist # macOS 权限配置 107 | ├── build-mac.sh # macOS 构建脚本 108 | ├── build-windows.ps1 # Windows PowerShell 构建脚本 109 | ├── build-windows.bat # Windows 批处理构建脚本 110 | └── dist/ # 构建输出目录 111 | ``` 112 | 113 | ## 配置说明 114 | 115 | ### electron-builder.config.js 116 | 117 | 主要配置项: 118 | 119 | - `appId`:应用程序标识符 120 | - `productName`:应用显示名称 121 | - `files`:打包文件列表 122 | - `win/mac/linux`:各平台特定配置 123 | 124 | ### AI 服务商设置 125 | 126 | 桌面应用支持通过内置设置页面进行配置: 127 | 128 | - DeepSeek 129 | - OpenAI 130 | - Qwen(通义千问) 131 | - Claude(Anthropic) 132 | - Ollama(本地部署) 133 | 134 | 配置存储在 `~/.offline-leet-practice/config.json`。 135 | 136 | ## 故障排除 137 | 138 | ### macOS:安全警告 139 | 140 | 首次打开应用时,macOS 可能显示安全警告。 141 | 142 | 解决方法: 143 | 1. 打开系统偏好设置 > 安全性与隐私 144 | 2. 点击"仍要打开" 145 | 146 | 或在终端执行: 147 | ```bash 148 | xattr -cr "/Applications/Algorithm Practice.app" 149 | ``` 150 | 151 | ### Windows:SmartScreen 警告 152 | 153 | Windows 可能对未签名的应用显示 SmartScreen 警告。 154 | 155 | 解决方法:点击"更多信息" > "仍要运行" 156 | 157 | ### 代码签名 158 | 159 | 生产环境分发建议进行代码签名: 160 | 161 | - **macOS**:需要 Apple Developer 证书 162 | - **Windows**:需要 EV 代码签名证书 163 | 164 | ## 更新日志 165 | 166 | ### v0.0.9 167 | - 迁移至纯 WASM 浏览器端代码执行 168 | - 新增 TypeScript 支持 169 | - 移除对服务器端执行的依赖 170 | - 改进 Electron 构建配置 171 | - 增强 AI 服务商设置页面 172 | 173 | ## 许可证 174 | 175 | MIT License 176 | -------------------------------------------------------------------------------- /public/desktop-main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Offline Leet Practice 7 | 73 | 74 | 75 |
76 | 77 |

Offline Leet Practice

78 |

Starting the application... Please wait while we prepare your coding environment.

79 |

Loading

80 | 81 | 85 |
86 | 104 | 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "algorithm-practice", 3 | "version": "0.1.7", 4 | "private": true, 5 | "main": "electron-main.js", 6 | "description": "离线算法练习应用 - 基于 WASM 的浏览器端代码执行,支持 JavaScript、TypeScript 和 Python", 7 | "author": { 8 | "name": "zxypro1", 9 | "url": "https://github.com/zxypro1" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/zxypro1/OfflineLeetPractice", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/zxypro1/OfflineLeetPractice.git" 16 | }, 17 | "keywords": [ 18 | "algorithm", 19 | "code-practice", 20 | "programming", 21 | "wasm", 22 | "electron", 23 | "offline" 24 | ], 25 | "scripts": { 26 | "dev": "next dev", 27 | "build": "next build", 28 | "start": "next start", 29 | "deploy-local": "npm run build && npm start", 30 | "setup": "npm install && npm run build", 31 | "test": "node scripts/test-runner.js", 32 | "test:watch": "jest --watch", 33 | "test:coverage": "node scripts/test-runner.js all --coverage", 34 | "test:api": "jest tests/api", 35 | "test:specific": "node scripts/test-runner.js specific", 36 | "test:solutions": "node scripts/test-runner.js specific solution", 37 | "test:data": "node scripts/test-runner.js specific data", 38 | "icons:generate": "node scripts/generate-icons.js", 39 | "electron:dev": "npm run build && electron .", 40 | "electron:start": "electron .", 41 | "electron:build": "npm run build && electron-builder --config electron-builder.config.js", 42 | "electron:build:mac": "npm run build && electron-builder --config electron-builder.config.js --mac", 43 | "electron:build:win": "npm run build && electron-builder --config electron-builder.config.js --win", 44 | "electron:build:linux": "npm run build && electron-builder --config electron-builder.config.js --linux", 45 | "electron:build:all": "npm run build && electron-builder --config electron-builder.config.js --mac --win --linux", 46 | "electron:pack": "npm run build && electron-builder --config electron-builder.config.js --dir", 47 | "dist": "npm run electron:build", 48 | "dist:mac": "npm run electron:build:mac", 49 | "dist:win": "npm run electron:build:win", 50 | "dist:linux": "npm run electron:build:linux", 51 | "dist:all": "npm run electron:build:all", 52 | "clean": "rm -rf .next dist node_modules/.cache", 53 | "clean:all": "rm -rf .next dist node_modules", 54 | "release": "bash scripts/release.sh", 55 | "release:patch": "bash scripts/release.sh patch", 56 | "release:minor": "bash scripts/release.sh minor", 57 | "release:major": "bash scripts/release.sh major", 58 | "release:dry": "bash scripts/release.sh patch --dry-run" 59 | }, 60 | "dependencies": { 61 | "@mantine/core": "7.6.1", 62 | "@mantine/hooks": "7.6.1", 63 | "@mantine/notifications": "7.6.1", 64 | "@monaco-editor/react": "^4.7.0", 65 | "@tabler/icons-react": "^3.34.1", 66 | "@types/katex": "^0.16.7", 67 | "@types/react-syntax-highlighter": "^15.5.13", 68 | "d3-sankey": "^0.12.3", 69 | "isomorphic-dompurify": "^2.26.0", 70 | "katex": "^0.16.22", 71 | "marked": "^16.2.0", 72 | "mermaid": "^11.10.1", 73 | "next": "13.5.8", 74 | "react": "18.2.0", 75 | "react-dom": "18.2.0", 76 | "react-markdown": "^10.1.0", 77 | "react-syntax-highlighter": "^15.6.3", 78 | "rehype-katex": "^7.0.1", 79 | "rehype-mermaid": "^3.0.0", 80 | "rehype-raw": "^7.0.0", 81 | "remark-gfm": "^4.0.1", 82 | "remark-math": "^6.0.0" 83 | }, 84 | "devDependencies": { 85 | "@types/jest": "^29.5.14", 86 | "@types/node": "20.4.2", 87 | "@types/react": "18.2.21", 88 | "@types/react-dom": "18.2.7", 89 | "@types/supertest": "^6.0.3", 90 | "electron": "^31.0.0", 91 | "electron-builder": "^24.9.1", 92 | "electron-next": "^3.1.5", 93 | "jest": "^29.7.0", 94 | "jest-environment-node": "^29.7.0", 95 | "png-to-ico": "^2.1.8", 96 | "sharp": "^0.33.5", 97 | "supertest": "^6.3.4", 98 | "typescript": "5.1.6" 99 | }, 100 | "engines": { 101 | "node": ">=18.0.0", 102 | "npm": ">=8.0.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pages/generator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Head from 'next/head'; 3 | import { AppShell, Container, Group, Title, Button, ActionIcon, Text } from '@mantine/core'; 4 | import { IconArrowLeft, IconBrain } from '@tabler/icons-react'; 5 | import { useRouter } from 'next/router'; 6 | import { useTranslation, useI18n } from '../src/contexts/I18nContext'; 7 | import { LanguageThemeControls } from '../src/components/LanguageThemeControls'; 8 | import ProblemGenerator from '../src/components/ProblemGenerator'; 9 | 10 | interface GeneratedProblem { 11 | id: string; 12 | title: { 13 | en: string; 14 | zh: string; 15 | }; 16 | difficulty: 'Easy' | 'Medium' | 'Hard'; 17 | tags: string[]; 18 | description: { 19 | en: string; 20 | zh: string; 21 | }; 22 | } 23 | 24 | const GeneratorPage: React.FC = () => { 25 | const router = useRouter(); 26 | const { t } = useTranslation(); 27 | const { locale } = useI18n(); 28 | const [lastGeneratedProblem, setLastGeneratedProblem] = useState(null); 29 | const [mounted, setMounted] = useState(false); 30 | 31 | useEffect(() => { 32 | setMounted(true); 33 | }, []); 34 | 35 | const handleProblemGenerated = (problem: GeneratedProblem) => { 36 | setLastGeneratedProblem(problem); 37 | }; 38 | 39 | const handleBackToHome = () => { 40 | router.push('/'); 41 | }; 42 | 43 | const handleTryProblem = () => { 44 | if (lastGeneratedProblem) { 45 | router.push(`/problems/${lastGeneratedProblem.id}`); 46 | } 47 | }; 48 | 49 | if (!mounted) { 50 | return ( 51 | <> 52 | 53 | {t('aiGenerator.title')} - {t('header.title')} 54 | 55 | 56 | 57 |
{t('common.loading')}
58 | 59 | ); 60 | } 61 | 62 | return ( 63 | <> 64 | 65 | {t('aiGenerator.title')} - {t('header.title')} 66 | 67 | 68 | 69 | 70 | 74 | {/* Header */} 75 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | {t('aiGenerator.title')} 90 | 91 | 92 | 93 | 94 | {lastGeneratedProblem && ( 95 | 101 | )} 102 | 103 | 104 | 105 | 106 | 107 | 108 | {/* Main Content */} 109 | 110 | 111 | 114 | 115 | 116 | {/* Footer */} 117 | 118 | 119 | {t('aiGenerator.poweredBy', { provider: 'AI' })} • {t('aiGenerator.unlimitedProblems')} 120 | 121 | 122 | 123 | 124 | 125 | ); 126 | }; 127 | 128 | export default GeneratorPage; -------------------------------------------------------------------------------- /DESKTOP-APP-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Desktop Application Build Guide 2 | 3 | This guide covers building Algorithm Practice as a cross-platform desktop application for Windows, macOS, and Linux. 4 | 5 | ## Overview 6 | 7 | The desktop application is a standalone, self-contained package that requires no external dependencies. Users can download and run the application immediately without installing Node.js, Python, or any development tools. 8 | 9 | ## Technology Stack 10 | 11 | - **Frontend Framework**: Next.js + React 12 | - **Desktop Framework**: Electron 13 | - **Code Execution**: Browser-side WASM 14 | - JavaScript: Native browser execution 15 | - TypeScript: TypeScript compiler transpilation 16 | - Python: Pyodide (CPython WASM) 17 | 18 | ## Prerequisites (For Building Only) 19 | 20 | These requirements apply only to developers building from source: 21 | 22 | - Node.js >= 18.x 23 | - npm >= 8.x 24 | - Platform-specific requirements: 25 | - macOS builds: macOS system 26 | - Windows builds: Windows system or Wine 27 | - Linux builds: Linux system or Docker 28 | 29 | ## Building the Application 30 | 31 | ### Development Mode 32 | 33 | ```bash 34 | npm install 35 | npm run build 36 | npm run electron:start 37 | ``` 38 | 39 | ### Production Builds 40 | 41 | #### macOS 42 | 43 | ```bash 44 | ./build-mac.sh 45 | # or 46 | npm run dist:mac 47 | ``` 48 | 49 | Output: 50 | - `dist/Algorithm Practice-x.x.x-macOS-x64.dmg` (Intel) 51 | - `dist/Algorithm Practice-x.x.x-macOS-arm64.dmg` (Apple Silicon) 52 | 53 | #### Windows 54 | 55 | PowerShell: 56 | ```powershell 57 | .\build-windows.ps1 58 | # or 59 | npm run dist:win 60 | ``` 61 | 62 | Command Prompt: 63 | ```cmd 64 | build-windows.bat 65 | ``` 66 | 67 | Output: 68 | - `dist/Algorithm Practice-x.x.x-Windows-x64.exe` (Installer) 69 | - `dist/Algorithm Practice-x.x.x-Windows-Portable.exe` (Portable) 70 | 71 | #### Linux 72 | 73 | ```bash 74 | npm run dist:linux 75 | ``` 76 | 77 | Output: 78 | - `dist/Algorithm Practice-x.x.x-Linux.AppImage` 79 | - `dist/Algorithm Practice-x.x.x-Linux.deb` 80 | - `dist/Algorithm Practice-x.x.x-Linux.rpm` 81 | 82 | #### All Platforms 83 | 84 | ```bash 85 | npm run dist:all 86 | ``` 87 | 88 | ## Supported Languages 89 | 90 | | Language | Status | Implementation | 91 | |----------|--------|----------------| 92 | | JavaScript | Supported | Native browser | 93 | | TypeScript | Supported | TypeScript compiler | 94 | | Python | Supported | Pyodide (WASM) | 95 | 96 | Note: Languages requiring native compilers (Java, C, C++, Go) are not supported in the browser-side WASM environment. 97 | 98 | ## Project Structure 99 | 100 | ``` 101 | . 102 | ├── electron-main.js # Electron main process 103 | ├── electron-preload.js # Electron preload script 104 | ├── electron-builder.config.js # Build configuration 105 | ├── build/ 106 | │ └── entitlements.mac.plist # macOS entitlements 107 | ├── build-mac.sh # macOS build script 108 | ├── build-windows.ps1 # Windows PowerShell build script 109 | ├── build-windows.bat # Windows batch build script 110 | └── dist/ # Build output directory 111 | ``` 112 | 113 | ## Configuration 114 | 115 | ### electron-builder.config.js 116 | 117 | Key configuration options: 118 | 119 | - `appId`: Application identifier 120 | - `productName`: Application display name 121 | - `files`: Files to include in package 122 | - `win/mac/linux`: Platform-specific configurations 123 | 124 | ### AI Provider Settings 125 | 126 | The desktop application supports configuration through the built-in settings page: 127 | 128 | - DeepSeek 129 | - OpenAI 130 | - Qwen (Alibaba Cloud) 131 | - Claude (Anthropic) 132 | - Ollama (Local) 133 | 134 | Configuration is stored in `~/.offline-leet-practice/config.json`. 135 | 136 | ## Troubleshooting 137 | 138 | ### macOS: Security Warning 139 | 140 | When first opening the application, macOS may display a security warning. 141 | 142 | Solution: 143 | 1. Open System Preferences > Security & Privacy 144 | 2. Click "Open Anyway" 145 | 146 | Or run in Terminal: 147 | ```bash 148 | xattr -cr "/Applications/Algorithm Practice.app" 149 | ``` 150 | 151 | ### Windows: SmartScreen Warning 152 | 153 | Windows may display a SmartScreen warning for unsigned applications. 154 | 155 | Solution: Click "More info" > "Run anyway" 156 | 157 | ### Code Signing 158 | 159 | For production distribution, code signing is recommended: 160 | 161 | - **macOS**: Requires Apple Developer certificate 162 | - **Windows**: Requires EV code signing certificate 163 | 164 | ## Changelog 165 | 166 | ### v0.0.9 167 | - Migrated to pure WASM browser-side code execution 168 | - Added TypeScript support 169 | - Removed dependency on server-side execution 170 | - Improved Electron build configuration 171 | - Enhanced settings page for AI provider configuration 172 | 173 | ## License 174 | 175 | MIT License 176 | -------------------------------------------------------------------------------- /scripts/generate-icons.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Icon Generator Script 4 | * 5 | * 生成 Electron 应用所需的各种图标格式 6 | * 7 | * 依赖:需要安装 sharp 和 png-to-ico 8 | * npm install sharp png-to-ico --save-dev 9 | * 10 | * 使用方法: 11 | * node scripts/generate-icons.js 12 | */ 13 | 14 | const fs = require('fs'); 15 | const path = require('path'); 16 | 17 | async function generateIcons() { 18 | let sharp; 19 | let pngToIco; 20 | 21 | try { 22 | sharp = require('sharp'); 23 | } catch (e) { 24 | console.log('Installing sharp for image processing...'); 25 | const { execSync } = require('child_process'); 26 | execSync('npm install sharp --save-dev', { stdio: 'inherit' }); 27 | sharp = require('sharp'); 28 | } 29 | 30 | try { 31 | pngToIco = require('png-to-ico'); 32 | } catch (e) { 33 | console.log('Installing png-to-ico for Windows icon generation...'); 34 | const { execSync } = require('child_process'); 35 | execSync('npm install png-to-ico --save-dev', { stdio: 'inherit' }); 36 | pngToIco = require('png-to-ico'); 37 | } 38 | 39 | const svgPath = path.join(__dirname, '../public/favicon.svg'); 40 | const buildDir = path.join(__dirname, '../build'); 41 | const publicDir = path.join(__dirname, '../public'); 42 | 43 | // Ensure build directory exists 44 | if (!fs.existsSync(buildDir)) { 45 | fs.mkdirSync(buildDir, { recursive: true }); 46 | } 47 | 48 | // Create icons subdirectory 49 | const iconsDir = path.join(buildDir, 'icons'); 50 | if (!fs.existsSync(iconsDir)) { 51 | fs.mkdirSync(iconsDir, { recursive: true }); 52 | } 53 | 54 | const svgBuffer = fs.readFileSync(svgPath); 55 | 56 | // Icon sizes needed for different platforms 57 | const sizes = { 58 | // macOS 59 | mac: [16, 32, 64, 128, 256, 512, 1024], 60 | // Windows (for .ico file) 61 | win: [16, 24, 32, 48, 64, 128, 256], 62 | // Linux 63 | linux: [16, 24, 32, 48, 64, 128, 256, 512] 64 | }; 65 | 66 | console.log('🎨 Generating application icons...\n'); 67 | 68 | try { 69 | // Generate main icon.png (512x512) 70 | console.log('📱 Creating main icon (512x512)...'); 71 | await sharp(svgBuffer) 72 | .resize(512, 512) 73 | .png() 74 | .toFile(path.join(publicDir, 'icon.png')); 75 | console.log(' ✅ public/icon.png'); 76 | 77 | // Generate 256x256 PNG for Windows ICO conversion 78 | const icon256Path = path.join(buildDir, 'icon-256.png'); 79 | await sharp(svgBuffer) 80 | .resize(256, 256) 81 | .png() 82 | .toFile(icon256Path); 83 | 84 | // Generate Windows .ico file 85 | console.log('\n🪟 Creating Windows .ico file...'); 86 | const icoBuffer = await pngToIco([icon256Path]); 87 | fs.writeFileSync(path.join(buildDir, 'icon.ico'), icoBuffer); 88 | console.log(' ✅ build/icon.ico'); 89 | 90 | // Generate macOS icons 91 | console.log('\n🍎 Creating macOS icons...'); 92 | for (const size of sizes.mac) { 93 | const filename = `icon_${size}x${size}.png`; 94 | await sharp(svgBuffer) 95 | .resize(size, size) 96 | .png() 97 | .toFile(path.join(iconsDir, filename)); 98 | console.log(` ✅ ${filename}`); 99 | } 100 | 101 | // Generate @2x versions for macOS 102 | for (const size of [16, 32, 128, 256, 512]) { 103 | const filename = `icon_${size}x${size}@2x.png`; 104 | await sharp(svgBuffer) 105 | .resize(size * 2, size * 2) 106 | .png() 107 | .toFile(path.join(iconsDir, filename)); 108 | console.log(` ✅ ${filename}`); 109 | } 110 | 111 | // Generate Windows icon sizes (individual PNGs) 112 | console.log('\n🪟 Creating Windows PNG icons...'); 113 | for (const size of sizes.win) { 114 | const filename = `icon_${size}.png`; 115 | await sharp(svgBuffer) 116 | .resize(size, size) 117 | .png() 118 | .toFile(path.join(iconsDir, filename)); 119 | console.log(` ✅ ${filename}`); 120 | } 121 | 122 | // Generate favicon.ico (multi-size) - simplified version 123 | console.log('\n🌐 Creating favicon...'); 124 | await sharp(svgBuffer) 125 | .resize(32, 32) 126 | .png() 127 | .toFile(path.join(publicDir, 'favicon-32.png')); 128 | console.log(' ✅ favicon-32.png'); 129 | 130 | await sharp(svgBuffer) 131 | .resize(16, 16) 132 | .png() 133 | .toFile(path.join(publicDir, 'favicon-16.png')); 134 | console.log(' ✅ favicon-16.png'); 135 | 136 | // Linux icons 137 | console.log('\n🐧 Creating Linux icons...'); 138 | for (const size of sizes.linux) { 139 | const filename = `${size}x${size}.png`; 140 | await sharp(svgBuffer) 141 | .resize(size, size) 142 | .png() 143 | .toFile(path.join(iconsDir, filename)); 144 | console.log(` ✅ ${filename}`); 145 | } 146 | 147 | // Cleanup temporary file 148 | fs.unlinkSync(icon256Path); 149 | 150 | console.log('\n✨ Icon generation complete!'); 151 | console.log('\n📁 Generated files:'); 152 | console.log(' - public/icon.png (main app icon)'); 153 | console.log(' - build/icon.ico (Windows installer icon)'); 154 | console.log(' - build/icons/ (platform-specific icons)'); 155 | 156 | } catch (error) { 157 | console.error('❌ Error generating icons:', error); 158 | process.exit(1); 159 | } 160 | } 161 | 162 | generateIcons(); 163 | 164 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # 离线算法练习 2 | 3 | [English](./README.md) 4 | 5 | 快速链接: [讨论区](https://github.com/zxypro1/OfflineLeetPractice/discussions) | [Issues](https://github.com/zxypro1/OfflineLeetPractice/issues) | [Pull Requests](https://github.com/zxypro1/OfflineLeetPractice/pulls) 6 | 7 | > 一款独立的算法编程练习应用,支持 100% 离线使用。无需配置任何本地开发环境——下载、安装、即可开始练习。支持 JavaScript、TypeScript 和 Python,采用 WASM 浏览器端代码执行。 8 | 9 | 2025-08-24165202 10 | 11 | 2025-08-2165223 12 | 13 | ## 快速开始 14 | 15 | ### 桌面应用程序(推荐) 16 | 17 | 桌面应用提供最佳体验,无需任何环境配置。下载后即可直接运行。 18 | 19 | **[下载最新版本](https://github.com/zxypro1/OfflineCodePractice/releases/latest)** 20 | 21 | | 平台 | 下载文件 | 22 | |------|----------| 23 | | **macOS** (Apple Silicon) | `Algorithm-Practice-*-macOS-arm64.dmg` | 24 | | **macOS** (Intel) | `Algorithm-Practice-*-macOS-x64.dmg` | 25 | | **Windows** (安装版) | `Algorithm-Practice-*-Windows-Setup.exe` | 26 | | **Windows** (便携版) | `Algorithm-Practice-*-Windows-Portable.exe` | 27 | | **Linux** (AppImage) | `Algorithm-Practice-*-Linux.AppImage` | 28 | | **Linux** (Debian/Ubuntu) | `Algorithm-Practice-*-Linux.deb` | 29 | | **Linux** (Fedora/RHEL) | `Algorithm-Practice-*-Linux.rpm` | 30 | 31 | **macOS 用户**:如果提示"应用已损坏,无法打开",请在终端执行: 32 | ```bash 33 | xattr -cr "/Applications/Algorithm Practice.app" 34 | ``` 35 | 36 | ### Web 版本(备选方案) 37 | 38 | 如果您更倾向于从源码运行,请参阅下方的[开发环境配置](#开发环境配置)章节。 39 | 40 | ## 功能特性 41 | 42 | ### 核心功能 43 | 44 | - **独立应用程序**:无需 Node.js、Python 或任何开发环境 45 | - **完全离线支持**:安装后无需网络即可使用 46 | - **内置题库**:包含 10+ 道经典算法题目 47 | - **AI 题目生成器**:使用多种 AI 服务生成自定义题目 48 | - **WASM 代码执行**:浏览器端执行 JavaScript、TypeScript 和 Python 49 | - **Monaco 代码编辑器**:VS Code 级别的编辑体验,支持语法高亮和自动补全 50 | - **即时测试**:立即运行测试并查看详细结果和执行时间 51 | - **跨平台支持**:支持 Windows、macOS 和 Linux 52 | 53 | ### 支持的语言 54 | 55 | | 语言 | 状态 | 实现方式 | 56 | |------|------|---------| 57 | | **JavaScript** | 支持 | 浏览器原生执行 | 58 | | **TypeScript** | 支持 | TypeScript 编译器转译 | 59 | | **Python** | 支持 | Pyodide (CPython WASM) | 60 | 61 | 所有代码执行都在浏览器端通过 WebAssembly 完成,无需服务器端执行。 62 | 63 | ### AI 智能题目生成 64 | 65 | - **自定义题目创建**:用自然语言描述想要练习的内容 66 | - **完整解法**:每个题目都包含可运行的参考解法 67 | - **全面测试**:自动生成包括边界情况的测试用例 68 | - **即时集成**:题目自动添加到本地题库 69 | 70 | ## 使用方法 71 | 72 | ### 题目练习 73 | 74 | 1. **浏览题目**:查看包含难度和标签的题目列表 75 | 2. **选择题目**:点击任意题目打开详情页面 76 | 3. **选择语言**:选择 JavaScript、TypeScript 或 Python 77 | 4. **编写解法**:使用具备完整 IDE 功能的 Monaco 编辑器 78 | 5. **运行测试**:点击"提交并运行测试"执行代码 79 | 6. **查看结果**:查看详细测试结果和执行时间 80 | 81 | ### AI 题目生成 82 | 83 | 1. **访问生成器**:点击首页的"AI 生成器"按钮 84 | 2. **描述需求**:输入想要的题目类型 85 | 3. **生成题目**:AI 创建包含测试用例和解法的完整题目 86 | 4. **开始练习**:生成的题目自动出现在题库中 87 | 88 | ### 设置配置 89 | 90 | 访问设置页面配置 AI 服务商: 91 | 92 | - **桌面模式**:通过"设置"按钮或应用程序菜单访问 93 | - **Web 模式**:导航到 `/settings`(例如:http://localhost:3000/settings) 94 | 95 | 支持的 AI 服务商: 96 | - DeepSeek 97 | - OpenAI 98 | - Qwen(通义千问) 99 | - Claude(Anthropic) 100 | - Ollama(本地部署) 101 | 102 | 桌面模式下配置保存在 `~/.offline-leet-practice/config.json`。详细配置请参阅 [AI_PROVIDER_GUIDE.md](./AI_PROVIDER_GUIDE.md)。 103 | 104 | ### 添加自定义题目 105 | 106 | 1. **通过界面**:使用应用内的"添加题目"页面 107 | 2. **JSON 导入**:上传或粘贴 JSON 格式的题目数据 108 | 3. **直接编辑**:修改 `public/problems.json` 即时生效 109 | 110 | 完整指南请参阅 [MODIFY-PROBLEMS-GUIDE.md](./MODIFY-PROBLEMS-GUIDE.md)。 111 | 112 | ## 开发环境配置 113 | 114 | 适用于希望从源码运行或参与项目开发的开发者。 115 | 116 | ### 前置要求 117 | 118 | - Node.js 18+([下载](https://nodejs.org/)) 119 | - npm 8+ 120 | 121 | ### 本地运行 122 | 123 | **Windows:** 124 | ```bash 125 | start-local.bat 126 | ``` 127 | 128 | **macOS / Linux:** 129 | ```bash 130 | chmod +x start-local.sh 131 | ./start-local.sh 132 | ``` 133 | 134 | **手动配置:** 135 | ```bash 136 | git clone https://github.com/zxypro1/OfflineLeetPractice.git 137 | cd OfflineLeetPractice 138 | npm install 139 | npm run build 140 | npm start 141 | ``` 142 | 143 | 然后在浏览器中打开 http://localhost:3000。 144 | 145 | ### 构建桌面应用 146 | 147 | ```bash 148 | # macOS 149 | npm run dist:mac 150 | 151 | # Windows 152 | npm run dist:win 153 | 154 | # Linux 155 | npm run dist:linux 156 | 157 | # 所有平台 158 | npm run dist:all 159 | ``` 160 | 161 | 详细构建说明请参阅 [DESKTOP-APP-GUIDE-zh.md](./DESKTOP-APP-GUIDE-zh.md)。 162 | 163 | ## 技术栈 164 | 165 | - **前端框架**:React 18、Next.js 13、TypeScript 166 | - **UI 框架**:Mantine v7 167 | - **代码编辑器**:Monaco Editor 168 | - **代码执行**:WebAssembly 169 | - JavaScript:浏览器原生 `Function` 构造函数 170 | - TypeScript:TypeScript 编译器(CDN) 171 | - Python:Pyodide(CPython 编译为 WASM) 172 | - **桌面框架**:Electron 173 | 174 | ## 项目结构 175 | 176 | ``` 177 | OfflineLeetPractice/ 178 | ├── pages/ # Next.js 页面和 API 路由 179 | │ ├── api/ 180 | │ │ ├── problems.ts # 题目数据 API 181 | │ │ ├── generate-problem.ts 182 | │ │ └── add-problem.ts 183 | │ ├── problems/[id].tsx # 题目详情页面 184 | │ ├── generator.tsx # AI 生成器页面 185 | │ └── index.tsx # 首页 186 | ├── src/ 187 | │ ├── components/ # React 组件 188 | │ └── hooks/ 189 | │ └── useWasmExecutor.ts 190 | ├── public/ 191 | │ └── problems.json # 题目数据库 192 | ├── electron-main.js # Electron 主进程 193 | └── electron-builder.config.js 194 | ``` 195 | 196 | ## 贡献 197 | 198 | 欢迎贡献。改进方向包括: 199 | 200 | - 添加更多算法题目 201 | - 性能分析功能 202 | - 用户体验优化 203 | - 文档完善 204 | 205 | ## 许可证 206 | 207 | MIT License 208 | 209 | --- 210 | 211 | **随时随地练习算法——飞机上、游轮上,或任何离线环境。** 212 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Release Script for Algorithm Practice 4 | # Usage: ./scripts/release.sh [patch|minor|major] [--dry-run] 5 | # 6 | # Examples: 7 | # ./scripts/release.sh patch # 0.0.9 -> 0.0.10 8 | # ./scripts/release.sh minor # 0.0.9 -> 0.1.0 9 | # ./scripts/release.sh major # 0.0.9 -> 1.0.0 10 | # ./scripts/release.sh patch --dry-run # Preview changes without committing 11 | 12 | set -e 13 | 14 | # Colors 15 | RED='\033[0;31m' 16 | GREEN='\033[0;32m' 17 | YELLOW='\033[1;33m' 18 | BLUE='\033[0;34m' 19 | NC='\033[0m' # No Color 20 | 21 | # Parse arguments 22 | BUMP_TYPE=${1:-patch} 23 | DRY_RUN=false 24 | 25 | if [[ "$2" == "--dry-run" ]] || [[ "$1" == "--dry-run" ]]; then 26 | DRY_RUN=true 27 | if [[ "$1" == "--dry-run" ]]; then 28 | BUMP_TYPE="patch" 29 | fi 30 | fi 31 | 32 | echo -e "${BLUE}========================================${NC}" 33 | echo -e "${BLUE} Algorithm Practice Release Script${NC}" 34 | echo -e "${BLUE}========================================${NC}" 35 | echo "" 36 | 37 | # Check if we're in the right directory 38 | if [ ! -f "package.json" ]; then 39 | echo -e "${RED}Error: package.json not found. Please run this script from the project root.${NC}" 40 | exit 1 41 | fi 42 | 43 | # Check for uncommitted changes 44 | if [ -n "$(git status --porcelain)" ]; then 45 | echo -e "${YELLOW}Warning: You have uncommitted changes.${NC}" 46 | if [ "$DRY_RUN" = false ]; then 47 | read -p "Do you want to continue? (y/N) " -n 1 -r 48 | echo 49 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 50 | exit 1 51 | fi 52 | fi 53 | fi 54 | 55 | # Get current version 56 | CURRENT_VERSION=$(node -p "require('./package.json').version") 57 | echo -e "Current version: ${YELLOW}v${CURRENT_VERSION}${NC}" 58 | 59 | # Calculate new version 60 | IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" 61 | MAJOR=${VERSION_PARTS[0]} 62 | MINOR=${VERSION_PARTS[1]} 63 | PATCH=${VERSION_PARTS[2]} 64 | 65 | case $BUMP_TYPE in 66 | major) 67 | MAJOR=$((MAJOR + 1)) 68 | MINOR=0 69 | PATCH=0 70 | ;; 71 | minor) 72 | MINOR=$((MINOR + 1)) 73 | PATCH=0 74 | ;; 75 | patch) 76 | PATCH=$((PATCH + 1)) 77 | ;; 78 | *) 79 | echo -e "${RED}Invalid bump type: $BUMP_TYPE${NC}" 80 | echo "Usage: $0 [patch|minor|major] [--dry-run]" 81 | exit 1 82 | ;; 83 | esac 84 | 85 | NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" 86 | echo -e "New version: ${GREEN}v${NEW_VERSION}${NC}" 87 | echo -e "Bump type: ${BLUE}${BUMP_TYPE}${NC}" 88 | echo "" 89 | 90 | if [ "$DRY_RUN" = true ]; then 91 | echo -e "${YELLOW}DRY RUN - No changes will be made${NC}" 92 | echo "" 93 | echo "Would perform the following actions:" 94 | echo " 1. Update version in package.json to ${NEW_VERSION}" 95 | echo " 2. Update version in electron-builder.config.js" 96 | echo " 3. Git commit with message: 'chore: release v${NEW_VERSION}'" 97 | echo " 4. Create git tag: v${NEW_VERSION}" 98 | echo " 5. Push commit and tag to origin" 99 | echo "" 100 | echo -e "${BLUE}To actually release, run without --dry-run:${NC}" 101 | echo " ./scripts/release.sh ${BUMP_TYPE}" 102 | exit 0 103 | fi 104 | 105 | # Confirm release 106 | echo -e "${YELLOW}This will:${NC}" 107 | echo " 1. Update version in package.json" 108 | echo " 2. Update version in electron-builder.config.js" 109 | echo " 3. Commit changes" 110 | echo " 4. Create a git tag" 111 | echo " 5. Push to origin (triggers GitHub Actions build)" 112 | echo "" 113 | read -p "Continue with release? (y/N) " -n 1 -r 114 | echo 115 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 116 | echo "Release cancelled." 117 | exit 1 118 | fi 119 | 120 | echo "" 121 | echo -e "${BLUE}Updating versions...${NC}" 122 | 123 | # Update package.json version 124 | npm version $NEW_VERSION --no-git-tag-version 125 | 126 | # Update electron-builder.config.js if it contains version 127 | if grep -q "bundleShortVersion" electron-builder.config.js; then 128 | sed -i.bak "s/bundleShortVersion: '[^']*'/bundleShortVersion: '${NEW_VERSION}'/" electron-builder.config.js 129 | rm -f electron-builder.config.js.bak 130 | fi 131 | 132 | echo -e "${GREEN}✓ Version updated to ${NEW_VERSION}${NC}" 133 | 134 | # Git operations 135 | echo "" 136 | echo -e "${BLUE}Creating git commit and tag...${NC}" 137 | 138 | git add package.json package-lock.json electron-builder.config.js 139 | git commit -m "chore: release v${NEW_VERSION}" 140 | git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" 141 | 142 | echo -e "${GREEN}✓ Git commit and tag created${NC}" 143 | 144 | # Push to origin 145 | echo "" 146 | echo -e "${BLUE}Pushing to origin...${NC}" 147 | 148 | git push origin main 149 | git push origin "v${NEW_VERSION}" 150 | 151 | echo -e "${GREEN}✓ Pushed to origin${NC}" 152 | 153 | echo "" 154 | echo -e "${GREEN}========================================${NC}" 155 | echo -e "${GREEN} Release v${NEW_VERSION} initiated!${NC}" 156 | echo -e "${GREEN}========================================${NC}" 157 | echo "" 158 | echo "GitHub Actions will now:" 159 | echo " 1. Build desktop apps for Windows, macOS, and Linux" 160 | echo " 2. Generate changelog" 161 | echo " 3. Create GitHub Release with all artifacts" 162 | echo "" 163 | echo -e "Track progress: ${BLUE}https://github.com/zxypro1/OfflineLeetPractice/actions${NC}" 164 | echo -e "Release page: ${BLUE}https://github.com/zxypro1/OfflineLeetPractice/releases/tag/v${NEW_VERSION}${NC}" 165 | 166 | -------------------------------------------------------------------------------- /src/components/MarkdownRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import remarkMath from 'remark-math'; 5 | import rehypeKatex from 'rehype-katex'; 6 | import rehypeRaw from 'rehype-raw'; 7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 8 | import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; 9 | import { Box, Paper, Text, Table, Code, Title } from '@mantine/core'; 10 | import { useTheme } from '../contexts/ThemeContext'; 11 | import 'katex/dist/katex.min.css'; 12 | 13 | interface MarkdownRendererProps { 14 | content: string; 15 | } 16 | 17 | // Mermaid component that loads dynamically 18 | const MermaidChart: React.FC<{ chart: string }> = ({ chart }) => { 19 | const [svg, setSvg] = useState(''); 20 | const [error, setError] = useState(''); 21 | 22 | useEffect(() => { 23 | const renderMermaid = async () => { 24 | try { 25 | const mermaid = (await import('mermaid')).default; 26 | mermaid.initialize({ 27 | startOnLoad: false, 28 | theme: 'default', 29 | securityLevel: 'loose' 30 | }); 31 | 32 | const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 33 | const { svg } = await mermaid.render(id, chart); 34 | setSvg(svg); 35 | } catch (err) { 36 | console.error('Mermaid rendering error:', err); 37 | setError('Failed to render diagram'); 38 | } 39 | }; 40 | 41 | if (chart.trim()) { 42 | renderMermaid(); 43 | } 44 | }, [chart]); 45 | 46 | if (error) { 47 | return ( 48 | 49 | Error rendering diagram: {error} 50 | {chart} 51 | 52 | ); 53 | } 54 | 55 | if (!svg) { 56 | return ( 57 | 58 | Loading diagram... 59 | 60 | ); 61 | } 62 | 63 | return ( 64 | 65 |
66 | 67 | ); 68 | }; 69 | 70 | export const MarkdownRenderer: React.FC = ({ content }) => { 71 | const { colorScheme } = useTheme(); 72 | const components = { 73 | // Custom code block renderer with syntax highlighting 74 | code({ node, inline, className, children, ...props }: any) { 75 | const match = /language-(\w+)/.exec(className || ''); 76 | const language = match ? match[1] : ''; 77 | const codeContent = String(children).replace(/\n$/, ''); 78 | 79 | // Handle Mermaid diagrams 80 | if (language === 'mermaid') { 81 | return ; 82 | } 83 | 84 | // Handle other code blocks with syntax highlighting 85 | if (!inline && match) { 86 | return ( 87 | 93 | {codeContent} 94 | 95 | ); 96 | } 97 | 98 | // Inline code 99 | return ( 100 | 101 | {children} 102 | 103 | ); 104 | }, 105 | 106 | // Custom table renderer using Mantine Table 107 | table({ children }: any) { 108 | return ( 109 | 110 | {children} 111 |
112 | ); 113 | }, 114 | 115 | // Custom heading renderers using Mantine Title 116 | h1({ children }: any) { 117 | return ( 118 | 119 | {children} 120 | 121 | ); 122 | }, 123 | 124 | h2({ children }: any) { 125 | return ( 126 | 127 | {children} 128 | 129 | ); 130 | }, 131 | 132 | h3({ children }: any) { 133 | return ( 134 | 135 | {children} 136 | 137 | ); 138 | }, 139 | 140 | h4({ children }: any) { 141 | return ( 142 | 143 | {children} 144 | 145 | ); 146 | }, 147 | 148 | h5({ children }: any) { 149 | return ( 150 | 151 | {children} 152 | 153 | ); 154 | }, 155 | 156 | h6({ children }: any) { 157 | return ( 158 | 159 | {children} 160 | 161 | ); 162 | }, 163 | 164 | // Custom paragraph renderer 165 | p({ children }: any) { 166 | return ( 167 | 168 | {children} 169 | 170 | ); 171 | }, 172 | 173 | // Custom blockquote renderer 174 | blockquote({ children }: any) { 175 | return ( 176 | 185 | {children} 186 | 187 | ); 188 | } 189 | }; 190 | 191 | return ( 192 | 193 | 198 | {content} 199 | 200 | 201 | ); 202 | }; 203 | 204 | export default MarkdownRenderer; -------------------------------------------------------------------------------- /pages/api/import-problems.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | interface Problem { 6 | id: string; 7 | title: { en: string; zh: string }; 8 | difficulty: string; 9 | tags?: string[]; 10 | description: { en: string; zh: string }; 11 | examples?: Array<{ input: string; output: string }>; 12 | template: Record; 13 | tests: Array<{ input: string; output: string }>; 14 | solution?: Record; 15 | solutions?: Array<{ 16 | title: { en: string; zh: string }; 17 | content: { en: string; zh: string }; 18 | }>; 19 | } 20 | 21 | function validateProblem(problem: any): problem is Problem { 22 | if (!problem || typeof problem !== 'object') return false; 23 | if (typeof problem.id !== 'string' || !problem.id) return false; 24 | if (!problem.title || typeof problem.title.en !== 'string') return false; 25 | if (!['Easy', 'Medium', 'Hard'].includes(problem.difficulty)) return false; 26 | if (!problem.description || typeof problem.description.en !== 'string') return false; 27 | if (!problem.template || typeof problem.template !== 'object') return false; 28 | if (!Array.isArray(problem.tests) || problem.tests.length === 0) return false; 29 | 30 | // Validate ID format 31 | if (!/^[a-z0-9-]+$/.test(problem.id)) return false; 32 | 33 | return true; 34 | } 35 | 36 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 37 | if (req.method !== 'POST') { 38 | return res.status(405).json({ error: 'Method not allowed' }); 39 | } 40 | 41 | try { 42 | const { url } = req.body; 43 | 44 | if (!url || typeof url !== 'string') { 45 | return res.status(400).json({ error: 'URL is required' }); 46 | } 47 | 48 | // Validate URL format 49 | try { 50 | new URL(url); 51 | } catch { 52 | return res.status(400).json({ error: 'Invalid URL format' }); 53 | } 54 | 55 | // Fetch remote JSON 56 | const response = await fetch(url, { 57 | headers: { 58 | 'Accept': 'application/json', 59 | 'User-Agent': 'OfflineLeetPractice/1.0', 60 | }, 61 | }); 62 | 63 | if (!response.ok) { 64 | return res.status(400).json({ 65 | error: `Failed to fetch remote JSON: ${response.status} ${response.statusText}` 66 | }); 67 | } 68 | 69 | const contentType = response.headers.get('content-type'); 70 | if (contentType && !contentType.includes('application/json') && !contentType.includes('text/')) { 71 | return res.status(400).json({ error: 'Remote URL does not return JSON content' }); 72 | } 73 | 74 | let importedData: any; 75 | try { 76 | const text = await response.text(); 77 | importedData = JSON.parse(text); 78 | } catch { 79 | return res.status(400).json({ error: 'Failed to parse remote JSON' }); 80 | } 81 | 82 | // Validate and normalize to array 83 | let importedProblems: any[]; 84 | if (Array.isArray(importedData)) { 85 | importedProblems = importedData; 86 | } else if (typeof importedData === 'object' && importedData !== null && importedData.id) { 87 | // If it's a single problem object, wrap it in an array 88 | importedProblems = [importedData]; 89 | } else { 90 | return res.status(400).json({ error: 'Remote JSON must be an array of problems or a single problem object' }); 91 | } 92 | 93 | // Read current problems 94 | const appRoot = process.env.APP_ROOT || process.cwd(); 95 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 96 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 97 | const currentProblems: Problem[] = JSON.parse(problemsData); 98 | 99 | const existingIds = new Set(currentProblems.map(p => p.id)); 100 | 101 | // Process imported problems 102 | let success = 0; 103 | let failed = 0; 104 | let skipped = 0; 105 | 106 | for (const problem of importedProblems) { 107 | // Skip if ID already exists 108 | if (existingIds.has(problem.id)) { 109 | skipped++; 110 | continue; 111 | } 112 | 113 | // Validate problem structure 114 | if (!validateProblem(problem)) { 115 | failed++; 116 | continue; 117 | } 118 | 119 | // Ensure required fields have defaults 120 | const normalizedProblem: Problem = { 121 | id: problem.id, 122 | title: { 123 | en: problem.title.en || problem.title.zh || 'Untitled', 124 | zh: problem.title.zh || problem.title.en || '无标题', 125 | }, 126 | difficulty: problem.difficulty, 127 | tags: Array.isArray(problem.tags) ? problem.tags : [], 128 | description: { 129 | en: problem.description.en || problem.description.zh || '', 130 | zh: problem.description.zh || problem.description.en || '', 131 | }, 132 | examples: Array.isArray(problem.examples) ? problem.examples : [], 133 | template: problem.template, 134 | tests: problem.tests, 135 | }; 136 | 137 | // Copy optional fields 138 | if (problem.solution) { 139 | normalizedProblem.solution = problem.solution; 140 | } 141 | if (problem.solutions) { 142 | normalizedProblem.solutions = problem.solutions; 143 | } 144 | 145 | currentProblems.push(normalizedProblem); 146 | existingIds.add(problem.id); 147 | success++; 148 | } 149 | 150 | // Save updated problems 151 | if (success > 0) { 152 | fs.writeFileSync(problemsPath, JSON.stringify(currentProblems, null, 2)); 153 | 154 | // Also sync to problems/problems.json 155 | const sourceProblemsPath = path.join(appRoot, 'problems', 'problems.json'); 156 | fs.writeFileSync(sourceProblemsPath, JSON.stringify(currentProblems, null, 2)); 157 | } 158 | 159 | return res.status(200).json({ 160 | success, 161 | failed, 162 | skipped, 163 | total: importedProblems.length, 164 | }); 165 | } catch (error) { 166 | console.error('Error importing problems:', error); 167 | return res.status(500).json({ 168 | error: error instanceof Error ? error.message : 'Failed to import problems' 169 | }); 170 | } 171 | } 172 | 173 | -------------------------------------------------------------------------------- /pages/markdown-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, Title, Paper, Space } from '@mantine/core'; 3 | import MarkdownRenderer from '../src/components/MarkdownRenderer'; 4 | 5 | const testMarkdown = ` 6 | # Enhanced MarkdownRenderer Test 7 | 8 | This page demonstrates all the enhanced features of the MarkdownRenderer component. 9 | 10 | ## Features 11 | 12 | ### 1. **Bold**, *Italic*, and \`inline code\` formatting 13 | 14 | Regular text with **bold text**, *italic text*, and \`inline code\` formatting. 15 | 16 | ### 2. Code Blocks with Syntax Highlighting 17 | 18 | JavaScript code: 19 | \`\`\`javascript 20 | function twoSum(nums, target) { 21 | const map = new Map(); 22 | for (let i = 0; i < nums.length; i++) { 23 | const complement = target - nums[i]; 24 | if (map.has(complement)) { 25 | return [map.get(complement), i]; 26 | } 27 | map.set(nums[i], i); 28 | } 29 | } 30 | \`\`\` 31 | 32 | Python code: 33 | \`\`\`python 34 | def two_sum(nums, target): 35 | num_map = {} 36 | for i, num in enumerate(nums): 37 | complement = target - num 38 | if complement in num_map: 39 | return [num_map[complement], i] 40 | num_map[num] = i 41 | return [] 42 | \`\`\` 43 | 44 | ### 3. Mathematical Formulas (KaTeX) 45 | 46 | Inline math: $E = mc^2$ 47 | 48 | Block math: 49 | $$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$ 50 | 51 | Time complexity: $O(n \\log n)$ 52 | 53 | Space complexity: $O(1)$ 54 | 55 | ### 4. Complex Tables 56 | 57 | | Algorithm | Time Complexity | Space Complexity | Pros | Cons | 58 | |-----------|----------------|------------------|------|------| 59 | | Hash Map | $O(n)$ | $O(n)$ | Fast, optimal | Uses extra space | 60 | | Brute Force | $O(n^2)$ | $O(1)$ | Simple, no extra space | Slow for large inputs | 61 | | Two Pointers | $O(n \\log n)$ | $O(n)$ | Good for sorted arrays | Requires sorting | 62 | 63 | ### 5. HTML Elements 64 | 65 |
66 | Note: This is a custom HTML element with inline styling. 67 |
68 | 69 |
70 | 71 |
72 | Click to expand details 73 |

This content is hidden by default and can be toggled by clicking the summary.

74 |
    75 |
  • Item 1
  • 76 |
  • Item 2
  • 77 |
  • Item 3
  • 78 |
79 |
80 | 81 | ### 6. Mermaid Diagrams 82 | 83 | #### Flowchart 84 | \`\`\`mermaid 85 | graph TB 86 | A[Start] --> B{Is it sorted?} 87 | B -->|Yes| C[Use Two Pointers] 88 | B -->|No| D[Use Hash Map] 89 | C --> E[O(n) time] 90 | D --> E 91 | E --> F[End] 92 | \`\`\` 93 | 94 | #### Sequence Diagram 95 | \`\`\`mermaid 96 | sequenceDiagram 97 | participant User 98 | participant Frontend 99 | participant Backend 100 | participant Database 101 | 102 | User->>Frontend: Submit solution 103 | Frontend->>Backend: POST /api/run 104 | Backend->>Database: Save result 105 | Database-->>Backend: Confirmation 106 | Backend-->>Frontend: Test results 107 | Frontend-->>User: Display results 108 | \`\`\` 109 | 110 | ### 7. Lists and Nested Content 111 | 112 | #### Unordered List 113 | - **Algorithm Analysis** 114 | - Time complexity considerations 115 | - Space complexity trade-offs 116 | - Edge cases and constraints 117 | - **Implementation Details** 118 | - Language-specific optimizations 119 | - Error handling strategies 120 | - Testing approaches 121 | 122 | #### Ordered List 123 | 1. **Understanding the Problem** 124 | - Read requirements carefully 125 | - Identify input/output constraints 126 | - Consider edge cases 127 | 2. **Planning the Solution** 128 | - Choose appropriate data structures 129 | - Design the algorithm 130 | - Analyze complexity 131 | 3. **Implementation** 132 | - Write clean, readable code 133 | - Add comments for clarity 134 | - Handle edge cases 135 | 136 | ### 8. Blockquotes 137 | 138 | > **Important:** Always consider the time and space complexity of your solution. 139 | > 140 | > Good algorithms are not just about correctness, but also about efficiency and maintainability. 141 | 142 | ### 9. Links and References 143 | 144 | For more information, visit [LeetCode](https://leetcode.com) or check out [Algorithm Visualizations](https://visualgo.net). 145 | 146 | ### 10. Mixed Content Example 147 | 148 | The **Two Sum** problem can be solved in multiple ways: 149 | 150 | | Approach | Implementation | Complexity | 151 | |----------|----------------|------------| 152 | | Brute Force | Nested loops | $O(n^2)$ time, $O(1)$ space | 153 | | Hash Map | Single pass with map | $O(n)$ time, $O(n)$ space | 154 | 155 | Here's the optimal solution: 156 | 157 | \`\`\`javascript 158 | function twoSum(nums, target) { 159 | const map = new Map(); 160 | 161 | for (let i = 0; i < nums.length; i++) { 162 | const complement = target - nums[i]; 163 | 164 | if (map.has(complement)) { 165 | return [map.get(complement), i]; 166 | } 167 | 168 | map.set(nums[i], i); 169 | } 170 | 171 | return []; // No solution found 172 | } 173 | \`\`\` 174 | 175 | The algorithm works by: 176 | 1. Creating a hash map to store seen numbers 177 | 2. For each number, calculating its complement: $complement = target - nums[i]$ 178 | 3. Checking if the complement exists in our map 179 | 4. If found, returning the indices; otherwise, storing the current number 180 | 181 | **Time Complexity:** $O(n)$ - single pass through the array 182 | **Space Complexity:** $O(n)$ - hash map can store up to n elements 183 | 184 | --- 185 | 186 | ## Conclusion 187 | 188 | The enhanced MarkdownRenderer now supports: 189 | - ✅ **Rich text formatting** (bold, italic, code) 190 | - ✅ **Syntax-highlighted code blocks** 191 | - ✅ **Mathematical formulas** with KaTeX 192 | - ✅ **Complex tables** with proper styling 193 | - ✅ **HTML elements** for custom formatting 194 | - ✅ **Mermaid diagrams** for flowcharts and sequences 195 | - ✅ **All standard Markdown features** 196 | 197 | This makes it perfect for technical documentation, algorithm explanations, and educational content! 198 | `; 199 | 200 | export default function MarkdownTest() { 201 | return ( 202 | 203 | 204 | 205 | 206 | 207 | ); 208 | } -------------------------------------------------------------------------------- /AI_PROVIDER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # AI Provider Configuration Guide 2 | 3 | This guide explains how to configure AI model providers for the problem generation feature in Offline Algorithm Practice. 4 | 5 | ## Accessing Settings 6 | 7 | ### Desktop Application 8 | 9 | - Click "Navigation" > "Settings" in the application menu 10 | - Click the "Settings" button on the loading screen 11 | 12 | ### Web Version 13 | 14 | Navigate to `/settings` (e.g., http://localhost:3000/settings) 15 | 16 | ## Supported Providers 17 | 18 | The application supports multiple AI providers. You can configure one or more, and the system will automatically select the best available option. 19 | 20 | ### DeepSeek 21 | 22 | **Configuration:** 23 | - **API Key**: Obtain from [DeepSeek Platform](https://platform.deepseek.com/) 24 | - **Model**: Default `deepseek-chat` 25 | - **Timeout**: Default 30000ms 26 | - **Max Tokens**: Default 4000 27 | 28 | **Environment Variables:** 29 | ```bash 30 | DEEPSEEK_API_KEY=your_api_key_here 31 | DEEPSEEK_MODEL=deepseek-chat # optional 32 | ``` 33 | 34 | ### OpenAI 35 | 36 | **Configuration:** 37 | - **API Key**: Obtain from [OpenAI Platform](https://platform.openai.com/) 38 | - **Model**: Default `gpt-4-turbo` 39 | 40 | **Environment Variables:** 41 | ```bash 42 | OPENAI_API_KEY=your_api_key_here 43 | OPENAI_MODEL=gpt-4-turbo # optional 44 | ``` 45 | 46 | ### Qwen (Alibaba Cloud) 47 | 48 | **Configuration:** 49 | - **API Key**: Obtain from [DashScope Console](https://dashscope.console.aliyun.com/) 50 | - **Model**: Default `qwen-turbo` 51 | 52 | **Environment Variables:** 53 | ```bash 54 | QWEN_API_KEY=your_api_key_here 55 | QWEN_MODEL=qwen-turbo # optional 56 | ``` 57 | 58 | ### Claude (Anthropic) 59 | 60 | **Configuration:** 61 | - **API Key**: Obtain from [Anthropic Console](https://console.anthropic.com/) 62 | - **Model**: Default `claude-3-haiku-20240307` 63 | 64 | **Environment Variables:** 65 | ```bash 66 | CLAUDE_API_KEY=your_api_key_here 67 | CLAUDE_MODEL=claude-3-haiku-20240307 # optional 68 | ``` 69 | 70 | ### Ollama (Local) 71 | 72 | **Prerequisites:** 73 | 1. Install Ollama from https://ollama.com/ 74 | 2. Download a model: `ollama pull llama3` 75 | 76 | **Configuration:** 77 | - **Endpoint**: Default `http://localhost:11434` 78 | - **Model**: Default `llama3` 79 | 80 | **Environment Variables:** 81 | ```bash 82 | OLLAMA_ENDPOINT=http://localhost:11434 # optional 83 | OLLAMA_MODEL=llama3 # optional 84 | ``` 85 | 86 | ## Configuration Methods 87 | 88 | ### Method 1: Settings Page (Recommended for Desktop) 89 | 90 | 1. Open the application 91 | 2. Navigate to Settings 92 | 3. Configure your preferred providers 93 | 4. Click "Save Configuration" 94 | 95 | Configuration is stored in `~/.offline-leet-practice/config.json`. 96 | 97 | ### Method 2: Environment File (For Web/Development) 98 | 99 | Create `.env.local` in the project root: 100 | 101 | ```bash 102 | # DeepSeek 103 | DEEPSEEK_API_KEY=your_key 104 | 105 | # OpenAI 106 | OPENAI_API_KEY=your_key 107 | 108 | # Qwen 109 | QWEN_API_KEY=your_key 110 | 111 | # Claude 112 | CLAUDE_API_KEY=your_key 113 | 114 | # Ollama 115 | OLLAMA_ENDPOINT=http://localhost:11434 116 | OLLAMA_MODEL=llama3 117 | ``` 118 | 119 | ### Method 3: System Environment Variables 120 | 121 | **Windows (PowerShell):** 122 | ```powershell 123 | $env:DEEPSEEK_API_KEY="your_key" 124 | ``` 125 | 126 | **Windows (Command Prompt):** 127 | ```cmd 128 | set DEEPSEEK_API_KEY=your_key 129 | ``` 130 | 131 | **macOS/Linux:** 132 | ```bash 133 | export DEEPSEEK_API_KEY="your_key" 134 | ``` 135 | 136 | ## Provider Priority 137 | 138 | When multiple providers are configured, the system selects in this order: 139 | 140 | 1. Ollama (local) 141 | 2. OpenAI 142 | 3. Claude 143 | 4. Qwen 144 | 5. DeepSeek 145 | 146 | You can manually select a provider in the AI Generator interface. 147 | 148 | ## First-Run Configuration 149 | 150 | When running the startup scripts (`start-local.sh` or `start-local.bat`) without an existing `.env` file, the script offers interactive configuration: 151 | 152 | - Choose whether to enable AI features 153 | - Select and configure each provider 154 | - Defaults are provided for quick setup 155 | 156 | Non-interactive mode (CI/automation): 157 | ```bash 158 | # Use --yes flag or environment variable 159 | start-local.bat --yes 160 | # or 161 | set START_LOCAL_NONINTERACTIVE=1 162 | ``` 163 | 164 | ## Using AI Generator 165 | 166 | 1. Navigate to the AI Generator page 167 | 2. Enter your problem request, for example: 168 | - "Generate a medium difficulty array manipulation problem" 169 | - "我想做一道动态规划题目" 170 | - "Create a binary search problem with edge cases" 171 | 3. Click "Generate Problem" 172 | 4. The generated problem is automatically added to your library 173 | 174 | ### Features 175 | 176 | - **Bilingual Support**: English and Chinese requests 177 | - **Multi-language Templates**: JavaScript, Python, Java, C++, C 178 | - **Complete Solutions**: Each problem includes reference solutions 179 | - **Comprehensive Test Cases**: Including edge cases 180 | - **Difficulty Control**: Specify in your request 181 | - **Algorithm Targeting**: Request specific algorithm types 182 | 183 | ## Security Notes 184 | 185 | - Keep API keys secure and never commit them to version control 186 | - `.env.local` is automatically ignored by Git 187 | - API calls are made server-side to protect keys 188 | - Sensitive variables are never exposed to the frontend 189 | 190 | ## Troubleshooting 191 | 192 | ### API Key Not Found 193 | 194 | - Verify the environment variable is set correctly 195 | - Restart the development server after changes 196 | - Check that `.env.local` is in the project root 197 | 198 | ### Ollama Connection Error 199 | 200 | - Ensure Ollama is running: `ollama serve` 201 | - Verify the endpoint is correct 202 | - Check that the model is downloaded: `ollama list` 203 | - Pull the model if needed: `ollama pull llama3` 204 | 205 | ### API Rate Limits 206 | 207 | - Verify your API key is valid and active 208 | - Check your account for usage limits 209 | - Ensure sufficient credits/quota 210 | 211 | ### Generation Errors 212 | 213 | - The AI generates problems in a specific JSON format 214 | - If parsing fails, try rephrasing your request 215 | - Be more specific about problem requirements 216 | 217 | ## Example Requests 218 | 219 | **Dynamic Programming (Chinese):** 220 | ``` 221 | 我想做一道中等难度的动态规划题目,关于最优子结构 222 | ``` 223 | 224 | **Array Manipulation (English):** 225 | ``` 226 | Generate a medium difficulty array manipulation problem using two pointers technique 227 | ``` 228 | 229 | **String Processing (Mixed):** 230 | ``` 231 | 创建一个关于字符串处理的题目,使用sliding window算法 232 | ``` 233 | 234 | ## API Reference 235 | 236 | **Endpoint:** `/api/generate-problem` 237 | 238 | **Method:** POST 239 | 240 | **Body:** 241 | ```json 242 | { 243 | "request": "your problem description" 244 | } 245 | ``` 246 | 247 | **Response:** Generated problem data or error message 248 | -------------------------------------------------------------------------------- /electron-builder.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Electron Builder Configuration 3 | * 支持 Windows、macOS 和 Linux 的跨平台构建 4 | */ 5 | module.exports = { 6 | // 应用标识 7 | appId: 'com.algorithm.practice', 8 | productName: 'Algorithm Practice', 9 | 10 | // 应用元数据 11 | copyright: 'Copyright © 2024 zxypro1', 12 | 13 | // 构建目录 14 | directories: { 15 | output: 'dist', 16 | buildResources: 'build' 17 | }, 18 | 19 | // 打包文件 20 | files: [ 21 | 'electron-main.js', 22 | 'electron-preload.js', 23 | 'next.config.js', 24 | 'package.json', 25 | 'pages/**/*', 26 | 'src/**/*', 27 | 'styles/**/*', 28 | '.next/**/*', 29 | 'public/**/*', 30 | 'problems/**/*', 31 | 'locales/**/*', 32 | 'node_modules/**/*', 33 | // 排除不必要的文件以减小体积 34 | '!node_modules/**/test/**', 35 | '!node_modules/**/tests/**', 36 | '!node_modules/**/__tests__/**', 37 | '!node_modules/**/testing/**', 38 | '!node_modules/**/*.md', 39 | '!node_modules/**/*.markdown', 40 | '!node_modules/**/LICENSE*', 41 | '!node_modules/**/license*', 42 | '!node_modules/**/CHANGELOG*', 43 | '!node_modules/**/changelog*', 44 | '!node_modules/**/HISTORY*', 45 | '!node_modules/**/history*', 46 | '!node_modules/**/.github/**', 47 | '!node_modules/**/.vscode/**', 48 | '!node_modules/**/*.map', 49 | '!node_modules/**/*.ts', 50 | '!node_modules/**/*.tsx', 51 | '!node_modules/**/tsconfig.json', 52 | '!node_modules/**/.eslintrc*', 53 | '!node_modules/**/.prettierrc*', 54 | '!node_modules/**/example/**', 55 | '!node_modules/**/examples/**', 56 | '!node_modules/**/docs/**', 57 | '!node_modules/**/doc/**', 58 | '!node_modules/**/*.d.ts.map', 59 | '!node_modules/**/Makefile', 60 | '!node_modules/**/Gruntfile.js', 61 | '!node_modules/**/gulpfile.js', 62 | '!node_modules/**/.travis.yml', 63 | '!node_modules/**/.npmignore', 64 | '!node_modules/**/.editorconfig', 65 | // 排除 devDependencies 相关 66 | '!node_modules/electron/**', 67 | '!node_modules/electron-builder/**', 68 | '!node_modules/jest/**', 69 | '!node_modules/@types/**', 70 | '!node_modules/typescript/**', 71 | '!node_modules/sharp/**', 72 | '!node_modules/png-to-ico/**' 73 | ], 74 | 75 | // 额外资源 76 | extraResources: [ 77 | { 78 | from: 'problems', 79 | to: 'problems', 80 | filter: ['**/*'] 81 | } 82 | ], 83 | 84 | // ASAR 配置 - 启用 asar 大幅加速安装 85 | // 注意:如果遇到 React Context/useContext 为空的问题,可以尝试设置 asar: false 86 | asar: true, 87 | asarUnpack: [ 88 | // 需要直接文件访问的资源 89 | 'public/problems.json', 90 | 'public/icon.png', 91 | 'public/favicon.ico' 92 | ], 93 | 94 | // ==================== Windows 配置 ==================== 95 | win: { 96 | target: [ 97 | { 98 | target: 'nsis', 99 | arch: ['x64'] 100 | }, 101 | { 102 | target: 'portable', 103 | arch: ['x64'] 104 | } 105 | ], 106 | icon: 'build/icon.ico', 107 | artifactName: '${productName}-${version}-Windows-${arch}.${ext}', 108 | fileAssociations: [ 109 | { 110 | ext: 'algo', 111 | name: 'Algorithm Problem', 112 | description: 'Algorithm Practice Problem File', 113 | role: 'Editor' 114 | } 115 | ] 116 | }, 117 | 118 | // NSIS 安装程序配置 - 优化安装速度 119 | nsis: { 120 | oneClick: true, // 一键安装,最快 121 | perMachine: false, // 仅当前用户,无需管理员权限 122 | allowToChangeInstallationDirectory: false, // 禁用目录选择,加速安装 123 | createDesktopShortcut: true, 124 | createStartMenuShortcut: true, 125 | shortcutName: 'Algorithm Practice', 126 | deleteAppDataOnUninstall: false, 127 | artifactName: '${productName}-${version}-Windows-Setup.${ext}' 128 | }, 129 | 130 | // 便携版配置 131 | portable: { 132 | artifactName: '${productName}-${version}-Windows-Portable.${ext}' 133 | }, 134 | 135 | // ==================== macOS 配置 ==================== 136 | mac: { 137 | target: ['dmg', 'zip'], 138 | icon: 'public/icon.png', 139 | category: 'public.app-category.developer-tools', 140 | artifactName: '${productName}-${version}-macOS-${arch}.${ext}', 141 | hardenedRuntime: true, 142 | gatekeeperAssess: false, 143 | entitlements: 'build/entitlements.mac.plist', 144 | entitlementsInherit: 'build/entitlements.mac.plist', 145 | bundleVersion: '1', 146 | bundleShortVersion: '0.0.9', 147 | fileAssociations: [ 148 | { 149 | ext: 'algo', 150 | name: 'Algorithm Problem', 151 | role: 'Editor' 152 | } 153 | ], 154 | darkModeSupport: true, 155 | notarize: process.env.APPLE_ID && process.env.APPLE_APP_SPECIFIC_PASSWORD ? { 156 | teamId: process.env.APPLE_TEAM_ID 157 | } : false 158 | }, 159 | 160 | // DMG 配置 161 | dmg: { 162 | contents: [ 163 | { 164 | x: 130, 165 | y: 220 166 | }, 167 | { 168 | x: 410, 169 | y: 220, 170 | type: 'link', 171 | path: '/Applications' 172 | } 173 | ], 174 | window: { 175 | width: 540, 176 | height: 380 177 | }, 178 | backgroundColor: '#1a1a2e', 179 | title: 'AlgorithmPractice', 180 | artifactName: '${productName}-${version}-macOS-${arch}.${ext}' 181 | }, 182 | 183 | // ==================== Linux 配置 ==================== 184 | linux: { 185 | target: [ 186 | { 187 | target: 'AppImage', 188 | arch: ['x64'] 189 | }, 190 | { 191 | target: 'deb', 192 | arch: ['x64'] 193 | }, 194 | { 195 | target: 'rpm', 196 | arch: ['x64'] 197 | } 198 | ], 199 | icon: 'public/icon.png', 200 | category: 'Development', 201 | artifactName: '${productName}-${version}-Linux-${arch}.${ext}', 202 | maintainer: 'zxypro1 ', 203 | vendor: 'Algorithm Practice', 204 | synopsis: '离线算法练习应用', 205 | description: '基于 WASM 的离线算法练习应用,支持 JavaScript、TypeScript 和 Python', 206 | desktop: { 207 | Name: 'Algorithm Practice', 208 | Comment: '离线算法练习', 209 | Categories: 'Development;IDE;', 210 | Keywords: 'algorithm;code;practice;programming;' 211 | }, 212 | fileAssociations: [ 213 | { 214 | ext: 'algo', 215 | name: 'Algorithm Problem', 216 | mimeType: 'application/x-algorithm-problem' 217 | } 218 | ] 219 | }, 220 | 221 | // AppImage 配置 222 | appImage: { 223 | artifactName: '${productName}-${version}-Linux.${ext}', 224 | category: 'Development' 225 | }, 226 | 227 | // Deb 包配置 228 | deb: { 229 | depends: ['libnotify4', 'libxtst6', 'libnss3'], 230 | artifactName: '${productName}-${version}-Linux.${ext}' 231 | }, 232 | 233 | // RPM 包配置 234 | rpm: { 235 | depends: ['libnotify', 'libXtst', 'nss'], 236 | artifactName: '${productName}-${version}-Linux.${ext}' 237 | }, 238 | 239 | // 发布配置 240 | publish: null 241 | }; 242 | -------------------------------------------------------------------------------- /MODIFY-PROBLEMS-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Problem Database Modification Guide 2 | 3 | This guide explains how to add or modify problems in the application without rebuilding or requiring internet access. 4 | 5 | ## Overview 6 | 7 | The application supports modifying the problem database in offline environments such as flights, remote locations, or secure networks. All problem data is stored in a single JSON file that can be edited directly. 8 | 9 | ## Problem File Location 10 | 11 | After building or installing the application, the problem database is located at: 12 | 13 | ``` 14 | your-app-folder/public/problems.json 15 | ``` 16 | 17 | **Important**: Always edit the file in the `public` folder, not the original `problems` folder, as the application reads from the public location at runtime. 18 | 19 | ## How It Works 20 | 21 | 1. **No Rebuild Required**: Changes to `problems.json` take effect immediately 22 | 2. **Runtime Loading**: The application reads the file on each request 23 | 3. **Offline Friendly**: Works completely without internet connection 24 | 4. **Real-time Updates**: Refresh your browser to see changes 25 | 26 | ## Adding a New Problem 27 | 28 | ### Step 1: Open the Problems File 29 | 30 | Navigate to your application folder and open: 31 | ``` 32 | public/problems.json 33 | ``` 34 | 35 | ### Step 2: Add Your Problem 36 | 37 | Copy this template and add it to the problems array: 38 | 39 | ```json 40 | { 41 | "id": "your-problem-id", 42 | "title": { 43 | "en": "Your Problem Title", 44 | "zh": "你的问题标题" 45 | }, 46 | "difficulty": "Easy", 47 | "tags": ["array", "hash-table"], 48 | "description": { 49 | "en": "Your problem description in English...", 50 | "zh": "你的问题描述中文版..." 51 | }, 52 | "examples": [ 53 | { 54 | "input": "nums = [1,2,3], target = 4", 55 | "output": "1" 56 | } 57 | ], 58 | "template": { 59 | "js": "function yourFunction(nums, target) {\n // Write your code here\n return -1;\n}\nmodule.exports = yourFunction;" 60 | }, 61 | "solution": { 62 | "js": "function yourFunction(nums, target) {\n // Reference solution\n return nums.indexOf(target);\n}\nmodule.exports = yourFunction;" 63 | }, 64 | "tests": [ 65 | { "input": "[1,2,3],2", "output": "1" }, 66 | { "input": "[4,5,6],7", "output": "-1" } 67 | ] 68 | } 69 | ``` 70 | 71 | ### Step 3: Save and Test 72 | 73 | 1. Save the file 74 | 2. Refresh your browser (or restart the desktop app) 75 | 3. Your new problem should appear immediately 76 | 77 | ## Field Reference 78 | 79 | ### Required Fields 80 | 81 | | Field | Description | 82 | |-------|-------------| 83 | | `id` | Unique identifier (lowercase with hyphens) | 84 | | `title` | Problem title in English and Chinese | 85 | | `difficulty` | "Easy", "Medium", or "Hard" | 86 | | `description` | Problem description in both languages | 87 | | `template` | Starting code template for users | 88 | | `tests` | Array of test cases for validation | 89 | 90 | ### Optional Fields 91 | 92 | | Field | Description | 93 | |-------|-------------| 94 | | `tags` | Array of tags like ["array", "hash-table"] | 95 | | `examples` | Sample input/output for clarification | 96 | | `solution` | Reference solution (hidden by default) | 97 | 98 | ## Testing Your Changes 99 | 100 | ### Quick Test 101 | 102 | 1. Add a problem using the template above 103 | 2. Visit the homepage to see it in the list 104 | 3. Click on it to test the code editor 105 | 4. Submit some code to verify the tests work 106 | 107 | ### Automated Testing 108 | 109 | ```bash 110 | npm run test:dynamic # Add a test problem 111 | # Visit the app to verify it appears 112 | npm run test:cleanup # Remove the test problem 113 | ``` 114 | 115 | ## Best Practices 116 | 117 | ### Problem ID Guidelines 118 | 119 | - Use lowercase letters and hyphens: `two-sum`, `binary-search` 120 | - Keep it descriptive but concise 121 | - Ensure uniqueness across all problems 122 | 123 | ### Test Case Guidelines 124 | 125 | - Include edge cases (empty arrays, single elements) 126 | - Test both positive and negative scenarios 127 | - Keep input/output format consistent 128 | 129 | ### Code Template Tips 130 | 131 | - Provide a meaningful function signature 132 | - Include helpful comments 133 | - Always end with `module.exports = yourFunction;` 134 | 135 | ## Example: Reverse String Problem 136 | 137 | ```json 138 | { 139 | "id": "reverse-string", 140 | "title": { 141 | "en": "Reverse String", 142 | "zh": "反转字符串" 143 | }, 144 | "difficulty": "Easy", 145 | "tags": ["string", "two-pointers"], 146 | "description": { 147 | "en": "Write a function that reverses a string. The input string is given as an array of characters.", 148 | "zh": "编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组的形式给出。" 149 | }, 150 | "examples": [ 151 | { 152 | "input": "s = ['h','e','l','l','o']", 153 | "output": "['o','l','l','e','h']" 154 | } 155 | ], 156 | "template": { 157 | "js": "function reverseString(s) {\n // Write your code here\n // Modify s in-place\n}\nmodule.exports = reverseString;" 158 | }, 159 | "solution": { 160 | "js": "function reverseString(s) {\n let left = 0, right = s.length - 1;\n while (left < right) {\n [s[left], s[right]] = [s[right], s[left]];\n left++;\n right--;\n }\n}\nmodule.exports = reverseString;" 161 | }, 162 | "tests": [ 163 | { "input": "[\"h\",\"e\",\"l\",\"l\",\"o\"]", "output": "[\"o\",\"l\",\"l\",\"e\",\"h\"]" }, 164 | { "input": "[\"H\",\"a\",\"n\",\"n\",\"a\",\"h\"]", "output": "[\"h\",\"a\",\"n\",\"n\",\"a\",\"H\"]" } 165 | ] 166 | } 167 | ``` 168 | 169 | ## Common Issues 170 | 171 | ### JSON Syntax Errors 172 | 173 | - Always validate JSON syntax before saving 174 | - Watch out for trailing commas 175 | - Ensure proper quote escaping in strings 176 | 177 | ### Test Format Issues 178 | 179 | - Input parameters must be JSON-parseable 180 | - Multiple parameters: use comma separation like `"[1,2,3],5"` 181 | - String inputs: use proper JSON string format `"\"hello\""` 182 | 183 | ### Function Export Issues 184 | 185 | - Always include `module.exports = yourFunction;` 186 | - Function name must match the one being exported 187 | - Ensure function signature matches test expectations 188 | 189 | ## Troubleshooting 190 | 191 | ### Problem Not Appearing 192 | 193 | 1. Check JSON syntax validity 194 | 2. Ensure the file is saved in `public/problems.json` 195 | 3. Refresh the browser page 196 | 4. Check browser console for errors 197 | 198 | ### Tests Failing 199 | 200 | 1. Verify input/output formats match 201 | 2. Check function signature 202 | 3. Ensure `module.exports` is correct 203 | 4. Test function logic independently 204 | 205 | ### Performance Issues 206 | 207 | - Large problem sets (100+ problems) may load slower 208 | - Consider splitting into categories if needed 209 | - Each problem adds ~1-2KB to the JSON file 210 | 211 | ## Advanced Tips 212 | 213 | ### Organizing Problems 214 | 215 | Group related problems using consistent naming: 216 | - `array-easy-1`, `array-easy-2` 217 | - `dp-medium-1`, `dp-medium-2` 218 | 219 | ### Multi-language Support 220 | 221 | Always provide both English and Chinese translations for: 222 | - Title 223 | - Description 224 | - Consider adding comments in both languages 225 | 226 | ### Custom Tags 227 | 228 | Create your own tag system: 229 | - `custom-algorithm` 230 | - `interview-prep` 231 | - `company-specific` 232 | 233 | --- 234 | 235 | **Practice algorithms offline, anywhere.** 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Offline Algorithm Practice 2 | 3 | [中文](./README-zh.md) 4 | 5 | Quick links: [Discussions](https://github.com/zxypro1/OfflineLeetPractice/discussions) | [Issues](https://github.com/zxypro1/OfflineLeetPractice/issues) | [Pull Requests](https://github.com/zxypro1/OfflineLeetPractice/pulls) 6 | 7 | > A standalone algorithm coding practice application that works 100% offline. No local development environment required — just download, install, and start practicing. Supports JavaScript, TypeScript, and Python with WASM-based browser-side code execution. 8 | 9 | 2025-08-24165250 10 | 11 | 2025-08-24165302 12 | 13 | ## Quick Start 14 | 15 | ### Desktop Application (Recommended) 16 | 17 | The desktop application provides the best experience with zero environment setup required. Simply download and run. 18 | 19 | **[Download Latest Release](https://github.com/zxypro1/OfflineCodePractice/releases/latest)** 20 | 21 | | Platform | Download | 22 | |----------|----------| 23 | | **macOS** (Apple Silicon) | `Algorithm-Practice-*-macOS-arm64.dmg` | 24 | | **macOS** (Intel) | `Algorithm-Practice-*-macOS-x64.dmg` | 25 | | **Windows** (Installer) | `Algorithm-Practice-*-Windows-Setup.exe` | 26 | | **Windows** (Portable) | `Algorithm-Practice-*-Windows-Portable.exe` | 27 | | **Linux** (AppImage) | `Algorithm-Practice-*-Linux.AppImage` | 28 | | **Linux** (Debian/Ubuntu) | `Algorithm-Practice-*-Linux.deb` | 29 | | **Linux** (Fedora/RHEL) | `Algorithm-Practice-*-Linux.rpm` | 30 | 31 | **macOS Users**: If you encounter "App is damaged and can't be opened", run in Terminal: 32 | ```bash 33 | xattr -cr "/Applications/Algorithm Practice.app" 34 | ``` 35 | 36 | ### Web Version (Alternative) 37 | 38 | For developers who prefer running from source, see [Development Setup](#development-setup) below. 39 | 40 | ## Features 41 | 42 | ### Core Functionality 43 | 44 | - **Standalone Application**: No Node.js, Python, or any development environment required 45 | - **Complete Offline Support**: Works without internet after installation 46 | - **Built-in Problem Library**: 10+ classic algorithm problems included 47 | - **AI Problem Generator**: Generate custom problems using various AI providers 48 | - **WASM Code Execution**: Browser-side execution for JavaScript, TypeScript, and Python 49 | - **Monaco Code Editor**: VS Code-like editing experience with syntax highlighting and autocomplete 50 | - **Instant Testing**: Run tests immediately with detailed results and execution time tracking 51 | - **Cross-platform**: Windows, macOS, and Linux supported 52 | 53 | ### Supported Languages 54 | 55 | | Language | Status | Implementation | 56 | |----------|--------|----------------| 57 | | **JavaScript** | Supported | Native browser execution | 58 | | **TypeScript** | Supported | TypeScript compiler transpilation | 59 | | **Python** | Supported | Pyodide (CPython WASM) | 60 | 61 | All code execution happens in the browser using WebAssembly. No server-side execution required. 62 | 63 | ### AI-Powered Problem Generation 64 | 65 | - **Custom Problem Creation**: Describe what you want to practice in natural language 66 | - **Complete Solutions**: Each problem includes working reference solutions 67 | - **Comprehensive Testing**: Auto-generated test cases including edge cases 68 | - **Instant Integration**: Problems automatically added to your local library 69 | 70 | ## How to Use 71 | 72 | ### Problem Solving 73 | 74 | 1. **Browse Problems**: View the problem list with difficulty and tags 75 | 2. **Select a Problem**: Click on any problem to open the detail page 76 | 3. **Choose Language**: Select JavaScript, TypeScript, or Python 77 | 4. **Write Solution**: Use the Monaco editor with full IDE features 78 | 5. **Run Tests**: Click "Submit & Run Tests" to execute your code 79 | 6. **View Results**: See detailed test results with execution time 80 | 81 | ### AI Problem Generation 82 | 83 | 1. **Access Generator**: Click "AI Generator" on the homepage 84 | 2. **Describe Requirements**: Enter what type of problem you want 85 | 3. **Generate**: AI creates a complete problem with test cases and solutions 86 | 4. **Practice**: Generated problem is automatically available in your library 87 | 88 | ### Settings Configuration 89 | 90 | Access the settings page to configure AI providers: 91 | 92 | - **Desktop Mode**: Via "Settings" button or application menu 93 | - **Web Mode**: Navigate to `/settings` (e.g., http://localhost:3000/settings) 94 | 95 | Supported AI providers: 96 | - DeepSeek 97 | - OpenAI 98 | - Qwen (Alibaba Cloud) 99 | - Claude (Anthropic) 100 | - Ollama (Local) 101 | 102 | Configuration is saved to `~/.offline-leet-practice/config.json` in desktop mode. See [AI_PROVIDER_GUIDE.md](./AI_PROVIDER_GUIDE.md) for detailed configuration. 103 | 104 | ### Adding Custom Problems 105 | 106 | 1. **Via UI**: Use the "Add Problem" page in the application 107 | 2. **JSON Import**: Upload or paste problem data in JSON format 108 | 3. **Direct Edit**: Modify `public/problems.json` for immediate changes 109 | 110 | See [MODIFY-PROBLEMS-GUIDE.md](./MODIFY-PROBLEMS-GUIDE.md) for the complete guide. 111 | 112 | ## Development Setup 113 | 114 | For developers who want to run from source or contribute to the project. 115 | 116 | ### Prerequisites 117 | 118 | - Node.js 18+ ([Download](https://nodejs.org/)) 119 | - npm 8+ 120 | 121 | ### Running Locally 122 | 123 | **Windows:** 124 | ```bash 125 | start-local.bat 126 | ``` 127 | 128 | **macOS / Linux:** 129 | ```bash 130 | chmod +x start-local.sh 131 | ./start-local.sh 132 | ``` 133 | 134 | **Manual Setup:** 135 | ```bash 136 | git clone https://github.com/zxypro1/OfflineLeetPractice.git 137 | cd OfflineLeetPractice 138 | npm install 139 | npm run build 140 | npm start 141 | ``` 142 | 143 | Then open http://localhost:3000 in your browser. 144 | 145 | ### Building Desktop Application 146 | 147 | ```bash 148 | # macOS 149 | npm run dist:mac 150 | 151 | # Windows 152 | npm run dist:win 153 | 154 | # Linux 155 | npm run dist:linux 156 | 157 | # All platforms 158 | npm run dist:all 159 | ``` 160 | 161 | See [DESKTOP-APP-GUIDE.md](./DESKTOP-APP-GUIDE.md) for detailed build instructions. 162 | 163 | ## Technology Stack 164 | 165 | - **Frontend**: React 18, Next.js 13, TypeScript 166 | - **UI Framework**: Mantine v7 167 | - **Code Editor**: Monaco Editor 168 | - **Code Execution**: WebAssembly 169 | - JavaScript: Native browser `Function` constructor 170 | - TypeScript: TypeScript compiler (CDN) 171 | - Python: Pyodide (CPython compiled to WASM) 172 | - **Desktop**: Electron 173 | 174 | ## Project Structure 175 | 176 | ``` 177 | OfflineLeetPractice/ 178 | ├── pages/ # Next.js pages and API routes 179 | │ ├── api/ 180 | │ │ ├── problems.ts # Problem data API 181 | │ │ ├── generate-problem.ts 182 | │ │ └── add-problem.ts 183 | │ ├── problems/[id].tsx # Problem detail page 184 | │ ├── generator.tsx # AI Generator page 185 | │ └── index.tsx # Homepage 186 | ├── src/ 187 | │ ├── components/ # React components 188 | │ └── hooks/ 189 | │ └── useWasmExecutor.ts 190 | ├── public/ 191 | │ └── problems.json # Problem database 192 | ├── electron-main.js # Electron main process 193 | └── electron-builder.config.js 194 | ``` 195 | 196 | ## Contributing 197 | 198 | Contributions are welcome. Areas for improvement: 199 | 200 | - Additional algorithm problems 201 | - Performance analytics features 202 | - User experience enhancements 203 | - Documentation improvements 204 | 205 | ## License 206 | 207 | MIT License 208 | 209 | --- 210 | 211 | **Practice algorithms anywhere — on flights, cruises, or any offline environment.** 212 | -------------------------------------------------------------------------------- /start-local.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | SETLOCAL 3 | 4 | :: ====================================================== 5 | :: Offline LeetCode Practice System - Local Startup Script (Windows Version) 6 | :: ====================================================== 7 | 8 | ECHO Offline LeetCode Practice System - Local Startup 9 | ECHO =================================================== 10 | ECHO. 11 | 12 | :: Support a non-interactive mode for automation/CI 13 | :: Usage: start-local.bat --yes OR set START_LOCAL_NONINTERACTIVE=1 & start-local.bat 14 | SET "NONINTERACTIVE=0" 15 | IF /I "%~1" == "--yes" SET "NONINTERACTIVE=1" 16 | IF /I "%~1" == "-y" SET "NONINTERACTIVE=1" 17 | IF DEFINED START_LOCAL_NONINTERACTIVE IF "%START_LOCAL_NONINTERACTIVE%" == "1" SET "NONINTERACTIVE=1" 18 | 19 | :: First-run check: interactive AI configuration for .env if it doesn't exist 20 | IF EXIST ".env" ( 21 | ECHO .env file exists -- skipping AI configuration step. If you want to change AI settings, edit the .env file. 22 | ) ELSE ( 23 | IF "%NONINTERACTIVE%" == "1" ( 24 | GOTO :setup_env_noninteractive 25 | ) ELSE ( 26 | GOTO :setup_env_interactive 27 | ) 28 | ) 29 | 30 | GOTO :checks 31 | 32 | :setup_env_noninteractive 33 | ECHO No .env file found -- non-interactive mode enabled. 34 | IF EXIST ".env.example" ( 35 | COPY ".env.example" ".env" > NUL 36 | ECHO .env created from .env.example (non-interactive). Edit .env to change settings. 37 | ) ELSE ( 38 | ECHO No .env.example found -- creating minimal .env with defaults and empty keys. 39 | ( 40 | ECHO # Generated by start-local.bat (non-interactive) 41 | ECHO # AI provider configuration 42 | ECHO OPENAI_MODEL=gpt-4-turbo 43 | ECHO OPENAI_API_KEY= 44 | ECHO DEEPSEEK_MODEL=deepseek-chat 45 | ECHO DEEPSEEK_API_KEY= 46 | ECHO QWEN_MODEL=qwen-turbo 47 | ECHO QWEN_API_KEY= 48 | ECHO CLAUDE_MODEL=claude-3-haiku-20240307 49 | ECHO CLAUDE_API_KEY= 50 | ECHO OLLAMA_ENDPOINT=http://localhost:11434 51 | ECHO OLLAMA_MODEL=llama3 52 | ECHO # Note: To change AI configuration later, edit the .env file. 53 | ) > .env 54 | ) 55 | GOTO :checks 56 | 57 | :setup_env_interactive 58 | ECHO No .env file found -- first time startup. 59 | SET "enable_ai=" 60 | SET /P enable_ai="Would you like to enable AI features? [Y/n]: " 61 | IF /I NOT "%enable_ai%" == "n" ( 62 | GOTO :configure_ai 63 | ) ELSE ( 64 | ECHO AI features will be disabled. Creating empty .env file. 65 | ( 66 | ECHO # Generated by start-local.bat - AI disabled 67 | ECHO # To enable AI later, edit this .env and add API keys and models. 68 | ) > .env 69 | GOTO :checks 70 | ) 71 | 72 | :configure_ai 73 | ECHO Creating .env and configuring AI providers... 74 | (ECHO # Generated by start-local.bat) > .env 75 | (ECHO # AI provider configuration) >> .env 76 | 77 | :: OpenAI 78 | SET "use_openai=" 79 | SET /P use_openai="Enable OpenAI? [Y/n]: " 80 | IF /I NOT "%use_openai%" == "n" ( 81 | SET "openai_model=gpt-4-turbo" 82 | SET /P "openai_model=OpenAI model (default: gpt-4-turbo): " 83 | SET "openai_key=" 84 | SET /P "openai_key=OpenAI API key (paste and press Enter): " 85 | (ECHO OPENAI_MODEL=%openai_model%) >> .env 86 | (ECHO OPENAI_API_KEY=%openai_key%) >> .env 87 | ) ELSE ( 88 | (ECHO OPENAI_API_KEY=) >> .env 89 | ) 90 | 91 | :: DeepSeek 92 | SET "use_deepseek=" 93 | SET /P use_deepseek="Enable DeepSeek? [y/N]: " 94 | IF /I "%use_deepseek%" == "y" ( 95 | SET "deepseek_model=deepseek-chat" 96 | SET /P "deepseek_model=DeepSeek model (default: deepseek-chat): " 97 | SET "deepseek_key=" 98 | SET /P "deepseek_key=DeepSeek API key (paste and press Enter): " 99 | (ECHO DEEPSEEK_MODEL=%deepseek_model%) >> .env 100 | (ECHO DEEPSEEK_API_KEY=%deepseek_key%) >> .env 101 | ) ELSE ( 102 | (ECHO DEEPSEEK_API_KEY=) >> .env 103 | ) 104 | 105 | :: Qwen 106 | SET "use_qwen=" 107 | SET /P use_qwen="Enable Qwen? [y/N]: " 108 | IF /I "%use_qwen%" == "y" ( 109 | SET "qwen_model=qwen-turbo" 110 | SET /P "qwen_model=Qwen model (default: qwen-turbo): " 111 | SET "qwen_key=" 112 | SET /P "qwen_key=Qwen API key (paste and press Enter): " 113 | (ECHO QWEN_MODEL=%qwen_model%) >> .env 114 | (ECHO QWEN_API_KEY=%qwen_key%) >> .env 115 | ) ELSE ( 116 | (ECHO QWEN_API_KEY=) >> .env 117 | ) 118 | 119 | :: Claude 120 | SET "use_claude=" 121 | SET /P use_claude="Enable Claude (Anthropic)? [y/N]: " 122 | IF /I "%use_claude%" == "y" ( 123 | SET "claude_model=claude-3-haiku-20240307" 124 | SET /P "claude_model=Claude model (default: claude-3-haiku-20240307): " 125 | SET "claude_key=" 126 | SET /P "claude_key=Claude API key (paste and press Enter): " 127 | (ECHO CLAUDE_MODEL=%claude_model%) >> .env 128 | (ECHO CLAUDE_API_KEY=%claude_key%) >> .env 129 | ) ELSE ( 130 | (ECHO CLAUDE_API_KEY=) >> .env 131 | ) 132 | 133 | :: Ollama 134 | SET "use_ollama=" 135 | SET /P use_ollama="Enable Ollama? [y/N]: " 136 | IF /I "%use_ollama%" == "y" ( 137 | SET "ollama_endpoint=http://localhost:11434" 138 | SET /P "ollama_endpoint=Ollama endpoint (default: http://localhost:11434): " 139 | SET "ollama_model=llama3" 140 | SET /P "ollama_model=Ollama model (default: llama3): " 141 | (ECHO OLLAMA_ENDPOINT=%ollama_endpoint%) >> .env 142 | (ECHO OLLAMA_MODEL=%ollama_model%) >> .env 143 | ) ELSE ( 144 | (ECHO OLLAMA_ENDPOINT=) >> .env 145 | (ECHO OLLAMA_MODEL=) >> .env 146 | ) 147 | 148 | (ECHO.) >> .env 149 | (ECHO # Note: To change AI configuration later, edit the .env file.) >> .env 150 | ECHO AI configuration complete. .env created. 151 | GOTO :checks 152 | 153 | :checks 154 | ECHO. 155 | :: Check if Node.js is installed 156 | WHERE node >nul 2>nul 157 | IF %ERRORLEVEL% NEQ 0 ( 158 | ECHO Error: Node.js not found 159 | ECHO Please install Node.js: https://nodejs.org 160 | EXIT /B 1 161 | ) 162 | FOR /F "tokens=*" %%v IN ('node --version') DO ECHO Node.js installed: %%v 163 | 164 | :: Check if npm is installed 165 | WHERE npm >nul 2>nul 166 | IF %ERRORLEVEL% NEQ 0 ( 167 | ECHO Error: npm not found 168 | ECHO Please install npm 169 | EXIT /B 1 170 | ) 171 | FOR /F "tokens=*" %%v IN ('npm --version') DO ECHO npm installed: %%v 172 | 173 | :: Check if dependencies are installed 174 | IF NOT EXIST "node_modules\" ( 175 | ECHO Installing dependencies... 176 | npm install 177 | IF %ERRORLEVEL% NEQ 0 ( 178 | ECHO Failed to install dependencies 179 | EXIT /B 1 180 | ) 181 | ECHO Dependencies installed successfully 182 | ) 183 | 184 | :: Check if project is built 185 | IF NOT EXIST ".next\" ( 186 | ECHO Building application... 187 | npm run build 188 | IF %ERRORLEVEL% NEQ 0 ( 189 | ECHO Build failed 190 | EXIT /B 1 191 | ) 192 | ECHO Build completed successfully 193 | ) 194 | 195 | ECHO. 196 | ECHO Starting application... 197 | ECHO URL: http://localhost:3000 198 | ECHO. 199 | ECHO Usage Instructions: 200 | ECHO - Open http://localhost:3000 in your browser 201 | ECHO - Fully local execution, no external network required 202 | ECHO - Press Ctrl+C to stop the server 203 | ECHO - To add problems: Edit public/problems.json (see MODIFY-PROBLEMS-GUIDE.md) 204 | ECHO - To change AI settings: Edit the .env file 205 | ECHO - Changes take effect immediately without rebuilding! 206 | ECHO. 207 | 208 | :: Start the application 209 | npm start 210 | 211 | ENDLOCAL -------------------------------------------------------------------------------- /pages/api/import-from-folder.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | interface Problem { 6 | id: string; 7 | title: { en: string; zh: string }; 8 | difficulty: string; 9 | tags?: string[]; 10 | description: { en: string; zh: string }; 11 | examples?: Array<{ input: string; output: string }>; 12 | template: Record; 13 | tests: Array<{ input: string; output: string }>; 14 | solution?: Record; 15 | solutions?: Array<{ 16 | title: { en: string; zh: string }; 17 | content: { en: string; zh: string }; 18 | }>; 19 | } 20 | 21 | function validateProblem(problem: any): problem is Problem { 22 | if (!problem || typeof problem !== 'object') return false; 23 | if (typeof problem.id !== 'string' || !problem.id) return false; 24 | if (!problem.title || typeof problem.title.en !== 'string') return false; 25 | if (!['Easy', 'Medium', 'Hard'].includes(problem.difficulty)) return false; 26 | if (!problem.description || typeof problem.description.en !== 'string') return false; 27 | if (!problem.template || typeof problem.template !== 'object') return false; 28 | if (!Array.isArray(problem.tests) || problem.tests.length === 0) return false; 29 | if (!/^[a-z0-9-]+$/.test(problem.id)) return false; 30 | return true; 31 | } 32 | 33 | function normalizeProblem(problem: any): Problem { 34 | return { 35 | id: problem.id, 36 | title: { 37 | en: problem.title.en || problem.title.zh || 'Untitled', 38 | zh: problem.title.zh || problem.title.en || '无标题', 39 | }, 40 | difficulty: problem.difficulty, 41 | tags: Array.isArray(problem.tags) ? problem.tags : [], 42 | description: { 43 | en: problem.description.en || problem.description.zh || '', 44 | zh: problem.description.zh || problem.description.en || '', 45 | }, 46 | examples: Array.isArray(problem.examples) ? problem.examples : [], 47 | template: problem.template, 48 | tests: problem.tests, 49 | ...(problem.solution && { solution: problem.solution }), 50 | ...(problem.solutions && { solutions: problem.solutions }), 51 | }; 52 | } 53 | 54 | function findJsonFiles(dir: string): string[] { 55 | const jsonFiles: string[] = []; 56 | 57 | try { 58 | const entries = fs.readdirSync(dir, { withFileTypes: true }); 59 | 60 | for (const entry of entries) { 61 | const fullPath = path.join(dir, entry.name); 62 | 63 | if (entry.isFile() && entry.name.endsWith('.json')) { 64 | jsonFiles.push(fullPath); 65 | } else if (entry.isDirectory()) { 66 | // Recursively search subdirectories 67 | jsonFiles.push(...findJsonFiles(fullPath)); 68 | } 69 | } 70 | } catch (error) { 71 | console.error(`Error reading directory ${dir}:`, error); 72 | } 73 | 74 | return jsonFiles; 75 | } 76 | 77 | function loadProblemsFromJsonFile(filePath: string): { problems: any[]; error?: string } { 78 | try { 79 | const content = fs.readFileSync(filePath, 'utf8'); 80 | const data = JSON.parse(content); 81 | 82 | // Handle both array and single object 83 | if (Array.isArray(data)) { 84 | return { problems: data }; 85 | } else if (data && typeof data === 'object' && data.id) { 86 | return { problems: [data] }; 87 | } else { 88 | return { problems: [], error: 'Invalid JSON structure' }; 89 | } 90 | } catch (error) { 91 | return { problems: [], error: error instanceof Error ? error.message : 'Failed to parse JSON' }; 92 | } 93 | } 94 | 95 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 96 | if (req.method !== 'POST') { 97 | return res.status(405).json({ error: 'Method not allowed' }); 98 | } 99 | 100 | try { 101 | const { folderPath, useProblemsFolder } = req.body; 102 | const appRoot = process.env.APP_ROOT || process.cwd(); 103 | 104 | let targetFolder: string; 105 | 106 | if (useProblemsFolder) { 107 | // Use the default problems folder 108 | targetFolder = path.join(appRoot, 'problems'); 109 | } else if (folderPath && typeof folderPath === 'string') { 110 | // Use user-specified folder 111 | targetFolder = folderPath; 112 | 113 | // Security check: ensure the path exists and is a directory 114 | if (!fs.existsSync(targetFolder)) { 115 | return res.status(400).json({ error: 'Folder does not exist' }); 116 | } 117 | 118 | const stats = fs.statSync(targetFolder); 119 | if (!stats.isDirectory()) { 120 | return res.status(400).json({ error: 'Path is not a directory' }); 121 | } 122 | } else { 123 | return res.status(400).json({ error: 'Either folderPath or useProblemsFolder is required' }); 124 | } 125 | 126 | // Find all JSON files in the folder 127 | const jsonFiles = findJsonFiles(targetFolder); 128 | 129 | if (jsonFiles.length === 0) { 130 | return res.status(200).json({ 131 | success: 0, 132 | failed: 0, 133 | skipped: 0, 134 | total: 0, 135 | message: 'No JSON files found in the folder', 136 | fileResults: [], 137 | }); 138 | } 139 | 140 | // Read current problems 141 | const problemsPath = path.join(appRoot, 'public', 'problems.json'); 142 | let currentProblems: Problem[] = []; 143 | 144 | try { 145 | const problemsData = fs.readFileSync(problemsPath, 'utf8'); 146 | currentProblems = JSON.parse(problemsData); 147 | } catch { 148 | // If file doesn't exist or is invalid, start with empty array 149 | currentProblems = []; 150 | } 151 | 152 | const existingIds = new Set(currentProblems.map(p => p.id)); 153 | 154 | // Process each JSON file 155 | let totalSuccess = 0; 156 | let totalFailed = 0; 157 | let totalSkipped = 0; 158 | const fileResults: Array<{ file: string; success: number; failed: number; skipped: number; error?: string }> = []; 159 | 160 | for (const jsonFile of jsonFiles) { 161 | const relativePath = path.relative(targetFolder, jsonFile); 162 | const { problems, error } = loadProblemsFromJsonFile(jsonFile); 163 | 164 | if (error) { 165 | fileResults.push({ file: relativePath, success: 0, failed: 0, skipped: 0, error }); 166 | continue; 167 | } 168 | 169 | let fileSuccess = 0; 170 | let fileFailed = 0; 171 | let fileSkipped = 0; 172 | 173 | for (const problem of problems) { 174 | if (existingIds.has(problem.id)) { 175 | fileSkipped++; 176 | totalSkipped++; 177 | continue; 178 | } 179 | 180 | if (!validateProblem(problem)) { 181 | fileFailed++; 182 | totalFailed++; 183 | continue; 184 | } 185 | 186 | const normalizedProblem = normalizeProblem(problem); 187 | currentProblems.push(normalizedProblem); 188 | existingIds.add(problem.id); 189 | fileSuccess++; 190 | totalSuccess++; 191 | } 192 | 193 | fileResults.push({ file: relativePath, success: fileSuccess, failed: fileFailed, skipped: fileSkipped }); 194 | } 195 | 196 | // Save updated problems 197 | if (totalSuccess > 0) { 198 | fs.writeFileSync(problemsPath, JSON.stringify(currentProblems, null, 2)); 199 | 200 | // Also sync to problems/problems.json 201 | const sourceProblemsPath = path.join(appRoot, 'problems', 'problems.json'); 202 | try { 203 | fs.writeFileSync(sourceProblemsPath, JSON.stringify(currentProblems, null, 2)); 204 | } catch { 205 | // Ignore if problems folder doesn't exist 206 | } 207 | } 208 | 209 | return res.status(200).json({ 210 | success: totalSuccess, 211 | failed: totalFailed, 212 | skipped: totalSkipped, 213 | total: jsonFiles.length, 214 | fileResults, 215 | }); 216 | } catch (error) { 217 | console.error('Error importing from folder:', error); 218 | return res.status(500).json({ 219 | error: error instanceof Error ? error.message : 'Failed to import from folder' 220 | }); 221 | } 222 | } 223 | 224 | -------------------------------------------------------------------------------- /start-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Offline LeetCode Practice System - Local Startup Script 4 | # ====================================================== 5 | 6 | echo "Offline LeetCode Practice System - Local Startup" 7 | echo "===================================================" 8 | echo "" 9 | 10 | # Support a non-interactive mode for automation/CI 11 | # Usage: ./start-local.sh --yes OR START_LOCAL_NONINTERACTIVE=1 ./start-local.sh 12 | NONINTERACTIVE=0 13 | if [ "$1" = "--yes" ] || [ "$1" = "-y" ] || [ "$START_LOCAL_NONINTERACTIVE" = "1" ]; then 14 | NONINTERACTIVE=1 15 | fi 16 | 17 | # First-run check: interactive AI configuration for .env (or copy from .env.example in non-interactive) 18 | if [ ! -f ".env" ]; then 19 | if [ "$NONINTERACTIVE" -eq 1 ]; then 20 | echo "No .env file found — non-interactive mode enabled." 21 | if [ -f ".env.example" ]; then 22 | cp .env.example .env 23 | echo ".env created from .env.example (non-interactive). Edit .env to change settings." 24 | else 25 | echo "No .env.example found — creating minimal .env with defaults and empty keys." 26 | echo "# Generated by start-local.sh (non-interactive)" > .env 27 | echo "# AI provider configuration" >> .env 28 | echo "OPENAI_MODEL=gpt-4-turbo" >> .env 29 | echo "OPENAI_API_KEY=" >> .env 30 | echo "DEEPSEEK_MODEL=deepseek-chat" >> .env 31 | echo "DEEPSEEK_API_KEY=" >> .env 32 | echo "QWEN_MODEL=qwen-turbo" >> .env 33 | echo "QWEN_API_KEY=" >> .env 34 | echo "CLAUDE_MODEL=claude-3-haiku-20240307" >> .env 35 | echo "CLAUDE_API_KEY=" >> .env 36 | echo "OLLAMA_ENDPOINT=http://localhost:11434" >> .env 37 | echo "OLLAMA_MODEL=llama3" >> .env 38 | echo "# Note: To change AI configuration later, edit the .env file." >> .env 39 | fi 40 | else 41 | echo "No .env file found — first time startup." 42 | read -p "Would you like to enable AI features? [Y/n]: " enable_ai 43 | enable_ai=${enable_ai:-Y} 44 | enable_ai=$(echo "$enable_ai" | tr '[:upper:]' '[:lower:]') 45 | 46 | if [ "$enable_ai" = "y" ] || [ "$enable_ai" = "yes" ] || [ "$enable_ai" = "" ]; then 47 | echo "Creating .env and configuring AI providers..." 48 | echo "# Generated by start-local.sh" > .env 49 | echo "# AI provider configuration" >> .env 50 | 51 | # OpenAI 52 | read -p "Enable OpenAI? [Y/n]: " use_openai 53 | use_openai=${use_openai:-Y} 54 | use_openai=$(echo "$use_openai" | tr '[:upper:]' '[:lower:]') 55 | if [ "$use_openai" = "y" ] || [ "$use_openai" = "yes" ] || [ "$use_openai" = "" ]; then 56 | read -p "OpenAI model (default: gpt-4-turbo): " openai_model 57 | openai_model=${openai_model:-gpt-4-turbo} 58 | read -p "OpenAI API key (paste and press Enter): " openai_key 59 | echo "OPENAI_MODEL=$openai_model" >> .env 60 | echo "OPENAI_API_KEY=$openai_key" >> .env 61 | else 62 | echo "OPENAI_API_KEY=" >> .env 63 | fi 64 | 65 | # DeepSeek 66 | read -p "Enable DeepSeek? [y/N]: " use_deepseek 67 | use_deepseek=${use_deepseek:-N} 68 | use_deepseek=$(echo "$use_deepseek" | tr '[:upper:]' '[:lower:]') 69 | if [ "$use_deepseek" = "y" ] || [ "$use_deepseek" = "yes" ]; then 70 | read -p "DeepSeek model (default: deepseek-chat): " deepseek_model 71 | deepseek_model=${deepseek_model:-deepseek-chat} 72 | read -p "DeepSeek API key (paste and press Enter): " deepseek_key 73 | echo "DEEPSEEK_MODEL=$deepseek_model" >> .env 74 | echo "DEEPSEEK_API_KEY=$deepseek_key" >> .env 75 | else 76 | echo "DEEPSEEK_API_KEY=" >> .env 77 | fi 78 | 79 | # Qwen 80 | read -p "Enable Qwen? [y/N]: " use_qwen 81 | use_qwen=${use_qwen:-N} 82 | use_qwen=$(echo "$use_qwen" | tr '[:upper:]' '[:lower:]') 83 | if [ "$use_qwen" = "y" ] || [ "$use_qwen" = "yes" ]; then 84 | read -p "Qwen model (default: qwen-turbo): " qwen_model 85 | qwen_model=${qwen_model:-qwen-turbo} 86 | read -p "Qwen API key (paste and press Enter): " qwen_key 87 | echo "QWEN_MODEL=$qwen_model" >> .env 88 | echo "QWEN_API_KEY=$qwen_key" >> .env 89 | else 90 | echo "QWEN_API_KEY=" >> .env 91 | fi 92 | 93 | # Claude 94 | read -p "Enable Claude (Anthropic)? [y/N]: " use_claude 95 | use_claude=${use_claude:-N} 96 | use_claude=$(echo "$use_claude" | tr '[:upper:]' '[:lower:]') 97 | if [ "$use_claude" = "y" ] || [ "$use_claude" = "yes" ]; then 98 | read -p "Claude model (default: claude-3-haiku-20240307): " claude_model 99 | claude_model=${claude_model:-claude-3-haiku-20240307} 100 | read -p "Claude API key (paste and press Enter): " claude_key 101 | echo "CLAUDE_MODEL=$claude_model" >> .env 102 | echo "CLAUDE_API_KEY=$claude_key" >> .env 103 | else 104 | echo "CLAUDE_API_KEY=" >> .env 105 | fi 106 | 107 | # Ollama (endpoint + model) 108 | read -p "Enable Ollama? [y/N]: " use_ollama 109 | use_ollama=${use_ollama:-N} 110 | use_ollama=$(echo "$use_ollama" | tr '[:upper:]' '[:lower:]') 111 | if [ "$use_ollama" = "y" ] || [ "$use_ollama" = "yes" ]; then 112 | read -p "Ollama endpoint (default: http://localhost:11434): " ollama_endpoint 113 | ollama_endpoint=${ollama_endpoint:-http://localhost:11434} 114 | read -p "Ollama model (default: llama3): " ollama_model 115 | ollama_model=${ollama_model:-llama3} 116 | echo "OLLAMA_ENDPOINT=$ollama_endpoint" >> .env 117 | echo "OLLAMA_MODEL=$ollama_model" >> .env 118 | else 119 | echo "OLLAMA_ENDPOINT=" >> .env 120 | echo "OLLAMA_MODEL=" >> .env 121 | fi 122 | 123 | echo "" >> .env 124 | echo "# Note: To change AI configuration later, edit the .env file." >> .env 125 | echo "AI configuration complete. .env created." 126 | else 127 | echo "AI features will be disabled. Creating empty .env file." 128 | echo "# Generated by start-local.sh - AI disabled" > .env 129 | echo "# To enable AI later, edit this .env and add API keys and models." >> .env 130 | fi 131 | fi 132 | else 133 | echo ".env exists — skipping AI configuration step. If you want to change AI settings, edit the .env file." 134 | fi 135 | 136 | # Check if Node.js is installed 137 | if ! command -v node &> /dev/null; then 138 | echo "Error: Node.js not found" 139 | echo "Please install Node.js: https://nodejs.org" 140 | exit 1 141 | fi 142 | 143 | echo "Node.js installed: $(node --version)" 144 | 145 | # Check if npm is installed 146 | if ! command -v npm &> /dev/null; then 147 | echo "Error: npm not found" 148 | echo "Please install npm" 149 | exit 1 150 | fi 151 | 152 | echo "npm installed: $(npm --version)" 153 | 154 | # Check if dependencies are installed 155 | if [ ! -d "node_modules" ]; then 156 | echo "Installing dependencies..." 157 | npm install 158 | if [ $? -ne 0 ]; then 159 | echo "Failed to install dependencies" 160 | exit 1 161 | fi 162 | echo "Dependencies installed successfully" 163 | fi 164 | 165 | # Check if project is built 166 | if [ ! -d ".next" ]; then 167 | echo "Building application..." 168 | npm run build 169 | if [ $? -ne 0 ]; then 170 | echo "Build failed" 171 | exit 1 172 | fi 173 | echo "Build completed successfully" 174 | fi 175 | 176 | echo "" 177 | echo "Starting application..." 178 | echo "URL: http://localhost:3000" 179 | echo "" 180 | echo "Usage Instructions:" 181 | echo " - Open http://localhost:3000 in your browser" 182 | echo " - Fully local execution, no external network required" 183 | echo " - Press Ctrl+C to stop the server" 184 | echo " - To add problems: Edit public/problems.json (see MODIFY-PROBLEMS-GUIDE.md)" 185 | echo " - To change AI settings: Edit the .env file" 186 | echo " - Changes take effect immediately without rebuilding!" 187 | echo "" 188 | 189 | # Start the application 190 | npm start -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # API Test Suite 2 | 3 | Comprehensive unit tests for the OfflineLeetPractice API endpoints, ensuring all existing problem solutions run as expected across multiple programming languages. 4 | 5 | ## Overview 6 | 7 | This test suite validates: 8 | - ✅ **API Functionality**: Core endpoints work correctly 9 | - ✅ **Multi-Language Support**: JavaScript, Java, Python, C++, C execution 10 | - ✅ **Solution Validation**: All provided solutions pass their test cases 11 | - ✅ **Data Integrity**: Problem data structure and consistency 12 | - ✅ **Template Handling**: Language-specific code templates 13 | - ✅ **Error Handling**: Graceful handling of compilation and runtime errors 14 | - ✅ **Performance**: Execution time and memory usage tracking 15 | 16 | ## Test Structure 17 | 18 | ### 1. Problem Data Integrity (`problem-data.test.js`) 19 | - **Problem Structure**: Validates required fields (id, title, tests, templates) 20 | - **Test Cases**: Ensures proper input/output format and edge case coverage 21 | - **Multi-language Content**: Validates English/Chinese translations 22 | - **Template Validation**: Checks language-specific template syntax 23 | - **File System Consistency**: Verifies problems.json synchronization 24 | 25 | ### 2. API Functionality (`run.test.js`) 26 | - **JavaScript Solutions**: Tests all JS solutions for correctness 27 | - **Java Solutions**: Tests Java class handling and method execution 28 | - **Python Solutions**: Tests Python function execution 29 | - **C++ Solutions**: Tests both class and standalone function formats 30 | - **C Solutions**: Tests C function execution 31 | - **Error Handling**: Invalid problem IDs, compilation errors, runtime errors 32 | - **Performance Metrics**: Execution time and memory usage validation 33 | 34 | ### 3. Language-Specific Features (`language-specific.test.js`) 35 | - **Java Template Handling**: Class vs method-only code, String arrays, boolean/array returns 36 | - **C++ Template Handling**: Class vs standalone functions, parameter type detection 37 | - **C Template Handling**: Function formats, boolean return types 38 | - **Python Template Handling**: Function naming conventions, type handling 39 | - **Edge Cases**: Empty functions, infinite loops, timeout handling 40 | - **Platform Support**: Cross-platform compatibility validation 41 | 42 | ### 4. Solution Validation (`solutions.test.js`) 43 | - **Solution Correctness**: All provided solutions pass their test cases 44 | - **Cross-Language Consistency**: Same logic produces identical results 45 | - **Performance Benchmarks**: Solutions complete within reasonable time 46 | - **Error Handling**: Malformed solutions handled gracefully 47 | - **Template Solutions**: Verified working solutions for all major problems 48 | 49 | ## Running Tests 50 | 51 | ### Prerequisites 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | ### Quick Start 57 | ```bash 58 | # Run all tests 59 | npm test 60 | 61 | # Run with coverage 62 | npm run test:coverage 63 | 64 | # Run specific test suite 65 | npm run test:solutions 66 | npm run test:data 67 | ``` 68 | 69 | ### Advanced Usage 70 | ```bash 71 | # Run specific test by pattern 72 | node scripts/test-runner.js specific "java" 73 | node scripts/test-runner.js specific "API" --bail 74 | 75 | # Show help 76 | node scripts/test-runner.js help 77 | ``` 78 | 79 | ### Available Commands 80 | - `npm test` - Run all test suites 81 | - `npm run test:coverage` - Run all tests with coverage report 82 | - `npm run test:solutions` - Test solution validation only 83 | - `npm run test:data` - Test problem data integrity only 84 | - `npm run test:api` - Test core API functionality only 85 | - `npm run test:watch` - Run tests in watch mode 86 | 87 | ## Test Configuration 88 | 89 | ### Jest Configuration (`jest.config.js`) 90 | - **Environment**: Node.js for API testing 91 | - **Timeout**: 30 seconds for language compilation/execution 92 | - **Workers**: Sequential execution to avoid conflicts 93 | - **Coverage**: API routes and core components 94 | 95 | ### Global Setup (`jest.setup.js`) 96 | - **Timeout**: Extended timeout for compilation tests 97 | - **Utilities**: Common validation functions 98 | - **Console**: Suppressed logs for cleaner output 99 | 100 | ## Expected Problems Coverage 101 | 102 | The test suite validates solutions for these problems: 103 | - ✅ **Two Sum** (all languages) - 6 test cases 104 | - ✅ **Reverse Integer** (all languages) - 7 test cases 105 | - ✅ **Palindrome Number** (all languages) - 8 test cases 106 | - ✅ **Longest Common Prefix** (JavaScript, Java, Python) 107 | - ✅ **Valid Parentheses** (JavaScript, Java, Python) 108 | - ✅ **Merge Sorted Lists** (linked-list conversion) - 3 test cases 109 | - ✅ **Remove Duplicates** (when available) 110 | - ✅ **Search Insert Position** (when available) - 7 test cases 111 | - ✅ **Maximum Subarray** (when available) - 3 test cases 112 | - ✅ **Climbing Stairs** (when available) - 3 test cases 113 | - ✅ **Contains Duplicate** (new) - 5 test cases 114 | - ✅ **Single Number** (new) - 4 test cases 115 | - ✅ **Move Zeroes** (new) - 4 test cases 116 | 117 | **Total: 13 problems with comprehensive test coverage** 118 | 119 | ## Language Support Matrix 120 | 121 | | Problem | JavaScript | Java | Python | C++ | C | 122 | |---------|------------|------|--------|-----|---| 123 | | Two Sum | ✅ | ✅ | ✅ | ✅ | ✅ | 124 | | Reverse Integer | ✅ | ✅ | ✅ | ✅ | ✅ | 125 | | Palindrome Number | ✅ | ✅ | ✅ | ✅ | ✅ | 126 | | Longest Common Prefix | ✅ | ✅ | ✅ | ⚪ | ⚪ | 127 | | Valid Parentheses | ✅ | ✅ | ✅ | ✅ | ⚪ | 128 | 129 | ✅ = Fully tested | ⚪ = Basic template support 130 | 131 | ## Performance Benchmarks 132 | 133 | ### Expected Performance 134 | - **Execution Time**: < 5 seconds total per problem 135 | - **Average Test Time**: < 1 second per test case 136 | - **Memory Usage**: Reasonable heap usage tracking 137 | - **Compilation**: < 10 seconds for compiled languages 138 | 139 | ### Memory Tracking 140 | Tests monitor: 141 | - Heap used/total 142 | - External memory 143 | - RSS (Resident Set Size) 144 | 145 | ## Error Handling Validation 146 | 147 | ### Compilation Errors 148 | - Invalid syntax in Java/C++/C 149 | - Missing imports/includes 150 | - Type mismatches 151 | 152 | ### Runtime Errors 153 | - Function exceptions 154 | - Infinite loops (with timeout) 155 | - Wrong return types 156 | - Missing implementations 157 | 158 | ### API Errors 159 | - Invalid problem IDs (404) 160 | - Invalid HTTP methods (405) 161 | - Malformed requests 162 | 163 | ## Continuous Integration 164 | 165 | ### Test Requirements 166 | 1. **All test suites must pass** (100% pass rate) 167 | 2. **Performance within limits** (< 30 seconds total) 168 | 3. **No compilation errors** for valid code 169 | 4. **Proper error handling** for invalid code 170 | 171 | ### Quality Gates 172 | - ✅ **Data Integrity**: All problems have valid structure 173 | - ✅ **Solution Correctness**: All solutions pass their tests 174 | - ✅ **Language Support**: All major languages work 175 | - ✅ **Error Resilience**: Graceful error handling 176 | 177 | ## Troubleshooting 178 | 179 | ### Common Issues 180 | 181 | **1. Test Timeouts** 182 | ```bash 183 | # Increase timeout for slow systems 184 | JEST_TIMEOUT=60000 npm test 185 | ``` 186 | 187 | **2. Java Compilation Issues** 188 | - Ensure Java is installed and in PATH 189 | - Check JDK version compatibility 190 | - Verify JAVA_HOME environment variable 191 | 192 | **3. C++ Compilation Issues** 193 | - Ensure g++ is installed 194 | - Check compiler version 195 | - Verify standard library availability 196 | 197 | **4. Memory Issues** 198 | ```bash 199 | # Run tests with more memory 200 | node --max-old-space-size=4096 scripts/test-runner.js 201 | ``` 202 | 203 | ### Debug Mode 204 | ```bash 205 | # Verbose output 206 | node scripts/test-runner.js all --verbose 207 | 208 | # Single test with debug 209 | jest tests/api/run.test.js --verbose --no-cache 210 | ``` 211 | 212 | ## Contributing 213 | 214 | ### Adding New Tests 215 | 1. Follow existing test structure 216 | 2. Use `global.testUtils.validateAPIResponse()` for API tests 217 | 3. Include both positive and negative test cases 218 | 4. Add performance validation where appropriate 219 | 220 | ### Test Naming Convention 221 | - **Describe blocks**: Feature or component being tested 222 | - **Test cases**: "should [expected behavior] when [condition]" 223 | - **File names**: `[feature].test.js` 224 | 225 | ### Best Practices 226 | - ✅ **Isolation**: Each test is independent 227 | - ✅ **Descriptive**: Clear test names and descriptions 228 | - ✅ **Comprehensive**: Cover happy path and edge cases 229 | - ✅ **Performance**: Include timing assertions 230 | - ✅ **Cleanup**: Proper resource cleanup in tests 231 | 232 | ## Support 233 | 234 | For test-related issues: 235 | 1. Check this README for troubleshooting 236 | 2. Run individual test suites to isolate issues 237 | 3. Use verbose mode for detailed output 238 | 4. Verify environment setup (Java, C++, etc.) 239 | 240 | The test suite ensures the API remains reliable and all existing solutions continue to work correctly across all supported programming languages. -------------------------------------------------------------------------------- /src/contexts/I18nContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, ReactNode, useState, useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | // Dynamically import locale files 4 | import en from '../../locales/en.json'; 5 | import zh from '../../locales/zh.json'; 6 | 7 | // Key for localStorage 8 | const LOCALE_STORAGE_KEY = 'app-locale'; 9 | 10 | type Translations = { 11 | common: { 12 | language: string; 13 | theme: string; 14 | light: string; 15 | dark: string; 16 | home: string; 17 | loading: string; 18 | error: string; 19 | success: string; 20 | settings: string; 21 | }; 22 | header: { 23 | title: string; 24 | subtitle: string; 25 | }; 26 | homepage: { 27 | title: string; 28 | subtitle: string; 29 | problemList: string; 30 | problems: string; 31 | addProblem: string; 32 | search: string; 33 | aiGenerator: string; 34 | searchPlaceholder: string; 35 | filterByDifficulty: string; 36 | filterByTags: string; 37 | allDifficulties: string; 38 | allTags: string; 39 | clearFilters: string; 40 | noResults: string; 41 | showingResults: string; 42 | of: string; 43 | difficulty: { 44 | Easy: string; 45 | Medium: string; 46 | Hard: string; 47 | }; 48 | }; 49 | problemPage: { 50 | description: string; 51 | examples: string; 52 | solution: string; 53 | solutions?: string; 54 | showSolution: string; 55 | hideSolution: string; 56 | solutionHidden: string; 57 | example: string; 58 | input: string; 59 | output: string; 60 | solutionTitle?: string; 61 | noSolutions?: string; 62 | }; 63 | codeRunner: { 64 | title: string; 65 | submit: string; 66 | running: string; 67 | testResults: string; 68 | passed: string; 69 | failed: string; 70 | testCase: string; 71 | expected: string; 72 | actual: string; 73 | executionTime: string; 74 | ms: string; 75 | runningTests: string; 76 | runError: string; 77 | networkError: string; 78 | totalExecutionTime: string; 79 | averageTime: string; 80 | memoryUsed: string; 81 | totalMemory: string; 82 | input: string; 83 | copy: string; 84 | copied: string; 85 | }; 86 | aiGenerator: { 87 | title: string; 88 | subtitle: string; 89 | backToHome: string; 90 | tryLastProblem: string; 91 | requestLabel: string; 92 | requestPlaceholder: string; 93 | suggestedRequests: string; 94 | generateButton: string; 95 | generating: string; 96 | cancel: string; 97 | errorTitle: string; 98 | successTitle: string; 99 | previewTitle: string; 100 | problemId: string; 101 | howToUse: string; 102 | instruction1: string; 103 | instruction2: string; 104 | instruction3: string; 105 | instruction4: string; 106 | instruction5: string; 107 | pleaseEnterRequest: string; 108 | poweredBy: string; 109 | unlimitedProblems: string; 110 | }; 111 | settings: { 112 | title: string; 113 | description: string; 114 | save: string; 115 | saving: string; 116 | deepseek: { 117 | apiKey: string; 118 | apiKeyPlaceholder: string; 119 | model: string; 120 | modelPlaceholder: string; 121 | timeout: string; 122 | timeoutPlaceholder: string; 123 | maxTokens: string; 124 | maxTokensPlaceholder: string; 125 | }; 126 | openai: { 127 | apiKey: string; 128 | apiKeyPlaceholder: string; 129 | model: string; 130 | modelPlaceholder: string; 131 | }; 132 | qwen: { 133 | apiKey: string; 134 | apiKeyPlaceholder: string; 135 | model: string; 136 | modelPlaceholder: string; 137 | }; 138 | claude: { 139 | apiKey: string; 140 | apiKeyPlaceholder: string; 141 | model: string; 142 | modelPlaceholder: string; 143 | }; 144 | ollama: { 145 | endpoint: string; 146 | endpointPlaceholder: string; 147 | model: string; 148 | modelPlaceholder: string; 149 | }; 150 | }; 151 | tags: { 152 | [key: string]: string; 153 | }; 154 | addProblem: { 155 | title: string; 156 | manualForm: string; 157 | importJson: string; 158 | uploadJsonFile: string; 159 | selectJsonFile: string; 160 | pasteJson: string; 161 | importJsonButton: string; 162 | basicInformation: string; 163 | problemId: string; 164 | problemIdHint: string; 165 | difficulty: string; 166 | titles: string; 167 | englishTitle: string; 168 | chineseTitle: string; 169 | tagsLabel: string; 170 | tagsPlaceholder: string; 171 | descriptions: string; 172 | englishDescription: string; 173 | chineseDescription: string; 174 | testCases: string; 175 | input: string; 176 | expectedOutput: string; 177 | removeTestCase: string; 178 | addTestCase: string; 179 | addProblemButton: string; 180 | addingProblem: string; 181 | problemAddedSuccess: string; 182 | invalidJsonFormat: string; 183 | jsonImportedSuccess: string; 184 | networkError: string; 185 | backToProblems: string; 186 | }; 187 | }; 188 | 189 | interface I18nContextType { 190 | locale: string; 191 | t: (key: string, params?: Record) => string; 192 | switchLocale: (locale: string) => void; 193 | } 194 | 195 | const I18nContext = createContext(undefined); 196 | 197 | // Load translations from locale files 198 | const translations: Record = { 199 | en, 200 | zh 201 | }; 202 | 203 | export function I18nProvider({ children }: { children: ReactNode }) { 204 | const router = useRouter(); 205 | 206 | // Initialize locale from localStorage or router, defaulting to 'zh' 207 | const [locale, setLocale] = useState('zh'); 208 | const [mounted, setMounted] = useState(false); 209 | 210 | // Load locale from localStorage on mount 211 | useEffect(() => { 212 | setMounted(true); 213 | if (typeof window !== 'undefined') { 214 | const savedLocale = localStorage.getItem(LOCALE_STORAGE_KEY); 215 | if (savedLocale && (savedLocale === 'zh' || savedLocale === 'en')) { 216 | setLocale(savedLocale); 217 | } else if (router.locale) { 218 | setLocale(router.locale); 219 | } 220 | } 221 | }, [router.locale]); 222 | 223 | const t = (key: string, params?: Record): string => { 224 | try { 225 | const keys = key.split('.'); 226 | const currentLocale = mounted ? locale : 'zh'; 227 | let value: any = translations[currentLocale]; 228 | 229 | for (const k of keys) { 230 | if (value && typeof value === 'object') { 231 | value = value[k]; 232 | } else { 233 | break; 234 | } 235 | } 236 | 237 | if (typeof value === 'string') { 238 | // 简单的参数替换 239 | if (params) { 240 | return Object.entries(params).reduce( 241 | (str, [paramKey, paramValue]) => 242 | str.replace(`{{${paramKey}}}`, String(paramValue)), 243 | value 244 | ); 245 | } 246 | return value; 247 | } 248 | 249 | // 如果找不到翻译,返回key或者使用中文作为fallback 250 | if (currentLocale !== 'zh') { 251 | let fallbackValue: any = translations.zh; 252 | for (const k of keys) { 253 | if (fallbackValue && typeof fallbackValue === 'object') { 254 | fallbackValue = fallbackValue[k]; 255 | } else { 256 | break; 257 | } 258 | } 259 | if (typeof fallbackValue === 'string') { 260 | return fallbackValue; 261 | } 262 | } 263 | 264 | return key; 265 | } catch (error) { 266 | console.warn(`Translation error for key: ${key}`, error); 267 | return key; 268 | } 269 | }; 270 | 271 | const switchLocale = (newLocale: string) => { 272 | // Save to localStorage 273 | if (typeof window !== 'undefined') { 274 | localStorage.setItem(LOCALE_STORAGE_KEY, newLocale); 275 | } 276 | // Update state (this will trigger re-render) 277 | setLocale(newLocale); 278 | 279 | // Also try to update router locale for consistency 280 | try { 281 | const { pathname, asPath, query } = router; 282 | router.push({ pathname, query }, asPath, { locale: newLocale, shallow: true }); 283 | } catch (e) { 284 | // Ignore router errors, state update will handle the UI 285 | } 286 | }; 287 | 288 | return ( 289 | 290 | {children} 291 | 292 | ); 293 | } 294 | 295 | export function useI18n() { 296 | const context = useContext(I18nContext); 297 | if (context === undefined) { 298 | throw new Error('useI18n must be used within an I18nProvider'); 299 | } 300 | return context; 301 | } 302 | 303 | // 便捷的翻译hook 304 | export function useTranslation() { 305 | const { t } = useI18n(); 306 | return { t }; 307 | } -------------------------------------------------------------------------------- /scripts/test-runner.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require('child_process'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | /** 8 | * Comprehensive test runner for the OfflineLeetPractice API 9 | * Ensures environment is ready and runs all test suites 10 | */ 11 | class TestRunner { 12 | constructor() { 13 | this.testSuites = [ 14 | { 15 | name: 'Problem Data Integrity', 16 | file: 'tests/api/problem-data.test.js', 17 | description: 'Validates problem structure and data integrity' 18 | }, 19 | { 20 | name: 'API Functionality', 21 | file: 'tests/api/run.test.js', 22 | description: 'Tests core API endpoints and basic functionality' 23 | }, 24 | { 25 | name: 'Language-Specific Features', 26 | file: 'tests/api/language-specific.test.js', 27 | description: 'Tests language-specific template handling and edge cases' 28 | }, 29 | { 30 | name: 'Solution Validation', 31 | file: 'tests/api/solutions.test.js', 32 | description: 'Validates all provided solutions work correctly' 33 | } 34 | ]; 35 | } 36 | 37 | /** 38 | * Check if required files exist 39 | */ 40 | checkEnvironment() { 41 | console.log('🔍 Checking test environment...'); 42 | 43 | const requiredFiles = [ 44 | 'public/problems.json', 45 | 'pages/api/run.ts', 46 | 'jest.config.js', 47 | 'jest.setup.js' 48 | ]; 49 | 50 | const missingFiles = requiredFiles.filter(file => 51 | !fs.existsSync(path.join(process.cwd(), file)) 52 | ); 53 | 54 | if (missingFiles.length > 0) { 55 | console.error('❌ Missing required files:'); 56 | missingFiles.forEach(file => console.error(` - ${file}`)); 57 | return false; 58 | } 59 | 60 | console.log('✅ Environment check passed'); 61 | return true; 62 | } 63 | 64 | /** 65 | * Validate problems.json structure 66 | */ 67 | validateProblems() { 68 | console.log('🔍 Validating problems.json...'); 69 | 70 | try { 71 | const problemsPath = path.join(process.cwd(), 'public', 'problems.json'); 72 | const problems = JSON.parse(fs.readFileSync(problemsPath, 'utf8')); 73 | 74 | if (!Array.isArray(problems) || problems.length === 0) { 75 | console.error('❌ Invalid problems.json: must be non-empty array'); 76 | return false; 77 | } 78 | 79 | const requiredFields = ['id', 'title', 'tests', 'template']; 80 | const invalidProblems = problems.filter(problem => 81 | !requiredFields.every(field => problem.hasOwnProperty(field)) 82 | ); 83 | 84 | if (invalidProblems.length > 0) { 85 | console.error('❌ Invalid problems found (missing required fields):'); 86 | invalidProblems.forEach(p => console.error(` - ${p.id || 'Unknown'}`)); 87 | return false; 88 | } 89 | 90 | console.log(`✅ Found ${problems.length} valid problems`); 91 | return true; 92 | } catch (error) { 93 | console.error('❌ Error validating problems.json:', error.message); 94 | return false; 95 | } 96 | } 97 | 98 | /** 99 | * Run a specific test suite 100 | */ 101 | async runTestSuite(suite, options = {}) { 102 | console.log(`\n🧪 Running ${suite.name}...`); 103 | console.log(` ${suite.description}`); 104 | 105 | return new Promise((resolve) => { 106 | const jestArgs = [ 107 | suite.file, 108 | '--verbose', 109 | '--no-cache', 110 | '--forceExit' 111 | ]; 112 | 113 | if (options.coverage) { 114 | jestArgs.push('--coverage'); 115 | } 116 | 117 | if (options.bail) { 118 | jestArgs.push('--bail'); 119 | } 120 | 121 | const jest = spawn('npx', ['jest', ...jestArgs], { 122 | stdio: 'inherit', 123 | shell: true 124 | }); 125 | 126 | jest.on('close', (code) => { 127 | if (code === 0) { 128 | console.log(`✅ ${suite.name} passed`); 129 | } else { 130 | console.log(`❌ ${suite.name} failed with code ${code}`); 131 | } 132 | resolve(code === 0); 133 | }); 134 | 135 | jest.on('error', (error) => { 136 | console.error(`❌ Error running ${suite.name}:`, error.message); 137 | resolve(false); 138 | }); 139 | }); 140 | } 141 | 142 | /** 143 | * Run all test suites 144 | */ 145 | async runAllTests(options = {}) { 146 | console.log('🚀 Starting comprehensive API test suite\n'); 147 | 148 | // Environment checks 149 | if (!this.checkEnvironment()) { 150 | process.exit(1); 151 | } 152 | 153 | if (!this.validateProblems()) { 154 | process.exit(1); 155 | } 156 | 157 | // Run test suites 158 | const results = []; 159 | for (const suite of this.testSuites) { 160 | const success = await this.runTestSuite(suite, options); 161 | results.push({ suite: suite.name, success }); 162 | 163 | if (!success && options.bail) { 164 | console.log('\n❌ Test suite failed, stopping due to --bail flag'); 165 | break; 166 | } 167 | } 168 | 169 | // Summary 170 | console.log('\n📊 Test Results Summary:'); 171 | console.log('========================'); 172 | 173 | const passed = results.filter(r => r.success).length; 174 | const total = results.length; 175 | 176 | results.forEach(result => { 177 | const status = result.success ? '✅' : '❌'; 178 | console.log(`${status} ${result.suite}`); 179 | }); 180 | 181 | console.log(`\nOverall: ${passed}/${total} test suites passed`); 182 | 183 | if (passed === total) { 184 | console.log('🎉 All tests passed! API is working correctly.'); 185 | process.exit(0); 186 | } else { 187 | console.log('💥 Some tests failed. Please check the output above.'); 188 | process.exit(1); 189 | } 190 | } 191 | 192 | /** 193 | * Run specific test by name or pattern 194 | */ 195 | async runSpecificTest(pattern, options = {}) { 196 | const matchingSuites = this.testSuites.filter(suite => 197 | suite.name.toLowerCase().includes(pattern.toLowerCase()) || 198 | suite.file.includes(pattern) 199 | ); 200 | 201 | if (matchingSuites.length === 0) { 202 | console.error(`❌ No test suites found matching pattern: ${pattern}`); 203 | console.log('\nAvailable test suites:'); 204 | this.testSuites.forEach(suite => { 205 | console.log(` - ${suite.name} (${suite.file})`); 206 | }); 207 | process.exit(1); 208 | } 209 | 210 | console.log(`🎯 Running ${matchingSuites.length} matching test suite(s):\n`); 211 | 212 | for (const suite of matchingSuites) { 213 | await this.runTestSuite(suite, options); 214 | } 215 | } 216 | 217 | /** 218 | * Display help information 219 | */ 220 | showHelp() { 221 | console.log(` 222 | 🧪 OfflineLeetPractice API Test Runner 223 | 224 | Usage: node scripts/test-runner.js [command] [options] 225 | 226 | Commands: 227 | all Run all test suites (default) 228 | specific Run specific test suite by name or pattern 229 | help Show this help message 230 | 231 | Options: 232 | --coverage Generate coverage report 233 | --bail Stop on first test failure 234 | --verbose Show detailed output 235 | 236 | Test Suites: 237 | ${this.testSuites.map(suite => ` • ${suite.name}\n ${suite.description}`).join('\n')} 238 | 239 | Examples: 240 | node scripts/test-runner.js 241 | node scripts/test-runner.js all --coverage 242 | node scripts/test-runner.js specific "API" --bail 243 | node scripts/test-runner.js specific "solution" --verbose 244 | `); 245 | } 246 | } 247 | 248 | // Main execution 249 | async function main() { 250 | const runner = new TestRunner(); 251 | const args = process.argv.slice(2); 252 | 253 | const command = args[0] || 'all'; 254 | const options = { 255 | coverage: args.includes('--coverage'), 256 | bail: args.includes('--bail'), 257 | verbose: args.includes('--verbose') 258 | }; 259 | 260 | switch (command) { 261 | case 'help': 262 | runner.showHelp(); 263 | break; 264 | 265 | case 'all': 266 | await runner.runAllTests(options); 267 | break; 268 | 269 | case 'specific': 270 | const pattern = args[1]; 271 | if (!pattern) { 272 | console.error('❌ Please specify a test pattern'); 273 | process.exit(1); 274 | } 275 | await runner.runSpecificTest(pattern, options); 276 | break; 277 | 278 | default: 279 | console.error(`❌ Unknown command: ${command}`); 280 | runner.showHelp(); 281 | process.exit(1); 282 | } 283 | } 284 | 285 | // Handle uncaught errors 286 | process.on('uncaughtException', (error) => { 287 | console.error('💥 Uncaught exception:', error); 288 | process.exit(1); 289 | }); 290 | 291 | process.on('unhandledRejection', (reason) => { 292 | console.error('💥 Unhandled rejection:', reason); 293 | process.exit(1); 294 | }); 295 | 296 | main().catch(error => { 297 | console.error('💥 Test runner error:', error); 298 | process.exit(1); 299 | }); -------------------------------------------------------------------------------- /locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "language": "语言", 4 | "theme": "主题", 5 | "light": "亮色", 6 | "dark": "暗色", 7 | "home": "首页", 8 | "loading": "加载中...", 9 | "error": "错误", 10 | "success": "成功", 11 | "settings": "设置" 12 | }, 13 | "header": { 14 | "title": "离线算法练习", 15 | "subtitle": "本地题库,支持在浏览器内编辑并运行测试(JavaScript、TypeScript、Python)" 16 | }, 17 | "homepage": { 18 | "title": "🚀 离线算法练习", 19 | "subtitle": "本地题库,支持在浏览器内编辑并运行测试(JavaScript、TypeScript、Python)", 20 | "problemList": "📚 题目列表", 21 | "problems": "题", 22 | "addProblem": "添加题目", 23 | "search": "搜索", 24 | "searchPlaceholder": "搜索题目标题或描述...", 25 | "filterByDifficulty": "按难度筛选", 26 | "filterByTags": "按标签筛选", 27 | "allDifficulties": "所有难度", 28 | "allTags": "所有标签", 29 | "clearFilters": "清除筛选", 30 | "noResults": "没有找到匹配的题目", 31 | "showingResults": "显示", 32 | "of": "共", 33 | "aiGenerator": "AI 生成器", 34 | "difficulty": { 35 | "Easy": "简单", 36 | "Medium": "中等", 37 | "Hard": "困难" 38 | } 39 | }, 40 | "problemPage": { 41 | "description": "题目描述", 42 | "examples": "示例", 43 | "solution": "参考解法", 44 | "solutions": "参考解法", 45 | "example": "示例", 46 | "input": "输入", 47 | "output": "输出", 48 | "showSolution": "显示解法", 49 | "hideSolution": "隐藏解法", 50 | "solutionHidden": "点击查看参考解法", 51 | "solutionTitle": "解法{{index}}:{{title}}", 52 | "noSolutions": "此题目暂无参考解法。" 53 | }, 54 | "codeRunner": { 55 | "title": "💻 代码编辑器", 56 | "submit": "🚀 提交并运行测试", 57 | "running": "运行中...", 58 | "testResults": "📋 测试结果", 59 | "passed": "通过", 60 | "failed": "失败", 61 | "testCase": "测试用例", 62 | "expected": "期望输出", 63 | "actual": "实际输出", 64 | "executionTime": "执行时间", 65 | "ms": "毫秒", 66 | "runningTests": "正在运行测试...", 67 | "runError": "运行错误", 68 | "networkError": "运行失败,请检查网络连接", 69 | "noResults": "暂无测试结果。运行代码后在此查看结果。", 70 | "totalExecutionTime": "总执行时间", 71 | "averageTime": "平均时间", 72 | "memoryUsed": "内存使用", 73 | "totalMemory": "总内存", 74 | "input": "输入", 75 | "copy": "复制", 76 | "copied": "已复制!", 77 | "wasmEnabled": "WASM 浏览器端执行", 78 | "runtimeReady": "就绪", 79 | "runtimeLoading": "加载中...", 80 | "runtimeError": "错误", 81 | "runtimeIdle": "待机", 82 | "executionError": "执行错误", 83 | "aiSolution": "AI 题解", 84 | "aiSolutionTooltip": "让 AI 生成多个解法", 85 | "generating": "生成中...", 86 | "aiSolutionConfirmTitle": "替换当前代码?", 87 | "aiSolutionConfirmMessage": "你已经修改了代码。生成 AI 题解将替换你当前的代码。是否继续?", 88 | "generateAnyway": "继续生成", 89 | "selectSolution": "选择解法", 90 | "comparisonAnalysis": "📊 解法对比分析", 91 | "approach": "💡 解题思路", 92 | "timeComplexity": "⏱️ 时间复杂度", 93 | "spaceComplexity": "💾 空间复杂度", 94 | "codePreview": "📝 代码预览", 95 | "applySolution": "应用此解法" 96 | }, 97 | "aiChat": { 98 | "title": "AI 助手", 99 | "emptyState": "有任何关于这道题的问题都可以问我!我可以提供提示、解题思路和讲解。", 100 | "quickPrompts": "快捷提问:", 101 | "you": "你", 102 | "assistant": "AI 助手", 103 | "thinking": "思考中...", 104 | "inputPlaceholder": "输入你的问题...", 105 | "clearChat": "清空对话" 106 | }, 107 | "tags": { 108 | "array": "数组", 109 | "hash-table": "哈希表", 110 | "math": "数学", 111 | "string": "字符串", 112 | "stack": "栈", 113 | "linked-list": "链表", 114 | "recursion": "递归", 115 | "two-pointers": "双指针", 116 | "binary-search": "二分查找", 117 | "divide-and-conquer": "分治", 118 | "dynamic-programming": "动态规划", 119 | "memoization": "记忆化" 120 | }, 121 | "addProblem": { 122 | "title": "添加新题目", 123 | "editTitle": "编辑题目", 124 | "manualForm": "手动表单", 125 | "importJson": "导入JSON", 126 | "uploadJsonFile": "上传JSON文件:", 127 | "selectJsonFile": "选择JSON文件", 128 | "pasteJson": "或粘贴JSON:", 129 | "importJsonButton": "导入JSON", 130 | "basicInformation": "基本信息", 131 | "problemId": "题目ID:", 132 | "problemIdHint": "仅使用小写字母、数字和连字符", 133 | "difficulty": "难度:", 134 | "titles": "标题", 135 | "englishTitle": "英文标题:", 136 | "chineseTitle": "中文标题:", 137 | "tagsLabel": "标签(逗号分隔):", 138 | "tagsPlaceholder": "例如:array, hash-table", 139 | "descriptions": "描述", 140 | "englishDescription": "英文描述:", 141 | "chineseDescription": "中文描述:", 142 | "testCases": "测试用例:", 143 | "input": "输入:", 144 | "expectedOutput": "期望输出:", 145 | "removeTestCase": "删除测试用例", 146 | "addTestCase": "添加测试用例", 147 | "addProblemButton": "添加题目", 148 | "updateProblemButton": "更新题目", 149 | "addingProblem": "正在添加题目...", 150 | "updatingProblem": "正在更新题目...", 151 | "problemAddedSuccess": "题目添加成功!ID: {{id}}", 152 | "problemUpdatedSuccess": "题目更新成功!ID: {{id}}", 153 | "invalidJsonFormat": "JSON格式无效", 154 | "jsonImportedSuccess": "JSON导入成功", 155 | "networkError": "网络错误", 156 | "backToProblems": "返回题目列表" 157 | }, 158 | "aiGenerator": { 159 | "title": "AI 题目生成器", 160 | "subtitle": "描述您想要练习的编程题目类型,AI 将为您生成自定义的算法题目。", 161 | "backToHome": "返回首页", 162 | "tryLastProblem": "尝试上一题", 163 | "requestLabel": "描述您的题目需求", 164 | "requestPlaceholder": "例如:我想做一道动态规划题目,或者我想做一道关于字符串处理的题目", 165 | "suggestedRequests": "建议的请求", 166 | "generateButton": "生成题目", 167 | "generating": "生成中...", 168 | "cancel": "取消", 169 | "errorTitle": "错误", 170 | "successTitle": "成功", 171 | "previewTitle": "生成的题目预览", 172 | "problemId": "题目 ID", 173 | "howToUse": "使用方法", 174 | "instruction1": "描述您想要的题目类型(算法类型、难度、主题)", 175 | "instruction2": "您可以使用中文或英文", 176 | "instruction3": "具体说明您想练习的内容(例如:\"动态规划\"、\"图算法\")", 177 | "instruction4": "AI 将生成包含测试用例和解决方案的完整题目", 178 | "instruction5": "生成的题目将自动添加到您的题目集合中", 179 | "pleaseEnterRequest": "请输入题目需求", 180 | "poweredBy": "由 {{provider}} 驱动", 181 | "unlimitedProblems": "生成无限编程题目", 182 | "usingOnlineAI": "使用在线 AI ({{provider}})", 183 | "usingLocalAI": "使用本地 AI (Ollama)", 184 | "changeInSettings": "在设置中更改", 185 | "selectAIProvider": "选择 AI 提供商", 186 | "autoSelect": "自动选择", 187 | "onlineAI": "在线 (DeepSeek)", 188 | "localAI": "本地 (Ollama)", 189 | "deepseek": "DeepSeek", 190 | "openai": "OpenAI", 191 | "qwen": "通义千问", 192 | "claude": "Claude", 193 | "noAIProviderConfigured": "未配置 AI 提供商。请在环境变量中配置支持的 AI 提供商。", 194 | "jsonParseError": "无法解析生成的JSON。您可以在下面的编辑器中进行修复。", 195 | "fixJsonTitle": "修复生成的JSON", 196 | "fixJsonInstructions": "AI生成的内容无法解析为有效的JSON。请修复任何语法错误并保存修正后的JSON。", 197 | "saveFixedJson": "保存修正的JSON", 198 | "pleaseEnterJson": "请输入JSON内容", 199 | "invalidJsonFormat": "JSON格式无效。请检查语法错误。" 200 | }, 201 | "settings": { 202 | "title": "设置", 203 | "description": "配置用于题目生成、AI 对话和 AI 题解的模型提供商", 204 | "save": "保存配置", 205 | "saving": "保存中...", 206 | "defaultProvider": { 207 | "title": "默认 AI 供应商", 208 | "description": "选择所有 AI 功能(题目生成、AI 对话、AI 题解)使用的默认供应商。您可以在下方配置多个供应商并在它们之间切换。", 209 | "label": "AI 供应商", 210 | "selectDescription": "选择默认使用哪个 AI 供应商", 211 | "auto": "自动(使用第一个可用的)", 212 | "currentSelection": "当前选择" 213 | }, 214 | "deepseek": { 215 | "apiKey": "API 密钥", 216 | "apiKeyPlaceholder": "输入您的 DeepSeek API 密钥", 217 | "model": "模型", 218 | "modelPlaceholder": "输入模型名称 (例如: deepseek-chat)", 219 | "timeout": "API 超时 (毫秒)", 220 | "timeoutPlaceholder": "输入超时时间 (毫秒)", 221 | "maxTokens": "最大令牌数", 222 | "maxTokensPlaceholder": "输入生成的最大令牌数" 223 | }, 224 | "openai": { 225 | "apiKey": "API 密钥", 226 | "apiKeyPlaceholder": "输入您的 OpenAI API 密钥", 227 | "model": "模型", 228 | "modelPlaceholder": "输入模型名称 (例如: gpt-4-turbo)" 229 | }, 230 | "qwen": { 231 | "apiKey": "API 密钥", 232 | "apiKeyPlaceholder": "输入您的通义千问 API 密钥", 233 | "model": "模型", 234 | "modelPlaceholder": "输入模型名称 (例如: qwen-turbo)" 235 | }, 236 | "claude": { 237 | "apiKey": "API 密钥", 238 | "apiKeyPlaceholder": "输入您的 Claude API 密钥", 239 | "model": "模型", 240 | "modelPlaceholder": "输入模型名称 (例如: claude-3-haiku-20240307)" 241 | }, 242 | "ollama": { 243 | "endpoint": "端点", 244 | "endpointPlaceholder": "输入 Ollama 端点 (例如: http://localhost:11434)", 245 | "model": "模型", 246 | "modelPlaceholder": "输入模型名称 (例如: llama3)" 247 | } 248 | }, 249 | "manage": { 250 | "title": "管理题目", 251 | "backToProblems": "返回题目列表", 252 | "importTab": "导入题目", 253 | "listTab": "题目列表", 254 | "importFromFolder": "从本地文件夹导入", 255 | "importFromFolderDescription": "从本地文件夹加载所有JSON文件。每个JSON文件可以包含单个题目或题目数组。", 256 | "problemsFolder": "默认题目文件夹", 257 | "problemsFolderHint": "从应用目录中的 'problems' 文件夹加载所有JSON文件", 258 | "loadFromProblemsFolder": "从题目文件夹加载", 259 | "or": "或者", 260 | "customFolderPath": "自定义文件夹路径", 261 | "customFolderHint": "输入包含JSON题目文件的文件夹完整路径", 262 | "importFromCustomFolder": "从文件夹导入", 263 | "filesScanned": "扫描了 {{count}} 个JSON文件", 264 | "errorDetails": "错误详情", 265 | "importFromUrl": "从远程URL导入", 266 | "importDescription": "输入一个或多个包含题目的JSON文件的URL。每个URL应指向一个题目对象数组。可以输入多个URL,每行一个。", 267 | "remoteUrl": "远程JSON URL", 268 | "multiUrlHint": "每行输入一个URL,可同时从多个源导入", 269 | "importButton": "导入", 270 | "importComplete": "导入完成", 271 | "importProgress": "正在导入第 {{current}} / {{total}} 个URL", 272 | "importResultSuccess": "成功导入 {{count}} 道题目", 273 | "importResultSkipped": "跳过 {{count}} 道题目(已存在)", 274 | "importResultFailed": "失败 {{count}} 道题目(格式无效)", 275 | "jsonFormat": "预期的JSON格式", 276 | "jsonFormatDescription": "JSON文件应该包含题目对象数组(或单个题目对象),具有以下结构:", 277 | "searchPlaceholder": "按ID或标题搜索...", 278 | "selectedCount": "已选择 {{count}} 项", 279 | "deleteSelected": "删除所选", 280 | "noProblems": "没有找到题目", 281 | "columnTitle": "标题", 282 | "columnDifficulty": "难度", 283 | "columnTags": "标签", 284 | "columnActions": "操作", 285 | "viewProblem": "查看题目", 286 | "editProblem": "编辑题目", 287 | "editProblemTitle": "编辑题目", 288 | "deleteProblem": "删除题目", 289 | "deleteConfirmTitle": "确认删除", 290 | "deleteConfirmMessage": "确定要删除 {{count}} 道题目吗?此操作无法撤销。", 291 | "cancel": "取消", 292 | "confirmDelete": "删除", 293 | "manageProblems": "管理" 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /docs/assets/site.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #6366f1; 3 | --primary-dark: #4f46e5; 4 | --primary-light: #818cf8; 5 | --secondary: #0ea5e9; 6 | --accent: #f472b6; 7 | --bg-dark: #0f0f23; 8 | --bg-card: #1a1a2e; 9 | --bg-card-hover: #252542; 10 | --text-primary: #f1f5f9; 11 | --text-secondary: #94a3b8; 12 | --text-muted: #64748b; 13 | --border: #334155; 14 | --success: #10b981; 15 | --warning: #f59e0b; 16 | --danger: #ef4444; 17 | } 18 | 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | } 24 | 25 | html { 26 | scroll-behavior: smooth; 27 | } 28 | 29 | body { 30 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 31 | background: var(--bg-dark); 32 | color: var(--text-primary); 33 | line-height: 1.6; 34 | overflow-x: hidden; 35 | } 36 | 37 | /* Animated Background */ 38 | .bg-gradient { 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | right: 0; 43 | bottom: 0; 44 | background: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99, 102, 241, 0.15), transparent), 45 | radial-gradient(ellipse 60% 40% at 100% 50%, rgba(14, 165, 233, 0.1), transparent), 46 | radial-gradient(ellipse 60% 40% at 0% 80%, rgba(244, 114, 182, 0.1), transparent); 47 | z-index: -1; 48 | } 49 | 50 | .container { 51 | max-width: 1200px; 52 | margin: 0 auto; 53 | padding: 0 24px; 54 | } 55 | 56 | /* Navigation */ 57 | nav { 58 | position: fixed; 59 | top: 0; 60 | left: 0; 61 | right: 0; 62 | z-index: 1000; 63 | padding: 16px 0; 64 | background: rgba(15, 15, 35, 0.8); 65 | backdrop-filter: blur(12px); 66 | border-bottom: 1px solid rgba(255, 255, 255, 0.05); 67 | } 68 | 69 | nav .container { 70 | display: flex; 71 | justify-content: space-between; 72 | align-items: center; 73 | } 74 | 75 | .logo { 76 | display: flex; 77 | align-items: center; 78 | gap: 12px; 79 | font-weight: 700; 80 | font-size: 1.25rem; 81 | color: var(--text-primary); 82 | text-decoration: none; 83 | } 84 | 85 | .logo img { 86 | width: 36px; 87 | height: 36px; 88 | border-radius: 8px; 89 | } 90 | 91 | .nav-links { 92 | display: flex; 93 | align-items: center; 94 | gap: 32px; 95 | } 96 | 97 | .nav-links a { 98 | color: var(--text-secondary); 99 | text-decoration: none; 100 | font-size: 0.95rem; 101 | font-weight: 500; 102 | transition: color 0.2s; 103 | } 104 | 105 | .nav-links a:hover { 106 | color: var(--text-primary); 107 | } 108 | 109 | .github-btn { 110 | display: flex; 111 | align-items: center; 112 | gap: 8px; 113 | padding: 10px 20px; 114 | background: var(--bg-card); 115 | border: 1px solid var(--border); 116 | border-radius: 8px; 117 | color: var(--text-primary); 118 | text-decoration: none; 119 | font-weight: 500; 120 | transition: all 0.2s; 121 | } 122 | 123 | .github-btn:hover { 124 | background: var(--bg-card-hover); 125 | border-color: var(--primary); 126 | } 127 | 128 | .github-btn svg { 129 | width: 20px; 130 | height: 20px; 131 | } 132 | 133 | .lang-btn { 134 | display: inline-flex; 135 | align-items: center; 136 | gap: 6px; 137 | padding: 8px 12px; 138 | border-radius: 8px; 139 | border: 1px solid rgba(255, 255, 255, 0.12); 140 | color: var(--text-secondary); 141 | text-decoration: none; 142 | font-size: 0.9rem; 143 | transition: all 0.2s; 144 | } 145 | 146 | .lang-btn:hover { 147 | color: var(--text-primary); 148 | border-color: var(--primary); 149 | background: rgba(99, 102, 241, 0.08); 150 | } 151 | 152 | /* Mobile menu button (present in markup, hidden on desktop) */ 153 | .mobile-menu-btn { 154 | display: none; 155 | background: transparent; 156 | border: 0; 157 | color: var(--text-primary); 158 | } 159 | 160 | /* Hero Section */ 161 | .hero { 162 | padding: 160px 0 100px; 163 | text-align: center; 164 | } 165 | 166 | .badge { 167 | display: inline-flex; 168 | align-items: center; 169 | gap: 8px; 170 | padding: 8px 16px; 171 | background: rgba(99, 102, 241, 0.1); 172 | border: 1px solid rgba(99, 102, 241, 0.3); 173 | border-radius: 100px; 174 | font-size: 0.875rem; 175 | color: var(--primary-light); 176 | margin-bottom: 24px; 177 | } 178 | 179 | .badge::before { 180 | content: ''; 181 | width: 8px; 182 | height: 8px; 183 | background: var(--success); 184 | border-radius: 50%; 185 | animation: pulse 2s infinite; 186 | } 187 | 188 | @keyframes pulse { 189 | 0%, 190 | 100% { 191 | opacity: 1; 192 | } 193 | 50% { 194 | opacity: 0.5; 195 | } 196 | } 197 | 198 | h1 { 199 | font-size: clamp(2.5rem, 6vw, 4rem); 200 | font-weight: 800; 201 | line-height: 1.1; 202 | margin-bottom: 24px; 203 | background: linear-gradient(135deg, var(--text-primary) 0%, var(--primary-light) 50%, var(--secondary) 100%); 204 | -webkit-background-clip: text; 205 | -webkit-text-fill-color: transparent; 206 | background-clip: text; 207 | } 208 | 209 | .hero p { 210 | font-size: 1.25rem; 211 | color: var(--text-secondary); 212 | max-width: 720px; 213 | margin: 0 auto 40px; 214 | } 215 | 216 | .hero-buttons { 217 | display: flex; 218 | justify-content: center; 219 | gap: 16px; 220 | margin-bottom: 60px; 221 | } 222 | 223 | .btn { 224 | display: inline-flex; 225 | align-items: center; 226 | gap: 10px; 227 | padding: 14px 28px; 228 | border-radius: 12px; 229 | font-weight: 600; 230 | text-decoration: none; 231 | transition: all 0.2s; 232 | cursor: pointer; 233 | } 234 | 235 | .btn-primary { 236 | background: var(--primary); 237 | color: white; 238 | } 239 | 240 | .btn-primary:hover { 241 | background: var(--primary-dark); 242 | transform: translateY(-2px); 243 | box-shadow: 0 12px 30px rgba(99, 102, 241, 0.35); 244 | } 245 | 246 | .btn-secondary { 247 | background: transparent; 248 | border: 1px solid var(--border); 249 | color: var(--text-primary); 250 | } 251 | 252 | .btn-secondary:hover { 253 | border-color: var(--primary); 254 | background: rgba(99, 102, 241, 0.1); 255 | } 256 | 257 | .hero-image { 258 | width: 100%; 259 | max-width: 1000px; 260 | border-radius: 16px; 261 | border: 1px solid rgba(255, 255, 255, 0.08); 262 | box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45); 263 | } 264 | 265 | /* Sections */ 266 | section { 267 | padding: 100px 0; 268 | } 269 | 270 | .section-title { 271 | text-align: center; 272 | margin-bottom: 60px; 273 | } 274 | 275 | .section-title h2 { 276 | font-size: 2.5rem; 277 | font-weight: 800; 278 | margin-bottom: 16px; 279 | } 280 | 281 | .section-title p { 282 | color: var(--text-secondary); 283 | font-size: 1.1rem; 284 | } 285 | 286 | /* Feature & language cards */ 287 | .features-grid, 288 | .languages-grid, 289 | .download-grid { 290 | display: grid; 291 | gap: 20px; 292 | } 293 | 294 | .features-grid { 295 | grid-template-columns: repeat(3, minmax(0, 1fr)); 296 | } 297 | 298 | .languages-grid { 299 | grid-template-columns: repeat(3, minmax(0, 1fr)); 300 | } 301 | 302 | .download-grid { 303 | grid-template-columns: repeat(3, minmax(0, 1fr)); 304 | } 305 | 306 | .feature-card, 307 | .language-card, 308 | .download-card, 309 | .ai-card { 310 | background: rgba(26, 26, 46, 0.72); 311 | border: 1px solid rgba(255, 255, 255, 0.08); 312 | border-radius: 16px; 313 | padding: 24px; 314 | transition: transform 0.2s, background 0.2s, border-color 0.2s; 315 | } 316 | 317 | .feature-card:hover, 318 | .language-card:hover, 319 | .download-card:hover, 320 | .ai-card:hover { 321 | transform: translateY(-4px); 322 | background: rgba(37, 37, 66, 0.82); 323 | border-color: rgba(99, 102, 241, 0.45); 324 | } 325 | 326 | .feature-icon, 327 | .language-icon { 328 | font-size: 2rem; 329 | margin-bottom: 12px; 330 | } 331 | 332 | .feature-card h3, 333 | .language-card h3, 334 | .download-card h3 { 335 | font-size: 1.25rem; 336 | margin-bottom: 10px; 337 | } 338 | 339 | .feature-card p, 340 | .language-card p, 341 | .download-card p { 342 | color: var(--text-secondary); 343 | } 344 | 345 | /* Download section */ 346 | .download .version { 347 | color: var(--text-muted); 348 | margin: 10px 0 14px; 349 | font-size: 0.95rem; 350 | } 351 | 352 | .platform-icon svg { 353 | width: 36px; 354 | height: 36px; 355 | fill: currentColor; 356 | color: var(--text-primary); 357 | } 358 | 359 | .download-options { 360 | display: flex; 361 | flex-direction: column; 362 | gap: 10px; 363 | margin-top: 14px; 364 | } 365 | 366 | .download-btn { 367 | display: inline-flex; 368 | align-items: center; 369 | gap: 10px; 370 | padding: 12px 14px; 371 | border-radius: 12px; 372 | background: var(--primary); 373 | color: white; 374 | text-decoration: none; 375 | font-weight: 600; 376 | transition: all 0.2s; 377 | } 378 | 379 | .download-btn:hover { 380 | background: var(--primary-dark); 381 | transform: translateY(-2px); 382 | } 383 | 384 | .download-btn.secondary { 385 | background: rgba(255, 255, 255, 0.06); 386 | border: 1px solid rgba(255, 255, 255, 0.1); 387 | color: var(--text-primary); 388 | } 389 | 390 | .download-btn.secondary:hover { 391 | border-color: rgba(99, 102, 241, 0.6); 392 | background: rgba(99, 102, 241, 0.12); 393 | } 394 | 395 | /* Footer */ 396 | footer { 397 | padding: 40px 0; 398 | border-top: 1px solid rgba(255, 255, 255, 0.08); 399 | } 400 | 401 | .footer-content { 402 | display: flex; 403 | justify-content: space-between; 404 | align-items: center; 405 | gap: 24px; 406 | } 407 | 408 | .footer-links { 409 | display: flex; 410 | flex-wrap: wrap; 411 | gap: 18px; 412 | } 413 | 414 | .footer-links a { 415 | color: var(--text-secondary); 416 | text-decoration: none; 417 | } 418 | 419 | .footer-links a:hover { 420 | color: var(--text-primary); 421 | } 422 | 423 | .footer-info { 424 | text-align: right; 425 | color: var(--text-secondary); 426 | } 427 | 428 | .footer-info a { 429 | color: var(--text-primary); 430 | } 431 | 432 | /* Responsive */ 433 | @media (max-width: 1024px) { 434 | .features-grid, 435 | .download-grid { 436 | grid-template-columns: repeat(2, minmax(0, 1fr)); 437 | } 438 | } 439 | 440 | @media (max-width: 768px) { 441 | .nav-links { 442 | display: none; 443 | } 444 | .mobile-menu-btn { 445 | display: block; 446 | } 447 | .hero { 448 | padding: 120px 0 60px; 449 | } 450 | .hero-buttons { 451 | flex-direction: column; 452 | align-items: center; 453 | } 454 | .btn { 455 | width: 100%; 456 | max-width: 320px; 457 | justify-content: center; 458 | } 459 | .footer-content { 460 | flex-direction: column; 461 | text-align: center; 462 | } 463 | .footer-info { 464 | text-align: center; 465 | } 466 | .features-grid, 467 | .languages-grid, 468 | .download-grid { 469 | grid-template-columns: 1fr; 470 | } 471 | } 472 | 473 | /* Scroll animations */ 474 | .fade-in { 475 | opacity: 0; 476 | transform: translateY(30px); 477 | transition: all 0.6s ease-out; 478 | } 479 | 480 | .fade-in.visible { 481 | opacity: 1; 482 | transform: translateY(0); 483 | } 484 | 485 | 486 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # 触发条件:推送以 v 开头的 tag,如 v1.0.0 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | # 生成 Changelog 13 | changelog: 14 | name: Generate Changelog 15 | runs-on: ubuntu-latest 16 | outputs: 17 | changelog: ${{ steps.changelog.outputs.changelog }} 18 | version: ${{ steps.version.outputs.version }} 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 # 获取完整历史以生成 changelog 24 | 25 | - name: Get version from tag 26 | id: version 27 | run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 28 | 29 | - name: Generate Changelog 30 | id: changelog 31 | uses: metcalfc/changelog-generator@v4.3.1 32 | with: 33 | myToken: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | # 构建 macOS 版本 36 | build-macos: 37 | name: Build macOS 38 | runs-on: macos-latest 39 | needs: changelog 40 | strategy: 41 | matrix: 42 | arch: [x64, arm64] 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup Node.js 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: '20' 51 | cache: 'npm' 52 | 53 | - name: Setup Python (required for DMG building) 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: '3.12' 57 | 58 | - name: Install dependencies 59 | run: npm ci 60 | 61 | - name: Generate icons 62 | run: npm run icons:generate 63 | 64 | - name: Build Next.js 65 | run: npm run build 66 | 67 | # 导入 Apple 开发者证书用于签名(如果配置了证书) 68 | - name: Import Code Signing Certificate 69 | env: 70 | APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} 71 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} 72 | run: | 73 | # 检查是否配置了证书 74 | if [ -z "$APPLE_CERTIFICATE_BASE64" ]; then 75 | echo "⚠️ No signing certificate configured, skipping code signing" 76 | echo "HAS_CERTIFICATE=false" >> $GITHUB_ENV 77 | exit 0 78 | fi 79 | 80 | echo "HAS_CERTIFICATE=true" >> $GITHUB_ENV 81 | 82 | # 创建临时钥匙串 83 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 84 | KEYCHAIN_PASSWORD=$(openssl rand -base64 32) 85 | 86 | # 创建钥匙串 87 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 88 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 89 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 90 | 91 | # 导入证书 92 | echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > $RUNNER_TEMP/certificate.p12 93 | security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 94 | 95 | # 设置钥匙串搜索列表 96 | security list-keychain -d user -s $KEYCHAIN_PATH 97 | 98 | # 允许 codesign 访问钥匙串中的密钥 99 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 100 | 101 | # 清理证书文件 102 | rm -f $RUNNER_TEMP/certificate.p12 103 | 104 | echo "✅ Code signing certificate imported successfully" 105 | 106 | - name: Build Electron app (macOS ${{ matrix.arch }}) 107 | run: | 108 | npx electron-builder --mac --${{ matrix.arch }} --config electron-builder.config.js 109 | env: 110 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 111 | # 如果没有证书,禁用自动签名发现 112 | CSC_IDENTITY_AUTO_DISCOVERY: ${{ env.HAS_CERTIFICATE == 'true' }} 113 | # 公证相关环境变量(仅在配置了证书时有效) 114 | APPLE_ID: ${{ secrets.APPLE_ID }} 115 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} 116 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 117 | 118 | # 清理钥匙串 119 | - name: Cleanup Keychain 120 | if: always() 121 | run: | 122 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 123 | if [ -f "$KEYCHAIN_PATH" ]; then 124 | security delete-keychain $KEYCHAIN_PATH 2>/dev/null || true 125 | fi 126 | 127 | - name: Upload macOS artifacts 128 | uses: actions/upload-artifact@v4 129 | with: 130 | name: macos-${{ matrix.arch }} 131 | path: | 132 | dist/*.dmg 133 | dist/*.zip 134 | if-no-files-found: error 135 | 136 | # 构建 Windows 版本 137 | build-windows: 138 | name: Build Windows 139 | runs-on: windows-latest 140 | needs: changelog 141 | steps: 142 | - name: Checkout code 143 | uses: actions/checkout@v4 144 | 145 | - name: Setup Node.js 146 | uses: actions/setup-node@v4 147 | with: 148 | node-version: '20' 149 | cache: 'npm' 150 | 151 | - name: Install dependencies 152 | run: npm ci 153 | 154 | - name: Generate icons 155 | run: npm run icons:generate 156 | 157 | - name: Build Next.js 158 | run: npm run build 159 | 160 | - name: Build Electron app (Windows) 161 | run: | 162 | npx electron-builder --win --config electron-builder.config.js 163 | env: 164 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 165 | 166 | - name: Upload Windows artifacts 167 | uses: actions/upload-artifact@v4 168 | with: 169 | name: windows 170 | path: | 171 | dist/*.exe 172 | if-no-files-found: error 173 | 174 | # 构建 Linux 版本 175 | build-linux: 176 | name: Build Linux 177 | runs-on: ubuntu-latest 178 | needs: changelog 179 | steps: 180 | - name: Checkout code 181 | uses: actions/checkout@v4 182 | 183 | - name: Setup Node.js 184 | uses: actions/setup-node@v4 185 | with: 186 | node-version: '20' 187 | cache: 'npm' 188 | 189 | - name: Install dependencies 190 | run: npm ci 191 | 192 | - name: Generate icons 193 | run: npm run icons:generate 194 | 195 | - name: Build Next.js 196 | run: npm run build 197 | 198 | - name: Build Electron app (Linux) 199 | run: | 200 | npx electron-builder --linux --config electron-builder.config.js 201 | env: 202 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 203 | 204 | - name: Upload Linux artifacts 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: linux 208 | path: | 209 | dist/*.AppImage 210 | dist/*.deb 211 | dist/*.rpm 212 | if-no-files-found: error 213 | 214 | # 创建 Release 215 | release: 216 | name: Create Release 217 | runs-on: ubuntu-latest 218 | needs: [changelog, build-macos, build-windows, build-linux] 219 | steps: 220 | - name: Checkout code 221 | uses: actions/checkout@v4 222 | 223 | - name: Download all artifacts 224 | uses: actions/download-artifact@v4 225 | with: 226 | path: artifacts 227 | 228 | - name: List artifacts 229 | run: | 230 | echo "=== Downloaded artifacts ===" 231 | find artifacts -type f -name "*" | head -50 232 | 233 | - name: Prepare release assets 234 | run: | 235 | mkdir -p release-assets 236 | # macOS 237 | cp artifacts/macos-x64/*.dmg release-assets/ 2>/dev/null || true 238 | cp artifacts/macos-x64/*.zip release-assets/ 2>/dev/null || true 239 | cp artifacts/macos-arm64/*.dmg release-assets/ 2>/dev/null || true 240 | cp artifacts/macos-arm64/*.zip release-assets/ 2>/dev/null || true 241 | # Windows 242 | cp artifacts/windows/*.exe release-assets/ 2>/dev/null || true 243 | # Linux 244 | cp artifacts/linux/*.AppImage release-assets/ 2>/dev/null || true 245 | cp artifacts/linux/*.deb release-assets/ 2>/dev/null || true 246 | cp artifacts/linux/*.rpm release-assets/ 2>/dev/null || true 247 | 248 | echo "=== Release assets ===" 249 | ls -la release-assets/ 250 | 251 | - name: Create Release 252 | uses: softprops/action-gh-release@v2 253 | with: 254 | name: Algorithm Practice v${{ needs.changelog.outputs.version }} 255 | tag_name: ${{ github.ref_name }} 256 | body: | 257 | ## 🚀 Algorithm Practice v${{ needs.changelog.outputs.version }} 258 | 259 | An offline algorithm practice application with WASM-based code execution. 260 | 261 | ### 📦 Downloads 262 | 263 | | Platform | Architecture | File | 264 | |----------|--------------|------| 265 | | **macOS** | Apple Silicon (M1/M2/M3) | `Algorithm-Practice-*-macOS-arm64.dmg` | 266 | | **macOS** | Intel | `Algorithm-Practice-*-macOS-x64.dmg` | 267 | | **Windows** | 64-bit Installer | `Algorithm-Practice-*-Windows-Setup.exe` | 268 | | **Windows** | 64-bit Portable | `Algorithm-Practice-*-Windows-Portable.exe` | 269 | | **Linux** | AppImage | `Algorithm-Practice-*-Linux.AppImage` | 270 | | **Linux** | Debian/Ubuntu | `Algorithm-Practice-*-Linux.deb` | 271 | | **Linux** | Fedora/RHEL | `Algorithm-Practice-*-Linux.rpm` | 272 | 273 | ### ⚠️ macOS Installation Note 274 | 275 | If you see **"damaged"** or **"cannot verify developer"** message when opening the app, run this command in Terminal: 276 | 277 | ```bash 278 | # For app installed from DMG 279 | sudo xattr -rd com.apple.quarantine /Applications/Algorithm\ Practice.app 280 | 281 | # Or for app extracted from ZIP (adjust path accordingly) 282 | sudo xattr -rd com.apple.quarantine ~/Downloads/Algorithm\ Practice.app 283 | ``` 284 | 285 | ### 📝 Changelog 286 | 287 | ${{ needs.changelog.outputs.changelog }} 288 | 289 | --- 290 | 291 | ### 中文说明 292 | 293 | 基于 WASM 的离线算法练习应用。 294 | 295 | **macOS 安装提示**:如果打开应用时提示"已损坏"或"无法验证开发者",请在终端运行: 296 | 297 | ```bash 298 | sudo xattr -rd com.apple.quarantine /Applications/Algorithm\ Practice.app 299 | ``` 300 | files: release-assets/* 301 | draft: false 302 | prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} 303 | generate_release_notes: false 304 | env: 305 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 306 | 307 | - name: Release Summary 308 | run: | 309 | echo "✅ Release created successfully!" 310 | echo "📦 Version: v${{ needs.changelog.outputs.version }}" 311 | echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}" 312 | 313 | -------------------------------------------------------------------------------- /tests/api/run-direct.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Direct API tests that bypass Next.js server setup 3 | * This avoids the dynamic import issues and tests the API handler directly 4 | */ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | // Mock Next.js types and request/response 10 | function createMockReq(method = 'POST', body = {}) { 11 | return { 12 | method, 13 | body, 14 | headers: {}, 15 | query: {} 16 | }; 17 | } 18 | 19 | function createMockRes() { 20 | const res = { 21 | status: jest.fn().mockReturnThis(), 22 | json: jest.fn().mockReturnThis(), 23 | end: jest.fn().mockReturnThis(), 24 | _statusCode: 200, 25 | _jsonData: null 26 | }; 27 | 28 | res.status.mockImplementation((code) => { 29 | res._statusCode = code; 30 | return res; 31 | }); 32 | 33 | res.json.mockImplementation((data) => { 34 | res._jsonData = data; 35 | return res; 36 | }); 37 | 38 | return res; 39 | } 40 | 41 | describe('Direct API Tests', () => { 42 | let handler; 43 | let problems; 44 | 45 | beforeAll(async () => { 46 | // Load problems data 47 | const problemsPath = path.join(process.cwd(), 'public', 'problems.json'); 48 | problems = JSON.parse(fs.readFileSync(problemsPath, 'utf8')); 49 | 50 | // Mock Next.js types for the handler 51 | global.NextApiRequest = class {}; 52 | global.NextApiResponse = class {}; 53 | 54 | // Import the handler after setting up mocks 55 | const handlerModule = require('../../pages/api/run.ts'); 56 | handler = handlerModule.default; 57 | }); 58 | 59 | describe('Linked List Problems', () => { 60 | test('merge-sorted-lists should convert linked list result to array', async () => { 61 | const solution = `function ListNode(val, next) { 62 | this.val = (val===undefined ? 0 : val); 63 | this.next = (next===undefined ? null : next); 64 | } 65 | 66 | function mergeTwoLists(list1, list2) { 67 | const dummy = new ListNode(0); 68 | let current = dummy; 69 | while (list1 && list2) { 70 | if (list1.val <= list2.val) { 71 | current.next = list1; 72 | list1 = list1.next; 73 | } else { 74 | current.next = list2; 75 | list2 = list2.next; 76 | } 77 | current = current.next; 78 | } 79 | current.next = list1 || list2; 80 | return dummy.next; 81 | } 82 | module.exports = mergeTwoLists;`; 83 | 84 | const req = createMockReq('POST', { 85 | id: 'merge-sorted-lists', 86 | code: solution, 87 | language: 'javascript' 88 | }); 89 | 90 | const res = createMockRes(); 91 | 92 | await handler(req, res); 93 | 94 | expect(res._statusCode).toBe(200); 95 | expect(res._jsonData).toBeDefined(); 96 | expect(res._jsonData.status).toBe('success'); 97 | expect(res._jsonData.passed).toBe(res._jsonData.total); 98 | expect(res._jsonData.passed).toBe(3); // merge-sorted-lists has 3 test cases 99 | 100 | // Verify each test case passed 101 | res._jsonData.results.forEach((result, index) => { 102 | expect(result.passed).toBe(true); 103 | expect(result.error).toBeNull(); 104 | expect(Array.isArray(result.actual)).toBe(true); 105 | expect(Array.isArray(result.expected)).toBe(true); 106 | }); 107 | 108 | // Verify specific test cases 109 | const testCase1 = res._jsonData.results.find(r => r.input === '[1,2,4],[1,3,4]'); 110 | expect(testCase1).toBeDefined(); 111 | expect(testCase1.actual).toEqual([1, 1, 2, 3, 4, 4]); 112 | expect(testCase1.expected).toEqual([1, 1, 2, 3, 4, 4]); 113 | 114 | const testCase2 = res._jsonData.results.find(r => r.input === '[],[]'); 115 | expect(testCase2).toBeDefined(); 116 | expect(testCase2.actual).toEqual([]); 117 | expect(testCase2.expected).toEqual([]); 118 | 119 | const testCase3 = res._jsonData.results.find(r => r.input === '[],[0]'); 120 | expect(testCase3).toBeDefined(); 121 | expect(testCase3.actual).toEqual([0]); 122 | expect(testCase3.expected).toEqual([0]); 123 | }); 124 | }); 125 | 126 | describe('Non-Linked List Problems', () => { 127 | test('two-sum should work normally without linked list conversion', async () => { 128 | const solution = `function twoSum(nums, target) { 129 | const map = new Map(); 130 | for (let i = 0; i < nums.length; i++) { 131 | const complement = target - nums[i]; 132 | if (map.has(complement)) return [map.get(complement), i]; 133 | map.set(nums[i], i); 134 | } 135 | } 136 | module.exports = twoSum;`; 137 | 138 | const req = createMockReq('POST', { 139 | id: 'two-sum', 140 | code: solution, 141 | language: 'javascript' 142 | }); 143 | 144 | const res = createMockRes(); 145 | 146 | await handler(req, res); 147 | 148 | expect(res._statusCode).toBe(200); 149 | expect(res._jsonData).toBeDefined(); 150 | expect(res._jsonData.status).toBe('success'); 151 | expect(res._jsonData.passed).toBe(res._jsonData.total); 152 | 153 | // Verify each test case passed 154 | res._jsonData.results.forEach((result, index) => { 155 | expect(result.passed).toBe(true); 156 | expect(result.error).toBeNull(); 157 | expect(Array.isArray(result.actual)).toBe(true); 158 | expect(Array.isArray(result.expected)).toBe(true); 159 | }); 160 | }); 161 | 162 | test('reverse-integer should work normally', async () => { 163 | const solution = `function reverse(x) { 164 | const sign = x < 0 ? -1 : 1; 165 | const reversed = parseInt(Math.abs(x).toString().split('').reverse().join('')); 166 | if (reversed > 2**31 - 1) return 0; 167 | return sign * reversed; 168 | } 169 | module.exports = reverse;`; 170 | 171 | const req = createMockReq('POST', { 172 | id: 'reverse-integer', 173 | code: solution, 174 | language: 'javascript' 175 | }); 176 | 177 | const res = createMockRes(); 178 | 179 | await handler(req, res); 180 | 181 | expect(res._statusCode).toBe(200); 182 | expect(res._jsonData).toBeDefined(); 183 | expect(res._jsonData.status).toBe('success'); 184 | expect(res._jsonData.passed).toBe(res._jsonData.total); 185 | 186 | // Verify each test case passed 187 | res._jsonData.results.forEach((result, index) => { 188 | expect(result.passed).toBe(true); 189 | expect(result.error).toBeNull(); 190 | expect(typeof result.actual).toBe('number'); 191 | expect(typeof result.expected).toBe('number'); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('Error Handling', () => { 197 | test('Invalid problem ID should return 404', async () => { 198 | const req = createMockReq('POST', { 199 | id: 'non-existent-problem', 200 | code: 'function test() { return 42; }', 201 | language: 'javascript' 202 | }); 203 | 204 | const res = createMockRes(); 205 | 206 | await handler(req, res); 207 | 208 | expect(res._statusCode).toBe(404); 209 | expect(res._jsonData).toBeDefined(); 210 | expect(res._jsonData.error).toBe('Problem not found'); 211 | }); 212 | 213 | test('Invalid HTTP method should return 405', async () => { 214 | const req = createMockReq('GET', {}); 215 | const res = createMockRes(); 216 | 217 | await handler(req, res); 218 | 219 | expect(res._statusCode).toBe(405); 220 | }); 221 | 222 | test('Runtime error should be handled gracefully', async () => { 223 | const runtimeErrorCode = `function twoSum(nums, target) { 224 | throw new Error('Intentional runtime error'); 225 | } 226 | module.exports = twoSum;`; 227 | 228 | const req = createMockReq('POST', { 229 | id: 'two-sum', 230 | code: runtimeErrorCode, 231 | language: 'javascript' 232 | }); 233 | 234 | const res = createMockRes(); 235 | 236 | await handler(req, res); 237 | 238 | expect(res._statusCode).toBe(200); 239 | expect(res._jsonData.status).toBe('success'); 240 | expect(res._jsonData.passed).toBe(0); 241 | expect(res._jsonData.results.every(r => r.error !== null)).toBe(true); 242 | }); 243 | }); 244 | 245 | describe('Solution Validation', () => { 246 | test('All problems with JavaScript solutions should pass', async () => { 247 | const problemsWithJSSolutions = problems.filter(problem => 248 | problem.solution && problem.solution.js 249 | ); 250 | 251 | expect(problemsWithJSSolutions.length).toBeGreaterThan(0); 252 | console.log(`Testing ${problemsWithJSSolutions.length} JavaScript solutions...`); 253 | 254 | for (const problem of problemsWithJSSolutions) { 255 | console.log(`\nTesting ${problem.id} JavaScript solution...`); 256 | 257 | const req = createMockReq('POST', { 258 | id: problem.id, 259 | code: problem.solution.js, 260 | language: 'javascript' 261 | }); 262 | 263 | const res = createMockRes(); 264 | 265 | await handler(req, res); 266 | 267 | expect(res._statusCode).toBe(200); 268 | expect(res._jsonData).toBeDefined(); 269 | expect(res._jsonData.status).toBe('success'); 270 | 271 | if (res._jsonData.passed !== res._jsonData.total) { 272 | console.log(`\n❌ FAILING PROBLEM: ${problem.id}`); 273 | console.log(`Passed: ${res._jsonData.passed}/${res._jsonData.total}`); 274 | console.log('Failed test cases:'); 275 | res._jsonData.results.forEach((result, index) => { 276 | if (!result.passed) { 277 | console.log(` Test ${index + 1}: input=${result.input}, expected=${JSON.stringify(result.expected)}, actual=${JSON.stringify(result.actual)}, error=${result.error}`); 278 | } 279 | }); 280 | } 281 | 282 | expect(res._jsonData.passed).toBe(res._jsonData.total); 283 | expect(res._jsonData.passed).toBe(problem.tests.length); 284 | 285 | // Verify each individual test case 286 | res._jsonData.results.forEach((result, index) => { 287 | expect(result.passed).toBe(true); 288 | expect(result.error).toBeNull(); 289 | expect(result).toHaveProperty('executionTime'); 290 | expect(typeof result.executionTime).toBe('number'); 291 | expect(result.executionTime).toBeGreaterThanOrEqual(0); 292 | }); 293 | 294 | // Performance validation 295 | expect(res._jsonData.performance.totalExecutionTime).toBeGreaterThan(0); 296 | expect(res._jsonData.performance.averageExecutionTime).toBeGreaterThan(0); 297 | expect(res._jsonData.performance.totalExecutionTime).toBeLessThan(10000); // Should complete within 10 seconds 298 | 299 | console.log(`✅ ${problem.id}: ${res._jsonData.passed}/${res._jsonData.total} tests passed`); 300 | } 301 | }); 302 | }); 303 | 304 | describe('Performance Metrics', () => { 305 | test('Response should include performance metrics', async () => { 306 | const req = createMockReq('POST', { 307 | id: 'two-sum', 308 | code: `function twoSum(nums, target) { 309 | const map = new Map(); 310 | for (let i = 0; i < nums.length; i++) { 311 | const complement = target - nums[i]; 312 | if (map.has(complement)) return [map.get(complement), i]; 313 | map.set(nums[i], i); 314 | } 315 | } 316 | module.exports = twoSum;`, 317 | language: 'javascript' 318 | }); 319 | 320 | const res = createMockRes(); 321 | 322 | await handler(req, res); 323 | 324 | expect(res._jsonData).toHaveProperty('performance'); 325 | expect(res._jsonData.performance).toHaveProperty('totalExecutionTime'); 326 | expect(res._jsonData.performance).toHaveProperty('averageExecutionTime'); 327 | expect(res._jsonData.performance).toHaveProperty('memoryUsage'); 328 | expect(typeof res._jsonData.performance.totalExecutionTime).toBe('number'); 329 | expect(typeof res._jsonData.performance.averageExecutionTime).toBe('number'); 330 | }); 331 | }); 332 | }); --------------------------------------------------------------------------------