From 3f5da90e3b209dc987a60604f82335a4b30f8815 Mon Sep 17 00:00:00 2001 From: alenryuichi Date: Tue, 24 Feb 2026 18:59:20 +0800 Subject: [PATCH] feat: add Deepseek & Zhipu API support with code quality improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Add Deepseek and Zhipu/GLM API providers alongside OpenAI, Gemini, Anthropic - Implement per-provider API key storage - Add GLM-4.5 and GLM-5 model options to Settings UI - Add collapsible "解题思路" section with markdown rendering - Fix malformed JSON from GLM API (Chinese quotes, single-quoted values) Security: - Add DOMPurify to sanitize AI-generated HTML content (XSS prevention) Code Quality: - Extract API URLs to constants (API_URLS) - Refactor Zhipu API calls into reusable callZhipuAPI() method - Remove unused variable (stringStart) - Remove debug console.log statements - Delete temporary test file (test-detect-quotes.js) Dependencies: - Add jsonrepair for JSON fixing - Add dompurify for HTML sanitization --- electron/ConfigHelper.ts | 204 ++++++- electron/ProcessingHelper.ts | 591 ++++++++++++++++----- package-lock.json | 36 ++ package.json | 3 + src/_pages/Solutions.tsx | 91 +++- src/components/Settings/SettingsDialog.tsx | 290 +++++++++- 6 files changed, 990 insertions(+), 225 deletions(-) diff --git a/electron/ConfigHelper.ts b/electron/ConfigHelper.ts index 6d1d2dba..3090fe33 100644 --- a/electron/ConfigHelper.ts +++ b/electron/ConfigHelper.ts @@ -5,14 +5,26 @@ import { app } from "electron" import { EventEmitter } from "events" import { OpenAI } from "openai" +// API URL constants for Chinese AI providers +const API_URLS = { + deepseek: 'https://api.deepseek.com', + zhipu: 'https://open.bigmodel.cn/api/paas/v4' +} as const; + interface Config { - apiKey: string; - apiProvider: "openai" | "gemini" | "anthropic"; // Added provider selection + apiKey: string; // Legacy: used for OpenAI/Gemini/Anthropic + apiProvider: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu"; extractionModel: string; solutionModel: string; debuggingModel: string; language: string; opacity: number; + // Separate API keys for each provider + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; } export class ConfigHelper extends EventEmitter { @@ -58,7 +70,7 @@ export class ConfigHelper extends EventEmitter { /** * Validate and sanitize model selection to ensure only allowed models are used */ - private sanitizeModelSelection(model: string, provider: "openai" | "gemini" | "anthropic"): string { + private sanitizeModelSelection(model: string, provider: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu"): string { if (provider === "openai") { // Only allow gpt-4o and gpt-4o-mini for OpenAI const allowedModels = ['gpt-4o', 'gpt-4o-mini']; @@ -72,10 +84,10 @@ export class ConfigHelper extends EventEmitter { const allowedModels = ['gemini-1.5-pro', 'gemini-2.0-flash']; if (!allowedModels.includes(model)) { console.warn(`Invalid Gemini model specified: ${model}. Using default model: gemini-2.0-flash`); - return 'gemini-2.0-flash'; // Changed default to flash + return 'gemini-2.0-flash'; } return model; - } else if (provider === "anthropic") { + } else if (provider === "anthropic") { // Only allow Claude models const allowedModels = ['claude-3-7-sonnet-20250219', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229']; if (!allowedModels.includes(model)) { @@ -83,6 +95,27 @@ export class ConfigHelper extends EventEmitter { return 'claude-3-7-sonnet-20250219'; } return model; + } else if (provider === "deepseek") { + // Deepseek models - OpenAI compatible API + const allowedModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner']; + if (!allowedModels.includes(model)) { + console.warn(`Invalid Deepseek model specified: ${model}. Using default model: deepseek-chat`); + return 'deepseek-chat'; + } + return model; + } else if (provider === "zhipu") { + // Zhipu/GLM models (including GLM-4.5 and GLM-5 series) + const allowedModels = [ + 'glm-4v-flash', 'glm-4v-plus', // Vision models + 'glm-4-flash', 'glm-4-plus', 'glm-4-long', // GLM-4 text models + 'glm-4.5', 'glm-4.5-air', 'glm-4.5-airx', // GLM-4.5 series + 'glm-5', 'glm-5-plus' // GLM-5 series (latest) + ]; + if (!allowedModels.includes(model)) { + console.warn(`Invalid Zhipu model specified: ${model}. Using default model: glm-4v-flash`); + return 'glm-4v-flash'; + } + return model; } // Default fallback return model; @@ -95,7 +128,8 @@ export class ConfigHelper extends EventEmitter { const config = JSON.parse(configData); // Ensure apiProvider is a valid value - if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic") { + const validProviders = ["openai", "gemini", "anthropic", "deepseek", "zhipu"]; + if (!validProviders.includes(config.apiProvider)) { config.apiProvider = "gemini"; // Default to Gemini if invalid } @@ -178,6 +212,14 @@ export class ConfigHelper extends EventEmitter { updates.extractionModel = "claude-3-7-sonnet-20250219"; updates.solutionModel = "claude-3-7-sonnet-20250219"; updates.debuggingModel = "claude-3-7-sonnet-20250219"; + } else if (updates.apiProvider === "deepseek") { + updates.extractionModel = "deepseek-chat"; + updates.solutionModel = "deepseek-chat"; + updates.debuggingModel = "deepseek-chat"; + } else if (updates.apiProvider === "zhipu") { + updates.extractionModel = "glm-4v-flash"; + updates.solutionModel = "glm-4v-flash"; + updates.debuggingModel = "glm-4v-flash"; } else { updates.extractionModel = "gemini-2.0-flash"; updates.solutionModel = "gemini-2.0-flash"; @@ -195,18 +237,34 @@ export class ConfigHelper extends EventEmitter { if (updates.debuggingModel) { updates.debuggingModel = this.sanitizeModelSelection(updates.debuggingModel, provider); } - + + // Save API key to the provider-specific field + if (updates.apiKey !== undefined) { + const targetProvider = updates.apiProvider || currentConfig.apiProvider; + if (targetProvider === "openai") { + updates.openaiApiKey = updates.apiKey; + } else if (targetProvider === "gemini") { + updates.geminiApiKey = updates.apiKey; + } else if (targetProvider === "anthropic") { + updates.anthropicApiKey = updates.apiKey; + } else if (targetProvider === "deepseek") { + updates.deepseekApiKey = updates.apiKey; + } else if (targetProvider === "zhipu") { + updates.zhipuApiKey = updates.apiKey; + } + } + const newConfig = { ...currentConfig, ...updates }; this.saveConfig(newConfig); - + // Only emit update event for changes other than opacity // This prevents re-initializing the AI client when only opacity changes - if (updates.apiKey !== undefined || updates.apiProvider !== undefined || - updates.extractionModel !== undefined || updates.solutionModel !== undefined || + if (updates.apiKey !== undefined || updates.apiProvider !== undefined || + updates.extractionModel !== undefined || updates.solutionModel !== undefined || updates.debuggingModel !== undefined || updates.language !== undefined) { this.emit('config-updated', newConfig); } - + return newConfig; } catch (error) { console.error('Error updating config:', error); @@ -215,17 +273,41 @@ export class ConfigHelper extends EventEmitter { } /** - * Check if the API key is configured + * Get the API key for the current provider */ - public hasApiKey(): boolean { + public getApiKeyForProvider(provider?: string): string { const config = this.loadConfig(); - return !!config.apiKey && config.apiKey.trim().length > 0; + const targetProvider = provider || config.apiProvider; + + // First check provider-specific keys + if (targetProvider === "openai" && config.openaiApiKey) { + return config.openaiApiKey; + } else if (targetProvider === "gemini" && config.geminiApiKey) { + return config.geminiApiKey; + } else if (targetProvider === "anthropic" && config.anthropicApiKey) { + return config.anthropicApiKey; + } else if (targetProvider === "deepseek" && config.deepseekApiKey) { + return config.deepseekApiKey; + } else if (targetProvider === "zhipu" && config.zhipuApiKey) { + return config.zhipuApiKey; + } + + // Fallback to legacy apiKey field + return config.apiKey || ""; + } + + /** + * Check if the API key is configured for the current provider + */ + public hasApiKey(): boolean { + const apiKey = this.getApiKeyForProvider(); + return !!apiKey && apiKey.trim().length > 0; } /** * Validate the API key format */ - public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" ): boolean { + public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu"): boolean { // If provider is not specified, attempt to auto-detect if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -238,18 +320,24 @@ export class ConfigHelper extends EventEmitter { provider = "gemini"; } } - + if (provider === "openai") { // Basic format validation for OpenAI API keys return /^sk-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); } else if (provider === "gemini") { // Basic format validation for Gemini API keys (usually alphanumeric with no specific prefix) - return apiKey.trim().length >= 10; // Assuming Gemini keys are at least 10 chars + return apiKey.trim().length >= 10; } else if (provider === "anthropic") { // Basic format validation for Anthropic API keys return /^sk-ant-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); + } else if (provider === "deepseek") { + // Deepseek API keys typically start with "sk-" + return apiKey.trim().length >= 10; + } else if (provider === "zhipu") { + // Zhipu/GLM API keys - format varies, just check length + return apiKey.trim().length >= 10; } - + return false; } @@ -288,7 +376,7 @@ export class ConfigHelper extends EventEmitter { /** * Test API key with the selected provider */ - public async testApiKey(apiKey: string, provider?: "openai" | "gemini" | "anthropic"): Promise<{valid: boolean, error?: string}> { + public async testApiKey(apiKey: string, provider?: "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu"): Promise<{valid: boolean, error?: string}> { // Auto-detect provider based on key format if not specified if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -304,15 +392,19 @@ export class ConfigHelper extends EventEmitter { console.log("Using Gemini API key format for testing (default)"); } } - + if (provider === "openai") { return this.testOpenAIKey(apiKey); } else if (provider === "gemini") { return this.testGeminiKey(apiKey); } else if (provider === "anthropic") { return this.testAnthropicKey(apiKey); + } else if (provider === "deepseek") { + return this.testDeepseekKey(apiKey); + } else if (provider === "zhipu") { + return this.testZhipuKey(apiKey); } - + return { valid: false, error: "Unknown API provider" }; } @@ -386,11 +478,77 @@ export class ConfigHelper extends EventEmitter { } catch (error: any) { console.error('Anthropic API key test failed:', error); let errorMessage = 'Unknown error validating Anthropic API key'; - + if (error.message) { errorMessage = `Error: ${error.message}`; } - + + return { valid: false, error: errorMessage }; + } + } + + /** + * Test Deepseek API key + * Deepseek uses OpenAI-compatible API + */ + private async testDeepseekKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + // Deepseek uses OpenAI-compatible API with different base URL + const openai = new OpenAI({ + apiKey, + baseURL: API_URLS.deepseek + }); + // Make a simple API call to test the key + await openai.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('Deepseek API key test failed:', error); + + let errorMessage = 'Unknown error validating Deepseek API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your Deepseek key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your Deepseek API key has reached its request limit.'; + } else if (error.status === 500) { + errorMessage = 'Deepseek server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + + return { valid: false, error: errorMessage }; + } + } + + /** + * Test Zhipu/GLM API key + * Zhipu uses OpenAI-compatible API + */ + private async testZhipuKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + // Zhipu uses OpenAI-compatible API with different base URL + const openai = new OpenAI({ + apiKey, + baseURL: API_URLS.zhipu + }); + // Make a simple API call to test the key + await openai.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('Zhipu API key test failed:', error); + + let errorMessage = 'Unknown error validating Zhipu API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your Zhipu key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your Zhipu API key has reached its request limit.'; + } else if (error.status === 500) { + errorMessage = 'Zhipu server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + return { valid: false, error: errorMessage }; } } diff --git a/electron/ProcessingHelper.ts b/electron/ProcessingHelper.ts index 0dcd26f0..bd0497ad 100644 --- a/electron/ProcessingHelper.ts +++ b/electron/ProcessingHelper.ts @@ -8,6 +8,13 @@ import { app, BrowserWindow, dialog } from "electron" import { OpenAI } from "openai" import { configHelper } from "./ConfigHelper" import Anthropic from '@anthropic-ai/sdk'; +import { jsonrepair } from 'jsonrepair'; + +// API URL constants for Chinese AI providers +const API_URLS = { + deepseek: 'https://api.deepseek.com', + zhipu: 'https://open.bigmodel.cn/api/paas/v4' +} as const; // Interface for Gemini API requests interface GeminiMessage { @@ -73,53 +80,69 @@ export class ProcessingHelper { private initializeAIClient(): void { try { const config = configHelper.loadConfig(); - + const apiKey = configHelper.getApiKeyForProvider(); + + // Reset all clients first + this.openaiClient = null; + this.geminiApiKey = null; + this.anthropicClient = null; + if (config.apiProvider === "openai") { - if (config.apiKey) { - this.openaiClient = new OpenAI({ - apiKey: config.apiKey, + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, timeout: 60000, // 60 second timeout maxRetries: 2 // Retry up to 2 times }); - this.geminiApiKey = null; - this.anthropicClient = null; console.log("OpenAI client initialized successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, OpenAI client not initialized"); } - } else if (config.apiProvider === "gemini"){ + } else if (config.apiProvider === "gemini") { // Gemini client initialization - this.openaiClient = null; - this.anthropicClient = null; - if (config.apiKey) { - this.geminiApiKey = config.apiKey; + if (apiKey) { + this.geminiApiKey = apiKey; console.log("Gemini API key set successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, Gemini client not initialized"); } } else if (config.apiProvider === "anthropic") { - // Reset other clients - this.openaiClient = null; - this.geminiApiKey = null; - if (config.apiKey) { + if (apiKey) { this.anthropicClient = new Anthropic({ - apiKey: config.apiKey, + apiKey: apiKey, timeout: 60000, maxRetries: 2 }); console.log("Anthropic client initialized successfully"); } else { - this.openaiClient = null; - this.geminiApiKey = null; - this.anthropicClient = null; console.warn("No API key available, Anthropic client not initialized"); } + } else if (config.apiProvider === "deepseek") { + // Deepseek uses OpenAI-compatible API + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, + baseURL: API_URLS.deepseek, + timeout: 60000, + maxRetries: 2 + }); + console.log("Deepseek client initialized successfully (OpenAI-compatible)"); + } else { + console.warn("No API key available, Deepseek client not initialized"); + } + } else if (config.apiProvider === "zhipu") { + // Zhipu/GLM uses OpenAI-compatible API + if (apiKey) { + this.openaiClient = new OpenAI({ + apiKey: apiKey, + baseURL: API_URLS.zhipu, + timeout: 60000, + maxRetries: 2 + }); + console.log("Zhipu/GLM client initialized successfully (OpenAI-compatible)"); + } else { + console.warn("No API key available, Zhipu client not initialized"); + } } } catch (error) { console.error("Failed to initialize AI client:", error); @@ -129,6 +152,144 @@ export class ProcessingHelper { } } + /** + * Fix malformed JSON from GLM API: + * 1. Single-quoted string values: "key": 'value' -> "key": "value" + * 2. Unescaped quotes inside string values: "例如:"abc"" -> "例如:\"abc\"" + * 3. Chinese curly quotes: " " -> escaped regular quotes + */ + private fixChineseQuotesInJson(text: string): string { + // Step 1: Replace single-quoted values with double-quoted values + let result = ''; + let i = 0; + + while (i < text.length) { + if (text[i] === ':') { + result += ':'; + i++; + + // Skip whitespace after colon + while (i < text.length && /\s/.test(text[i])) { + result += text[i]; + i++; + } + + // Check if the value starts with a single quote + if (i < text.length && text[i] === "'") { + // Find the matching closing single quote (followed by , } ] or end) + let endPos = -1; + let j = i + 1; + while (j < text.length) { + if (text[j] === "'") { + let k = j + 1; + while (k < text.length && /\s/.test(text[k])) k++; + if (k >= text.length || text[k] === ',' || text[k] === '}' || text[k] === ']') { + endPos = j; + break; + } + } + j++; + } + + if (endPos !== -1) { + const content = text.substring(i + 1, endPos); + const escaped = content.replace(/"/g, '\\"'); + result += '"' + escaped + '"'; + i = endPos + 1; + continue; + } + } + } else { + result += text[i]; + i++; + } + } + + // Step 2: Fix unescaped quotes inside JSON string values + // This handles cases like: "problem": "例如:"0.1" 和 "1.2"" + // The inner quotes need to be escaped + let finalResult = ''; + let inString = false; + i = 0; + + while (i < result.length) { + const char = result[i]; + + if (char === '"') { + // Check if this quote is escaped + let backslashCount = 0; + let j = i - 1; + while (j >= 0 && result[j] === '\\') { + backslashCount++; + j--; + } + const isEscaped = backslashCount % 2 === 1; + + if (!isEscaped) { + if (!inString) { + // Starting a string + inString = true; + finalResult += char; + } else { + // This might be the end of string OR an unescaped quote inside + // Look ahead to determine if this is really the end of the string + let k = i + 1; + while (k < result.length && /\s/.test(result[k])) k++; + + // If followed by : , } ] or end of input, it's a real string end + if (k >= result.length || result[k] === ':' || result[k] === ',' || + result[k] === '}' || result[k] === ']') { + inString = false; + finalResult += char; + } else { + // This is an unescaped quote inside the string - escape it + finalResult += '\\"'; + } + } + } else { + finalResult += char; + } + } + // Replace Chinese curly quotes with escaped regular quotes + else if (inString && (char === '\u201C' || char === '\u201D')) { + finalResult += '\\"'; + } + // Replace Chinese single quotes + else if (inString && (char === '\u2018' || char === '\u2019')) { + finalResult += "'"; + } + else { + finalResult += char; + } + i++; + } + + return finalResult; + } + + /** + * Make a Zhipu API call using native HTTP (bypasses OpenAI SDK quirks) + */ + private async callZhipuAPI( + messages: Array<{ role: string; content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> }>, + model: string, + timeout: number = 60000 + ): Promise<{ choices: Array<{ message: { content: string } }> }> { + const zhipuApiKey = configHelper.getApiKeyForProvider("zhipu"); + const response = await axios.default.post( + `${API_URLS.zhipu}/chat/completions`, + { model, messages }, + { + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${zhipuApiKey}` + }, + timeout + } + ); + return response.data; + } + private async waitForInitialization( mainWindow: BrowserWindow ): Promise { @@ -459,63 +620,120 @@ export class ProcessingHelper { } let problemInfo; - - if (config.apiProvider === "openai") { - // Verify OpenAI client + + // OpenAI, Deepseek, and Zhipu all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu") { + // Verify OpenAI-compatible client if (!this.openaiClient) { this.initializeAIClient(); // Try to reinitialize - + if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured or invalid. Please check your settings." + error: `${providerName} API key not configured or invalid. Please check your settings.` }; } } - // Use OpenAI for processing - const messages = [ - { - role: "system" as const, - content: "You are a coding challenge interpreter. Analyze the screenshot of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text." - }, - { - role: "user" as const, - content: [ - { - type: "text" as const, - text: `Extract the coding problem details from these screenshots. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` - }, - ...imageDataList.map(data => ({ - type: "image_url" as const, - image_url: { url: `data:image/png;base64,${data}` } - })) - ] - } - ]; + // Get the appropriate model for the provider + let extractionModel = config.extractionModel; + if (config.apiProvider === "deepseek") { + extractionModel = extractionModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + // GLM-4V models support vision + extractionModel = extractionModel || "glm-4v-flash"; + } else { + extractionModel = extractionModel || "gpt-4o"; + } - // Send to OpenAI Vision API - const extractionResponse = await this.openaiClient.chat.completions.create({ - model: config.extractionModel || "gpt-4o", - messages: messages, - max_tokens: 4000, - temperature: 0.2 - }); + // Build messages based on provider + let messages; + + let extractionResponse; + + if (config.apiProvider === "zhipu") { + // Zhipu GLM-4V: Use native HTTP request to bypass OpenAI SDK quirks + const systemPrompt = "You are a coding challenge interpreter. Analyze the screenshot of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text."; + + // For Zhipu, use only the first image if multiple + const firstImage = imageDataList[0]; + const imageUrl = firstImage.startsWith('data:') ? firstImage : `data:image/png;base64,${firstImage}`; + + const zhipuMessages = [ + { + role: "user", + content: [ + { + type: "text", + text: `${systemPrompt}\n\nExtract the coding problem details from this screenshot. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` + }, + { + type: "image_url", + image_url: { url: imageUrl } + } + ] + } + ]; + + extractionResponse = await this.callZhipuAPI(zhipuMessages, extractionModel, 60000); + } else { + // OpenAI and Deepseek: standard format via OpenAI SDK + messages = [ + { + role: "system" as const, + content: "You are a coding challenge interpreter. Analyze the screenshot of the coding problem and extract all relevant information. Return the information in JSON format with these fields: problem_statement, constraints, example_input, example_output. Just return the structured JSON without any other text." + }, + { + role: "user" as const, + content: [ + { + type: "text" as const, + text: `Extract the coding problem details from these screenshots. Return in JSON format. Preferred coding language we gonna use for this problem is ${language}.` + }, + ...imageDataList.map(data => ({ + type: "image_url" as const, + image_url: { url: `data:image/png;base64,${data}` } + })) + ] + } + ]; + + // Send to OpenAI-compatible Vision API + extractionResponse = await this.openaiClient.chat.completions.create({ + model: extractionModel, + messages: messages, + max_tokens: 4000, + temperature: 0.2 + }); + } // Parse the response try { const responseText = extractionResponse.choices[0].message.content; - // Handle when OpenAI might wrap the JSON in markdown code blocks - const jsonText = responseText.replace(/```json|```/g, '').trim(); - problemInfo = JSON.parse(jsonText); + // Handle when AI might wrap the JSON in markdown code blocks + let jsonText = responseText.replace(/```json\n?|```\n?/g, '').trim(); + // Also try to extract JSON from text if AI added extra content + const jsonMatch = jsonText.match(/\{[\s\S]*\}/); + if (jsonMatch) { + jsonText = jsonMatch[0]; + } + // Fix Chinese quotes and single-quoted arrays using character traversal + // This safely handles Chinese quotes (U+201C, U+201D) inside JSON strings + jsonText = this.fixChineseQuotesInJson(jsonText); + + // Use jsonrepair to fix remaining issues (handles single quotes, trailing commas, etc.) + const repairedJson = jsonrepair(jsonText); + problemInfo = JSON.parse(repairedJson); } catch (error) { - console.error("Error parsing OpenAI response:", error); + console.error("Error parsing API response:", error); return { success: false, error: "Failed to parse problem information. Please try again or use clearer screenshots." }; } - } else if (config.apiProvider === "gemini") { + } else if (config.apiProvider === "gemini") { // Use Gemini API if (!this.geminiApiKey) { return { @@ -735,57 +953,86 @@ export class ProcessingHelper { // Create prompt for solution generation const promptText = ` -Generate a detailed solution for the following coding problem: +请为以下编程题目提供详细的解答。**重要:除了代码本身,所有文字说明必须使用中文!** -PROBLEM STATEMENT: +【题目描述】 ${problemInfo.problem_statement} -CONSTRAINTS: -${problemInfo.constraints || "No specific constraints provided."} - -EXAMPLE INPUT: -${problemInfo.example_input || "No example input provided."} +【约束条件】 +${problemInfo.constraints || "未提供具体约束条件。"} -EXAMPLE OUTPUT: -${problemInfo.example_output || "No example output provided."} +【输入示例】 +${problemInfo.example_input || "未提供输入示例。"} -LANGUAGE: ${language} +【输出示例】 +${problemInfo.example_output || "未提供输出示例。"} -I need the response in the following format: -1. Code: A clean, optimized implementation in ${language} -2. Your Thoughts: A list of key insights and reasoning behind your approach -3. Time complexity: O(X) with a detailed explanation (at least 2 sentences) -4. Space complexity: O(X) with a detailed explanation (at least 2 sentences) +【编程语言】${language} -For complexity explanations, please be thorough. For example: "Time complexity: O(n) because we iterate through the array only once. This is optimal as we need to examine each element at least once to find the solution." or "Space complexity: O(n) because in the worst case, we store all elements in the hashmap. The additional space scales linearly with the input size." +请严格按以下格式回答: +1. **代码**:提供一个简洁、高效的 ${language} 实现(代码注释用中文) +2. **解题思路**:用中文列出你的关键思路和推理过程,例如: + - **回溯法**:我们使用回溯法探索所有可能的组合... + - **剪枝优化**:当某个条件不满足时提前终止... +3. **时间复杂度**:O(X),用中文详细解释原因(至少2句话) +4. **空间复杂度**:O(X),用中文详细解释原因(至少2句话) -Your solution should be efficient, well-commented, and handle edge cases. +**注意**:解题思路、复杂度分析等所有说明文字必须是中文!代码中的注释也要用中文! `; let responseContent; - - if (config.apiProvider === "openai") { - // OpenAI processing + + // OpenAI, Deepseek, and Zhipu all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu") { if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured. Please check your settings." + error: `${providerName} API key not configured. Please check your settings.` }; } - - // Send to OpenAI API - const solutionResponse = await this.openaiClient.chat.completions.create({ - model: config.solutionModel || "gpt-4o", - messages: [ - { role: "system", content: "You are an expert coding interview assistant. Provide clear, optimal solutions with detailed explanations." }, - { role: "user", content: promptText } - ], - max_tokens: 4000, - temperature: 0.2 - }); - responseContent = solutionResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + // Get the appropriate model for the provider + // Re-read config to get latest solutionModel + const latestConfig = configHelper.loadConfig(); + let solutionModel = latestConfig.solutionModel || config.solutionModel; + if (config.apiProvider === "deepseek") { + solutionModel = solutionModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + solutionModel = solutionModel || "glm-4-flash"; + } else { + solutionModel = solutionModel || "gpt-4o"; + } + + let solutionResponse; + + if (config.apiProvider === "zhipu") { + // Zhipu: Use native HTTP request + const zhipuMessages = [ + { role: "user", content: `你是一位资深的编程面试助手。请提供清晰、最优的解决方案,并附带详细的解释。\n\n${promptText}` } + ]; + + const zhipuResponse = await this.callZhipuAPI(zhipuMessages, solutionModel, 120000); + responseContent = zhipuResponse.choices[0].message.content; + } else { + // OpenAI/Deepseek: Use OpenAI SDK + const solutionMessages = [ + { role: "system" as const, content: "你是一位资深的编程面试助手。请提供清晰、最优的解决方案,并附带详细的解释。" }, + { role: "user" as const, content: promptText } + ]; + + // Send to OpenAI-compatible API + solutionResponse = await this.openaiClient.chat.completions.create({ + model: solutionModel, + messages: solutionMessages, + max_tokens: 4000, + temperature: 0.2 + }); + + responseContent = solutionResponse.choices[0].message.content; + } + } else if (config.apiProvider === "gemini") { // Gemini processing if (!this.geminiApiKey) { return { @@ -801,7 +1048,7 @@ Your solution should be efficient, well-commented, and handle edge cases. role: "user", parts: [ { - text: `You are an expert coding interview assistant. Provide a clear, optimal solution with detailed explanations for this problem:\n\n${promptText}` + text: `你是一位资深的编程面试助手。请为以下问题提供清晰、最优的解决方案,并附带详细的解释:\n\n${promptText}` } ] } @@ -850,7 +1097,7 @@ Your solution should be efficient, well-commented, and handle edge cases. content: [ { type: "text" as const, - text: `You are an expert coding interview assistant. Provide a clear, optimal solution with detailed explanations for this problem:\n\n${promptText}` + text: `你是一位资深的编程面试助手。请为以下问题提供清晰、最优的解决方案,并附带详细的解释:\n\n${promptText}` } ] } @@ -892,32 +1139,40 @@ Your solution should be efficient, well-commented, and handle edge cases. const codeMatch = responseContent.match(/```(?:\w+)?\s*([\s\S]*?)```/); const code = codeMatch ? codeMatch[1].trim() : responseContent; - // Extract thoughts, looking for bullet points or numbered lists - const thoughtsRegex = /(?:Thoughts:|Key Insights:|Reasoning:|Approach:)([\s\S]*?)(?:Time complexity:|$)/i; + // Extract thoughts, looking for bullet points or numbered lists (supports both English and Chinese) + const thoughtsRegex = /(?:Thoughts:|Key Insights:|Reasoning:|Approach:|解题思路|思路|关键思路)[::]?\s*([\s\S]*?)(?:Time complexity:|时间复杂度|$)/i; const thoughtsMatch = responseContent.match(thoughtsRegex); let thoughts: string[] = []; - + if (thoughtsMatch && thoughtsMatch[1]) { - // Extract bullet points or numbered items - const bulletPoints = thoughtsMatch[1].match(/(?:^|\n)\s*(?:[-*•]|\d+\.)\s*(.*)/g); + // Extract bullet points - be careful not to match markdown bold (**text**) + // Only match single dash/asterisk at line start, not double asterisks + const bulletPoints = thoughtsMatch[1].match(/(?:^|\n)\s*(?:[-•]|\d+\.)\s+(.+)/g); if (bulletPoints) { - thoughts = bulletPoints.map(point => - point.replace(/^\s*(?:[-*•]|\d+\.)\s*/, '').trim() + thoughts = bulletPoints.map(point => + point.replace(/^\s*(?:[-•]|\d+\.)\s+/, '').trim() ).filter(Boolean); } else { // If no bullet points found, split by newlines and filter empty lines + // Keep markdown formatting intact thoughts = thoughtsMatch[1].split('\n') .map((line) => line.trim()) - .filter(Boolean); + .filter(line => { + // Filter out empty lines, code blocks, and orphaned markdown markers + if (!line || line.startsWith('```')) return false; + // Filter out lines that are only asterisks or markdown markers + if (/^[\*\-_]+$/.test(line)) return false; + return true; + }); } } - // Extract complexity information - const timeComplexityPattern = /Time complexity:?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:Space complexity|$))/i; - const spaceComplexityPattern = /Space complexity:?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:[A-Z]|$))/i; + // Extract complexity information (supports both English and Chinese) + const timeComplexityPattern = /(?:Time complexity|时间复杂度)[::]?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:Space complexity|空间复杂度|$))/i; + const spaceComplexityPattern = /(?:Space complexity|空间复杂度)[::]?\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:[A-Z#]|$))/i; - let timeComplexity = "O(n) - Linear time complexity because we only iterate through the array once. Each element is processed exactly one time, and the hashmap lookups are O(1) operations."; - let spaceComplexity = "O(n) - Linear space complexity because we store elements in the hashmap. In the worst case, we might need to store all elements before finding the solution pair."; + let timeComplexity = "O(n) - 线性时间复杂度,因为我们只遍历数组一次。每个元素只处理一次,哈希表查找是 O(1) 操作。"; + let spaceComplexity = "O(n) - 线性空间复杂度,因为我们在哈希表中存储元素。最坏情况下需要存储所有元素。"; const timeMatch = responseContent.match(timeComplexityPattern); if (timeMatch && timeMatch[1]) { @@ -951,7 +1206,7 @@ Your solution should be efficient, well-commented, and handle edge cases. const formattedResponse = { code: code, - thoughts: thoughts.length > 0 ? thoughts : ["Solution approach based on efficiency and readability"], + thoughts: thoughts.length > 0 ? thoughts : ["基于效率和可读性的解题方法"], time_complexity: timeComplexity, space_complexity: spaceComplexity }; @@ -1008,19 +1263,30 @@ Your solution should be efficient, well-commented, and handle edge cases. const imageDataList = screenshots.map(screenshot => screenshot.data); let debugContent; - - if (config.apiProvider === "openai") { + + // OpenAI, Deepseek, and Zhipu all use OpenAI-compatible API + if (config.apiProvider === "openai" || config.apiProvider === "deepseek" || config.apiProvider === "zhipu") { if (!this.openaiClient) { + const providerName = config.apiProvider === "deepseek" ? "Deepseek" : + config.apiProvider === "zhipu" ? "Zhipu/GLM" : "OpenAI"; return { success: false, - error: "OpenAI API key not configured. Please check your settings." + error: `${providerName} API key not configured. Please check your settings.` }; } - - const messages = [ - { - role: "system" as const, - content: `You are a coding interview assistant helping debug and improve solutions. Analyze these screenshots which include either error messages, incorrect outputs, or test cases, and provide detailed debugging help. + + // Get the appropriate model for the provider + let debuggingModel = config.debuggingModel; + if (config.apiProvider === "deepseek") { + debuggingModel = debuggingModel || "deepseek-chat"; + } else if (config.apiProvider === "zhipu") { + // GLM-4V models support vision + debuggingModel = debuggingModel || "glm-4v-flash"; + } else { + debuggingModel = debuggingModel || "gpt-4o"; + } + + const systemPrompt = `You are a coding interview assistant helping debug and improve solutions. Analyze these screenshots which include either error messages, incorrect outputs, or test cases, and provide detailed debugging help. Your response MUST follow this exact structure with these section headers (use ### for headers): ### Issues Identified @@ -1038,26 +1304,13 @@ Here provide a clear explanation of why the changes are needed ### Key Points - Summary bullet points of the most important takeaways -If you include code examples, use proper markdown code blocks with language specification (e.g. \`\`\`java).` - }, - { - role: "user" as const, - content: [ - { - type: "text" as const, - text: `I'm solving this coding problem: "${problemInfo.problem_statement}" in ${language}. I need help with debugging or improving my solution. Here are screenshots of my code, the errors or test cases. Please provide a detailed analysis with: +If you include code examples, use proper markdown code blocks with language specification (e.g. \`\`\`java).`; + + const userPrompt = `I'm solving this coding problem: "${problemInfo.problem_statement}" in ${language}. I need help with debugging or improving my solution. Here are screenshots of my code, the errors or test cases. Please provide a detailed analysis with: 1. What issues you found in my code 2. Specific improvements and corrections 3. Any optimizations that would make the solution better -4. A clear explanation of the changes needed` - }, - ...imageDataList.map(data => ({ - type: "image_url" as const, - image_url: { url: `data:image/png;base64,${data}` } - })) - ] - } - ]; +4. A clear explanation of the changes needed`; if (mainWindow) { mainWindow.webContents.send("processing-status", { @@ -1066,15 +1319,61 @@ If you include code examples, use proper markdown code blocks with language spec }); } - const debugResponse = await this.openaiClient.chat.completions.create({ - model: config.debuggingModel || "gpt-4o", - messages: messages, - max_tokens: 4000, - temperature: 0.2 - }); - - debugContent = debugResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + if (config.apiProvider === "zhipu") { + // Zhipu: Use native HTTP request for vision + const firstImage = imageDataList[0]; + const imageUrl = firstImage.startsWith('data:') ? firstImage : `data:image/png;base64,${firstImage}`; + + const zhipuMessages = [ + { + role: "user", + content: [ + { + type: "text", + text: `${systemPrompt}\n\n${userPrompt}` + }, + { + type: "image_url", + image_url: { url: imageUrl } + } + ] + } + ]; + + const zhipuResponse = await this.callZhipuAPI(zhipuMessages, debuggingModel, 120000); + debugContent = zhipuResponse.choices[0].message.content; + } else { + // OpenAI/Deepseek: Use OpenAI SDK + const debugMessages = [ + { + role: "system" as const, + content: systemPrompt + }, + { + role: "user" as const, + content: [ + { + type: "text" as const, + text: userPrompt + }, + ...imageDataList.map(data => ({ + type: "image_url" as const, + image_url: { url: `data:image/png;base64,${data}` } + })) + ] + } + ]; + + const debugResponse = await this.openaiClient.chat.completions.create({ + model: debuggingModel, + messages: debugMessages, + max_tokens: 4000, + temperature: 0.2 + }); + + debugContent = debugResponse.choices[0].message.content; + } + } else if (config.apiProvider === "gemini") { if (!this.geminiApiKey) { return { success: false, diff --git a/package-lock.json b/package-lock.json index da87e3a0..4f6cfbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,15 +19,18 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "diff": "^7.0.0", + "dompurify": "^3.3.1", "dotenv": "^16.4.7", "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", "form-data": "^4.0.1", + "jsonrepair": "^3.13.2", "lucide-react": "^0.460.0", "openai": "^4.28.4", "react": "^18.2.0", @@ -2847,6 +2850,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/electron-store": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/electron-store/-/electron-store-1.3.1.tgz", @@ -3037,6 +3049,12 @@ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5451,6 +5469,15 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -7809,6 +7836,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonrepair": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.2.tgz", + "integrity": "sha512-Leuly0nbM4R+S5SVJk3VHfw1oxnlEK9KygdZvfUtEtTawNDyzB4qa1xWTmFt1aeoA7sXZkVTRuIixJ8bAvqVUg==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 1fffcfb5..608a4f1d 100644 --- a/package.json +++ b/package.json @@ -132,15 +132,18 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/dompurify": "^3.0.5", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "diff": "^7.0.0", + "dompurify": "^3.3.1", "dotenv": "^16.4.7", "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", "form-data": "^4.0.1", + "jsonrepair": "^3.13.2", "lucide-react": "^0.460.0", "openai": "^4.28.4", "react": "^18.2.0", diff --git a/src/_pages/Solutions.tsx b/src/_pages/Solutions.tsx index e1939451..176a5cb4 100644 --- a/src/_pages/Solutions.tsx +++ b/src/_pages/Solutions.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef } from "react" import { useQuery, useQueryClient } from "@tanstack/react-query" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" +import DOMPurify from "dompurify" import ScreenshotQueue from "../components/Queue/ScreenshotQueue" @@ -15,29 +16,48 @@ import { COMMAND_KEY } from "../utils/platform" export const ContentSection = ({ title, content, - isLoading + isLoading, + collapsible = false, + defaultCollapsed = false }: { title: string content: React.ReactNode isLoading: boolean -}) => ( -
-

