From b538deeba5a8c79ada95a7564ddb1eece37b8d43 Mon Sep 17 00:00:00 2001 From: memit0 Date: Sun, 5 Oct 2025 12:52:20 -0400 Subject: [PATCH 1/5] added openrouter --- README.md | 27 -- electron/ConfigHelper.ts | 76 +++- electron/ProcessingHelper.ts | 397 +++++++++++++++++---- src/components/Settings/SettingsDialog.tsx | 105 +++++- stealth-run.sh | 0 5 files changed, 487 insertions(+), 118 deletions(-) mode change 100644 => 100755 stealth-run.sh diff --git a/README.md b/README.md index 47aeaf6..052d32f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,3 @@ -# CodeInterviewAssist - -> ## ⚠️ IMPORTANT NOTICE TO THE COMMUNITY ⚠️ -> -> **This is a free, open-source initiative - NOT a full-service product!** -> -> There are numerous paid interview preparation tools charging hundreds of dollars for comprehensive features like live audio capture, automated answer generation, and more. This project is fundamentally different: -> -> - This is a **small, non-profit, community-driven project** with zero financial incentive behind it -> - The entire codebase is freely available for anyone to use, modify, or extend -> - Want features like voice support? You're welcome to integrate tools like OpenAI's Whisper or other APIs -> - New features should come through **community contributions** - it's unreasonable to expect a single maintainer to implement premium features for free -> - The maintainer receives no portfolio benefit, monetary compensation, or recognition for this work -> -> **Before submitting feature requests or expecting personalized support, please understand this project exists purely as a community resource.** If you value what's been created, the best way to show appreciation is by contributing code, documentation, or helping other users. - -> ## 🔑 API KEY INFORMATION - UPDATED -> -> We have tested and confirmed that **both Gemini and OpenAI APIs work properly** with the current version. If you are experiencing issues with your API keys: -> -> - Try deleting your API key entry from the config file located in your user data directory -> - Log out and log back in to the application -> - Check your API key dashboard to verify the key is active and has sufficient credits -> - Ensure you're using the correct API key format (OpenAI keys start with "sk-") -> -> The configuration file is stored at: `C:\Users\[USERNAME]\AppData\Roaming\interview-coder-v1\config.json` (on Windows) or `/Users/[USERNAME]/Library/Application Support/interview-coder-v1/config.json` (on macOS) - ## Free, Open-Source AI-Powered Interview Preparation Tool This project provides a powerful alternative to premium coding interview platforms. It delivers the core functionality of paid interview preparation tools but in a free, open-source package. Using your own OpenAI API key, you get access to advanced features like AI-powered problem analysis, solution generation, and debugging assistance - all running locally on your machine. diff --git a/electron/ConfigHelper.ts b/electron/ConfigHelper.ts index 6d1d2db..3d9ea26 100644 --- a/electron/ConfigHelper.ts +++ b/electron/ConfigHelper.ts @@ -7,7 +7,7 @@ import { OpenAI } from "openai" interface Config { apiKey: string; - apiProvider: "openai" | "gemini" | "anthropic"; // Added provider selection + apiProvider: "openai" | "gemini" | "anthropic" | "openrouter"; // Added provider selection extractionModel: string; solutionModel: string; debuggingModel: string; @@ -58,7 +58,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" | "openrouter"): string { if (provider === "openai") { // Only allow gpt-4o and gpt-4o-mini for OpenAI const allowedModels = ['gpt-4o', 'gpt-4o-mini']; @@ -83,6 +83,14 @@ export class ConfigHelper extends EventEmitter { return 'claude-3-7-sonnet-20250219'; } return model; + } else if (provider === "openrouter") { + // Only allow specific OpenRouter models + const allowedModels = ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'google/gemini-pro-1.5']; + if (!allowedModels.includes(model)) { + console.warn(`Invalid OpenRouter model specified: ${model}. Using default model: openai/gpt-4o`); + return 'openai/gpt-4o'; + } + return model; } // Default fallback return model; @@ -95,7 +103,7 @@ 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") { + if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic" && config.apiProvider !== "openrouter") { config.apiProvider = "gemini"; // Default to Gemini if invalid } @@ -153,7 +161,10 @@ export class ConfigHelper extends EventEmitter { // 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-')) { + if (updates.apiKey.trim().startsWith('sk-or-')) { + provider = "openrouter"; + console.log("Auto-detected OpenRouter API key format"); + } else if (updates.apiKey.trim().startsWith('sk-')) { provider = "openai"; console.log("Auto-detected OpenAI API key format"); } else if (updates.apiKey.trim().startsWith('sk-ant-')) { @@ -178,6 +189,10 @@ 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 === "openrouter") { + updates.extractionModel = "openai/gpt-4o"; + updates.solutionModel = "openai/gpt-4o"; + updates.debuggingModel = "openai/gpt-4o"; } else { updates.extractionModel = "gemini-2.0-flash"; updates.solutionModel = "gemini-2.0-flash"; @@ -225,10 +240,12 @@ export class ConfigHelper extends EventEmitter { /** * Validate the API key format */ - public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" ): boolean { + public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" | "openrouter" ): boolean { // If provider is not specified, attempt to auto-detect if (!provider) { - if (apiKey.trim().startsWith('sk-')) { + if (apiKey.trim().startsWith('sk-or-')) { + provider = "openrouter"; + } else if (apiKey.trim().startsWith('sk-')) { if (apiKey.trim().startsWith('sk-ant-')) { provider = "anthropic"; } else { @@ -248,6 +265,9 @@ export class ConfigHelper extends EventEmitter { } 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 === "openrouter") { + // Basic format validation for OpenRouter API keys + return /^sk-or-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); } return false; @@ -288,10 +308,13 @@ 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" | "openrouter"): Promise<{valid: boolean, error?: string}> { // Auto-detect provider based on key format if not specified if (!provider) { - if (apiKey.trim().startsWith('sk-')) { + if (apiKey.trim().startsWith('sk-or-')) { + provider = "openrouter"; + console.log("Auto-detected OpenRouter API key format for testing"); + } else if (apiKey.trim().startsWith('sk-')) { if (apiKey.trim().startsWith('sk-ant-')) { provider = "anthropic"; console.log("Auto-detected Anthropic API key format for testing"); @@ -311,6 +334,8 @@ export class ConfigHelper extends EventEmitter { return this.testGeminiKey(apiKey); } else if (provider === "anthropic") { return this.testAnthropicKey(apiKey); + } else if (provider === "openrouter") { + return this.testOpenRouterKey(apiKey); } return { valid: false, error: "Unknown API provider" }; @@ -394,6 +419,41 @@ export class ConfigHelper extends EventEmitter { return { valid: false, error: errorMessage }; } } + + /** + * Test OpenRouter API key + */ + private async testOpenRouterKey(apiKey: string): Promise<{valid: boolean, error?: string}> { + try { + const openrouter = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "https://github.com/your-repo", + "X-Title": "OIC - Online Interview Companion" + } + }); + // Make a simple API call to test the key + await openrouter.models.list(); + return { valid: true }; + } catch (error: any) { + console.error('OpenRouter API key test failed:', error); + + let errorMessage = 'Unknown error validating OpenRouter API key'; + + if (error.status === 401) { + errorMessage = 'Invalid API key. Please check your OpenRouter key and try again.'; + } else if (error.status === 429) { + errorMessage = 'Rate limit exceeded. Your OpenRouter API key has reached its request limit or has insufficient quota.'; + } else if (error.status === 500) { + errorMessage = 'OpenRouter server error. Please try again later.'; + } else if (error.message) { + errorMessage = `Error: ${error.message}`; + } + + return { valid: false, error: errorMessage }; + } + } } // Export a singleton instance diff --git a/electron/ProcessingHelper.ts b/electron/ProcessingHelper.ts index 0dcd26f..01dd2a1 100644 --- a/electron/ProcessingHelper.ts +++ b/electron/ProcessingHelper.ts @@ -49,6 +49,7 @@ export class ProcessingHelper { private openaiClient: OpenAI | null = null private geminiApiKey: string | null = null private anthropicClient: Anthropic | null = null + private openrouterClient: OpenAI | null = null // AbortControllers for API requests private currentProcessingAbortController: AbortController | null = null @@ -57,43 +58,46 @@ export class ProcessingHelper { constructor(deps: IProcessingHelperDeps) { this.deps = deps this.screenshotHelper = deps.getScreenshotHelper() - + // Initialize AI client based on config this.initializeAIClient(); - + // Listen for config changes to re-initialize the AI client configHelper.on('config-updated', () => { this.initializeAIClient(); }); } - + /** * Initialize or reinitialize the AI client with current config */ private initializeAIClient(): void { try { const config = configHelper.loadConfig(); - + if (config.apiProvider === "openai") { if (config.apiKey) { - this.openaiClient = new OpenAI({ + this.openaiClient = new OpenAI({ apiKey: config.apiKey, timeout: 60000, // 60 second timeout maxRetries: 2 // Retry up to 2 times }); this.geminiApiKey = null; this.anthropicClient = null; + this.openrouterClient = null; console.log("OpenAI client initialized successfully"); } else { this.openaiClient = null; this.geminiApiKey = null; this.anthropicClient = null; + this.openrouterClient = 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; + this.openrouterClient = null; if (config.apiKey) { this.geminiApiKey = config.apiKey; console.log("Gemini API key set successfully"); @@ -101,12 +105,14 @@ export class ProcessingHelper { this.openaiClient = null; this.geminiApiKey = null; this.anthropicClient = null; + this.openrouterClient = 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; + this.openrouterClient = null; if (config.apiKey) { this.anthropicClient = new Anthropic({ apiKey: config.apiKey, @@ -118,14 +124,40 @@ export class ProcessingHelper { this.openaiClient = null; this.geminiApiKey = null; this.anthropicClient = null; + this.openrouterClient = null; console.warn("No API key available, Anthropic client not initialized"); } + } else if (config.apiProvider === "openrouter") { + // OpenRouter client initialization + this.openaiClient = null; + this.geminiApiKey = null; + this.anthropicClient = null; + if (config.apiKey) { + this.openrouterClient = new OpenAI({ + apiKey: config.apiKey, + baseURL: "https://openrouter.ai/api/v1", + timeout: 60000, + maxRetries: 2, + defaultHeaders: { + "HTTP-Referer": "https://github.com/your-repo", // Optional + "X-Title": "OIC - Online Interview Companion" // Optional + } + }); + console.log("OpenRouter client initialized successfully"); + } else { + this.openaiClient = null; + this.geminiApiKey = null; + this.anthropicClient = null; + this.openrouterClient = null; + console.warn("No API key available, OpenRouter client not initialized"); + } } } catch (error) { console.error("Failed to initialize AI client:", error); this.openaiClient = null; this.geminiApiKey = null; this.anthropicClient = null; + this.openrouterClient = null; } } @@ -166,7 +198,7 @@ export class ProcessingHelper { if (config.language) { return config.language; } - + // Fallback to window variable if config doesn't have language const mainWindow = this.deps.getMainWindow() if (mainWindow) { @@ -187,7 +219,7 @@ export class ProcessingHelper { console.warn("Could not get language from window", err); } } - + // Default fallback return "python"; } catch (error) { @@ -201,11 +233,11 @@ export class ProcessingHelper { if (!mainWindow) return const config = configHelper.loadConfig(); - + // First verify we have a valid AI client if (config.apiProvider === "openai" && !this.openaiClient) { this.initializeAIClient(); - + if (!this.openaiClient) { console.error("OpenAI client not initialized"); mainWindow.webContents.send( @@ -215,7 +247,7 @@ export class ProcessingHelper { } } else if (config.apiProvider === "gemini" && !this.geminiApiKey) { this.initializeAIClient(); - + if (!this.geminiApiKey) { console.error("Gemini API key not initialized"); mainWindow.webContents.send( @@ -226,7 +258,7 @@ export class ProcessingHelper { } else if (config.apiProvider === "anthropic" && !this.anthropicClient) { // Add check for Anthropic client this.initializeAIClient(); - + if (!this.anthropicClient) { console.error("Anthropic client not initialized"); mainWindow.webContents.send( @@ -234,6 +266,17 @@ export class ProcessingHelper { ); return; } + } else if (config.apiProvider === "openrouter" && !this.openrouterClient) { + // Add check for OpenRouter client + this.initializeAIClient(); + + if (!this.openrouterClient) { + console.error("OpenRouter client not initialized"); + mainWindow.webContents.send( + this.deps.PROCESSING_EVENTS.API_KEY_INVALID + ); + return; + } } const view = this.deps.getView() @@ -243,7 +286,7 @@ export class ProcessingHelper { mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.INITIAL_START) const screenshotQueue = this.screenshotHelper.getScreenshotQueue() console.log("Processing main queue screenshots:", screenshotQueue) - + // Check if the queue is empty if (!screenshotQueue || screenshotQueue.length === 0) { console.log("No screenshots found in queue"); @@ -281,7 +324,7 @@ export class ProcessingHelper { // Filter out any nulls from failed screenshots const validScreenshots = screenshots.filter(Boolean); - + if (validScreenshots.length === 0) { throw new Error("Failed to load screenshot data"); } @@ -341,12 +384,12 @@ export class ProcessingHelper { const extraScreenshotQueue = this.screenshotHelper.getExtraScreenshotQueue() console.log("Processing extra queue screenshots:", extraScreenshotQueue) - + // Check if the extra queue is empty if (!extraScreenshotQueue || extraScreenshotQueue.length === 0) { console.log("No extra screenshots found in queue"); mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS); - + return; } @@ -357,7 +400,7 @@ export class ProcessingHelper { mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS); return; } - + mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.DEBUG_START) // Initialize AbortController @@ -370,7 +413,7 @@ export class ProcessingHelper { ...this.screenshotHelper.getScreenshotQueue(), ...existingExtraScreenshots ]; - + const screenshots = await Promise.all( allPaths.map(async (path) => { try { @@ -378,7 +421,7 @@ export class ProcessingHelper { console.warn(`Screenshot file does not exist: ${path}`); return null; } - + return { path, preview: await this.screenshotHelper.getImagePreview(path), @@ -390,14 +433,14 @@ export class ProcessingHelper { } }) ) - + // Filter out any nulls from failed screenshots const validScreenshots = screenshots.filter(Boolean); - + if (validScreenshots.length === 0) { throw new Error("Failed to load screenshot data for debugging"); } - + console.log( "Combined screenshots for processing:", validScreenshots.map((s) => s.path) @@ -446,10 +489,10 @@ export class ProcessingHelper { const config = configHelper.loadConfig(); const language = await this.getLanguage(); const mainWindow = this.deps.getMainWindow(); - + // Step 1: Extract problem info using AI Vision API (OpenAI or Gemini) const imageDataList = screenshots.map(screenshot => screenshot.data); - + // Update the user on progress if (mainWindow) { mainWindow.webContents.send("processing-status", { @@ -459,12 +502,12 @@ export class ProcessingHelper { } let problemInfo; - + if (config.apiProvider === "openai") { // Verify OpenAI client if (!this.openaiClient) { this.initializeAIClient(); // Try to reinitialize - + if (!this.openaiClient) { return { success: false, @@ -476,14 +519,14 @@ export class ProcessingHelper { // Use OpenAI for processing const messages = [ { - role: "system" as const, + 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, + 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 => ({ @@ -515,7 +558,78 @@ export class ProcessingHelper { error: "Failed to parse problem information. Please try again or use clearer screenshots." }; } - } else if (config.apiProvider === "gemini") { + } else if (config.apiProvider === "openrouter") { + // Use OpenRouter API + if (!this.openrouterClient) { + return { + success: false, + error: "OpenRouter API key not configured. Please check your settings." + }; + } + + try { + 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}` } + })) + ] + } + ]; + + // Send to OpenRouter API + const extractionResponse = await this.openrouterClient.chat.completions.create({ + model: config.extractionModel || "openai/gpt-4o", + messages: messages, + max_tokens: 4000, + temperature: 0.2 + }); + + // Parse the response + try { + const responseText = extractionResponse.choices[0].message.content; + // Handle when OpenRouter might wrap the JSON in markdown code blocks + const jsonText = responseText.replace(/```json|```/g, '').trim(); + problemInfo = JSON.parse(jsonText); + } catch (error) { + console.error("Error parsing OpenRouter response:", error); + return { + success: false, + error: "Failed to parse problem information. Please try again or use clearer screenshots." + }; + } + } catch (error: any) { + console.error("Error using OpenRouter API:", error); + + if (error.status === 401) { + return { + success: false, + error: "Invalid OpenRouter API key. Please check your settings." + }; + } else if (error.status === 429) { + return { + success: false, + error: "OpenRouter API rate limit exceeded. Please wait a few minutes before trying again." + }; + } + + return { + success: false, + error: "Failed to process with OpenRouter API. Please check your API key or try again later." + }; + } + } else if (config.apiProvider === "gemini") { // Use Gemini API if (!this.geminiApiKey) { return { @@ -557,13 +671,13 @@ export class ProcessingHelper { ); const responseData = response.data as GeminiResponse; - + if (!responseData.candidates || responseData.candidates.length === 0) { throw new Error("Empty response from Gemini API"); } - + const responseText = responseData.candidates[0].content.parts[0].text; - + // Handle when Gemini might wrap the JSON in markdown code blocks const jsonText = responseText.replace(/```json|```/g, '').trim(); problemInfo = JSON.parse(jsonText); @@ -635,7 +749,7 @@ export class ProcessingHelper { }; } } - + // Update the user on progress if (mainWindow) { mainWindow.webContents.send("processing-status", { @@ -659,13 +773,13 @@ export class ProcessingHelper { if (solutionsResult.success) { // Clear any existing extra screenshots before transitioning to solutions view this.screenshotHelper.clearExtraScreenshotQueue(); - + // Final progress update mainWindow.webContents.send("processing-status", { message: "Solution generated successfully", progress: 100 }); - + mainWindow.webContents.send( this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, solutionsResult.data @@ -687,7 +801,7 @@ export class ProcessingHelper { error: "Processing was canceled by the user." }; } - + // Handle OpenAI API errors specifically if (error?.response?.status === 401) { return { @@ -707,9 +821,9 @@ export class ProcessingHelper { } console.error("API Error Details:", error); - return { - success: false, - error: error.message || "Failed to process screenshots. Please try again." + return { + success: false, + error: error.message || "Failed to process screenshots. Please try again." }; } } @@ -763,7 +877,7 @@ Your solution should be efficient, well-commented, and handle edge cases. `; let responseContent; - + if (config.apiProvider === "openai") { // OpenAI processing if (!this.openaiClient) { @@ -772,7 +886,7 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "OpenAI 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", @@ -785,7 +899,7 @@ Your solution should be efficient, well-commented, and handle edge cases. }); responseContent = solutionResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + } else if (config.apiProvider === "gemini") { // Gemini processing if (!this.geminiApiKey) { return { @@ -793,7 +907,7 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "Gemini API key not configured. Please check your settings." }; } - + try { // Create Gemini message structure const geminiMessages = [ @@ -821,11 +935,11 @@ Your solution should be efficient, well-commented, and handle edge cases. ); 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); @@ -842,7 +956,7 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "Anthropic API key not configured. Please check your settings." }; } - + try { const messages = [ { @@ -886,22 +1000,65 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "Failed to generate solution with Anthropic API. Please check your API key or try again later." }; } + } else if (config.apiProvider === "openrouter") { + // OpenRouter processing + if (!this.openrouterClient) { + return { + success: false, + error: "OpenRouter API key not configured. Please check your settings." + }; + } + + try { + // Send to OpenRouter API + const solutionResponse = await this.openrouterClient.chat.completions.create({ + model: config.solutionModel || "openai/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; + } catch (error: any) { + console.error("Error using OpenRouter API for solution:", error); + + // Add specific handling for OpenRouter's limitations + if (error.status === 429) { + return { + success: false, + error: "OpenRouter API rate limit exceeded. Please wait a few minutes before trying again." + }; + } else if (error.status === 401) { + return { + success: false, + error: "Invalid OpenRouter API key. Please check your settings." + }; + } + + return { + success: false, + error: "Failed to generate solution with OpenRouter API. Please check your API key or try again later." + }; + } } - + // Extract parts from the response 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; 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); if (bulletPoints) { - thoughts = bulletPoints.map(point => + thoughts = bulletPoints.map(point => point.replace(/^\s*(?:[-*•]|\d+\.)\s*/, '').trim() ).filter(Boolean); } else { @@ -911,14 +1068,14 @@ Your solution should be efficient, well-commented, and handle edge cases. .filter(Boolean); } } - + // 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(); @@ -933,7 +1090,7 @@ Your solution should be efficient, well-commented, and handle edge cases. } } } - + const spaceMatch = responseContent.match(spaceComplexityPattern); if (spaceMatch && spaceMatch[1]) { spaceComplexity = spaceMatch[1].trim(); @@ -964,7 +1121,7 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "Processing was canceled by the user." }; } - + if (error?.response?.status === 401) { return { success: false, @@ -976,7 +1133,7 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "OpenAI API rate limit exceeded or insufficient credits. Please try again later." }; } - + console.error("Solution generation error:", error); return { success: false, error: error.message || "Failed to generate solution" }; } @@ -1006,9 +1163,9 @@ Your solution should be efficient, well-commented, and handle edge cases. // Prepare the images for the API call const imageDataList = screenshots.map(screenshot => screenshot.data); - + let debugContent; - + if (config.apiProvider === "openai") { if (!this.openaiClient) { return { @@ -1016,10 +1173,10 @@ Your solution should be efficient, well-commented, and handle edge cases. error: "OpenAI API key not configured. Please check your settings." }; } - + const messages = [ { - role: "system" as const, + 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. Your response MUST follow this exact structure with these section headers (use ### for headers): @@ -1044,12 +1201,12 @@ If you include code examples, use proper markdown code blocks with language spec role: "user" as const, content: [ { - type: "text" as const, + 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: 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` +4. A clear explanation of the changes needed` }, ...imageDataList.map(data => ({ type: "image_url" as const, @@ -1072,16 +1229,102 @@ If you include code examples, use proper markdown code blocks with language spec max_tokens: 4000, temperature: 0.2 }); - + debugContent = debugResponse.choices[0].message.content; - } else if (config.apiProvider === "gemini") { + } else if (config.apiProvider === "openrouter") { + if (!this.openrouterClient) { + return { + success: false, + error: "OpenRouter API key not configured. Please check your settings." + }; + } + + try { + 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. + +Your response MUST follow this exact structure with these section headers (use ### for headers): +### Issues Identified +- List each issue as a bullet point with clear explanation + +### Specific Improvements and Corrections +- List specific code changes needed as bullet points + +### Optimizations +- List any performance optimizations if applicable + +### Explanation of Changes Needed +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: +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}` } + })) + ] + } + ]; + + if (mainWindow) { + mainWindow.webContents.send("processing-status", { + message: "Analyzing code and generating debug feedback with OpenRouter...", + progress: 60 + }); + } + + const debugResponse = await this.openrouterClient.chat.completions.create({ + model: config.debuggingModel || "openai/gpt-4o", + messages: messages, + max_tokens: 4000, + temperature: 0.2 + }); + + debugContent = debugResponse.choices[0].message.content; + } catch (error: any) { + console.error("Error using OpenRouter API for debugging:", error); + + if (error.status === 429) { + return { + success: false, + error: "OpenRouter API rate limit exceeded. Please wait a few minutes before trying again." + }; + } else if (error.status === 401) { + return { + success: false, + error: "Invalid OpenRouter API key. Please check your settings." + }; + } + + return { + success: false, + error: "Failed to process debug request with OpenRouter API. Please check your API key or try again later." + }; + } + } else if (config.apiProvider === "gemini") { if (!this.geminiApiKey) { return { success: false, error: "Gemini API key not configured. Please check your settings." }; } - + try { const debugPrompt = ` 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. @@ -1142,11 +1385,11 @@ If you include code examples, use proper markdown code blocks with language spec ); const responseData = response.data as GeminiResponse; - + if (!responseData.candidates || responseData.candidates.length === 0) { throw new Error("Empty response from Gemini API"); } - + debugContent = responseData.candidates[0].content.parts[0].text; } catch (error) { console.error("Error using Gemini API for debugging:", error); @@ -1162,7 +1405,7 @@ If you include code examples, use proper markdown code blocks with language spec error: "Anthropic API key not configured. Please check your settings." }; } - + try { const debugPrompt = ` 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. @@ -1200,7 +1443,7 @@ If you include code examples, use proper markdown code blocks with language spec type: "image" as const, source: { type: "base64" as const, - media_type: "image/png" as const, + media_type: "image/png" as const, data: data } })) @@ -1221,11 +1464,11 @@ If you include code examples, use proper markdown code blocks with language spec messages: messages, temperature: 0.2 }); - + debugContent = (response.content[0] as { type: 'text', text: string }).text; } catch (error: any) { console.error("Error using Anthropic API for debugging:", error); - + // Add specific handling for Claude's limitations if (error.status === 429) { return { @@ -1238,15 +1481,15 @@ If you include code examples, use proper markdown code blocks with language spec error: "Your screenshots contain too much information for Claude to process. Switch to OpenAI or Gemini in settings which can handle larger inputs." }; } - + return { success: false, error: "Failed to process debug request with Anthropic API. Please check your API key or try again later." }; } } - - + + if (mainWindow) { mainWindow.webContents.send("processing-status", { message: "Debug analysis complete", @@ -1261,7 +1504,7 @@ If you include code examples, use proper markdown code blocks with language spec } let formattedDebugContent = debugContent; - + if (!debugContent.includes('# ') && !debugContent.includes('## ')) { formattedDebugContent = debugContent .replace(/issues identified|problems found|bugs found/i, '## Issues Identified') @@ -1271,10 +1514,10 @@ If you include code examples, use proper markdown code blocks with language spec } const bulletPoints = formattedDebugContent.match(/(?:^|\n)[ ]*(?:[-*•]|\d+\.)[ ]+([^\n]+)/g); - const thoughts = bulletPoints + const thoughts = bulletPoints ? bulletPoints.map(point => point.replace(/^[ ]*(?:[-*•]|\d+\.)[ ]+/, '').trim()).slice(0, 5) : ["Debug analysis based on your screenshots"]; - + const response = { code: extractedCode, debug_analysis: formattedDebugContent, diff --git a/src/components/Settings/SettingsDialog.tsx b/src/components/Settings/SettingsDialog.tsx index 463ea12..10da8f3 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" | "openrouter"; type AIModel = { id: string; @@ -28,6 +28,7 @@ type ModelCategory = { openaiModels: AIModel[]; geminiModels: AIModel[]; anthropicModels: AIModel[]; + openrouterModels: AIModel[]; }; // Define available models for each category @@ -76,6 +77,23 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + openrouterModels: [ + { + id: "openai/gpt-4o", + name: "GPT-4o (via OpenRouter)", + description: "OpenAI's flagship model via OpenRouter" + }, + { + id: "anthropic/claude-3.5-sonnet", + name: "Claude 3.5 Sonnet (via OpenRouter)", + description: "Anthropic's Claude via OpenRouter" + }, + { + id: "google/gemini-pro-1.5", + name: "Gemini Pro 1.5 (via OpenRouter)", + description: "Google's Gemini via OpenRouter" + } ] }, { @@ -122,6 +140,23 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + openrouterModels: [ + { + id: "openai/gpt-4o", + name: "GPT-4o (via OpenRouter)", + description: "OpenAI's flagship model via OpenRouter" + }, + { + id: "anthropic/claude-3.5-sonnet", + name: "Claude 3.5 Sonnet (via OpenRouter)", + description: "Anthropic's Claude via OpenRouter" + }, + { + id: "google/gemini-pro-1.5", + name: "Gemini Pro 1.5 (via OpenRouter)", + description: "Google's Gemini via OpenRouter" + } ] }, { @@ -168,6 +203,23 @@ const modelCategories: ModelCategory[] = [ name: "Claude 3 Opus", description: "Top-level intelligence, fluency, and understanding" } + ], + openrouterModels: [ + { + id: "openai/gpt-4o", + name: "GPT-4o (via OpenRouter)", + description: "OpenAI's flagship model via OpenRouter" + }, + { + id: "anthropic/claude-3.5-sonnet", + name: "Claude 3.5 Sonnet (via OpenRouter)", + description: "Anthropic's Claude via OpenRouter" + }, + { + id: "google/gemini-pro-1.5", + name: "Gemini Pro 1.5 (via OpenRouter)", + description: "Google's Gemini via OpenRouter" + } ] } ]; @@ -251,6 +303,10 @@ 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 === "openrouter") { + setExtractionModel("openai/gpt-4o"); + setSolutionModel("openai/gpt-4o"); + setDebuggingModel("openai/gpt-4o"); } }; @@ -387,13 +443,36 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia +
+
handleProviderChange("openrouter")} + > +
+
+
+

