diff --git a/electron/ConfigHelper.ts b/electron/ConfigHelper.ts index 6d1d2db..17ed1c2 100644 --- a/electron/ConfigHelper.ts +++ b/electron/ConfigHelper.ts @@ -5,26 +5,34 @@ import { app } from "electron" import { EventEmitter } from "events" import { OpenAI } from "openai" +export type ApiProvider = "openai" | "gemini" | "anthropic"; + interface Config { - apiKey: string; - apiProvider: "openai" | "gemini" | "anthropic"; // Added provider selection + apiKeys: Record; // Map of provider to API key + apiProvider: ApiProvider; // Currently selected provider extractionModel: string; solutionModel: string; debuggingModel: string; language: string; opacity: number; + keyboardModifier: string; // For keyboard shortcut configuration } export class ConfigHelper extends EventEmitter { private configPath: string; private defaultConfig: Config = { - apiKey: "", + apiKeys: { + openai: "", + gemini: "", + anthropic: "" + }, apiProvider: "gemini", // Default to Gemini - extractionModel: "gemini-2.0-flash", // Default to Flash for faster responses + extractionModel: "gemini-2.0-flash", solutionModel: "gemini-2.0-flash", debuggingModel: "gemini-2.0-flash", language: "python", - opacity: 1.0 + opacity: 0.8, + keyboardModifier: "CommandOrControl" }; constructor() { @@ -55,61 +63,26 @@ 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 { - if (provider === "openai") { - // Only allow gpt-4o and gpt-4o-mini for OpenAI - const allowedModels = ['gpt-4o', 'gpt-4o-mini']; - if (!allowedModels.includes(model)) { - console.warn(`Invalid OpenAI model specified: ${model}. Using default model: gpt-4o`); - return 'gpt-4o'; - } - return model; - } else if (provider === "gemini") { - // Only allow gemini-1.5-pro and gemini-2.0-flash for Gemini - 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 model; - } 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)) { - console.warn(`Invalid Anthropic model specified: ${model}. Using default model: claude-3-7-sonnet-20250219`); - return 'claude-3-7-sonnet-20250219'; - } - return model; - } - // Default fallback - return model; - } - public loadConfig(): Config { try { if (fs.existsSync(this.configPath)) { const configData = fs.readFileSync(this.configPath, 'utf8'); - const config = JSON.parse(configData); + let config = JSON.parse(configData); // Ensure apiProvider is a valid value - if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic") { + if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic") { config.apiProvider = "gemini"; // Default to Gemini if invalid } - // Sanitize model selections to ensure only allowed models are used - if (config.extractionModel) { - config.extractionModel = this.sanitizeModelSelection(config.extractionModel, config.apiProvider); - } - if (config.solutionModel) { - config.solutionModel = this.sanitizeModelSelection(config.solutionModel, config.apiProvider); - } - if (config.debuggingModel) { - config.debuggingModel = this.sanitizeModelSelection(config.debuggingModel, config.apiProvider); + // Ensure apiKeys is properly initialized + if (!config.apiKeys) { + config.apiKeys = { + openai: "", + gemini: "", + anthropic: "" + }; } - + return { ...this.defaultConfig, ...config @@ -143,65 +116,55 @@ export class ConfigHelper extends EventEmitter { } /** + * TODO: the default way of updating is kinda sus * Update specific configuration values */ - public updateConfig(updates: Partial): Config { + public updateConfig(updates: Partial & { apiKey?: string }): Config { try { + // get the current config const currentConfig = this.loadConfig(); - let provider = updates.apiProvider || currentConfig.apiProvider; - - // Auto-detect provider based on API key format if a new key is provided - if (updates.apiKey && !updates.apiProvider) { - // If API key starts with "sk-", it's likely an OpenAI key - if (updates.apiKey.trim().startsWith('sk-')) { - provider = "openai"; - console.log("Auto-detected OpenAI API key format"); - } else if (updates.apiKey.trim().startsWith('sk-ant-')) { - provider = "anthropic"; - console.log("Auto-detected Anthropic API key format"); - } else { - provider = "gemini"; - console.log("Using Gemini API key format (default)"); + + const _defaultModels = { + openai: { + extractionModel: "gpt-4o", + solutionModel: "gpt-4o", + debuggingModel: "gpt-4o" + }, + gemini: { + extractionModel: "gemini-2.0-flash", + solutionModel: "gemini-2.0-flash", + debuggingModel: "gemini-2.0-flash" + }, + anthropic: { + extractionModel: "claude-3-7-sonnet-20250219", + solutionModel: "claude-3-7-sonnet-20250219", + debuggingModel: "claude-3-7-sonnet-20250219" } - - // Update the provider in the updates object - updates.apiProvider = provider; } - + // If provider is changing, reset models to the default for that provider if (updates.apiProvider && updates.apiProvider !== currentConfig.apiProvider) { if (updates.apiProvider === "openai") { - updates.extractionModel = "gpt-4o"; - updates.solutionModel = "gpt-4o"; - updates.debuggingModel = "gpt-4o"; + updates.extractionModel = _defaultModels.openai.extractionModel; + updates.solutionModel = _defaultModels.openai.solutionModel; + updates.debuggingModel = _defaultModels.openai.debuggingModel; + } else if (updates.apiProvider === "gemini") { + updates.extractionModel = _defaultModels.gemini.extractionModel; + updates.solutionModel = _defaultModels.gemini.solutionModel; + updates.debuggingModel = _defaultModels.gemini.debuggingModel; } else if (updates.apiProvider === "anthropic") { - updates.extractionModel = "claude-3-7-sonnet-20250219"; - updates.solutionModel = "claude-3-7-sonnet-20250219"; - updates.debuggingModel = "claude-3-7-sonnet-20250219"; - } else { - updates.extractionModel = "gemini-2.0-flash"; - updates.solutionModel = "gemini-2.0-flash"; - updates.debuggingModel = "gemini-2.0-flash"; + updates.extractionModel = _defaultModels.anthropic.extractionModel; + updates.solutionModel = _defaultModels.anthropic.solutionModel; + updates.debuggingModel = _defaultModels.anthropic.debuggingModel; } } - // Sanitize model selections in the updates - if (updates.extractionModel) { - updates.extractionModel = this.sanitizeModelSelection(updates.extractionModel, provider); - } - if (updates.solutionModel) { - updates.solutionModel = this.sanitizeModelSelection(updates.solutionModel, provider); - } - if (updates.debuggingModel) { - updates.debuggingModel = this.sanitizeModelSelection(updates.debuggingModel, provider); - } - 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 || + if (updates.apiKeys !== undefined || updates.apiProvider !== undefined || updates.extractionModel !== undefined || updates.solutionModel !== undefined || updates.debuggingModel !== undefined || updates.language !== undefined) { this.emit('config-updated', newConfig); @@ -215,17 +178,33 @@ export class ConfigHelper extends EventEmitter { } /** - * Check if the API key is configured + * Get the current API key for the selected provider + */ + public getCurrentApiKey(): string { + const config = this.loadConfig(); + return config.apiKeys[config.apiProvider] || ""; + } + + /** + * Get API key for a specific provider + */ + public getApiKeyForProvider(provider: ApiProvider): string { + const config = this.loadConfig(); + return config.apiKeys[provider] || ""; + } + + /** + * Check if the current API provider has an API key configured */ public hasApiKey(): boolean { const config = this.loadConfig(); - return !!config.apiKey && config.apiKey.trim().length > 0; + return !!config.apiKeys[config.apiProvider] && config.apiKeys[config.apiProvider].trim().length > 0; } /** * Validate the API key format */ - public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" ): boolean { + public isValidApiKeyFormat(apiKey: string, provider?: ApiProvider): boolean { // If provider is not specified, attempt to auto-detect if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -288,7 +267,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?: ApiProvider): Promise<{valid: boolean, error?: string}> { // Auto-detect provider based on key format if not specified if (!provider) { if (apiKey.trim().startsWith('sk-')) { @@ -325,20 +304,22 @@ export class ConfigHelper extends EventEmitter { // Make a simple API call to test the key await openai.models.list(); return { valid: true }; - } catch (error: any) { + } catch (error: unknown) { console.error('OpenAI API key test failed:', error); // Determine the specific error type for better error messages let errorMessage = 'Unknown error validating OpenAI API key'; - if (error.status === 401) { + const err = error as { status?: number; message?: string }; + + if (err.status === 401) { errorMessage = 'Invalid API key. Please check your OpenAI key and try again.'; - } else if (error.status === 429) { + } else if (err.status === 429) { errorMessage = 'Rate limit exceeded. Your OpenAI API key has reached its request limit or has insufficient quota.'; - } else if (error.status === 500) { + } else if (err.status === 500) { errorMessage = 'OpenAI server error. Please try again later.'; - } else if (error.message) { - errorMessage = `Error: ${error.message}`; + } else if (err.message) { + errorMessage = `Error: ${err.message}`; } return { valid: false, error: errorMessage }; @@ -358,12 +339,14 @@ export class ConfigHelper extends EventEmitter { return { valid: true }; } return { valid: false, error: 'Invalid Gemini API key format.' }; - } catch (error: any) { + } catch (error: unknown) { console.error('Gemini API key test failed:', error); let errorMessage = 'Unknown error validating Gemini API key'; - if (error.message) { - errorMessage = `Error: ${error.message}`; + const err = error as { message?: string }; + + if (err.message) { + errorMessage = `Error: ${err.message}`; } return { valid: false, error: errorMessage }; @@ -383,17 +366,35 @@ export class ConfigHelper extends EventEmitter { return { valid: true }; } return { valid: false, error: 'Invalid Anthropic API key format.' }; - } catch (error: any) { + } catch (error: unknown) { console.error('Anthropic API key test failed:', error); let errorMessage = 'Unknown error validating Anthropic API key'; - if (error.message) { - errorMessage = `Error: ${error.message}`; + const err = error as { message?: string }; + + if (err.message) { + errorMessage = `Error: ${err.message}`; } return { valid: false, error: errorMessage }; } } + + /** + * Get the configured keyboard modifier + */ + public getKeyboardModifier(): string { + const config = this.loadConfig(); + return config.keyboardModifier || this.defaultConfig.keyboardModifier; + } + + /** + * Set the keyboard modifier for shortcuts + */ + public setKeyboardModifier(modifier: string): void { + this.updateConfig({ keyboardModifier: modifier }); + this.emit('keyboard-modifier-updated', modifier); + } } // Export a singleton instance diff --git a/electron/ProcessingHelper.ts b/electron/ProcessingHelper.ts index 0dcd26f..eff0866 100644 --- a/electron/ProcessingHelper.ts +++ b/electron/ProcessingHelper.ts @@ -73,11 +73,15 @@ export class ProcessingHelper { private initializeAIClient(): void { try { const config = configHelper.loadConfig(); + const apiKey = config.apiKeys[config.apiProvider] + console.log("config.apiProvider", config.apiProvider) + console.log("config.apiKeys", config.apiKeys) + console.log("apiKey", apiKey) if (config.apiProvider === "openai") { - if (config.apiKey) { + if (apiKey) { this.openaiClient = new OpenAI({ - apiKey: config.apiKey, + apiKey: apiKey, timeout: 60000, // 60 second timeout maxRetries: 2 // Retry up to 2 times }); @@ -94,8 +98,8 @@ export class ProcessingHelper { // 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; @@ -107,9 +111,9 @@ export class ProcessingHelper { // 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 }); @@ -666,10 +670,7 @@ export class ProcessingHelper { progress: 100 }); - mainWindow.webContents.send( - this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, - solutionsResult.data - ); + // The solution success event is already sent in generateSolutionsHelper return { success: true, data: solutionsResult.data }; } else { throw new Error( @@ -725,17 +726,25 @@ export class ProcessingHelper { throw new Error("No problem info available"); } - // Update progress status + // Initial setup for all API calls + const apiProvider = config.apiProvider; + let finalResponse = { + code: "", + thoughts: [] as string[], + time_complexity: "", + space_complexity: "" + }; + + // ================ STAGE 1: EDGE CASES ================ if (mainWindow) { mainWindow.webContents.send("processing-status", { - message: "Creating optimal solution with detailed explanations...", - progress: 60 + message: "Analyzing edge cases...", + progress: 40 }); } - // Create prompt for solution generation - const promptText = ` -Generate a detailed solution for the following coding problem: + const edgeCasePrompt = ` +Analyze the following coding problem and identify potential edge cases: PROBLEM STATEMENT: ${problemInfo.problem_statement} @@ -751,212 +760,582 @@ ${problemInfo.example_output || "No example output provided."} LANGUAGE: ${language} -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) - -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." - -Your solution should be efficient, well-commented, and handle edge cases. +Just list 3-5 important edge cases that a solution needs to handle. +Format the output as a simple list of edge cases without explanations. +Keep each edge case to a single line and make them very concise. `; - let responseContent; + let edgeCases: string[] = []; - if (config.apiProvider === "openai") { - // OpenAI processing - if (!this.openaiClient) { - return { - success: false, - error: "OpenAI API key not configured. Please check your settings." - }; - } - - // Send to OpenAI API - const solutionResponse = await this.openaiClient.chat.completions.create({ + // API Call for edge cases based on provider + if (apiProvider === "openai" && this.openaiClient) { + const edgeCaseResponse = 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 } + { role: "system", content: "You are an expert coding interview assistant. Identify critical edge cases for coding problems. List them concisely without explanations." }, + { role: "user", content: edgeCasePrompt } ], - max_tokens: 4000, + max_tokens: 1000, temperature: 0.2 }); - - responseContent = solutionResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { - // Gemini processing - if (!this.geminiApiKey) { - return { - success: false, - error: "Gemini API key not configured. Please check your settings." - }; - } - try { - // Create Gemini message structure - const geminiMessages = [ - { - 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}` - } - ] - } - ]; + const responseText = edgeCaseResponse.choices[0].message.content; + edgeCases = this.extractBulletPoints(responseText); + } + else if (apiProvider === "gemini" && this.geminiApiKey) { + // Gemini implementation for edge cases + const geminiMessages = [ + { + role: "user", + parts: [{ text: edgeCasePrompt }] + } + ]; - // Make API request to Gemini - const response = await axios.default.post( - `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, - { - contents: geminiMessages, - generationConfig: { - temperature: 0.2, - maxOutputTokens: 4000 - } - }, - { signal } - ); + const response = await axios.default.post( + `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, + { + contents: geminiMessages, + generationConfig: { + temperature: 0.2, + maxOutputTokens: 1000 + } + }, + { signal } + ); - const responseData = response.data as GeminiResponse; - - if (!responseData.candidates || responseData.candidates.length === 0) { - throw new Error("Empty response from Gemini API"); - } - - responseContent = responseData.candidates[0].content.parts[0].text; - } catch (error) { - console.error("Error using Gemini API for solution:", error); - return { - success: false, - error: "Failed to generate solution with Gemini API. Please check your API key or try again later." - }; - } - } else if (config.apiProvider === "anthropic") { - // Anthropic processing - if (!this.anthropicClient) { - return { - success: false, - error: "Anthropic API key not configured. Please check your settings." - }; + const responseData = response.data as GeminiResponse; + if (responseData.candidates && responseData.candidates.length > 0) { + const responseText = responseData.candidates[0].content.parts[0].text; + edgeCases = this.extractBulletPoints(responseText); } - - try { - const messages = [ - { - role: "user" as const, - 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}` - } - ] - } - ]; + } + else if (apiProvider === "anthropic" && this.anthropicClient) { + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: edgeCasePrompt }] + } + ]; - // Send to Anthropic API - const response = await this.anthropicClient.messages.create({ - model: config.solutionModel || "claude-3-7-sonnet-20250219", - max_tokens: 4000, - messages: messages, - temperature: 0.2 - }); + const response = await this.anthropicClient.messages.create({ + model: config.solutionModel || "claude-3-7-sonnet-20250219", + max_tokens: 1000, + messages: messages, + temperature: 0.2 + }); - responseContent = (response.content[0] as { type: 'text', text: string }).text; - } catch (error: any) { - console.error("Error using Anthropic API for solution:", error); + const responseText = (response.content[0] as { type: 'text', text: string }).text; + edgeCases = this.extractBulletPoints(responseText); + } + + // Update thoughts with edge cases + finalResponse.thoughts = edgeCases; - // Add specific handling for Claude's limitations - if (error.status === 429) { - return { - success: false, - error: "Claude API rate limit exceeded. Please wait a few minutes before trying again." - }; - } else if (error.status === 413 || (error.message && error.message.includes("token"))) { - return { - success: false, - error: "Your screenshots contain too much information for Claude to process. Switch to OpenAI or Gemini in settings which can handle larger inputs." - }; + // Send edge case results + if (mainWindow) { + console.log("EDGE_CASES_EXTRACTED - Sending data:", { thoughts: edgeCases }); + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.EDGE_CASES_EXTRACTED, + { thoughts: edgeCases } + ); + } + + // ================ STAGE 2: SOLUTION THINKING ================ + if (mainWindow) { + mainWindow.webContents.send("processing-status", { + message: "Developing solution insights...", + progress: 50 + }); + } + + const solutionThinkingPrompt = ` +For the following coding problem, provide very concise insights about your approach: + +PROBLEM STATEMENT: +${problemInfo.problem_statement} + +CONSTRAINTS: +${problemInfo.constraints || "No specific constraints provided."} + +EXAMPLE INPUT: +${problemInfo.example_input || "No example input provided."} + +EXAMPLE OUTPUT: +${problemInfo.example_output || "No example output provided."} + +LANGUAGE: ${language} + +Provide exactly 3-5 key insights about how to approach this problem. +Each insight must be: +1. Presented as a bullet point +2. Maximum 1-2 sentences each +3. Clear and direct +4. Focused on a single insight or technique + +IMPORTANT: Keep each bullet point extremely concise and direct. +`; + + let solutionThoughts: string[] = []; + + // API Call for solution thinking based on provider + if (apiProvider === "openai" && this.openaiClient) { + const thinkingResponse = await this.openaiClient.chat.completions.create({ + model: config.solutionModel || "gpt-4o", + messages: [ + { role: "system", content: "You are an expert coding interview assistant. Provide extremely concise insights (1-2 sentences per point max)." }, + { role: "user", content: solutionThinkingPrompt } + ], + max_tokens: 1500, + temperature: 0.2 + }); + + const responseText = thinkingResponse.choices[0].message.content; + solutionThoughts = this.extractBulletPoints(responseText); + } + else if (apiProvider === "gemini" && this.geminiApiKey) { + // Gemini implementation for solution thinking + const geminiMessages = [ + { + role: "user", + parts: [{ text: solutionThinkingPrompt }] } + ]; - return { - success: false, - error: "Failed to generate solution with Anthropic API. Please check your API key or try again later." - }; + const response = await axios.default.post( + `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, + { + contents: geminiMessages, + generationConfig: { + temperature: 0.2, + maxOutputTokens: 1500 + } + }, + { signal } + ); + + const responseData = response.data as GeminiResponse; + if (responseData.candidates && responseData.candidates.length > 0) { + const responseText = responseData.candidates[0].content.parts[0].text; + solutionThoughts = this.extractBulletPoints(responseText); } } + else if (apiProvider === "anthropic" && this.anthropicClient) { + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: solutionThinkingPrompt }] + } + ]; + + const response = await this.anthropicClient.messages.create({ + model: config.solutionModel || "claude-3-7-sonnet-20250219", + max_tokens: 1500, + messages: messages, + temperature: 0.2 + }); + + const responseText = (response.content[0] as { type: 'text', text: string }).text; + solutionThoughts = this.extractBulletPoints(responseText); + } - // Extract parts from the response - const codeMatch = responseContent.match(/```(?:\w+)?\s*([\s\S]*?)```/); - const code = codeMatch ? codeMatch[1].trim() : responseContent; + // Do NOT accumulate thoughts - keep them separated by section + // finalResponse.thoughts = [...finalResponse.thoughts, ...solutionThoughts]; - // Extract thoughts, looking for bullet points or numbered lists - const thoughtsRegex = /(?:Thoughts:|Key Insights:|Reasoning:|Approach:)([\s\S]*?)(?:Time complexity:|$)/i; - const thoughtsMatch = responseContent.match(thoughtsRegex); + // Log the breakdown of thoughts for debugging + console.log("Thoughts breakdown:"); + console.log("- Edge cases:", edgeCases); + console.log("- Solution thinking:", solutionThoughts); + + // Send solution thinking results + if (mainWindow) { + console.log("SOLUTION_THINKING - Sending data:", { + solutionThoughts: solutionThoughts + }); + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.SOLUTION_THINKING, + { + thoughts: solutionThoughts, // Only send solution thoughts, not accumulated + edgeCases: edgeCases, + solutionThoughts: solutionThoughts + } + ); + } + + // ================ STAGE 3: APPROACH AND PSEUDOCODE ================ + if (mainWindow) { + mainWindow.webContents.send("processing-status", { + message: "Developing solution approach...", + progress: 65 + }); + } + + const approachPrompt = ` +Develop a solution approach 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."} + +EXAMPLE OUTPUT: +${problemInfo.example_output || "No example output provided."} + +LANGUAGE: ${language} + +First, provide a high-level algorithm approach (step by step). + +THEN, PROVIDE A SECTION CLEARLY LABELED AS "PSEUDOCODE:" that contains a detailed pseudocode implementation of your solution. +Format the pseudocode in a code block using triple backticks, like this: + +\`\`\` +function sampleAlgorithm(input): + // initialization steps + initialize variables + + // main logic with proper indentation + for each element in input: + if condition: + do something + nested operations + else: + do something else + + // return the result + return result +\`\`\` + +IMPORTANT FORMATTING REQUIREMENTS: +1. Use proper indentation (4 spaces per level) to show nested blocks and structure +2. Include clear comments before major sections +3. Use consistent naming and syntax +4. Structure the code with clear logical blocks +5. Make sure conditionals and loops are properly indented + +If your solution involves recursion, clearly explain the recursion logic, base case, and stopping condition. +If your solution involves dynamic programming, explain the DP table structure, state transitions, and base cases. +`; + let thoughts: string[] = []; + let pseudoCode = ""; - if (thoughtsMatch && thoughtsMatch[1]) { - // Extract bullet points or numbered items - const bulletPoints = thoughtsMatch[1].match(/(?:^|\n)\s*(?:[-*•]|\d+\.)\s*(.*)/g); - if (bulletPoints) { - 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 - thoughts = thoughtsMatch[1].split('\n') - .map((line) => line.trim()) - .filter(Boolean); + console.log("Sending approach prompt:", approachPrompt); + + // API Call for approach based on provider + if (apiProvider === "openai" && this.openaiClient) { + const approachResponse = await this.openaiClient.chat.completions.create({ + model: config.solutionModel || "gpt-4o", + messages: [ + { role: "system", content: "You are an expert coding interview assistant. Develop clear solution approaches for coding problems." }, + { role: "user", content: approachPrompt } + ], + max_tokens: 2000, + temperature: 0.2 + }); + + const responseText = approachResponse.choices[0].message.content; + console.log("Approach response full text:", responseText); + thoughts = this.extractBulletPoints(responseText); + pseudoCode = this.extractPseudoCode(responseText); + console.log("Extracted pseudocode:", pseudoCode); + } + else if (apiProvider === "gemini" && this.geminiApiKey) { + // Gemini implementation for approach + const geminiMessages = [ + { + role: "user", + parts: [{ text: approachPrompt }] + } + ]; + + const response = await axios.default.post( + `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, + { + contents: geminiMessages, + generationConfig: { + temperature: 0.2, + maxOutputTokens: 2000 + } + }, + { signal } + ); + + const responseData = response.data as GeminiResponse; + if (responseData.candidates && responseData.candidates.length > 0) { + const responseText = responseData.candidates[0].content.parts[0].text; + console.log("Gemini approach response:", responseText); + thoughts = this.extractBulletPoints(responseText); + pseudoCode = this.extractPseudoCode(responseText); + console.log("Extracted pseudocode from Gemini:", pseudoCode); } } + else if (apiProvider === "anthropic" && this.anthropicClient) { + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: approachPrompt }] + } + ]; + + const response = await this.anthropicClient.messages.create({ + model: config.solutionModel || "claude-3-7-sonnet-20250219", + max_tokens: 2000, + messages: messages, + temperature: 0.2 + }); + + const responseText = (response.content[0] as { type: 'text', text: string }).text; + console.log("Anthropic approach response:", responseText); + thoughts = this.extractBulletPoints(responseText); + pseudoCode = this.extractPseudoCode(responseText); + console.log("Extracted pseudocode from Anthropic:", pseudoCode); + } - // 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; - - 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."; - - const timeMatch = responseContent.match(timeComplexityPattern); - if (timeMatch && timeMatch[1]) { - timeComplexity = timeMatch[1].trim(); - if (!timeComplexity.match(/O\([^)]+\)/i)) { - timeComplexity = `O(n) - ${timeComplexity}`; - } else if (!timeComplexity.includes('-') && !timeComplexity.includes('because')) { - const notationMatch = timeComplexity.match(/O\([^)]+\)/i); - if (notationMatch) { - const notation = notationMatch[0]; - const rest = timeComplexity.replace(notation, '').trim(); - timeComplexity = `${notation} - ${rest}`; + // Update thoughts with approach insights + finalResponse.thoughts = [...edgeCases, ...solutionThoughts, ...thoughts]; + + // Send approach results + if (mainWindow) { + // Debug log to check what we're sending + console.log("APPROACH_DEVELOPED - Sending data with pseudo_code:", { + thoughts: thoughts.length, + pseudo_code: pseudoCode, + pseudo_code_length: pseudoCode ? pseudoCode.length : 0, + is_null: pseudoCode === null, + is_empty: pseudoCode === "", + typeof: typeof pseudoCode + }); + + console.log("APPROACH_DEVELOPED - Sending data:", { + thoughts: thoughts, + pseudo_code: pseudoCode + }); + + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.APPROACH_DEVELOPED, + { + thoughts: thoughts, // Only send approach thoughts, not accumulated + pseudo_code: pseudoCode } + ); + } + + // ================ STAGE 4: SOLUTION CODE ================ + if (mainWindow) { + mainWindow.webContents.send("processing-status", { + message: "Generating solution code...", + progress: 80 + }); + } + + const codePrompt = ` +Generate a clean, optimized 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."} + +EXAMPLE OUTPUT: +${problemInfo.example_output || "No example output provided."} + +LANGUAGE: ${language} + +Based on the following approach: +${pseudoCode} + +Provide only the full, runnable code solution. Make it efficient, handle edge cases, and include necessary comments for clarity. +`; + + // API Call for code based on provider + if (apiProvider === "openai" && this.openaiClient) { + const codeResponse = await this.openaiClient.chat.completions.create({ + model: config.solutionModel || "gpt-4o", + messages: [ + { role: "system", content: "You are an expert coding interview assistant. Generate clean, optimized code solutions." }, + { role: "user", content: codePrompt } + ], + max_tokens: 2000, + temperature: 0.2 + }); + + const responseText = codeResponse.choices[0].message.content; + finalResponse.code = this.extractCodeBlock(responseText); + } + else if (apiProvider === "gemini" && this.geminiApiKey) { + // Gemini implementation for code + const geminiMessages = [ + { + role: "user", + parts: [{ text: codePrompt }] + } + ]; + + const response = await axios.default.post( + `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, + { + contents: geminiMessages, + generationConfig: { + temperature: 0.2, + maxOutputTokens: 2000 + } + }, + { signal } + ); + + const responseData = response.data as GeminiResponse; + if (responseData.candidates && responseData.candidates.length > 0) { + const responseText = responseData.candidates[0].content.parts[0].text; + finalResponse.code = this.extractCodeBlock(responseText); } } + else if (apiProvider === "anthropic" && this.anthropicClient) { + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: codePrompt }] + } + ]; + + const response = await this.anthropicClient.messages.create({ + model: config.solutionModel || "claude-3-7-sonnet-20250219", + max_tokens: 2000, + messages: messages, + temperature: 0.2 + }); + + const responseText = (response.content[0] as { type: 'text', text: string }).text; + finalResponse.code = this.extractCodeBlock(responseText); + } - const spaceMatch = responseContent.match(spaceComplexityPattern); - if (spaceMatch && spaceMatch[1]) { - spaceComplexity = spaceMatch[1].trim(); - if (!spaceComplexity.match(/O\([^)]+\)/i)) { - spaceComplexity = `O(n) - ${spaceComplexity}`; - } else if (!spaceComplexity.includes('-') && !spaceComplexity.includes('because')) { - const notationMatch = spaceComplexity.match(/O\([^)]+\)/i); - if (notationMatch) { - const notation = notationMatch[0]; - const rest = spaceComplexity.replace(notation, '').trim(); - spaceComplexity = `${notation} - ${rest}`; + // Send code results + if (mainWindow) { + console.log("CODE_GENERATED - Sending data:", { + thoughts: finalResponse.thoughts, + code: finalResponse.code + }); + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.CODE_GENERATED, + { + thoughts: finalResponse.thoughts, + code: finalResponse.code + } + ); + } + + // ================ STAGE 5: COMPLEXITY ANALYSIS ================ + if (mainWindow) { + mainWindow.webContents.send("processing-status", { + message: "Analyzing time and space complexity...", + progress: 90 + }); + } + + const complexityPrompt = ` +Analyze the time and space complexity of the following solution: + +PROBLEM STATEMENT: +${problemInfo.problem_statement} + +SOLUTION: +${finalResponse.code} + +Provide a detailed analysis of: +1. Time Complexity: What is the Big O notation? Explain why in 2-3 sentences. +2. Space Complexity: What is the Big O notation? Explain why in 2-3 sentences. + +Be specific about how you arrived at your complexity analysis. +`; + + // API Call for complexity based on provider + if (apiProvider === "openai" && this.openaiClient) { + const complexityResponse = await this.openaiClient.chat.completions.create({ + model: config.solutionModel || "gpt-4o", + messages: [ + { role: "system", content: "You are an expert coding interview assistant. Provide detailed complexity analysis for algorithms." }, + { role: "user", content: complexityPrompt } + ], + max_tokens: 1000, + temperature: 0.2 + }); + + const responseText = complexityResponse.choices[0].message.content; + const complexities = this.extractComplexity(responseText); + finalResponse.time_complexity = complexities.time; + finalResponse.space_complexity = complexities.space; + } + else if (apiProvider === "gemini" && this.geminiApiKey) { + // Gemini implementation for complexity + const geminiMessages = [ + { + role: "user", + parts: [{ text: complexityPrompt }] } + ]; + + const response = await axios.default.post( + `https://generativelanguage.googleapis.com/v1beta/models/${config.solutionModel || "gemini-2.0-flash"}:generateContent?key=${this.geminiApiKey}`, + { + contents: geminiMessages, + generationConfig: { + temperature: 0.2, + maxOutputTokens: 1000 + } + }, + { signal } + ); + + const responseData = response.data as GeminiResponse; + if (responseData.candidates && responseData.candidates.length > 0) { + const responseText = responseData.candidates[0].content.parts[0].text; + const complexities = this.extractComplexity(responseText); + finalResponse.time_complexity = complexities.time; + finalResponse.space_complexity = complexities.space; } } + else if (apiProvider === "anthropic" && this.anthropicClient) { + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: complexityPrompt }] + } + ]; - const formattedResponse = { - code: code, - thoughts: thoughts.length > 0 ? thoughts : ["Solution approach based on efficiency and readability"], - time_complexity: timeComplexity, - space_complexity: spaceComplexity - }; + const response = await this.anthropicClient.messages.create({ + model: config.solutionModel || "claude-3-7-sonnet-20250219", + max_tokens: 1000, + messages: messages, + temperature: 0.2 + }); + + const responseText = (response.content[0] as { type: 'text', text: string }).text; + const complexities = this.extractComplexity(responseText); + finalResponse.time_complexity = complexities.time; + finalResponse.space_complexity = complexities.space; + } + + // Final solution success + if (mainWindow) { + console.log("SOLUTION_SUCCESS - Sending final data:", { + code: finalResponse.code, + thoughts: finalResponse.thoughts, + time_complexity: finalResponse.time_complexity, + space_complexity: finalResponse.space_complexity + }); + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, + finalResponse + ); + } - return { success: true, data: formattedResponse }; + return { success: true, data: finalResponse }; } catch (error: any) { if (axios.isCancel(error)) { return { @@ -982,6 +1361,113 @@ Your solution should be efficient, well-commented, and handle edge cases. } } + // Helper methods to extract different parts from AI responses + private extractBulletPoints(text: string): string[] { + // Find bullet points or numbered lists + const bulletRegex = /(?:^|\n)\s*(?:[-*•]|\d+\.)\s*(.*)/g; + const matches = [...text.matchAll(bulletRegex)]; + + if (matches.length > 0) { + return matches.map(match => match[1].trim()).filter(Boolean); + } + + // If no bullet points found, try to extract sentences + const sentences = text.split(/(?:[.!?]\s+|\n)/).map(s => s.trim()).filter(Boolean); + return sentences.slice(0, Math.min(5, sentences.length)); + } + + private extractPseudoCode(text: string): string { + console.log("Extracting pseudocode from text:", text.substring(0, 300) + "..."); + + // First try to extract code blocks - most reliable format + const codeBlockRegexes = [ + // Standard code block with pseudocode or algorithm language + /```(?:pseudocode|algorithm|plaintext)?\s*([\s\S]*?)```/i, + + // Any code block if the first pattern doesn't match + /```\s*([\s\S]*?)```/i + ]; + + for (const regex of codeBlockRegexes) { + const match = text.match(regex); + if (match && match[1] && match[1].trim().length > 0) { + const extracted = match[1].trim(); + console.log("Extracted pseudocode using code block pattern:", extracted); + return extracted; + } + } + + // Try to find pseudocode section between markers as fallback + const pseudoRegexes = [ + // Look for "Pseudo-code:" or "Pseudocode:" or "Algorithm:" heading + /(?:Pseudo[ -]?[Cc]ode:?|Algorithm:?)([\s\S]*?)(?=\n\s*(?:[A-Z][a-z]+:|\d+\.|$))/i, + + // Look for sections between headers + /(?:##\s*Pseudo[ -]?[Cc]ode|##\s*Algorithm)([\s\S]*?)(?=##|$)/i, + + // Look for numbered steps for algorithm + /(?:Steps:|Algorithm steps:|Approach:)([\s\S]*?)(?=\n\s*(?:[A-Z][a-z]+:|\d+\.|Time|Space|$))/i + ]; + + // Try each regex pattern + for (const regex of pseudoRegexes) { + const match = text.match(regex); + if (match && match[1] && match[1].trim().length > 0) { + const extracted = match[1].trim(); + console.log("Extracted pseudocode using heading pattern:", extracted); + return extracted; + } + } + + // Last resort: look for numbered steps (1., 2., etc.) as a fallback + const stepLines = text.match(/(?:^|\n)\s*(?:\d+\.\s+)(.+)/g); + if (stepLines && stepLines.length > 0) { + const extracted = stepLines.join('\n').trim(); + console.log("Extracted pseudocode using numbered steps pattern:", extracted); + return extracted; + } + + console.log("No pseudocode found in text"); + return ""; + } + + private extractCodeBlock(text: string): string { + const codeBlockRegex = /```(?:\w+)?\s*([\s\S]*?)```/; + const match = text.match(codeBlockRegex); + + return match ? match[1].trim() : text.trim(); + } + + private extractComplexity(text: string): { time: string, space: string } { + // Extract time complexity + const timeRegex = /(?:Time[- ]?[Cc]omplexity:?|Time:?)\s*([^\n]+(?:\n[^\n]+)*?)(?=\n\s*(?:Space|$))/i; + const timeMatch = text.match(timeRegex); + + // Extract space complexity + const spaceRegex = /(?:Space[- ]?[Cc]omplexity:?|Space:?)\s*([^\n]+(?:\n[^\n]+)*?)(?=\n|$)/i; + const spaceMatch = text.match(spaceRegex); + + const formatComplexity = (complexity: string | null): string => { + if (!complexity) return "O(n) - Complexity not available"; + + const bigORegex = /O\([^)]+\)/i; + if (bigORegex.test(complexity)) { + if (!complexity.includes('-') && !complexity.includes('because')) { + const notation = complexity.match(bigORegex)?.[0] || ""; + return `${notation} - ${complexity.replace(notation, '').trim()}`; + } + return complexity.trim(); + } + + return `O(n) - ${complexity.trim()}`; + }; + + return { + time: formatComplexity(timeMatch?.[1] || null), + space: formatComplexity(spaceMatch?.[1] || null) + }; + } + private async processExtraScreenshotsHelper( screenshots: Array<{ path: string; data: string }>, signal: AbortSignal diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index f05a9ae..0a23061 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -1,7 +1,6 @@ // ipcHandlers.ts -import { ipcMain, shell, dialog } from "electron" -import { randomBytes } from "crypto" +import { ipcMain, shell } from "electron" import { IIpcHandlerDeps } from "./main" import { configHelper } from "./ConfigHelper" @@ -14,7 +13,40 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { }) ipcMain.handle("update-config", (_event, updates) => { - return configHelper.updateConfig(updates); + // Log updates for debugging + console.log("Updating config with:", { + ...updates, + }); + + const currentConfig = configHelper.loadConfig(); + + // Check if keyboard modifier has changed and handle it specially + if (updates.keyboardModifier && updates.keyboardModifier !== currentConfig.keyboardModifier) { + console.log(`Keyboard modifier changing from ${currentConfig.keyboardModifier} to ${updates.keyboardModifier}`); + + // We need to call setKeyboardModifier specifically to trigger the event + configHelper.setKeyboardModifier(updates.keyboardModifier); + } + + // Make sure we have the typed updates for the config helper + const configUpdates = { + apiKeys: updates.apiKeys, // Add the apiKeys property + apiKey: updates.apiKey, // Keep for backward compatibility + apiProvider: updates.apiProvider, + extractionModel: updates.extractionModel, + solutionModel: updates.solutionModel, + debuggingModel: updates.debuggingModel, + language: updates.language, + opacity: updates.opacity, + // Don't include keyboardModifier here since we handled it separately + }; + + // Debug logging + console.log("config.apiProvider", configHelper.loadConfig().apiProvider); + console.log("config.apiKeys", configHelper.loadConfig().apiKeys); + console.log("apiKey", configHelper.getCurrentApiKey()); + + return configHelper.updateConfig(configUpdates); }) ipcMain.handle("check-api-key", () => { @@ -35,43 +67,6 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { return result; }) - // Credits handlers - ipcMain.handle("set-initial-credits", async (_event, credits: number) => { - const mainWindow = deps.getMainWindow() - if (!mainWindow) return - - try { - // Set the credits in a way that ensures atomicity - await mainWindow.webContents.executeJavaScript( - `window.__CREDITS__ = ${credits}` - ) - mainWindow.webContents.send("credits-updated", credits) - } catch (error) { - console.error("Error setting initial credits:", error) - throw error - } - }) - - ipcMain.handle("decrement-credits", async () => { - const mainWindow = deps.getMainWindow() - if (!mainWindow) return - - try { - const currentCredits = await mainWindow.webContents.executeJavaScript( - "window.__CREDITS__" - ) - if (currentCredits > 0) { - const newCredits = currentCredits - 1 - await mainWindow.webContents.executeJavaScript( - `window.__CREDITS__ = ${newCredits}` - ) - mainWindow.webContents.send("credits-updated", newCredits) - } - } catch (error) { - console.error("Error decrementing credits:", error) - } - }) - // Screenshot queue handlers ipcMain.handle("get-screenshot-queue", () => { return deps.getScreenshotQueue() @@ -221,6 +216,28 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { } }) + // Click-through handler + ipcMain.handle("toggle-click-through", () => { + try { + deps.toggleClickThrough() + return { success: true } + } catch (error) { + console.error("Error toggling click-through:", error) + return { error: "Failed to toggle click-through" } + } + }) + + // Get click-through state + ipcMain.handle("get-click-through-state", () => { + try { + const isClickThrough = deps.getClickThroughState(); + return { success: true, isClickThrough } + } catch (error) { + console.error("Error getting click-through state:", error) + return { success: false, error: "Failed to get click-through state" } + } + }) + ipcMain.handle("reset-queues", async () => { try { deps.clearQueues() diff --git a/electron/main.ts b/electron/main.ts index 0eae187..f051826 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, screen, shell, ipcMain } from "electron" +import { app, BrowserWindow, screen, shell } from "electron" import path from "path" import fs from "fs" import { initializeIpcHandlers } from "./ipcHandlers" @@ -17,6 +17,7 @@ const state = { // Window management properties mainWindow: null as BrowserWindow | null, isWindowVisible: false, + isClickThrough: true, windowPosition: null as { x: number; y: number } | null, windowSize: null as { width: number; height: number } | null, screenWidth: 0, @@ -43,6 +44,10 @@ const state = { API_KEY_INVALID: "api-key-invalid", INITIAL_START: "initial-start", PROBLEM_EXTRACTED: "problem-extracted", + EDGE_CASES_EXTRACTED: "edge-cases-extracted", + SOLUTION_THINKING: "solution-thinking", + APPROACH_DEVELOPED: "approach-developed", + CODE_GENERATED: "code-generated", SOLUTION_SUCCESS: "solution-success", INITIAL_SOLUTION_ERROR: "solution-error", DEBUG_START: "debug-start", @@ -81,6 +86,7 @@ export interface IShortcutsHelperDeps { setView: (view: "queue" | "solutions" | "debug") => void isVisible: () => boolean toggleMainWindow: () => void + toggleClickThrough: () => void moveWindowLeft: () => void moveWindowRight: () => void moveWindowUp: () => void @@ -101,6 +107,8 @@ export interface IIpcHandlerDeps { takeScreenshot: () => Promise getView: () => "queue" | "solutions" | "debug" toggleMainWindow: () => void + toggleClickThrough: () => void + getClickThroughState: () => boolean clearQueues: () => void setView: (view: "queue" | "solutions" | "debug") => void moveWindowLeft: () => void @@ -138,6 +146,7 @@ function initializeHelpers() { setView, isVisible: () => state.isWindowVisible, toggleMainWindow, + toggleClickThrough, moveWindowLeft: () => moveWindowHorizontal((x) => Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) @@ -364,6 +373,10 @@ async function createWindow(): Promise { state.mainWindow.setOpacity(savedOpacity); state.isWindowVisible = true; } + + // Enable click-through behavior by default + state.mainWindow.setIgnoreMouseEvents(true, { forward: true }) + state.isClickThrough = true } function handleWindowMove(): void { @@ -408,7 +421,9 @@ function showMainWindow(): void { ...state.windowSize }); } - state.mainWindow.setIgnoreMouseEvents(false); + // Only disable click-through if it's not supposed to be in click-through mode + // This preserves the user's click-through preference + state.mainWindow.setIgnoreMouseEvents(state.isClickThrough, { forward: true }); state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1); state.mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true @@ -418,7 +433,7 @@ function showMainWindow(): void { state.mainWindow.showInactive(); // Use showInactive instead of show+focus state.mainWindow.setOpacity(1); // Then set opacity to 1 after showing state.isWindowVisible = true; - console.log('Window shown with showInactive(), opacity set to 1'); + console.log(`Window shown with showInactive(), opacity set to 1, click-through: ${state.isClickThrough}`); } } @@ -543,6 +558,8 @@ async function initializeApp() { takeScreenshot, getView, toggleMainWindow, + toggleClickThrough, + getClickThroughState: () => state.isClickThrough, clearQueues, setView, moveWindowLeft: () => @@ -685,6 +702,20 @@ function getHasDebugged(): boolean { return state.hasDebugged } +// Toggle click-through functionality +function toggleClickThrough(): void { + if (!state.mainWindow?.isDestroyed()) { + state.isClickThrough = !state.isClickThrough; + state.mainWindow.setIgnoreMouseEvents(state.isClickThrough, { forward: true }); + console.log(`Click-through ${state.isClickThrough ? 'enabled' : 'disabled'}`); + + // Send notification to renderer process + state.mainWindow.webContents.send("click-through-toggled", { + enabled: state.isClickThrough + }); + } +} + // Export state and functions for other modules export { state, @@ -692,6 +723,7 @@ export { hideMainWindow, showMainWindow, toggleMainWindow, + toggleClickThrough, setWindowDimensions, moveWindowHorizontal, moveWindowVertical, diff --git a/electron/preload.ts b/electron/preload.ts index 85f3215..e91048d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -12,6 +12,10 @@ export const PROCESSING_EVENTS = { //states for generating the initial solution INITIAL_START: "initial-start", PROBLEM_EXTRACTED: "problem-extracted", + EDGE_CASES_EXTRACTED: "edge-cases-extracted", + SOLUTION_THINKING: "solution-thinking", + APPROACH_DEVELOPED: "approach-developed", + CODE_GENERATED: "code-generated", SOLUTION_SUCCESS: "solution-success", INITIAL_SOLUTION_ERROR: "solution-error", RESET: "reset", @@ -156,6 +160,8 @@ const electronAPI = { triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), + toggleClickThrough: () => ipcRenderer.invoke("toggle-click-through"), + getClickThroughState: () => ipcRenderer.invoke("get-click-through-state"), onSubscriptionUpdated: (callback: () => void) => { const subscription = () => callback() ipcRenderer.on("subscription-updated", subscription) @@ -205,8 +211,18 @@ const electronAPI = { // New methods for OpenAI API integration getConfig: () => ipcRenderer.invoke("get-config"), - updateConfig: (config: { apiKey?: string; model?: string; language?: string; opacity?: number }) => - ipcRenderer.invoke("update-config", config), + updateConfig: (config: { + apiKeys?: Record + apiKey?: string // For backward compatibility + apiProvider: string + extractionModel: string + solutionModel: string + debuggingModel: string + keyboardModifier?: string + language?: string + }) => ipcRenderer.invoke("update-config", config), + testAPIKey: (apiKey: string, provider: string) => + ipcRenderer.invoke("test-api-key", apiKey, provider), onShowSettings: (callback: () => void) => { const subscription = () => callback() ipcRenderer.on("show-settings-dialog", subscription) @@ -236,7 +252,64 @@ const electronAPI = { ipcRenderer.removeListener("delete-last-screenshot", subscription) } }, - deleteLastScreenshot: () => ipcRenderer.invoke("delete-last-screenshot") + deleteLastScreenshot: () => ipcRenderer.invoke("delete-last-screenshot"), + onProcessingUpdate: (callback: (status: any) => void) => { + ipcRenderer.on("processing-update", (_, status) => callback(status)) + return () => { + ipcRenderer.removeAllListeners("processing-update") + } + }, + onClickThroughToggled: (callback: (data: { enabled: boolean }) => void) => { + ipcRenderer.on("click-through-toggled", (_, data) => callback(data)) + return () => { + ipcRenderer.removeAllListeners("click-through-toggled") + } + }, + // Add new event handlers for sequential processing + onEdgeCasesExtracted: (callback: (data: any) => void) => { + const subscription = (_: any, data: any) => callback(data) + ipcRenderer.on(PROCESSING_EVENTS.EDGE_CASES_EXTRACTED, subscription) + return () => { + ipcRenderer.removeListener( + PROCESSING_EVENTS.EDGE_CASES_EXTRACTED, + subscription + ) + } + }, + + onApproachDeveloped: (callback: (data: any) => void) => { + const subscription = (_: any, data: any) => callback(data) + ipcRenderer.on(PROCESSING_EVENTS.APPROACH_DEVELOPED, subscription) + return () => { + ipcRenderer.removeListener( + PROCESSING_EVENTS.APPROACH_DEVELOPED, + subscription + ) + } + }, + + onCodeGenerated: (callback: (data: any) => void) => { + const subscription = (_: any, data: any) => callback(data) + ipcRenderer.on(PROCESSING_EVENTS.CODE_GENERATED, subscription) + return () => { + ipcRenderer.removeListener( + PROCESSING_EVENTS.CODE_GENERATED, + subscription + ) + } + }, + + // Solution Thinking event handler + onSolutionThinking: (callback: (data: any) => void) => { + const subscription = (_: any, data: any) => callback(data) + ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_THINKING, subscription) + return () => { + ipcRenderer.removeListener( + PROCESSING_EVENTS.SOLUTION_THINKING, + subscription + ) + } + } } // Before exposing the API diff --git a/electron/shortcuts.ts b/electron/shortcuts.ts index a6fa5eb..a225b90 100644 --- a/electron/shortcuts.ts +++ b/electron/shortcuts.ts @@ -2,11 +2,89 @@ import { globalShortcut, app } from "electron" import { IShortcutsHelperDeps } from "./main" import { configHelper } from "./ConfigHelper" +// Define shortcut actions for better type safety +type ShortcutAction = + | "takeScreenshot" + | "processScreenshots" + | "resetQueues" + | "moveWindowLeft" + | "moveWindowRight" + | "moveWindowDown" + | "moveWindowUp" + | "toggleWindow" + | "quitApp" + | "decreaseOpacity" + | "increaseOpacity" + | "zoomOut" + | "resetZoom" + | "zoomIn" + | "deleteLastScreenshot" + | "toggleClickThrough"; + +// Map action to key (the unchangeable part of the shortcut) +const SHORTCUT_KEYS: Record = { + takeScreenshot: "H", + processScreenshots: "Enter", + resetQueues: "R", + moveWindowLeft: "Left", + moveWindowRight: "Right", + moveWindowDown: "Down", + moveWindowUp: "Up", + toggleWindow: "B", + quitApp: "Q", + decreaseOpacity: "[", + increaseOpacity: "]", + zoomOut: "-", + resetZoom: "0", + zoomIn: "=", + deleteLastScreenshot: "L", + toggleClickThrough: "T" +}; + export class ShortcutsHelper { private deps: IShortcutsHelperDeps + private registeredShortcuts: string[] = [] constructor(deps: IShortcutsHelperDeps) { this.deps = deps + + // Listen for keyboard modifier updates + configHelper.on('keyboard-modifier-updated', (modifier) => { + console.log(`⌨️ Keyboard modifier updated to: ${modifier}`); + this.unregisterAllShortcuts(); + this.registerGlobalShortcuts(); + }); + } + + private unregisterAllShortcuts(): void { + // Log the number of shortcuts we're trying to unregister + console.log(`Unregistering ${this.registeredShortcuts.length} shortcuts...`); + + // Unregister all previously registered shortcuts + let unregisteredCount = 0; + this.registeredShortcuts.forEach(shortcut => { + try { + globalShortcut.unregister(shortcut); + unregisteredCount++; + console.log(`Unregistered shortcut: ${shortcut}`); + } catch (error) { + console.error(`Error unregistering shortcut ${shortcut}:`, error); + } + }); + + console.log(`Unregistered ${unregisteredCount}/${this.registeredShortcuts.length} shortcuts`); + + // As a safety measure, unregister all shortcuts + globalShortcut.unregisterAll(); + + // Clear the registered shortcuts array + this.registeredShortcuts = []; + } + + private getShortcutString(action: ShortcutAction): string { + const modifier = configHelper.getKeyboardModifier(); + const key = SHORTCUT_KEYS[action]; + return `${modifier}+${key}`; } private adjustOpacity(delta: number): void { @@ -34,8 +112,34 @@ export class ShortcutsHelper { } } + private registerShortcut(action: ShortcutAction, callback: () => void): void { + const shortcutString = this.getShortcutString(action); + + try { + // Check if shortcut is already registered + if (globalShortcut.isRegistered(shortcutString)) { + console.warn(`Shortcut ${shortcutString} is already registered. Unregistering first.`); + globalShortcut.unregister(shortcutString); + } + + const success = globalShortcut.register(shortcutString, callback); + if (success) { + console.log(`✅ Registered shortcut: ${shortcutString} for ${action}`); + this.registeredShortcuts.push(shortcutString); + } else { + console.error(`❌ Failed to register shortcut: ${shortcutString} for ${action}`); + } + } catch (error) { + console.error(`💥 Error registering shortcut ${shortcutString}:`, error); + } + } + public registerGlobalShortcuts(): void { - globalShortcut.register("CommandOrControl+H", async () => { + const keyboardModifier = configHelper.getKeyboardModifier(); + console.log(`🔄 Registering global shortcuts with modifier: ${keyboardModifier}`); + + // Register each shortcut with its corresponding action + this.registerShortcut("takeScreenshot", async () => { const mainWindow = this.deps.getMainWindow() if (mainWindow) { console.log("Taking screenshot...") @@ -50,15 +154,15 @@ export class ShortcutsHelper { console.error("Error capturing screenshot:", error) } } - }) + }); - globalShortcut.register("CommandOrControl+Enter", async () => { + this.registerShortcut("processScreenshots", async () => { await this.deps.processingHelper?.processScreenshots() - }) + }); - globalShortcut.register("CommandOrControl+R", () => { + this.registerShortcut("resetQueues", () => { console.log( - "Command + R pressed. Canceling requests and resetting queues..." + "Canceling requests and resetting queues..." ) // Cancel ongoing API requests @@ -78,90 +182,94 @@ export class ShortcutsHelper { mainWindow.webContents.send("reset-view") mainWindow.webContents.send("reset") } - }) + }); - // New shortcuts for moving the window - globalShortcut.register("CommandOrControl+Left", () => { - console.log("Command/Ctrl + Left pressed. Moving window left.") + // Shortcuts for moving the window + this.registerShortcut("moveWindowLeft", () => { + console.log("Moving window left.") this.deps.moveWindowLeft() - }) + }); - globalShortcut.register("CommandOrControl+Right", () => { - console.log("Command/Ctrl + Right pressed. Moving window right.") + this.registerShortcut("moveWindowRight", () => { + console.log("Moving window right.") this.deps.moveWindowRight() - }) + }); - globalShortcut.register("CommandOrControl+Down", () => { - console.log("Command/Ctrl + down pressed. Moving window down.") + this.registerShortcut("moveWindowDown", () => { + console.log("Moving window down.") this.deps.moveWindowDown() - }) + }); - globalShortcut.register("CommandOrControl+Up", () => { - console.log("Command/Ctrl + Up pressed. Moving window Up.") + this.registerShortcut("moveWindowUp", () => { + console.log("Moving window up.") this.deps.moveWindowUp() - }) + }); - globalShortcut.register("CommandOrControl+B", () => { - console.log("Command/Ctrl + B pressed. Toggling window visibility.") + this.registerShortcut("toggleWindow", () => { + console.log("Toggling window visibility.") this.deps.toggleMainWindow() - }) + }); - globalShortcut.register("CommandOrControl+Q", () => { - console.log("Command/Ctrl + Q pressed. Quitting application.") + this.registerShortcut("quitApp", () => { + console.log("Quitting application.") app.quit() - }) + }); // Adjust opacity shortcuts - globalShortcut.register("CommandOrControl+[", () => { - console.log("Command/Ctrl + [ pressed. Decreasing opacity.") + this.registerShortcut("decreaseOpacity", () => { + console.log("Decreasing opacity.") this.adjustOpacity(-0.1) - }) + }); - globalShortcut.register("CommandOrControl+]", () => { - console.log("Command/Ctrl + ] pressed. Increasing opacity.") + this.registerShortcut("increaseOpacity", () => { + console.log("Increasing opacity.") this.adjustOpacity(0.1) - }) + }); // Zoom controls - globalShortcut.register("CommandOrControl+-", () => { - console.log("Command/Ctrl + - pressed. Zooming out.") + this.registerShortcut("zoomOut", () => { + console.log("Zooming out.") const mainWindow = this.deps.getMainWindow() if (mainWindow) { const currentZoom = mainWindow.webContents.getZoomLevel() mainWindow.webContents.setZoomLevel(currentZoom - 0.5) } - }) + }); - globalShortcut.register("CommandOrControl+0", () => { - console.log("Command/Ctrl + 0 pressed. Resetting zoom.") + this.registerShortcut("resetZoom", () => { + console.log("Resetting zoom.") const mainWindow = this.deps.getMainWindow() if (mainWindow) { mainWindow.webContents.setZoomLevel(0) } - }) + }); - globalShortcut.register("CommandOrControl+=", () => { - console.log("Command/Ctrl + = pressed. Zooming in.") + this.registerShortcut("zoomIn", () => { + console.log("Zooming in.") const mainWindow = this.deps.getMainWindow() if (mainWindow) { const currentZoom = mainWindow.webContents.getZoomLevel() mainWindow.webContents.setZoomLevel(currentZoom + 0.5) } - }) + }); - // Delete last screenshot shortcut - globalShortcut.register("CommandOrControl+L", () => { - console.log("Command/Ctrl + L pressed. Deleting last screenshot.") + this.registerShortcut("deleteLastScreenshot", () => { + console.log("Deleting last screenshot.") const mainWindow = this.deps.getMainWindow() if (mainWindow) { - // Send an event to the renderer to delete the last screenshot mainWindow.webContents.send("delete-last-screenshot") } - }) + }); + + // Toggle click-through mode + this.registerShortcut("toggleClickThrough", () => { + console.log("Toggling click-through mode.") + this.deps.toggleClickThrough() + }); // Unregister shortcuts when quitting app.on("will-quit", () => { - globalShortcut.unregisterAll() - }) + this.unregisterAllShortcuts(); + }); } } diff --git a/src/App.tsx b/src/App.tsx index f2dd348..5890eb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -147,6 +147,38 @@ function App() { }; }, []); + // Maintain click-through state across view changes + useEffect(() => { + // Listen for solution success and ensure click-through state is preserved + const unsubscribeSolutionSuccess = window.electronAPI.onSolutionSuccess(async () => { + console.log("Solution success - ensuring click-through state is preserved"); + // Short delay to ensure the view has changed + setTimeout(async () => { + try { + // Check the current click-through state + const response = await window.electronAPI.getClickThroughState(); + if (response && response.success) { + const shouldBeClickThrough = response.isClickThrough; + + // If click-through was enabled, ensure it stays enabled in the Solutions view + if (shouldBeClickThrough) { + console.log("Ensuring click-through remains enabled in Solutions view"); + // Toggle twice to reset the state (in case it was changed during view transition) + await window.electronAPI.toggleClickThrough(); + await window.electronAPI.toggleClickThrough(); + } + } + } catch (error) { + console.error("Error restoring click-through state:", error); + } + }, 300); + }); + + return () => { + unsubscribeSolutionSuccess(); + }; + }, []); + // Initialize basic app state useEffect(() => { // Load config and set values diff --git a/src/_pages/Solutions.tsx b/src/_pages/Solutions.tsx index e193945..0ecc117 100644 --- a/src/_pages/Solutions.tsx +++ b/src/_pages/Solutions.tsx @@ -1,6 +1,6 @@ // Solutions.tsx import React, { useState, useEffect, useRef } from "react" -import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useQueryClient } from "@tanstack/react-query" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" @@ -10,7 +10,6 @@ import { ProblemStatementData } from "../types/solutions" import SolutionCommands from "../components/Solutions/SolutionCommands" import Debug from "./Debug" import { useToast } from "../contexts/toast" -import { COMMAND_KEY } from "../utils/platform" export const ContentSection = ({ title, @@ -49,16 +48,6 @@ const SolutionSection = ({ isLoading: boolean currentLanguage: string }) => { - const [copied, setCopied] = useState(false) - - const copyToClipboard = () => { - if (typeof content === "string") { - navigator.clipboard.writeText(content).then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }) - } - } return (
@@ -75,12 +64,6 @@ const SolutionSection = ({
) : (
-

@@ -171,7 +154,24 @@ export interface SolutionsProps { currentLanguage: string setLanguage: (language: string) => void } -const Solutions: React.FC = ({ + +// Define types for the solution data structure +interface SolutionData { + code: string; + thoughts: string[]; + time_complexity: string; + space_complexity: string; +} + +interface DebugData { + code: string; + debug_analysis: string; + thoughts: string[]; + time_complexity: string; + space_complexity: string; +} + +export const Solutions: React.FC = ({ setView, credits, currentLanguage, @@ -192,6 +192,18 @@ const Solutions: React.FC = ({ null ) + // New state for sequential processing stages + const [edgeCasesData, setEdgeCasesData] = useState(null) + const [pseudoCodeData, setPseudoCodeData] = useState(null) + const [solutionThinkingData, setSolutionThinkingData] = useState(null) + + // Loading states for different stages + const [loadingEdgeCases, setLoadingEdgeCases] = useState(false) + const [loadingSolutionThinking, setLoadingSolutionThinking] = useState(false) + const [loadingApproach, setLoadingApproach] = useState(false) + const [loadingCode, setLoadingCode] = useState(false) + const [loadingComplexity, setLoadingComplexity] = useState(false) + const [isTooltipVisible, setIsTooltipVisible] = useState(false) const [tooltipHeight, setTooltipHeight] = useState(0) @@ -299,20 +311,78 @@ const Solutions: React.FC = ({ setThoughtsData(null) setTimeComplexityData(null) setSpaceComplexityData(null) + setEdgeCasesData(null) + setSolutionThinkingData(null) + setPseudoCodeData(null) + + // Reset loading states + setLoadingEdgeCases(true) + setLoadingSolutionThinking(false) + setLoadingApproach(false) + setLoadingCode(false) + setLoadingComplexity(false) }), - window.electronAPI.onProblemExtracted((data) => { + window.electronAPI.onProblemExtracted((data: ProblemStatementData) => { queryClient.setQueryData(["problem_statement"], data) + + // After problem extraction, we're ready for edge cases + setLoadingEdgeCases(true) }), + + // New event handlers for sequential processing + window.electronAPI.onEdgeCasesExtracted((data: { + thoughts: string[] + }) => { + const edgeCases = data.thoughts || []; + console.log("Received edge cases:", edgeCases); + setEdgeCasesData(edgeCases); + // Don't set the combined thoughts here + setLoadingEdgeCases(false); + setLoadingSolutionThinking(true); + }), + + window.electronAPI.onSolutionThinking((data: { + thoughts: string[], + edgeCases: string[], + solutionThoughts: string[] + }) => { + // Use the already separated data + console.log("Received solution thinking data:", data); + // Don't combine thoughts here, just use the solution thoughts directly + setSolutionThinkingData(data.solutionThoughts || []); + setLoadingSolutionThinking(false); + setLoadingApproach(true); + }), + + window.electronAPI.onApproachDeveloped((data: { thoughts: string[], pseudo_code?: string }) => { + console.log("Received approach data:", data); + console.log("Received pseudo_code:", { + value: data.pseudo_code, + length: data.pseudo_code ? data.pseudo_code.length : 0, + is_null: data.pseudo_code === null, + is_undefined: data.pseudo_code === undefined, + is_empty: data.pseudo_code === "", + typeof: typeof data.pseudo_code + }); + + // Set the pseudocode data + setPseudoCodeData(data.pseudo_code || null); + setLoadingApproach(false); + setLoadingCode(true); + }), + + window.electronAPI.onCodeGenerated((data: { thoughts: string[], code: string }) => { + // Only set the code, not the accumulated thoughts + setSolutionData(data.code || null); + setLoadingCode(false); + setLoadingComplexity(true); + }), + //if there was an error processing the initial solution window.electronAPI.onSolutionError((error: string) => { showToast("Processing Failed", error, "error") - // Reset solutions in the cache (even though this shouldn't ever happen) and complexities to previous states - const solution = queryClient.getQueryData(["solution"]) as { - code: string - thoughts: string[] - time_complexity: string - space_complexity: string - } | null + // Reset solutions in the cache and complexities to previous states + const solution = queryClient.getQueryData(["solution"]) as SolutionData | null if (!solution) { setView("queue") } @@ -320,15 +390,26 @@ const Solutions: React.FC = ({ setThoughtsData(solution?.thoughts || null) setTimeComplexityData(solution?.time_complexity || null) setSpaceComplexityData(solution?.space_complexity || null) + + // Reset loading states + setLoadingEdgeCases(false) + setLoadingSolutionThinking(false) + setLoadingApproach(false) + setLoadingCode(false) + setLoadingComplexity(false) + console.error("Processing error:", error) }), - //when the initial solution is generated, we'll set the solution data to that - window.electronAPI.onSolutionSuccess((data) => { + + //when the final solution is generated, we'll set all the data + window.electronAPI.onSolutionSuccess((data: SolutionData) => { if (!data) { console.warn("Received empty or invalid solution data") return } - console.log({ data }) + console.log("Solution success with data:", data) + + // Store the final solution data in the query cache const solutionData = { code: data.code, thoughts: data.thoughts, @@ -337,17 +418,28 @@ const Solutions: React.FC = ({ } queryClient.setQueryData(["solution"], solutionData) + + // Set the solution code, complexity data setSolutionData(solutionData.code || null) - setThoughtsData(solutionData.thoughts || null) setTimeComplexityData(solutionData.time_complexity || null) setSpaceComplexityData(solutionData.space_complexity || null) + // Note: We don't update the thoughts data here since we already have them separated + // by section (edge cases, solution thinking, etc.) + + // Complete all loading states + setLoadingEdgeCases(false) + setLoadingSolutionThinking(false) + setLoadingApproach(false) + setLoadingCode(false) + setLoadingComplexity(false) + // Fetch latest screenshots when solution is successful const fetchScreenshots = async () => { try { const existing = await window.electronAPI.getScreenshots() const screenshots = - existing.previews?.map((p) => ({ + existing.previews?.map((p: { path: string, preview: string }) => ({ id: p.path, path: p.path, preview: p.preview, @@ -370,7 +462,7 @@ const Solutions: React.FC = ({ setDebugProcessing(true) }), //the first time debugging works, we'll set the view to debug and populate the cache with the data - window.electronAPI.onDebugSuccess((data) => { + window.electronAPI.onDebugSuccess((data: DebugData) => { queryClient.setQueryData(["new_solution"], data) setDebugProcessing(false) }), @@ -475,65 +567,76 @@ const Solutions: React.FC = ({ ) : (
- {/* Conditionally render the screenshot queue if solutionData is available */} - {solutionData && ( -
-
-
- + {/* Conditionally render the screenshot queue if solutionData is available */} + {solutionData && ( +
+
+
+ +
-
- )} - - {/* Navbar of commands with the SolutionsHelper */} - - - {/* Main Content - Modified width constraints */} -
-
-
- {!solutionData && ( - <> + )} + + {/* Navbar of commands with the SolutionsHelper */} + + + {/* Main Content - Modified width constraints */} +
+
+
+ {/* Problem Statement (always shows if available) */} + {problemStatementData && ( - {problemStatementData && ( -
-

- Generating solutions... -

-
- )} - - )} - - {solutionData && ( - <> + )} + + {/* Edge Cases Section - Shows when loaded or loading */} + {(loadingEdgeCases || edgeCasesData) && (
- {thoughtsData.map((thought, index) => ( -
+ {edgeCasesData.map((edgeCase, index) => ( +
+
+
{edgeCase}
+
+ ))} +
+
+ ) + } + isLoading={loadingEdgeCases} + /> + )} + + {/* Solution Thinking Section - Shows when loading or loaded */} + {(loadingSolutionThinking || solutionThinkingData) && ( + +
+ {solutionThinkingData.map((thought, index) => ( +
{thought}
@@ -542,28 +645,52 @@ const Solutions: React.FC = ({
) } - isLoading={!thoughtsData} + isLoading={loadingSolutionThinking} /> + )} + {/* Approach Section - Shows when approach is loading or loaded */} + {(loadingApproach || pseudoCodeData) && ( + + )} + + {/* Solution Section - Shows when solution code is loading or loaded */} + {(loadingCode || solutionData) && ( + )} + {/* Complexity Section - Shows when complexity is loading or loaded */} + {(loadingComplexity || timeComplexityData) && ( - - )} + )} + + {/* Loading indicator when we're at initial stages */} + {problemStatementData && !thoughtsData && !loadingEdgeCases && !loadingApproach && ( +
+

+ Starting solution generation... +

+
+ )} +
-
)} ) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index b82b799..9018654 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; -import { Settings, LogOut, ChevronDown, ChevronUp } from 'lucide-react'; +import React from 'react'; +import { Settings } from 'lucide-react'; import { Button } from '../ui/button'; -import { useToast } from '../../contexts/toast'; +import { LanguageSelector } from '../shared/LanguageSelector'; interface HeaderProps { currentLanguage: string; @@ -9,99 +9,14 @@ interface HeaderProps { onOpenSettings: () => void; } -// Available programming languages -const LANGUAGES = [ - { value: 'python', label: 'Python' }, - { value: 'javascript', label: 'JavaScript' }, - { value: 'java', label: 'Java' }, - { value: 'cpp', label: 'C++' }, - { value: 'csharp', label: 'C#' }, - { value: 'go', label: 'Go' }, - { value: 'rust', label: 'Rust' }, - { value: 'typescript', label: 'TypeScript' }, -]; - export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderProps) { - const [dropdownOpen, setDropdownOpen] = useState(false); - const { showToast } = useToast(); - - // Handle logout - clear API key and reload app - const handleLogout = async () => { - try { - // Update config with empty API key - await window.electronAPI.updateConfig({ - apiKey: '', - }); - - showToast('Success', 'Logged out successfully', 'success'); - - // Reload the app after a short delay - setTimeout(() => { - window.location.reload(); - }, 1500); - } catch (error) { - console.error('Error logging out:', error); - showToast('Error', 'Failed to log out', 'error'); - } - }; - - // Handle language selection - const handleLanguageSelect = (lang: string) => { - setLanguage(lang); - setDropdownOpen(false); - - // Also save the language preference to config - window.electronAPI.updateConfig({ - language: lang - }).catch(error => { - console.error('Failed to save language preference:', error); - }); - }; - - const toggleDropdown = () => { - setDropdownOpen(!dropdownOpen); - }; - - // Find the current language object - const currentLangObj = LANGUAGES.find(lang => lang.value === currentLanguage) || LANGUAGES[0]; - return (
- Language: -
- - - {dropdownOpen && ( -
-
- {LANGUAGES.map((lang) => ( - - ))} -
-
- )} -
+
@@ -115,17 +30,6 @@ export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderP Settings - -
); diff --git a/src/components/Settings/SettingsDialog.tsx b/src/components/Settings/SettingsDialog.tsx index 463ea12..4b789f5 100644 --- a/src/components/Settings/SettingsDialog.tsx +++ b/src/components/Settings/SettingsDialog.tsx @@ -1,7 +1,6 @@ -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Dialog, - DialogTrigger, DialogContent, DialogDescription, DialogHeader, @@ -10,8 +9,59 @@ import { } from "../ui/dialog"; import { Input } from "../ui/input"; import { Button } from "../ui/button"; -import { Settings } from "lucide-react"; import { useToast } from "../../contexts/toast"; +import { ApiProvider } from "../../../electron/ConfigHelper"; + +// Simple dropdown implementation for keyboard modifier selection +interface DropdownOption { + value: string; + label: string; +} + +// Simplified dropdown component +const Dropdown = ({ + value, + options, + onChange +}: { + value: string; + options: DropdownOption[]; + onChange: (value: string) => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + const selectedOption = options.find(option => option.value === value); + + return ( +
+
setIsOpen(!isOpen)} + > + {selectedOption?.label || "Select an option"} + + + +
+ + {isOpen && ( +
+ {options.map(option => ( +
{ + onChange(option.value); + setIsOpen(false); + }} + > + {option.label} +
+ ))} +
+ )} +
+ ); +}; type APIProvider = "openai" | "gemini" | "anthropic"; @@ -46,6 +96,16 @@ const modelCategories: ModelCategory[] = [ id: "gpt-4o-mini", name: "gpt-4o-mini", description: "Faster, more cost-effective option" + }, + { + id: "gpt-4.5-preview-2025-02-27", + name: "gpt-4.5-preview", + description: "Best overall performance for problem extraction" + }, + { + id: "o3-mini-2025-01-31", + name: "o3-mini", + description: "Best overall performance for problem extraction" } ], geminiModels: [ @@ -92,6 +152,16 @@ const modelCategories: ModelCategory[] = [ id: "gpt-4o-mini", name: "gpt-4o-mini", description: "Faster, more cost-effective option" + }, + { + id: "gpt-4.5-preview-2025-02-27", + name: "gpt-4.5-preview", + description: "Best overall performance for problem extraction" + }, + { + id: "o3-mini-2025-01-31", + name: "o3-mini", + description: "Best overall performance for problem extraction" } ], geminiModels: [ @@ -138,6 +208,16 @@ const modelCategories: ModelCategory[] = [ id: "gpt-4o-mini", name: "gpt-4o-mini", description: "Faster, more cost-effective option" + }, + { + id: "gpt-4.5-preview-2025-02-27", + name: "gpt-4.5-preview", + description: "Best overall performance for problem extraction" + }, + { + id: "o3-mini-2025-01-31", + name: "o3-mini", + description: "Best overall performance for problem extraction" } ], geminiModels: [ @@ -179,14 +259,62 @@ interface SettingsDialogProps { export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDialogProps) { const [open, setOpen] = useState(externalOpen || false); - const [apiKey, setApiKey] = useState(""); - const [apiProvider, setApiProvider] = useState("openai"); - const [extractionModel, setExtractionModel] = useState("gpt-4o"); - const [solutionModel, setSolutionModel] = useState("gpt-4o"); - const [debuggingModel, setDebuggingModel] = useState("gpt-4o"); + // Store API keys for each provider separately but initialize empty + const [openaiKey, setOpenaiKey] = useState(""); + const [geminiKey, setGeminiKey] = useState(""); + const [anthropicKey, setAnthropicKey] = useState(""); + // Match default provider from ConfigHelper ("gemini" not "openai") + const [apiProvider, setApiProvider] = useState("gemini"); + // Match default models from ConfigHelper + const [extractionModel, setExtractionModel] = useState("gemini-2.0-flash"); + const [solutionModel, setSolutionModel] = useState("gemini-2.0-flash"); + const [debuggingModel, setDebuggingModel] = useState("gemini-2.0-flash"); + const [keyboardModifier, setKeyboardModifier] = useState("CommandOrControl"); const [isLoading, setIsLoading] = useState(false); const { showToast } = useToast(); + // Helper to get the current API key based on selected provider + const getCurrentApiKey = () => { + switch(apiProvider) { + case "openai": return openaiKey; + case "gemini": return geminiKey; + case "anthropic": return anthropicKey; + default: return ""; + } + }; + + // Helper to set the current API key based on selected provider + const setCurrentApiKey = (key: string) => { + switch(apiProvider) { + case "openai": setOpenaiKey(key); break; + case "gemini": setGeminiKey(key); break; + case "anthropic": setAnthropicKey(key); break; + } + }; + + // Define modifier options + const keyboardModifierOptions = [ + { value: "CommandOrControl", label: "Ctrl / Cmd" }, + { value: "Control", label: "Ctrl" }, + { value: "Alt", label: "Alt" }, + { value: "Option", label: "Option (macOS)" }, + { value: "CommandOrControl+Shift", label: "Ctrl+Shift / Cmd+Shift" }, + { value: "CommandOrControl+Alt", label: "Ctrl+Alt / Cmd+Alt" } + ]; + + // Format modifier for display + const formatModifierForDisplay = (modifier: string): string => { + switch (modifier) { + case "CommandOrControl": return "Ctrl / Cmd"; + case "Control": return "Ctrl"; + case "Alt": return "Alt"; + case "Option": return "Option"; + case "CommandOrControl+Shift": return "Ctrl+Shift / Cmd+Shift"; + case "CommandOrControl+Alt": return "Ctrl+Alt / Cmd+Alt"; + default: return modifier; + } + }; + // Sync with external open state useEffect(() => { if (externalOpen !== undefined) { @@ -208,21 +336,31 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia if (open) { setIsLoading(true); interface Config { - apiKey?: string; + apiKeys?: Record; apiProvider?: APIProvider; extractionModel?: string; solutionModel?: string; debuggingModel?: string; + keyboardModifier?: string; } window.electronAPI .getConfig() .then((config: Config) => { - setApiKey(config.apiKey || ""); - setApiProvider(config.apiProvider || "openai"); - setExtractionModel(config.extractionModel || "gpt-4o"); - setSolutionModel(config.solutionModel || "gpt-4o"); - setDebuggingModel(config.debuggingModel || "gpt-4o"); + console.log("config", config) + // Handle API keys + if (config.apiKeys) { + // New format with separate keys for each provider + setOpenaiKey(config.apiKeys.openai || ""); + setGeminiKey(config.apiKeys.gemini || ""); + setAnthropicKey(config.apiKeys.anthropic || ""); + } + + setApiProvider(config.apiProvider || "gemini"); + setExtractionModel(config.extractionModel || "gemini-2.0-flash"); + setSolutionModel(config.solutionModel || "gemini-2.0-flash"); + setDebuggingModel(config.debuggingModel || "gemini-2.0-flash"); + setKeyboardModifier(config.keyboardModifier || "CommandOrControl"); }) .catch((error: unknown) => { console.error("Failed to load config:", error); @@ -238,15 +376,15 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia const handleProviderChange = (provider: APIProvider) => { setApiProvider(provider); - // Reset models to defaults when changing provider + // Reset models to defaults for the selected provider, matching ConfigHelper logic if (provider === "openai") { setExtractionModel("gpt-4o"); setSolutionModel("gpt-4o"); setDebuggingModel("gpt-4o"); } else if (provider === "gemini") { - setExtractionModel("gemini-1.5-pro"); - setSolutionModel("gemini-1.5-pro"); - setDebuggingModel("gemini-1.5-pro"); + setExtractionModel("gemini-2.0-flash"); + setSolutionModel("gemini-2.0-flash"); + setDebuggingModel("gemini-2.0-flash"); } else if (provider === "anthropic") { setExtractionModel("claude-3-7-sonnet-20250219"); setSolutionModel("claude-3-7-sonnet-20250219"); @@ -257,22 +395,42 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia const handleSave = async () => { setIsLoading(true); try { + // Create an apiKeys object with all provider keys + const apiKeys: Record = { + openai: openaiKey, + gemini: geminiKey, + anthropic: anthropicKey + }; + + // Get current config to check if any API keys changed + const currentConfig = await window.electronAPI.getConfig(); + + // Check if any API key has changed + const hasApiKeyChanged = + (currentConfig.apiKeys?.openai || "") !== openaiKey || + (currentConfig.apiKeys?.gemini || "") !== geminiKey || + (currentConfig.apiKeys?.anthropic || "") !== anthropicKey; + const result = await window.electronAPI.updateConfig({ - apiKey, + apiKeys, apiProvider, extractionModel, solutionModel, debuggingModel, + keyboardModifier, }); if (result) { showToast("Success", "Settings saved successfully", "success"); handleOpenChange(false); - // Force reload the app to apply the API key - setTimeout(() => { - window.location.reload(); - }, 1500); + // Only force reload if any API key has changed + if (hasApiKeyChanged) { + showToast("Reloading", "Reloading application to apply API key changes", "success"); + setTimeout(() => { + window.location.reload(); + }, 1500); + } } } catch (error) { console.error("Failed to save settings:", error); @@ -318,7 +476,7 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia API Settings - Configure your API key and model preferences. You'll need your own API key to use this application. + Configure your API keys and model preferences. You'll need your own API key(s) to use this application.
@@ -342,7 +500,6 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia />

OpenAI

-

GPT-4o models

@@ -362,7 +519,6 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia />

Gemini

-

Gemini 1.5 models

@@ -382,7 +538,6 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia />

Claude

-

Claude 3 models

@@ -398,22 +553,22 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia setApiKey(e.target.value)} + value={getCurrentApiKey()} + onChange={(e) => setCurrentApiKey(e.target.value)} placeholder={ apiProvider === "openai" ? "sk-..." : apiProvider === "gemini" ? "Enter your Gemini API key" : - "sk-ant-..." + "sk-ant-..." // Claude API key } className="bg-black/50 border-white/10 text-white" /> - {apiKey && ( + {getCurrentApiKey() && (

- Current: {maskApiKey(apiKey)} + Current: {maskApiKey(getCurrentApiKey())}

)}

- Your API key is stored locally and never sent to any server except {apiProvider === "openai" ? "OpenAI" : "Google"} + Your API keys are stored locally and never sent to any server except the respective API providers

Don't have an API key?

@@ -459,43 +614,50 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia
+ +
+ + +

Choose the modifier key for all shortcuts

+
+
Toggle Visibility
-
Ctrl+B / Cmd+B
+
{formatModifierForDisplay(keyboardModifier)}+B
Take Screenshot
-
Ctrl+H / Cmd+H
+
{formatModifierForDisplay(keyboardModifier)}+H
Process Screenshots
-
Ctrl+Enter / Cmd+Enter
+
{formatModifierForDisplay(keyboardModifier)}+Enter
Delete Last Screenshot
-
Ctrl+L / Cmd+L
+
{formatModifierForDisplay(keyboardModifier)}+L
Reset View
-
Ctrl+R / Cmd+R
+
{formatModifierForDisplay(keyboardModifier)}+R
Quit Application
-
Ctrl+Q / Cmd+Q
+
{formatModifierForDisplay(keyboardModifier)}+Q
Move Window
-
Ctrl+Arrow Keys
+
{formatModifierForDisplay(keyboardModifier)}+Arrow Keys
Decrease Opacity
-
Ctrl+[ / Cmd+[
+
{formatModifierForDisplay(keyboardModifier)}+[
Increase Opacity
-
Ctrl+] / Cmd+]
+
{formatModifierForDisplay(keyboardModifier)}+]
Zoom Out
-
Ctrl+- / Cmd+-
+
{formatModifierForDisplay(keyboardModifier)}+-
Reset Zoom
-
Ctrl+0 / Cmd+0
+
{formatModifierForDisplay(keyboardModifier)}+0
Zoom In
-
Ctrl+= / Cmd+=
+
{formatModifierForDisplay(keyboardModifier)}+=
@@ -575,7 +737,7 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia diff --git a/src/components/WelcomeScreen.tsx b/src/components/WelcomeScreen.tsx index 142ffeb..b98390d 100644 --- a/src/components/WelcomeScreen.tsx +++ b/src/components/WelcomeScreen.tsx @@ -43,6 +43,10 @@ export const WelcomeScreen: React.FC = ({ onOpenSettings }) Reset View Ctrl+R / Cmd+R +
  • + Toggle Click-Through + Ctrl+T / Cmd+T +
  • Quit App Ctrl+Q / Cmd+Q diff --git a/src/utils/platform.ts b/src/utils/platform.ts index e06651e..07a5eff 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -8,7 +8,7 @@ const getPlatform = () => { } // Platform-specific command key symbol -export const COMMAND_KEY = getPlatform() === 'darwin' ? '⌘' : 'Ctrl' +export const COMMAND_KEY = "sc" // Helper to check if we're on Windows export const isWindows = getPlatform() === 'win32' diff --git a/stealth-run.sh b/stealth-run.sh old mode 100644 new mode 100755