- {title} -

- {isLoading ? ( -
-

- Extracting problem statement... -

-
- ) : ( -
- {content} -
- )} -
-) + collapsible?: boolean + defaultCollapsed?: boolean +}) => { + const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); + + return ( +
+

collapsible && setIsCollapsed(!isCollapsed)} + > + {collapsible && ( + + )} + {title} + {collapsible && ( + + {isCollapsed ? '(点击展开)' : '(点击收起)'} + + )} +

+ {isLoading ? ( +
+

+ Extracting problem statement... +

+
+ ) : ( +
+ {content} +
+ )} +
+ ); +} const SolutionSection = ({ title, content, @@ -524,20 +544,33 @@ const Solutions: React.FC = ({ {solutionData && ( <>
- {thoughtsData.map((thought, index) => ( -
-
-
{thought}
-
- ))} + {thoughtsData.map((thought, index) => { + // Simple markdown rendering: **bold** -> bold + const formattedThought = thought + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1'); + // Sanitize HTML to prevent XSS attacks from AI-generated content + const sanitizedHtml = DOMPurify.sanitize(formattedThought, { + ALLOWED_TAGS: ['strong', 'code'], + ALLOWED_ATTR: ['class'] + }); + return ( +
+
+
+
+ ); + })}
) diff --git a/src/components/Settings/SettingsDialog.tsx b/src/components/Settings/SettingsDialog.tsx index 463ea120..72734402 100644 --- a/src/components/Settings/SettingsDialog.tsx +++ b/src/components/Settings/SettingsDialog.tsx @@ -13,7 +13,7 @@ import { Button } from "../ui/button"; import { Settings } from "lucide-react"; import { useToast } from "../../contexts/toast"; -type APIProvider = "openai" | "gemini" | "anthropic"; +type APIProvider = "openai" | "gemini" | "anthropic" | "deepseek" | "zhipu"; type AIModel = { id: string; @@ -28,6 +28,8 @@ type ModelCategory = { openaiModels: AIModel[]; geminiModels: AIModel[]; anthropicModels: AIModel[]; + deepseekModels: AIModel[]; + zhipuModels: AIModel[]; }; // Define available models for each category @@ -76,6 +78,35 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for general coding tasks" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code generation" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning capabilities" + } + ], + zhipuModels: [ + { + id: "glm-4v-flash", + name: "GLM-4V Flash", + description: "Fast vision model for screenshots" + }, + { + id: "glm-4v-plus", + name: "GLM-4V Plus", + description: "Enhanced vision model" + } ] }, { @@ -122,6 +153,45 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for general coding tasks" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code generation" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning capabilities" + } + ], + zhipuModels: [ + { + id: "glm-4-flash", + name: "GLM-4 Flash", + description: "Fast and efficient text model" + }, + { + id: "glm-4-plus", + name: "GLM-4 Plus", + description: "Enhanced text model" + }, + { + id: "glm-4.5", + name: "GLM-4.5", + description: "Latest GLM-4.5 model (recommended)" + }, + { + id: "glm-5", + name: "GLM-5", + description: "Latest GLM-5 series" + } ] }, { @@ -168,6 +238,35 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + deepseekModels: [ + { + id: "deepseek-chat", + name: "DeepSeek Chat", + description: "Best for debugging and code analysis" + }, + { + id: "deepseek-coder", + name: "DeepSeek Coder", + description: "Specialized for code analysis" + }, + { + id: "deepseek-reasoner", + name: "DeepSeek Reasoner", + description: "Advanced reasoning for complex bugs" + } + ], + zhipuModels: [ + { + id: "glm-4v-flash", + name: "GLM-4V Flash", + description: "Fast vision model for debugging screenshots" + }, + { + id: "glm-4v-plus", + name: "GLM-4V Plus", + description: "Enhanced vision model" + } ] } ]; @@ -203,6 +302,32 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia } }; + // Store all provider API keys + const [providerApiKeys, setProviderApiKeys] = useState<{ + openai?: string; + gemini?: string; + anthropic?: string; + deepseek?: string; + zhipu?: string; + }>({}); + + // Helper to get API key for a specific provider + const getApiKeyForProvider = (provider: APIProvider, config: { + apiKey?: string; + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; + }): string => { + if (provider === "openai" && config.openaiApiKey) return config.openaiApiKey; + if (provider === "gemini" && config.geminiApiKey) return config.geminiApiKey; + if (provider === "anthropic" && config.anthropicApiKey) return config.anthropicApiKey; + if (provider === "deepseek" && config.deepseekApiKey) return config.deepseekApiKey; + if (provider === "zhipu" && config.zhipuApiKey) return config.zhipuApiKey; + return config.apiKey || ""; + }; + // Load current config on dialog open useEffect(() => { if (open) { @@ -213,13 +338,28 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia extractionModel?: string; solutionModel?: string; debuggingModel?: string; + openaiApiKey?: string; + geminiApiKey?: string; + anthropicApiKey?: string; + deepseekApiKey?: string; + zhipuApiKey?: string; } window.electronAPI .getConfig() .then((config: Config) => { - setApiKey(config.apiKey || ""); - setApiProvider(config.apiProvider || "openai"); + const provider = config.apiProvider || "openai"; + // Store all provider keys + setProviderApiKeys({ + openai: config.openaiApiKey || "", + gemini: config.geminiApiKey || "", + anthropic: config.anthropicApiKey || "", + deepseek: config.deepseekApiKey || "", + zhipu: config.zhipuApiKey || "", + }); + // Set current provider's API key + setApiKey(getApiKeyForProvider(provider, config)); + setApiProvider(provider); setExtractionModel(config.extractionModel || "gpt-4o"); setSolutionModel(config.solutionModel || "gpt-4o"); setDebuggingModel(config.debuggingModel || "gpt-4o"); @@ -236,8 +376,18 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia // Handle API provider change const handleProviderChange = (provider: APIProvider) => { + // Save current API key to the current provider before switching + setProviderApiKeys(prev => ({ + ...prev, + [apiProvider]: apiKey + })); + + // Switch to new provider setApiProvider(provider); - + + // Load the API key for the new provider + setApiKey(providerApiKeys[provider] || ""); + // Reset models to defaults when changing provider if (provider === "openai") { setExtractionModel("gpt-4o"); @@ -251,6 +401,14 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia setExtractionModel("claude-3-7-sonnet-20250219"); setSolutionModel("claude-3-7-sonnet-20250219"); setDebuggingModel("claude-3-7-sonnet-20250219"); + } else if (provider === "deepseek") { + setExtractionModel("deepseek-chat"); + setSolutionModel("deepseek-chat"); + setDebuggingModel("deepseek-chat"); + } else if (provider === "zhipu") { + setExtractionModel("glm-4v-flash"); + setSolutionModel("glm-4-flash"); + setDebuggingModel("glm-4v-flash"); } }; @@ -387,13 +545,57 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia
+
+
handleProviderChange("deepseek")} + > +
+
+
+

DeepSeek

+

DeepSeek models

+
+
+
+
handleProviderChange("zhipu")} + > +
+
+
+

智谱 GLM

+

GLM-4 models

+
+
+
+
setApiKey(e.target.value)} placeholder={ - apiProvider === "openai" ? "sk-..." : + apiProvider === "openai" ? "sk-..." : apiProvider === "gemini" ? "Enter your Gemini API key" : - "sk-ant-..." + apiProvider === "anthropic" ? "sk-ant-..." : + apiProvider === "deepseek" ? "sk-..." : + "Enter your Zhipu API key" } className="bg-black/50 border-white/10 text-white" /> @@ -413,46 +617,76 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia

)}