OpenRouter

+

Multiple models via OpenRouter

+
+
+
+
@@ -413,7 +493,7 @@ 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" : "OpenRouter"}

Don't have an API key?

@@ -441,7 +521,7 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia

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

+ )}
@@ -511,7 +603,8 @@ export function SettingsDialog({ open: externalOpen, onOpenChange }: SettingsDia const models = apiProvider === "openai" ? category.openaiModels : apiProvider === "gemini" ? category.geminiModels : - category.anthropicModels; + apiProvider === "anthropic" ? category.anthropicModels : + category.openrouterModels; return (
diff --git a/stealth-run.sh b/stealth-run.sh old mode 100644 new mode 100755 From 0aa1f736ebb32dcd047447c52d2f9a1930cc7848 Mon Sep 17 00:00:00 2001 From: memit0 Date: Sun, 5 Oct 2025 13:45:11 -0400 Subject: [PATCH 2/5] recording audio module added to settings --- AUDIO_FEATURE_README.md | 80 +++++++++++ electron/ipcHandlers.ts | 119 +++++++++++++++ electron/preload.ts | 6 +- package-lock.json | 128 ++++++++++++++++- package.json | 4 +- src/_pages/Queue.tsx | 7 +- src/_pages/SubscribedApp.tsx | 34 ++++- src/components/AudioRecorder.tsx | 191 +++++++++++++++++++++++++ src/components/Header/Header.tsx | 169 +++++++++++++++++++++- src/components/Queue/QueueCommands.tsx | 157 +++++++++++++++++++- src/env.d.ts | 16 +++ 11 files changed, 898 insertions(+), 13 deletions(-) create mode 100644 AUDIO_FEATURE_README.md create mode 100644 src/components/AudioRecorder.tsx diff --git a/AUDIO_FEATURE_README.md b/AUDIO_FEATURE_README.md new file mode 100644 index 0000000..0627727 --- /dev/null +++ b/AUDIO_FEATURE_README.md @@ -0,0 +1,80 @@ +# Audio Recording Feature for Behavioral Questions + +## Overview +The Interview Coder app now includes an audio recording feature that helps you practice behavioral interview questions. This feature records your voice, transcribes the question using OpenRouter's Whisper API, and generates professional answers using the STAR method. + +## Features +- **Audio Recording**: Record questions using your computer's microphone +- **Speech-to-Text**: Automatic transcription using OpenRouter Whisper +- **Answer Generation**: AI-powered behavioral interview answers using the STAR method +- **Playback**: Review your recorded audio before processing +- **Professional Answers**: Detailed, structured responses suitable for interviews + +## How to Use + +### Prerequisites +1. Ensure you have a valid OpenRouter API key configured in the app settings +2. Grant microphone permissions when prompted by your browser/system + +### Step-by-Step Usage +1. **Open the App**: Launch the Interview Coder application +2. **Navigate to Queue**: The audio recorder is located in the main Queue interface +3. **Start Recording**: Click the "Start Recording" button (red microphone icon) +4. **Ask Your Question**: Speak your behavioral interview question clearly +5. **Stop Recording**: Click "Stop Recording" when finished +6. **Review (Optional)**: Click "Play" to review your recorded audio +7. **Generate Answer**: Click "Generate Answer" to process the audio +8. **View Results**: The transcribed question and generated answer will appear below + +### Example Questions +The feature works best with behavioral interview questions such as: +- "Tell me about a time when you had to work with a difficult team member" +- "Describe a situation where you had to meet a tight deadline" +- "Give me an example of when you had to solve a complex problem" +- "Tell me about a time when you showed leadership" + +## Technical Details + +### Audio Format +- Records in WebM format with Opus codec +- Optimized for speech recognition with echo cancellation and noise suppression +- Sample rate: 16kHz for optimal Whisper API performance + +### Answer Generation +- Uses OpenRouter models (configurable in settings, defaults to Claude 3.5 Sonnet) +- Follows the STAR method (Situation, Task, Action, Result) +- Generates 300-450 word responses (2-3 minutes when spoken) +- Professional, conversational tone suitable for interviews + +### Privacy & Security +- Audio files are temporarily stored during processing and immediately deleted +- No audio data is permanently stored on your device +- All processing uses your personal OpenRouter API key + +## Troubleshooting + +### Common Issues +1. **Microphone Not Working**: Check browser/system permissions for microphone access +2. **No Transcription**: Ensure you're speaking clearly and the recording has audio +3. **API Errors**: Verify your OpenRouter API key is valid and has sufficient credits +4. **Poor Audio Quality**: Try recording in a quieter environment + +### Error Messages +- "Failed to start recording": Check microphone permissions +- "No speech detected": The recording may be too quiet or empty +- "OpenRouter API key required": Configure your API key in settings +- "Failed to process audio": Check your internet connection and API key + +## Tips for Best Results +1. **Speak Clearly**: Enunciate words clearly for better transcription +2. **Quiet Environment**: Record in a quiet space to minimize background noise +3. **Complete Questions**: Ask full, complete behavioral interview questions +4. **Review Answers**: Use the generated answers as a starting point and personalize them +5. **Practice**: Use the feature regularly to improve your interview skills + +## Integration +The audio recording feature is seamlessly integrated into the existing Interview Coder interface: +- Located below the screenshot queue in the main interface +- Uses the same toast notification system for feedback +- Shares the OpenRouter API configuration with other features +- Maintains the app's dark theme and consistent UI design diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index f05a9ae..7248463 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -348,4 +348,123 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { return { success: false, error: "Failed to delete last screenshot" } } }) + + // Audio processing handlers + ipcMain.handle("transcribe-audio", async (_event, audioBuffer: Buffer, filename: string) => { + try { + // Check for API key before processing + if (!configHelper.hasApiKey()) { + throw new Error("OpenRouter API key is required for audio transcription") + } + + const config = configHelper.loadConfig() + const apiKey = config.apiKey + + if (!apiKey) { + throw new Error("OpenRouter API key not found") + } + + const fs = require('fs') + const path = require('path') + const os = require('os') + const OpenAI = require('openai') + + // Use OpenRouter API for Whisper + const openai = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1" + }) + + // Create a temporary file + const tempDir = os.tmpdir() + const tempFilePath = path.join(tempDir, `temp_audio_${Date.now()}_${filename}`) + + // Write the buffer to a temporary file + fs.writeFileSync(tempFilePath, audioBuffer) + + try { + // Use OpenRouter's Whisper API for transcription + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream(tempFilePath), + model: "openai/whisper-1", + language: "en" + }) + + return { text: transcription.text } + } finally { + // Clean up the temporary file + try { + fs.unlinkSync(tempFilePath) + } catch (cleanupError) { + console.warn("Failed to clean up temporary audio file:", cleanupError) + } + } + } catch (error) { + console.error("Error transcribing audio:", error) + throw error + } + }) + + ipcMain.handle("generate-behavioral-answer", async (_event, question: string) => { + try { + // Check for API key before processing + if (!configHelper.hasApiKey()) { + throw new Error("OpenRouter API key is required for answer generation") + } + + const config = configHelper.loadConfig() + const apiKey = config.apiKey + + if (!apiKey) { + throw new Error("OpenRouter API key not found") + } + + const OpenAI = require('openai') + + // Use OpenRouter API for chat completions + const openai = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1" + }) + + const prompt = `You are an expert interview coach helping someone prepare for behavioral interviews. + +The interviewer asked: "${question}" + +Please provide a comprehensive, professional answer using the STAR method (Situation, Task, Action, Result). The answer should: +1. Be specific and detailed +2. Show leadership, problem-solving, or relevant skills +3. Include quantifiable results when possible +4. Be authentic and conversational +5. Be around 2-3 minutes when spoken (approximately 300-450 words) + +Provide only the answer, without any prefacing text like "Here's a good answer:" or similar.` + + // Use a good OpenRouter model for behavioral questions + const modelToUse = config.solutionModel || "anthropic/claude-3.5-sonnet" + + const completion = await openai.chat.completions.create({ + model: modelToUse, + messages: [ + { + role: "system", + content: "You are an expert interview coach specializing in behavioral interview questions. Provide detailed, professional answers using the STAR method." + }, + { + role: "user", + content: prompt + } + ], + max_tokens: 600, + temperature: 0.7 + }) + + const answer = completion.choices[0]?.message?.content || "I apologize, but I couldn't generate an answer for this question." + + return { answer } + } catch (error) { + console.error("Error generating behavioral answer:", error) + throw error + } + }) } diff --git a/electron/preload.ts b/electron/preload.ts index 85f3215..89be1e3 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -236,7 +236,11 @@ const electronAPI = { ipcRenderer.removeListener("delete-last-screenshot", subscription) } }, - deleteLastScreenshot: () => ipcRenderer.invoke("delete-last-screenshot") + deleteLastScreenshot: () => ipcRenderer.invoke("delete-last-screenshot"), + + // Audio processing methods + transcribeAudio: (audioBuffer: ArrayBuffer, filename: string) => ipcRenderer.invoke("transcribe-audio", Buffer.from(audioBuffer), filename), + generateBehavioralAnswer: (question: string) => ipcRenderer.invoke("generate-behavioral-answer", question) } // Before exposing the API diff --git a/package-lock.json b/package-lock.json index da87e3a..9bf0244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "electron-updater": "^6.3.9", "form-data": "^4.0.1", "lucide-react": "^0.460.0", + "node-record-lpcm16": "^1.0.1", "openai": "^4.28.4", "react": "^18.2.0", "react-code-blocks": "^0.1.6", @@ -37,7 +38,8 @@ "react-syntax-highlighter": "^15.6.1", "screenshot-desktop": "^1.15.0", "tailwind-merge": "^2.5.5", - "uuid": "^11.0.3" + "uuid": "^11.0.3", + "wav": "^1.0.2" }, "devDependencies": { "@electron/typescript-definitions": "^8.14.0", @@ -4146,6 +4148,22 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4169,11 +4187,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/builder-util": { @@ -4959,7 +4982,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -9176,6 +9198,30 @@ } } }, + "node_modules/node-record-lpcm16": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-record-lpcm16/-/node-record-lpcm16-1.0.1.tgz", + "integrity": "sha512-H75GMOP8ErnF67m21+qSgj4USnzv5RLfm7OkEItdIi+soNKoJZpMQPX6umM8Cn9nVPSgd/dBUtc1msst5MmABA==", + "license": "ISC", + "dependencies": { + "debug": "^2.6.8" + } + }, + "node_modules/node-record-lpcm16/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/node-record-lpcm16/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -10943,6 +10989,30 @@ "node": ">= 6" } }, + "node_modules/stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", + "license": "MIT", + "dependencies": { + "debug": "2" + } + }, + "node_modules/stream-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stream-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11781,6 +11851,58 @@ "node": ">=12.0.0" } }, + "node_modules/wav": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wav/-/wav-1.0.2.tgz", + "integrity": "sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.1.0", + "buffer-from": "^1.0.0", + "debug": "^2.2.0", + "readable-stream": "^1.1.14", + "stream-parser": "^0.3.1" + } + }, + "node_modules/wav/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/wav/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/wav/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/wav/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/wav/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 1fffcfb..8538f23 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,9 @@ "react-syntax-highlighter": "^15.6.1", "screenshot-desktop": "^1.15.0", "tailwind-merge": "^2.5.5", - "uuid": "^11.0.3" + "uuid": "^11.0.3", + "node-record-lpcm16": "^1.0.1", + "wav": "^1.0.2" }, "devDependencies": { "@electron/typescript-definitions": "^8.14.0", diff --git a/src/_pages/Queue.tsx b/src/_pages/Queue.tsx index c9194d5..0469f53 100644 --- a/src/_pages/Queue.tsx +++ b/src/_pages/Queue.tsx @@ -21,13 +21,15 @@ interface QueueProps { credits: number currentLanguage: string setLanguage: (language: string) => void + onTranscriptionComplete?: (transcription: string, answer: string) => void } const Queue: React.FC = ({ setView, credits, currentLanguage, - setLanguage + setLanguage, + onTranscriptionComplete }) => { const { showToast } = useToast() @@ -135,7 +137,7 @@ const Queue: React.FC = ({ const handleOpenSettings = () => { window.electronAPI.openSettingsPortal(); }; - + return (
@@ -152,6 +154,7 @@ const Queue: React.FC = ({ credits={credits} currentLanguage={currentLanguage} setLanguage={setLanguage} + onTranscriptionComplete={onTranscriptionComplete} />
diff --git a/src/_pages/SubscribedApp.tsx b/src/_pages/SubscribedApp.tsx index 2163e86..23ee6cc 100644 --- a/src/_pages/SubscribedApp.tsx +++ b/src/_pages/SubscribedApp.tsx @@ -1,6 +1,6 @@ // file: src/components/SubscribedApp.tsx import { useQueryClient } from "@tanstack/react-query" -import { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState, useCallback } from "react" import Queue from "../_pages/Queue" import Solutions from "../_pages/Solutions" import { useToast } from "../contexts/toast" @@ -18,6 +18,8 @@ const SubscribedApp: React.FC = ({ }) => { const queryClient = useQueryClient() const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") + const [transcription, setTranscription] = useState('') + const [answer, setAnswer] = useState('') const containerRef = useRef(null) const { showToast } = useToast() @@ -134,14 +136,44 @@ const SubscribedApp: React.FC = ({ return () => cleanupFunctions.forEach((fn) => fn()) }, [view]) + // Handle transcription completion from QueueCommands + const handleTranscriptionComplete = (newTranscription: string, newAnswer: string) => { + setTranscription(newTranscription) + setAnswer(newAnswer) + showToast('Success', 'Behavioral question answered successfully!', 'success') + } + return (
+ {/* Audio Results Display */} + {(transcription || answer) && ( +
+ {transcription && ( +
+

Question Detected:

+
+

{transcription}

+
+
+ )} + + {answer && ( +
+

Suggested Answer:

+
+

{answer}

+
+
+ )} +
+ )} {view === "queue" ? ( ) : view === "solutions" ? ( void +} + +export const AudioRecorder: React.FC = ({ + onTranscriptionComplete +}) => { + const [isRecording, setIsRecording] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [audioBlob, setAudioBlob] = useState(null) + const [transcription, setTranscription] = useState('') + const [answer, setAnswer] = useState('') + + const mediaRecorderRef = useRef(null) + const audioChunksRef = useRef([]) + const { showToast } = useToast() + + const startRecording = useCallback(async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 16000 + } + }) + + const mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm;codecs=opus' + }) + + mediaRecorderRef.current = mediaRecorder + audioChunksRef.current = [] + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data) + } + } + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }) + setAudioBlob(audioBlob) + stream.getTracks().forEach(track => track.stop()) + } + + mediaRecorder.start(1000) // Collect data every second + setIsRecording(true) + showToast('Recording', 'Audio recording started', 'success') + } catch (error) { + console.error('Error starting recording:', error) + showToast('Error', 'Failed to start recording. Please check microphone permissions.', 'error') + } + }, [showToast]) + + const stopRecording = useCallback(() => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop() + setIsRecording(false) + showToast('Recording', 'Audio recording stopped', 'neutral') + } + }, [isRecording, showToast]) + + const processAudio = useCallback(async () => { + if (!audioBlob) return + + setIsProcessing(true) + showToast('Processing', 'Transcribing audio and generating answer...', 'neutral') + + try { + // Convert blob to ArrayBuffer + const arrayBuffer = await audioBlob.arrayBuffer() + + // Call the transcription API + const transcriptionResponse = await window.electronAPI.transcribeAudio(arrayBuffer, 'recording.webm') + const transcribedText = transcriptionResponse.text + + setTranscription(transcribedText) + + if (transcribedText.trim()) { + // Generate behavioral question answer + const answerResponse = await window.electronAPI.generateBehavioralAnswer(transcribedText) + const generatedAnswer = answerResponse.answer + + setAnswer(generatedAnswer) + onTranscriptionComplete(transcribedText, generatedAnswer) + + showToast('Success', 'Audio processed and answer generated!', 'success') + } else { + showToast('Warning', 'No speech detected in the recording', 'error') + } + } catch (error) { + console.error('Error processing audio:', error) + showToast('Error', 'Failed to process audio. Please try again.', 'error') + } finally { + setIsProcessing(false) + } + }, [audioBlob, onTranscriptionComplete, showToast]) + + const playRecording = useCallback(() => { + if (audioBlob) { + const audioUrl = URL.createObjectURL(audioBlob) + const audio = new Audio(audioUrl) + audio.play() + } + }, [audioBlob]) + + return ( +
+
+

Behavioral Question Assistant

+
+ {!isRecording ? ( + + ) : ( + + )} + + {audioBlob && !isRecording && ( + <> + + + + )} +
+
+ + {isRecording && ( +
+
+ Recording in progress... +
+ )} + + {transcription && ( +
+

Question Detected:

+
+

{transcription}

+
+
+ )} + + {answer && ( +
+

Suggested Answer:

+
+

{answer}

+
+
+ )} +
+ ) +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index b82b799..e670e3f 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Settings, LogOut, ChevronDown, ChevronUp } from 'lucide-react'; +import React, { useState, useCallback, useRef } from 'react'; +import { Settings, LogOut, ChevronDown, ChevronUp, Mic, MicOff, Square, Play } from 'lucide-react'; import { Button } from '../ui/button'; import { useToast } from '../../contexts/toast'; @@ -7,6 +7,7 @@ interface HeaderProps { currentLanguage: string; setLanguage: (language: string) => void; onOpenSettings: () => void; + onTranscriptionComplete?: (transcription: string, answer: string) => void; } // Available programming languages @@ -21,8 +22,16 @@ const LANGUAGES = [ { value: 'typescript', label: 'TypeScript' }, ]; -export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderProps) { +export function Header({ currentLanguage, setLanguage, onOpenSettings, onTranscriptionComplete }: HeaderProps) { const [dropdownOpen, setDropdownOpen] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [audioBlob, setAudioBlob] = useState(null); + const [transcription, setTranscription] = useState(''); + const [answer, setAnswer] = useState(''); + + const mediaRecorderRef = useRef(null); + const audioChunksRef = useRef([]); const { showToast } = useToast(); // Handle logout - clear API key and reload app @@ -58,6 +67,85 @@ export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderP }); }; + // Audio recording functions + const startRecording = useCallback(async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 16000 + } + }); + + const mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm;codecs=opus' + }); + + mediaRecorderRef.current = mediaRecorder; + audioChunksRef.current = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); + setAudioBlob(audioBlob); + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(1000); + setIsRecording(true); + showToast('Recording', 'Audio recording started', 'success'); + } catch (error) { + console.error('Error starting recording:', error); + showToast('Error', 'Failed to start recording. Please check microphone permissions.', 'error'); + } + }, [showToast]); + + const stopRecording = useCallback(() => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + showToast('Recording', 'Audio recording stopped', 'neutral'); + } + }, [isRecording, showToast]); + + const processAudio = useCallback(async () => { + if (!audioBlob) return; + + setIsProcessing(true); + showToast('Processing', 'Transcribing audio and generating answer...', 'neutral'); + + try { + const arrayBuffer = await audioBlob.arrayBuffer(); + + const transcriptionResponse = await window.electronAPI.transcribeAudio(arrayBuffer, 'recording.webm'); + const transcribedText = transcriptionResponse.text; + + setTranscription(transcribedText); + + if (transcribedText.trim()) { + const answerResponse = await window.electronAPI.generateBehavioralAnswer(transcribedText); + const generatedAnswer = answerResponse.answer; + + setAnswer(generatedAnswer); + onTranscriptionComplete?.(transcribedText, generatedAnswer); + showToast('Success', 'Audio processed and answer generated!', 'success'); + } else { + showToast('Warning', 'No speech detected in the recording', 'error'); + } + } catch (error) { + console.error('Error processing audio:', error); + showToast('Error', 'Failed to process audio. Please try again.', 'error'); + } finally { + setIsProcessing(false); + } + }, [audioBlob, showToast]); + const toggleDropdown = () => { setDropdownOpen(!dropdownOpen); }; @@ -105,6 +193,54 @@ export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderP
+ {!isRecording ? ( + + ) : ( + + )} + + {audioBlob && !isRecording && ( + + )} +
+ {/* Record Audio Command */} + {!isRecording ? ( +
+
+ Record Question +
+ + {COMMAND_KEY} + + + R + +
+
+

+ Record a behavioral interview question. +

+
+ ) : ( +
+
+ Stop Recording +
+ + Recording... + +
+
+

+ Click to stop recording. +

+
+ )} + + {/* Generate Answer Command */} + {audioBlob && !isRecording && ( +
+
+ + {isProcessing ? "Processing..." : "Generate Answer"} + +
+ + {COMMAND_KEY} + + + G + +
+
+

+ {isProcessing + ? "Transcribing and generating answer..." + : "Generate behavioral interview answer." + } +

+
+ )} + {/* Solve Command */}
void onUpdateAvailable: (callback: (info: any) => void) => () => void onUpdateDownloaded: (callback: (info: any) => void) => () => void + + // Configuration methods + getConfig: () => Promise + updateConfig: (config: { apiKey?: string; model?: string; language?: string; opacity?: number }) => Promise + onShowSettings: (callback: () => void) => () => void + checkApiKey: () => Promise + validateApiKey: (apiKey: string) => Promise<{ valid: boolean; error?: string }> + openSettingsPortal: () => Promise<{ success: boolean; error?: string }> + onApiKeyInvalid: (callback: () => void) => () => void + removeListener: (eventName: string, callback: (...args: any[]) => void) => void + onDeleteLastScreenshot: (callback: () => void) => () => void + deleteLastScreenshot: () => Promise<{ success: boolean; error?: string }> + + // Audio processing methods + transcribeAudio: (audioBuffer: ArrayBuffer, filename: string) => Promise<{ text: string }> + generateBehavioralAnswer: (question: string) => Promise<{ answer: string }> } interface Window { From d500e7991f24eb031cf412ecbbac3b7d9c05cddc Mon Sep 17 00:00:00 2001 From: memit0 Date: Sun, 5 Oct 2025 14:17:38 -0400 Subject: [PATCH 3/5] audio generation and result working --- AUDIO_FEATURE_README.md | 48 +++---- electron/ipcHandlers.ts | 210 +++++++++++++++++++++++++------ electron/ipcHandlers_backup.ts | 102 +++++++++++++++ package-lock.json | 135 ++++++++++++++++++-- package.json | 5 +- src/components/AudioRecorder.tsx | 45 ++++++- 6 files changed, 459 insertions(+), 86 deletions(-) create mode 100644 electron/ipcHandlers_backup.ts diff --git a/AUDIO_FEATURE_README.md b/AUDIO_FEATURE_README.md index 0627727..317e51b 100644 --- a/AUDIO_FEATURE_README.md +++ b/AUDIO_FEATURE_README.md @@ -1,20 +1,21 @@ # Audio Recording Feature for Behavioral Questions ## Overview -The Interview Coder app now includes an audio recording feature that helps you practice behavioral interview questions. This feature records your voice, transcribes the question using OpenRouter's Whisper API, and generates professional answers using the STAR method. +The Interview Coder app now includes an audio recording feature that helps you practice behavioral interview questions. This feature records your voice, transcribes the question using OpenRouter's GPT-4o audio capabilities, and generates professional answers using the STAR method. ## Features - **Audio Recording**: Record questions using your computer's microphone -- **Speech-to-Text**: Automatic transcription using OpenRouter Whisper -- **Answer Generation**: AI-powered behavioral interview answers using the STAR method +- **Speech-to-Text**: Automatic transcription using OpenRouter GPT-4o audio or OpenAI Whisper +- **Answer Generation**: AI-powered behavioral interview answers using GPT-4o - **Playback**: Review your recorded audio before processing - **Professional Answers**: Detailed, structured responses suitable for interviews ## How to Use ### Prerequisites -1. Ensure you have a valid OpenRouter API key configured in the app settings -2. Grant microphone permissions when prompted by your browser/system +1. **OpenRouter API Key** (Recommended): For both audio transcription and answer generation using GPT-4o audio +2. **OpenAI API Key** (Alternative): If you prefer to use OpenAI's Whisper for transcription +3. Grant microphone permissions when prompted by your browser/system ### Step-by-Step Usage 1. **Open the App**: Launch the Interview Coder application @@ -26,43 +27,37 @@ The Interview Coder app now includes an audio recording feature that helps you p 7. **Generate Answer**: Click "Generate Answer" to process the audio 8. **View Results**: The transcribed question and generated answer will appear below -### Example Questions -The feature works best with behavioral interview questions such as: -- "Tell me about a time when you had to work with a difficult team member" -- "Describe a situation where you had to meet a tight deadline" -- "Give me an example of when you had to solve a complex problem" -- "Tell me about a time when you showed leadership" - ## Technical Details -### Audio Format -- Records in WebM format with Opus codec -- Optimized for speech recognition with echo cancellation and noise suppression -- Sample rate: 16kHz for optimal Whisper API performance +### Audio Processing +- **OpenRouter Users**: Uses GPT-4o audio model for transcription via multimodal chat completions +- **OpenAI Users**: Uses Whisper-1 model for transcription via dedicated audio API +- **Supported Formats**: WebM (recorded), WAV, MP3 +- **Base64 Encoding**: Audio is automatically converted to base64 for OpenRouter processing ### Answer Generation -- Uses OpenRouter models (configurable in settings, defaults to Claude 3.5 Sonnet) +- Uses GPT-4o model (configurable in settings) - Follows the STAR method (Situation, Task, Action, Result) - Generates 300-450 word responses (2-3 minutes when spoken) - Professional, conversational tone suitable for interviews -### Privacy & Security -- Audio files are temporarily stored during processing and immediately deleted -- No audio data is permanently stored on your device -- All processing uses your personal OpenRouter API key +### API Key Detection +The app automatically detects your API key type: +- **OpenRouter keys** (`sk-or-...`): Uses multimodal audio API for transcription +- **OpenAI keys** (`sk-...`): Uses traditional Whisper API for transcription ## Troubleshooting ### Common Issues 1. **Microphone Not Working**: Check browser/system permissions for microphone access 2. **No Transcription**: Ensure you're speaking clearly and the recording has audio -3. **API Errors**: Verify your OpenRouter API key is valid and has sufficient credits +3. **API Errors**: Verify your API key is valid and has sufficient credits 4. **Poor Audio Quality**: Try recording in a quieter environment ### Error Messages - "Failed to start recording": Check microphone permissions - "No speech detected": The recording may be too quiet or empty -- "OpenRouter API key required": Configure your API key in settings +- "API key required": Configure your API key in settings - "Failed to process audio": Check your internet connection and API key ## Tips for Best Results @@ -71,10 +66,3 @@ The feature works best with behavioral interview questions such as: 3. **Complete Questions**: Ask full, complete behavioral interview questions 4. **Review Answers**: Use the generated answers as a starting point and personalize them 5. **Practice**: Use the feature regularly to improve your interview skills - -## Integration -The audio recording feature is seamlessly integrated into the existing Interview Coder interface: -- Located below the screenshot queue in the main interface -- Uses the same toast notification system for feedback -- Shares the OpenRouter API configuration with other features -- Maintains the app's dark theme and consistent UI design diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index 7248463..f60dd10 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -4,6 +4,67 @@ import { ipcMain, shell, dialog } from "electron" import { randomBytes } from "crypto" import { IIpcHandlerDeps } from "./main" import { configHelper } from "./ConfigHelper" +import ffmpeg from 'fluent-ffmpeg' +import ffmpegStatic from 'ffmpeg-static' +import * as fs from 'fs' +import * as path from 'path' +import * as os from 'os' + +// Set FFmpeg path to the bundled binary +if (ffmpegStatic) { + ffmpeg.setFfmpegPath(ffmpegStatic) +} + +// WebM to WAV conversion function using FFmpeg +async function convertWebMToWAV(webmBuffer: Buffer): Promise { + return new Promise((resolve, reject) => { + const tempDir = os.tmpdir() + const inputPath = path.join(tempDir, `input_${Date.now()}.webm`) + const outputPath = path.join(tempDir, `output_${Date.now()}.wav`) + + try { + // Write WebM buffer to temporary file + fs.writeFileSync(inputPath, webmBuffer) + + // Convert WebM to WAV using FFmpeg + ffmpeg(inputPath) + .toFormat('wav') + .audioFrequency(16000) // 16kHz sample rate for OpenRouter + .audioChannels(1) // Mono audio + .audioBitrate('16k') // 16-bit audio + .on('end', () => { + try { + // Read the converted WAV file + const wavBuffer = fs.readFileSync(outputPath) + + // Cleanup temporary files + try { fs.unlinkSync(inputPath) } catch {} + try { fs.unlinkSync(outputPath) } catch {} + + resolve(wavBuffer) + } catch (readError) { + reject(new Error(`Failed to read converted WAV file: ${readError.message}`)) + } + }) + .on('error', (err: any) => { + // Cleanup temporary files on error + try { fs.unlinkSync(inputPath) } catch {} + try { fs.unlinkSync(outputPath) } catch {} + + reject(new Error(`FFmpeg conversion failed: ${err.message}`)) + }) + .save(outputPath) + + } catch (error) { + // Cleanup on any error + try { fs.unlinkSync(inputPath) } catch {} + try { fs.unlinkSync(outputPath) } catch {} + + reject(new Error(`WebM to WAV conversion setup failed: ${error.message}`)) + } + }) +} + export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { console.log("Initializing IPC handlers") @@ -26,11 +87,11 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { if (!configHelper.isValidApiKeyFormat(apiKey)) { return { valid: false, - error: "Invalid API key format. OpenAI API keys start with 'sk-'" + error: "Invalid API key format. OpenRouter API keys start with 'sk-or-', OpenAI keys start with 'sk-'" }; } - // Then test the API key with OpenAI + // Then test the API key with the appropriate provider const result = await configHelper.testApiKey(apiKey); return result; }) @@ -354,14 +415,14 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { try { // Check for API key before processing if (!configHelper.hasApiKey()) { - throw new Error("OpenRouter API key is required for audio transcription") + throw new Error("API key is required for audio transcription") } const config = configHelper.loadConfig() const apiKey = config.apiKey if (!apiKey) { - throw new Error("OpenRouter API key not found") + throw new Error("API key not found") } const fs = require('fs') @@ -369,34 +430,97 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { const os = require('os') const OpenAI = require('openai') - // Use OpenRouter API for Whisper - const openai = new OpenAI({ - apiKey, - baseURL: "https://openrouter.ai/api/v1" - }) - - // Create a temporary file - const tempDir = os.tmpdir() - const tempFilePath = path.join(tempDir, `temp_audio_${Date.now()}_${filename}`) + // For OpenRouter, we need to convert WebM to WAV since OpenRouter only supports wav/mp3 + // Determine the actual format we'll send (always WAV for WebM input) + const isWebM = filename.toLowerCase().includes('webm') + const isMp3 = filename.toLowerCase().endsWith('.mp3') + const audioFormat = isMp3 ? 'mp3' : 'wav' - // Write the buffer to a temporary file - fs.writeFileSync(tempFilePath, audioBuffer) + if (apiKey.startsWith('sk-or-')) { + // Use OpenRouter's multimodal audio API + const openai = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "https://github.com/your-repo", + "X-Title": "OIC - Online Interview Companion" + } + }) - try { - // Use OpenRouter's Whisper API for transcription - const transcription = await openai.audio.transcriptions.create({ - file: fs.createReadStream(tempFilePath), - model: "openai/whisper-1", - language: "en" + let processedAudioBuffer = audioBuffer + let finalFormat = audioFormat + + // If it's WebM, convert it to WAV using FFmpeg + if (isWebM) { + try { + console.log('Converting WebM to WAV using FFmpeg...') + processedAudioBuffer = await convertWebMToWAV(audioBuffer) + finalFormat = 'wav' + console.log('Successfully converted WebM to WAV') + } catch (conversionError) { + console.error('WebM to WAV conversion failed:', conversionError) + throw new Error(`Failed to convert WebM audio to WAV format: ${conversionError.message}. Please ensure FFmpeg is properly installed.`) + } + } + + // Convert processed audio buffer to base64 + const base64Audio = processedAudioBuffer.toString('base64') + + // Use chat completions with audio input for transcription + const completion = await openai.chat.completions.create({ + model: "openai/gpt-4o-audio-preview", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Please transcribe this audio file. Return only the transcribed text without any additional commentary." + }, + { + type: "input_audio", + input_audio: { + data: base64Audio, + format: finalFormat + } + } + ] + } + ], + max_tokens: 500, + temperature: 0.1 }) - return { text: transcription.text } - } finally { - // Clean up the temporary file + const transcribedText = completion.choices[0]?.message?.content || "" + return { text: transcribedText } + + } else { + // Use OpenAI directly for Whisper transcription + const openai = new OpenAI({ apiKey }) + + // Create a temporary file + const tempDir = os.tmpdir() + const tempFilePath = path.join(tempDir, `temp_audio_${Date.now()}_${filename}`) + + // Write the buffer to a temporary file + fs.writeFileSync(tempFilePath, audioBuffer) + try { - fs.unlinkSync(tempFilePath) - } catch (cleanupError) { - console.warn("Failed to clean up temporary audio file:", cleanupError) + // Use OpenAI's Whisper for transcription + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream(tempFilePath), + model: "whisper-1", + language: "en" + }) + + return { text: transcription.text } + } finally { + // Clean up the temporary file + try { + fs.unlinkSync(tempFilePath) + } catch (cleanupError) { + console.warn("Failed to clean up temporary audio file:", cleanupError) + } } } } catch (error) { @@ -409,23 +533,38 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { try { // Check for API key before processing if (!configHelper.hasApiKey()) { - throw new Error("OpenRouter API key is required for answer generation") + throw new Error("API key is required for answer generation") } const config = configHelper.loadConfig() const apiKey = config.apiKey if (!apiKey) { - throw new Error("OpenRouter API key not found") + throw new Error("API key not found") } const OpenAI = require('openai') - // Use OpenRouter API for chat completions - const openai = new OpenAI({ - apiKey, - baseURL: "https://openrouter.ai/api/v1" - }) + // Use OpenRouter for answer generation if available, otherwise use OpenAI + let openai + let modelToUse + + if (apiKey.startsWith('sk-or-')) { + // Use OpenRouter API for chat completions + openai = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "https://github.com/your-repo", + "X-Title": "OIC - Online Interview Companion" + } + }) + modelToUse = config.solutionModel || "openai/gpt-4o" + } else { + // Use OpenAI directly + openai = new OpenAI({ apiKey }) + modelToUse = config.solutionModel || "gpt-4o" + } const prompt = `You are an expert interview coach helping someone prepare for behavioral interviews. @@ -440,9 +579,6 @@ Please provide a comprehensive, professional answer using the STAR method (Situa Provide only the answer, without any prefacing text like "Here's a good answer:" or similar.` - // Use a good OpenRouter model for behavioral questions - const modelToUse = config.solutionModel || "anthropic/claude-3.5-sonnet" - const completion = await openai.chat.completions.create({ model: modelToUse, messages: [ diff --git a/electron/ipcHandlers_backup.ts b/electron/ipcHandlers_backup.ts new file mode 100644 index 0000000..1e4e333 --- /dev/null +++ b/electron/ipcHandlers_backup.ts @@ -0,0 +1,102 @@ +import { ipcMain } from "electron" +import { configHelper } from "./ConfigHelper" + +// Audio processing handlers + ipcMain.handle("transcribe-audio", async (_event, audioBuffer: Buffer, filename: string) => { + try { + // Check for API key before processing + if (!configHelper.hasApiKey()) { + throw new Error("API key is required for audio transcription") + } + + const config = configHelper.loadConfig() + const apiKey = config.apiKey + + if (!apiKey) { + throw new Error("API key not found") + } + + const fs = require('fs') + const path = require('path') + const os = require('os') + const OpenAI = require('openai') + + // Determine audio format from filename + const audioFormat = filename.toLowerCase().endsWith('.mp3') ? 'mp3' : 'wav' + + if (apiKey.startsWith('sk-or-')) { + // Use OpenRouter's multimodal audio API + const openai = new OpenAI({ + apiKey, + baseURL: "https://openrouter.ai/api/v1", + defaultHeaders: { + "HTTP-Referer": "https://github.com/your-repo", + "X-Title": "OIC - Online Interview Companion" + } + }) + + // Convert audio buffer to base64 + const base64Audio = audioBuffer.toString('base64') + + // Use chat completions with audio input for transcription + const completion = await openai.chat.completions.create({ + model: "openai/gpt-4o-audio-preview", + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Please transcribe this audio file. Return only the transcribed text without any additional commentary." + }, + { + type: "input_audio", + input_audio: { + data: base64Audio, + format: audioFormat + } + } + ] + } + ], + max_tokens: 500, + temperature: 0.1 + }) + + const transcribedText = completion.choices[0]?.message?.content || "" + return { text: transcribedText } + + } else { + // Use OpenAI directly for Whisper transcription + const openai = new OpenAI({ apiKey }) + + // Create a temporary file + const tempDir = os.tmpdir() + const tempFilePath = path.join(tempDir, `temp_audio_${Date.now()}_${filename}`) + + // Write the buffer to a temporary file + fs.writeFileSync(tempFilePath, audioBuffer) + + try { + // Use OpenAI's Whisper for transcription + const transcription = await openai.audio.transcriptions.create({ + file: fs.createReadStream(tempFilePath), + model: "whisper-1", + language: "en" + }) + + return { text: transcription.text } + } finally { + // Clean up the temporary file + try { + fs.unlinkSync(tempFilePath) + } catch (cleanupError) { + console.warn("Failed to clean up temporary audio file:", cleanupError) + } + } + } + } catch (error) { + console.error("Error transcribing audio:", error) + throw error + } + }) diff --git a/package-lock.json b/package-lock.json index 9bf0244..56597ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/fluent-ffmpeg": "^2.1.27", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -27,6 +28,8 @@ "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", + "ffmpeg-static": "^5.2.0", + "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.1", "lucide-react": "^0.460.0", "node-record-lpcm16": "^1.0.1", @@ -449,6 +452,21 @@ "node": ">=6.9.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -2866,6 +2884,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.27.tgz", + "integrity": "sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3507,7 +3534,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -4420,6 +4446,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -4753,6 +4785,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", @@ -5927,7 +5974,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6530,6 +6576,22 @@ "pend": "~1.2.0" } }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6641,6 +6703,37 @@ "dev": true, "license": "ISC" }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -7262,6 +7355,21 @@ "node": ">= 6" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7280,7 +7388,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -7573,7 +7680,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { @@ -9480,6 +9586,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -9868,7 +9979,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -10269,9 +10379,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10661,7 +10769,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10676,8 +10783,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -11017,9 +11123,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -11510,6 +11614,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -11705,7 +11815,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { diff --git a/package.json b/package.json index 8538f23..161b5c6 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@radix-ui/react-toast": "^1.2.2", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.64.0", + "@types/fluent-ffmpeg": "^2.1.27", "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -140,8 +141,11 @@ "electron-log": "^5.2.4", "electron-store": "^10.0.0", "electron-updater": "^6.3.9", + "ffmpeg-static": "^5.2.0", + "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.1", "lucide-react": "^0.460.0", + "node-record-lpcm16": "^1.0.1", "openai": "^4.28.4", "react": "^18.2.0", "react-code-blocks": "^0.1.6", @@ -151,7 +155,6 @@ "screenshot-desktop": "^1.15.0", "tailwind-merge": "^2.5.5", "uuid": "^11.0.3", - "node-record-lpcm16": "^1.0.1", "wav": "^1.0.2" }, "devDependencies": { diff --git a/src/components/AudioRecorder.tsx b/src/components/AudioRecorder.tsx index 1af15e6..b4e798c 100644 --- a/src/components/AudioRecorder.tsx +++ b/src/components/AudioRecorder.tsx @@ -14,6 +14,7 @@ export const AudioRecorder: React.FC = ({ const [audioBlob, setAudioBlob] = useState(null) const [transcription, setTranscription] = useState('') const [answer, setAnswer] = useState('') + const [audioFormat, setAudioFormat] = useState('webm') const mediaRecorderRef = useRef(null) const audioChunksRef = useRef([]) @@ -29,8 +30,41 @@ export const AudioRecorder: React.FC = ({ } }) + // Try multiple audio formats in order of preference for OpenRouter compatibility + let mimeType = 'audio/webm;codecs=opus' + let fileExtension = 'webm' + + // Check for MP3 support first (best for OpenRouter) + if (MediaRecorder.isTypeSupported('audio/mp3')) { + mimeType = 'audio/mp3' + fileExtension = 'mp3' + } + // Then check for WAV support + else if (MediaRecorder.isTypeSupported('audio/wav')) { + mimeType = 'audio/wav' + fileExtension = 'wav' + } + // Check for other WAV variants + else if (MediaRecorder.isTypeSupported('audio/wave')) { + mimeType = 'audio/wave' + fileExtension = 'wav' + } + else if (MediaRecorder.isTypeSupported('audio/x-wav')) { + mimeType = 'audio/x-wav' + fileExtension = 'wav' + } + // Fallback to WebM (will be converted to WAV in backend) + else { + mimeType = 'audio/webm;codecs=opus' + fileExtension = 'webm' + console.log('Browser using WebM audio recording. Will be converted to WAV in backend.') + showToast('Info', 'Recording in WebM format. Will be converted to WAV for OpenRouter compatibility.', 'neutral') + } + + console.log(`Using audio format: ${mimeType} (${fileExtension})`) + const mediaRecorder = new MediaRecorder(stream, { - mimeType: 'audio/webm;codecs=opus' + mimeType: mimeType }) mediaRecorderRef.current = mediaRecorder @@ -43,8 +77,9 @@ export const AudioRecorder: React.FC = ({ } mediaRecorder.onstop = () => { - const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }) + const audioBlob = new Blob(audioChunksRef.current, { type: mimeType }) setAudioBlob(audioBlob) + setAudioFormat(fileExtension) stream.getTracks().forEach(track => track.stop()) } @@ -75,8 +110,8 @@ export const AudioRecorder: React.FC = ({ // Convert blob to ArrayBuffer const arrayBuffer = await audioBlob.arrayBuffer() - // Call the transcription API - const transcriptionResponse = await window.electronAPI.transcribeAudio(arrayBuffer, 'recording.webm') + // Call the transcription API with the correct format + const transcriptionResponse = await window.electronAPI.transcribeAudio(arrayBuffer, `recording.${audioFormat}`) const transcribedText = transcriptionResponse.text setTranscription(transcribedText) @@ -99,7 +134,7 @@ export const AudioRecorder: React.FC = ({ } finally { setIsProcessing(false) } - }, [audioBlob, onTranscriptionComplete, showToast]) + }, [audioBlob, audioFormat, onTranscriptionComplete, showToast]) const playRecording = useCallback(() => { if (audioBlob) { From 97fa46588bb8360d2d7735ec5873714b6858cd60 Mon Sep 17 00:00:00 2001 From: memit0 Date: Mon, 6 Oct 2025 13:59:00 -0400 Subject: [PATCH 4/5] removed upload feature --- electron/ipcHandlers.ts | 14 +- electron/preload.ts | 2 +- electron/uniparser.d.ts | 8 + package-lock.json | 804 ++++++++++++++++++++++++- package.json | 2 + src/components/Queue/QueueCommands.tsx | 100 ++- 6 files changed, 844 insertions(+), 86 deletions(-) create mode 100644 electron/uniparser.d.ts diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index f60dd10..5b4b647 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -67,7 +67,9 @@ async function convertWebMToWAV(webmBuffer: Buffer): Promise { export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { - console.log("Initializing IPC handlers") + console.log("\n" + "🚀 INITIALIZING IPC HANDLERS") + console.log("🎤 Audio processing functionality enabled") + console.log("=".repeat(50)) // Configuration handlers ipcMain.handle("get-config", () => { @@ -531,6 +533,9 @@ export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { ipcMain.handle("generate-behavioral-answer", async (_event, question: string) => { try { + console.log("\n" + "🎯 BEHAVIORAL ANSWER GENERATION") + console.log("❓ Question:", question) + // Check for API key before processing if (!configHelper.hasApiKey()) { throw new Error("API key is required for answer generation") @@ -579,6 +584,13 @@ Please provide a comprehensive, professional answer using the STAR method (Situa Provide only the answer, without any prefacing text like "Here's a good answer:" or similar.` + console.log("\n" + "🤖 FULL PROMPT BEING SENT TO AI") + console.log("─".repeat(60)) + console.log(prompt) + console.log("─".repeat(60)) + console.log("🚀 Sending to AI model:", modelToUse) + console.log("=".repeat(50) + "\n") + const completion = await openai.chat.completions.create({ model: modelToUse, messages: [ diff --git a/electron/preload.ts b/electron/preload.ts index 89be1e3..79b1a7f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -240,7 +240,7 @@ const electronAPI = { // Audio processing methods transcribeAudio: (audioBuffer: ArrayBuffer, filename: string) => ipcRenderer.invoke("transcribe-audio", Buffer.from(audioBuffer), filename), - generateBehavioralAnswer: (question: string) => ipcRenderer.invoke("generate-behavioral-answer", question) + generateBehavioralAnswer: (question: string) => ipcRenderer.invoke("generate-behavioral-answer", question), } // Before exposing the API diff --git a/electron/uniparser.d.ts b/electron/uniparser.d.ts new file mode 100644 index 0000000..51c6055 --- /dev/null +++ b/electron/uniparser.d.ts @@ -0,0 +1,8 @@ +declare module 'uniparser' { + export function autoParse(filePath: string): Promise; + export function parsePDF(filePath: string): Promise; + export function parseDOCX(filePath: string): Promise; + export function parseTXT(filePath: string): string; + export function parseHTML(filePath: string): string; + export function parseMarkdown(filePath: string): string; +} diff --git a/package-lock.json b/package-lock.json index 56597ce..909c552 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.1", "lucide-react": "^0.460.0", + "mammoth": "^1.11.0", "node-record-lpcm16": "^1.0.1", "openai": "^4.28.4", "react": "^18.2.0", @@ -41,6 +42,7 @@ "react-syntax-highlighter": "^15.6.1", "screenshot-desktop": "^1.15.0", "tailwind-merge": "^2.5.5", + "uniparser": "^1.3.2", "uuid": "^11.0.3", "wav": "^1.0.2" }, @@ -1849,11 +1851,195 @@ "node": ">= 10.0.0" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1867,7 +2053,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1877,7 +2062,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -2903,6 +3087,16 @@ "@types/node": "*" } }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", @@ -2952,6 +3146,12 @@ "@types/unist": "*" } }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3482,7 +3682,6 @@ "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3881,7 +4080,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4024,7 +4222,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4084,6 +4281,12 @@ "bluebird": "^3.5.5" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -4107,7 +4310,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -4519,6 +4721,48 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4710,6 +4954,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "license": "MIT" + }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -5139,6 +5389,22 @@ "node": ">=4" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-to-react-native": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", @@ -5150,6 +5416,18 @@ "postcss-value-parser": "^4.0.2" } }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5392,6 +5670,12 @@ "node": ">=0.3.1" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -5431,7 +5715,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -5520,6 +5803,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", @@ -5554,6 +5892,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5960,6 +6307,19 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -5970,6 +6330,18 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6487,7 +6859,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6504,7 +6875,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6547,7 +6917,6 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -6632,7 +7001,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7333,6 +7701,37 @@ "dev": true, "license": "ISC" }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -7428,7 +7827,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7462,12 +7860,17 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7596,7 +7999,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7616,7 +8018,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -7639,7 +8040,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7655,13 +8055,20 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.4", @@ -7937,6 +8344,48 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8017,6 +8466,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -8155,6 +8613,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -8198,6 +8667,60 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/mammoth/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/mammoth/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/mammoth/node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -8209,6 +8732,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", + "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -8456,7 +8991,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -9057,7 +9591,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9368,6 +9901,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9466,6 +10011,12 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9574,6 +10125,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9647,6 +10204,55 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9722,6 +10328,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "4.10.38", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.65" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -9739,7 +10357,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9971,9 +10588,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -10041,7 +10656,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10552,7 +11166,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -10731,11 +11344,86 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-copy/node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rollup-plugin-copy/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/rollup-plugin-copy/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -10789,7 +11477,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -10883,6 +11570,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -10949,7 +11642,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11528,7 +12220,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11646,12 +12337,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/uniparser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uniparser/-/uniparser-1.3.2.tgz", + "integrity": "sha512-tSpfUgLuJdRQrgY9zUwjzV2a8DMbVdctMVgWftziI2jO1HnFG78hCk6K4U4HSnpfNdhjM5zjf5y2ckgqFb2C1Q==", + "license": "MIT", + "dependencies": { + "cheerio": "^1.0.0", + "mammoth": "^1.8.0", + "marked": "^14.1.3", + "pdfjs-dist": "^4.7.76", + "rollup-plugin-copy": "^3.5.0" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -12037,6 +12756,27 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/package.json b/package.json index 161b5c6..dcf54eb 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.1", "lucide-react": "^0.460.0", + "mammoth": "^1.11.0", "node-record-lpcm16": "^1.0.1", "openai": "^4.28.4", "react": "^18.2.0", @@ -154,6 +155,7 @@ "react-syntax-highlighter": "^15.6.1", "screenshot-desktop": "^1.15.0", "tailwind-merge": "^2.5.5", + "uniparser": "^1.3.2", "uuid": "^11.0.3", "wav": "^1.0.2" }, diff --git a/src/components/Queue/QueueCommands.tsx b/src/components/Queue/QueueCommands.tsx index feb609c..0bdaf90 100644 --- a/src/components/Queue/QueueCommands.tsx +++ b/src/components/Queue/QueueCommands.tsx @@ -34,33 +34,33 @@ const QueueCommands: React.FC = ({ // Audio recording functions const startRecording = useCallback(async () => { try { - const stream = await navigator.mediaDevices.getUserMedia({ + const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000 - } + } }); - + const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); - + mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; - + mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; - + mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); setAudioBlob(audioBlob); stream.getTracks().forEach(track => track.stop()); }; - + mediaRecorder.start(1000); setIsRecording(true); showToast('Recording', 'Audio recording started', 'success'); @@ -86,14 +86,14 @@ const QueueCommands: React.FC = ({ try { const arrayBuffer = await audioBlob.arrayBuffer(); - + const transcriptionResponse = await window.electronAPI.transcribeAudio(arrayBuffer, 'recording.webm'); const transcribedText = transcriptionResponse.text; - + if (transcribedText.trim()) { const answerResponse = await window.electronAPI.generateBehavioralAnswer(transcribedText); const generatedAnswer = answerResponse.answer; - + onTranscriptionComplete?.(transcribedText, generatedAnswer); showToast('Success', 'Audio processed and answer generated!', 'success'); } else { @@ -114,16 +114,16 @@ const QueueCommands: React.FC = ({ hiddenRenderContainer.style.position = 'absolute'; hiddenRenderContainer.style.left = '-9999px'; document.body.appendChild(hiddenRenderContainer); - + // Create a root and render the LanguageSelector temporarily const root = createRoot(hiddenRenderContainer); root.render( - {}} + { }} /> ); - + // Use a small delay to ensure the component has rendered // 50ms is generally enough for React to complete a render cycle setTimeout(() => { @@ -132,11 +132,11 @@ const QueueCommands: React.FC = ({ if (selectElement) { const options = Array.from(selectElement.options); const values = options.map(opt => opt.value); - + // Find current language index const currentIndex = values.indexOf(currentLanguage); let newIndex = currentIndex; - + if (direction === 'prev') { // Go to previous language newIndex = (currentIndex - 1 + values.length) % values.length; @@ -144,13 +144,13 @@ const QueueCommands: React.FC = ({ // Default to next language newIndex = (currentIndex + 1) % values.length; } - + if (newIndex !== currentIndex) { setLanguage(values[newIndex]); window.electronAPI.updateConfig({ language: values[newIndex] }); } } - + // Clean up root.unmount(); document.body.removeChild(hiddenRenderContainer); @@ -170,14 +170,14 @@ const QueueCommands: React.FC = ({ // Clear any local storage or electron-specific data localStorage.clear(); sessionStorage.clear(); - + // Clear the API key in the configuration await window.electronAPI.updateConfig({ apiKey: '', }); - + showToast('Success', 'Logged out successfully', 'success'); - + // Reload the app after a short delay setTimeout(() => { window.location.reload(); @@ -220,14 +220,14 @@ const QueueCommands: React.FC = ({ {screenshotCount === 0 ? "Take first screenshot" : screenshotCount === 1 - ? "Take second screenshot" - : screenshotCount === 2 - ? "Take third screenshot" - : screenshotCount === 3 - ? "Take fourth screenshot" - : screenshotCount === 4 - ? "Take fifth screenshot" - : "Next will replace first screenshot"} + ? "Take second screenshot" + : screenshotCount === 2 + ? "Take third screenshot" + : screenshotCount === 3 + ? "Take fourth screenshot" + : screenshotCount === 4 + ? "Take fifth screenshot" + : "Next will replace first screenshot"}