- Your API key is stored locally and never sent to any server except {apiProvider === "openai" ? "OpenAI" : "Google"} + Your API key is stored locally and never sent to any server except { + apiProvider === "openai" ? "OpenAI" : + apiProvider === "gemini" ? "Google" : + apiProvider === "anthropic" ? "Anthropic" : + apiProvider === "deepseek" ? "DeepSeek" : + "智谱 AI" + }

Don't have an API key?

{apiProvider === "openai" ? ( <> -

1. Create an account at

-

2. Go to section

3. Create a new secret key and paste it here

- ) : apiProvider === "gemini" ? ( + ) : apiProvider === "gemini" ? ( <> -

1. Create an account at

-

2. Go to the section

3. Create a new API key and paste it here

- ) : ( + ) : apiProvider === "anthropic" ? ( <> -

1. Create an account at

-

2. Go to the section

3. Create a new API key and paste it here

+ ) : apiProvider === "deepseek" ? ( + <> +

1. Create an account at +

+

2. Go to the section +

+

3. Create a new API key and paste it here

+ + ) : ( + <> +

1. 在 注册账号 +

+

2. 进入 页面 +

+

3. 创建新的 API Key 并粘贴到这里

+ )}
@@ -508,10 +742,12 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia {modelCategories.map((category) => { // Get the appropriate model list based on selected provider - const models = - apiProvider === "openai" ? category.openaiModels : + const models = + apiProvider === "openai" ? category.openaiModels : apiProvider === "gemini" ? category.geminiModels : - category.anthropicModels; + apiProvider === "anthropic" ? category.anthropicModels : + apiProvider === "deepseek" ? category.deepseekModels : + category.zhipuModels; return (