├── .env ├── .gitignore ├── README.md ├── dist ├── index.html ├── main.js ├── preload.js └── renderer.js ├── docs ├── ADDITIONAL_DOCS.md ├── DEVELOPMENT_GUIDE.md └── USER_GUIDE.md ├── package-lock.json ├── package.json ├── src ├── index.html ├── main.ts ├── preload.ts └── renderer.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Interview Coder - Improved Version 2 | 3 | ### For Windows, please use https://github.com/phulelouch/open-interview-coder/tree/window-version 4 | 5 | 6 | 7 | https://github.com/user-attachments/assets/23365245-2f4b-4f9a-af44-5358402b0546 8 | 9 | 10 | 11 | 12 | 13 | ## Table of Contents 14 | 1. [Introduction](#introduction) 15 | 2. [Installation](#installation) 16 | 3. [Getting Started](#getting-started) 17 | 4. [Features](#features) 18 | 5. [User Interface](#user-interface) 19 | 6. [Keyboard Shortcuts](#keyboard-shortcuts) 20 | 7. [Taking Screenshots](#taking-screenshots) 21 | 8. [Analyzing Code Problems](#analyzing-code-problems) 22 | 9. [Window Management](#window-management) 23 | 10. [Troubleshooting](#troubleshooting) 24 | 25 | ### Introduction: 26 | You know about the Interview Coder guy who made invisible AI window? 🫠🫠🫠 I don't like the fact that he taking advantage of poor job market and rip off poor students 😐😐. I rather make a fair game for everyone. I reverse engineering it and his whole thing was based on one functionality: `setContentProtection` from electron https://www.electronjs.org/docs/latest/api/browser-window 27 | This project is an improved version of the Open Interview Coder, an invisible desktop application that helps with technical interviews. It provides features like screenshot capture, AI-powered analysis, and solution generation without requiring login or authentication. 28 | 29 | Open Interview Coder is an opensource invisible desktop application designed to help you succeed in technical coding interviews. It provides a discreet way to capture, analyze, and solve coding problems during interviews without being detected by most screen sharing and recording software. 30 | 31 | The application works by creating an invisible window that can be toggled on and off with keyboard shortcuts. When visible, you can take screenshots of coding problems, get AI-powered analysis and solutions, and manage the application window. 32 | 33 | ## Installation 34 | 35 | ### Prerequisites 36 | - Node.js (v18.19.0 or higher) 37 | - OpenAI API Key 38 | 39 | ### Installation Steps 40 | 41 | 1. Download the latest release from the GitHub repository or clone the repository: 42 | ```bash 43 | git clone https://github.com/yourusername/open-interview-coder.git 44 | cd open-interview-coder 45 | ``` 46 | 47 | 2. Install dependencies: 48 | ```bash 49 | npm install 50 | ``` 51 | 52 | 3. Build the application: 53 | ```bash 54 | npm run build 55 | ``` 56 | 57 | 4. Start the application: 58 | ```bash 59 | npm start 60 | ``` 61 | 5. Apply your OpenAI API key in settings 62 | 63 | ## Getting Started 64 | 65 | 1. After starting the application, the window will be invisible by default. 66 | 2. Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) to toggle the window visibility. 67 | 3. When the window appears, you'll see the main interface with tabs for Prompt, Screenshots, and Shortcuts. 68 | 4. You can now use the application to take screenshots, analyze coding problems, and get solutions. 69 | 70 | ## Features 71 | 72 | ## Features 73 | 74 | - 🎯 Invisibility: Undetectable window that bypasses most screen capture methods 75 | - 📸 Screenshot Capture: Capture screenshots of coding problems with a simple keyboard shortcut. 76 | - 🤖 AI-Powered Analysis: Automatically analyzes coding problems 77 | - 💡 Solution Generation: Get detailed explanations and solutions 78 | - 🔧 Real-time Debugging: Debug your code with AI assistance (GPT for now) 79 | - 🎨 Window Management: Freely move and position the window anywhere on screen 80 | 81 | 82 | ## Keyboard Shortcuts 83 | 84 | | Shortcut | Action | 85 | |----------|--------| 86 | | Ctrl/Cmd+Shift+A | Toggle window visibility | 87 | | Ctrl/Cmd+Shift+S | Take screenshot | 88 | | Ctrl/Cmd+Shift+P | Process screenshots | 89 | | Ctrl/Cmd+Shift+↑ | Move window up | 90 | | Ctrl/Cmd+Shift+↓ | Move window down | 91 | | Ctrl/Cmd+Shift+← | Move window left | 92 | | Ctrl/Cmd+Shift+→ | Move window right | 93 | 94 | ## Taking Screenshots 95 | 96 | There are two ways to take screenshots: 97 | 98 | 1. **Using keyboard shortcut**: 99 | - Press `Ctrl+Shift+S` (or `Cmd+Shift+S` on macOS) to capture a screenshot 100 | - The screenshot will be automatically added to the queue 101 | 102 | 2. **Using the UI**: 103 | - Navigate to the Screenshots tab 104 | - Click the "Take Screenshot" button 105 | - The screenshot will be added to the list below 106 | 107 | The application stores up to 5 recent screenshots. When you exceed this limit, the oldest screenshot will be automatically removed. 108 | 109 | ## Analyzing Code Problems 110 | 111 | To analyze coding problems from your screenshots: 112 | 113 | 1. Take one or more screenshots of the problem 114 | 2. Process the screenshots using one of these methods: 115 | - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) 116 | - Click the "Process Screenshots" button in the Screenshots tab 117 | 118 | 3. The AI will analyze the screenshots and generate a solution 119 | 4. The solution will be displayed in the Prompt tab, including: 120 | - Problem explanation 121 | - Solution approach 122 | - Code implementation 123 | - Explanation of the code 124 | 125 | You can also enter custom prompts in the Prompt tab for specific questions about the problem. 126 | 127 | ## Window Management 128 | 129 | ### Moving the Window 130 | You can move the window using keyboard shortcuts: 131 | - `Ctrl+Shift+↑` (or `Cmd+Shift+↑` on macOS): Move window up 132 | - `Ctrl+Shift+↓` (or `Cmd+Shift+↓` on macOS): Move window down 133 | - `Ctrl+Shift+←` (or `Cmd+Shift+←` on macOS): Move window left 134 | - `Ctrl+Shift+→` (or `Cmd+Shift+→` on macOS): Move window right 135 | 136 | ### Hiding the Window 137 | To hide the window: 138 | - Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) 139 | - Click the minimize button in the top-right corner 140 | 141 | ### Showing the Window 142 | To show the window after it's been hidden: 143 | - Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) 144 | 145 | ## Troubleshooting 146 | 147 | ### Application Not Starting 148 | - Ensure Node.js is installed and is version 18.19.0 149 | - Check that all dependencies are installed with `npm install` 150 | 151 | ### Screenshots Not Working 152 | - Ensure the application has screen recording permissions 153 | - On macOS: System Preferences > Security & Privacy > Privacy > Screen Recording 154 | - On Windows: No special permissions needed 155 | - On Linux: May require `xhost` access depending on your distribution 156 | 157 | ### OpenAI API Errors 158 | - Check your internet connection 159 | - Ensure your OpenAI account has available credits 160 | 161 | ### Window Not Invisible in Screen Sharing 162 | - Some newer versions of screen sharing software may detect the window 163 | - Try positioning the window in a less noticeable area of the screen 164 | - Use the keyboard shortcuts to hide the window when not in use 165 | 166 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Interview Coder 7 | 8 | 9 | 10 | 11 | 12 | 355 | 356 | 357 |
358 |
359 |

Open Interview Coder

360 |
361 | 362 | 363 |
364 |
365 | 366 |
367 |
Prompt
368 |
Screenshots
369 |
Settings
370 |
Shortcuts
371 |
372 | 373 |
374 |
375 |
376 | 377 | 378 |
379 |
380 |
381 | 382 |
383 |
384 |
385 | 386 | 387 |
388 |
389 |
390 |
391 | No screenshots 392 |
393 |
394 |
395 |
396 |
397 | 398 |
399 |
400 |
401 |

API Settings

402 |
403 | 404 | 405 | Your API key is stored locally and never sent to our servers. 406 |
407 |
408 | 409 |
410 |
411 | 412 |
413 |

Preferences

414 |
415 | 416 | 423 |
424 |
425 | 426 |
427 |
428 |
429 |
430 | 431 |
432 |
433 |
434 | Toggle Window Visibility 435 |
436 | Ctrl/Cmd+Shift+A 437 |
438 |
439 |
440 | Toggle Mouse Events 441 |
442 | Ctrl/Cmd+Shift+W 443 |
444 |
445 |
446 | Take Screenshot 447 |
448 | Ctrl/Cmd+Shift+S 449 |
450 |
451 |
452 | Process Screenshots 453 |
454 | Ctrl/Cmd+Shift+P 455 |
456 |
457 |
458 | Move Window Up 459 |
460 | Ctrl/Cmd+Shift+ 461 |
462 |
463 |
464 | Move Window Down 465 |
466 | Ctrl/Cmd+Shift+ 467 |
468 |
469 |
470 | Move Window Left 471 |
472 | Ctrl/Cmd+Shift+ 473 |
474 |
475 |
476 | Move Window Right 477 |
478 | Ctrl/Cmd+Shift+ 479 |
480 |
481 |
482 |
483 |
484 | 485 | 492 |
493 | 494 |
495 | 496 | 744 | 745 | 746 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __importDefault = (this && this.__importDefault) || function (mod) { 36 | return (mod && mod.__esModule) ? mod : { "default": mod }; 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | const electron_1 = require("electron"); 40 | const path = __importStar(require("path")); 41 | const url = __importStar(require("url")); 42 | const dotenv = __importStar(require("dotenv")); 43 | const screenshot_desktop_1 = __importDefault(require("screenshot-desktop")); 44 | const fs = __importStar(require("fs")); 45 | const os = __importStar(require("os")); 46 | const electronLog = __importStar(require("electron-log")); 47 | // Use CommonJS require for electron-store with Node 18 48 | const Store = require('electron-store'); 49 | // Use CommonJS require for OpenAI with Node 18 50 | const { OpenAI } = require('openai'); 51 | // Configure logging 52 | electronLog.initialize(); 53 | electronLog.transports.file.level = 'info'; 54 | const log = electronLog; 55 | // Load environment variables 56 | dotenv.config(); 57 | // Create store with schema 58 | const store = new Store({ 59 | defaults: { 60 | windowPosition: { x: 100, y: 100 }, 61 | windowSize: { width: 1600, height: 1200 }, 62 | preferredLanguage: 'python' 63 | } 64 | }); 65 | // Global variables 66 | let mainWindow = null; 67 | let screenshotQueue = []; 68 | const tempDir = path.join(os.tmpdir(), 'open-interview-coder'); 69 | // Ensure temp directory exists 70 | if (!fs.existsSync(tempDir)) { 71 | fs.mkdirSync(tempDir, { recursive: true }); 72 | } 73 | function createWindow() { 74 | // Get saved position and size or use defaults 75 | const savedPosition = store.get('windowPosition'); 76 | const savedSize = store.get('windowSize'); 77 | // Get screen dimensions 78 | const primaryDisplay = electron_1.screen.getPrimaryDisplay(); 79 | const { width, height } = primaryDisplay.workAreaSize; 80 | // Ensure window is within screen bounds 81 | const x = Math.min(Math.max(savedPosition.x, 0), width - savedSize.width); 82 | const y = Math.min(Math.max(savedPosition.y, 0), height - savedSize.height); 83 | mainWindow = new electron_1.BrowserWindow({ 84 | width: savedSize.width, 85 | height: savedSize.height, 86 | x: x, 87 | y: y, 88 | show: false, // Window is initially hidden 89 | // Enable transparent window 90 | transparent: true, 91 | backgroundColor: '#00000000', 92 | frame: false, 93 | titleBarStyle: 'customButtonsOnHover', 94 | // WebPreferences 95 | webPreferences: { 96 | preload: path.join(__dirname, 'preload.js'), 97 | contextIsolation: true, 98 | nodeIntegration: false, 99 | } 100 | }); 101 | // Keep track of the current ignore state 102 | let isIgnoringMouseEvents = true; 103 | // Register a new global shortcut for toggling 104 | electron_1.globalShortcut.register('CommandOrControl+Shift+W', () => { 105 | // Flip the ignore state 106 | isIgnoringMouseEvents = !isIgnoringMouseEvents; 107 | // Apply the updated ignore state to the main window 108 | if (mainWindow) { 109 | mainWindow.setIgnoreMouseEvents(isIgnoringMouseEvents, { forward: true }); 110 | } 111 | console.log('Toggled mouse events ignoring:', isIgnoringMouseEvents); 112 | }); 113 | // Enhanced screen capture resistance 114 | mainWindow.setContentProtection(true); 115 | mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); 116 | mainWindow.setAlwaysOnTop(true, 'screen-saver', 1); 117 | if (process.platform === 'darwin') { 118 | mainWindow.setHiddenInMissionControl(true); 119 | mainWindow.setWindowButtonVisibility(false); 120 | mainWindow.setSkipTaskbar(true); 121 | mainWindow.setHasShadow(false); 122 | } 123 | mainWindow.webContents.setBackgroundThrottling(false); 124 | mainWindow.webContents.setFrameRate(60); 125 | // Load index.html 126 | mainWindow.loadURL(url.format({ 127 | pathname: path.join(__dirname, 'index.html'), 128 | protocol: 'file:', 129 | slashes: true 130 | })); 131 | // Save window position when moved 132 | mainWindow.on('moved', () => { 133 | if (mainWindow) { 134 | const position = mainWindow.getPosition(); 135 | store.set('windowPosition', { x: position[0], y: position[1] }); 136 | } 137 | }); 138 | // Save window size when resized 139 | mainWindow.on('resized', () => { 140 | if (mainWindow) { 141 | const size = mainWindow.getSize(); 142 | store.set('windowSize', { width: size[0], height: size[1] }); 143 | } 144 | }); 145 | // Cleanup when closed 146 | mainWindow.on('closed', () => { 147 | mainWindow = null; 148 | }); 149 | } 150 | // Take a screenshot and save it to temp directory 151 | async function takeScreenshot() { 152 | try { 153 | const timestamp = new Date().getTime(); 154 | const screenshotPath = path.join(tempDir, `screenshot-${timestamp}.png`); 155 | // Take screenshot 156 | const imgBuffer = await (0, screenshot_desktop_1.default)(); 157 | fs.writeFileSync(screenshotPath, imgBuffer); 158 | log.info(`Screenshot saved to ${screenshotPath}`); 159 | return screenshotPath; 160 | } 161 | catch (error) { 162 | log.error('Failed to take screenshot:', error); 163 | console.error('Failed to take screenshot:', error); 164 | throw new Error(`Failed to take screenshot: ${error.message}`); 165 | } 166 | } 167 | // Convert image to base64 168 | function imageToBase64(imagePath) { 169 | try { 170 | const imageBuffer = fs.readFileSync(imagePath); 171 | return imageBuffer.toString('base64'); 172 | } 173 | catch (error) { 174 | log.error('Failed to convert image to base64:', error); 175 | console.error('Failed to convert image to base64:', error); 176 | throw new Error(`Failed to convert image to base64: ${error.message}`); 177 | } 178 | } 179 | // Handle calls from the renderer to ChatGPT 180 | electron_1.ipcMain.handle('chatgpt-request', async (_event, prompt) => { 181 | // Get API key from store 182 | const apiKey = store.get('apiKey'); 183 | if (!apiKey) { 184 | const errorMsg = 'Missing OpenAI API Key. Please add your API key in the Settings tab.'; 185 | log.error(errorMsg); 186 | console.error(errorMsg); 187 | throw new Error(errorMsg); 188 | } 189 | try { 190 | log.info('Sending request to OpenAI API'); 191 | // Initialize OpenAI client 192 | const openai = new OpenAI({ 193 | apiKey: apiKey 194 | }); 195 | // Make a request to OpenAI 196 | const completion = await openai.chat.completions.create({ 197 | model: "gpt-4o-mini", 198 | messages: [{ role: 'user', content: prompt }] 199 | }); 200 | // Extract the response 201 | const assistantReply = completion.choices[0].message.content || ''; 202 | log.info('Received response from OpenAI API'); 203 | return assistantReply; 204 | } 205 | catch (error) { 206 | log.error('Failed to fetch from OpenAI:', error); 207 | console.error('Failed to fetch from OpenAI:', error); 208 | throw new Error(`Failed to fetch from OpenAI: ${error.message}`); 209 | } 210 | }); 211 | // Handle screenshot request 212 | electron_1.ipcMain.handle('take-screenshot', async () => { 213 | try { 214 | const screenshotPath = await takeScreenshot(); 215 | screenshotQueue.push(screenshotPath); 216 | // Keep only the last 5 screenshots 217 | if (screenshotQueue.length > 5) { 218 | const oldScreenshot = screenshotQueue.shift(); 219 | if (oldScreenshot && fs.existsSync(oldScreenshot)) { 220 | fs.unlinkSync(oldScreenshot); 221 | } 222 | } 223 | return { success: true, path: screenshotPath }; 224 | } 225 | catch (error) { 226 | log.error('Error taking screenshot:', error); 227 | console.error('Error taking screenshot:', error); 228 | return { success: false, error: error.message }; 229 | } 230 | }); 231 | // Handle screenshot analysis request with image upload using modern OpenAI SDK 232 | electron_1.ipcMain.handle('analyze-screenshots', async (_event, options) => { 233 | if (screenshotQueue.length === 0) { 234 | const errorMsg = 'No screenshots available to analyze'; 235 | log.error(errorMsg); 236 | console.error(errorMsg); 237 | return { success: false, error: errorMsg }; 238 | } 239 | try { 240 | // Get API key from store 241 | const apiKey = store.get('apiKey'); 242 | if (!apiKey) { 243 | const errorMsg = 'Missing OpenAI API Key. Please add your API key in the Settings tab.'; 244 | log.error(errorMsg); 245 | console.error(errorMsg); 246 | return { success: false, error: errorMsg }; 247 | } 248 | // Initialize OpenAI client 249 | const openai = new OpenAI({ 250 | apiKey: apiKey 251 | }); 252 | // Prepare screenshots for analysis 253 | const screenshots = [...screenshotQueue]; 254 | const language = options.language || store.get('preferredLanguage') || 'python'; 255 | // Build prompt for OpenAI 256 | const promptText = `I'm taking a coding interview and need help with the following problem. Please analyze these screenshots and provide a solution in ${language}. First explain the problem, then provide a step-by-step solution with code examples. But make it short and condense`; 257 | // Prepare message content array 258 | const messageContent = [ 259 | { type: 'text', text: promptText } 260 | ]; 261 | // Add images to the message content 262 | for (const screenshotPath of screenshots) { 263 | try { 264 | // Convert image to base64 265 | const base64Image = imageToBase64(screenshotPath); 266 | // Add image content 267 | messageContent.push({ 268 | type: 'image_url', 269 | image_url: { 270 | url: `data:image/png;base64,${base64Image}` 271 | } 272 | }); 273 | } 274 | catch (error) { 275 | log.error(`Error processing image ${screenshotPath}:`, error); 276 | console.error(`Error processing image ${screenshotPath}:`, error); 277 | } 278 | } 279 | log.info('Sending request to OpenAI API with images'); 280 | console.log('Sending request to OpenAI API with images'); 281 | // Make a request to OpenAI with images using the SDK 282 | const completion = await openai.chat.completions.create({ 283 | model: "gpt-4o-mini", 284 | messages: [{ 285 | role: "user", 286 | content: messageContent 287 | }], 288 | max_tokens: 2000 289 | }); 290 | // Extract the response 291 | const analysis = completion.choices[0].message.content || 'Analysis completed, but no specific solution was generated.'; 292 | log.info('Received analysis from OpenAI API'); 293 | console.log('Received analysis from OpenAI API'); 294 | return { 295 | success: true, 296 | analysis: analysis, 297 | screenshots: screenshots 298 | }; 299 | } 300 | catch (error) { 301 | log.error('Error analyzing screenshots:', error); 302 | console.error('Error analyzing screenshots:', error); 303 | return { success: false, error: error.message }; 304 | } 305 | }); 306 | // API Key and Preferences handlers 307 | electron_1.ipcMain.handle('save-api-key', (_event, apiKey) => { 308 | try { 309 | store.set('apiKey', apiKey); 310 | return { success: true }; 311 | } 312 | catch (error) { 313 | log.error('Error saving API key:', error); 314 | console.error('Error saving API key:', error); 315 | return { success: false, error: error.message }; 316 | } 317 | }); 318 | electron_1.ipcMain.handle('get-api-key', () => { 319 | return store.get('apiKey') || ''; 320 | }); 321 | electron_1.ipcMain.handle('save-preferences', (_event, preferences) => { 322 | try { 323 | if (preferences.preferredLanguage) { 324 | store.set('preferredLanguage', preferences.preferredLanguage); 325 | } 326 | return { success: true }; 327 | } 328 | catch (error) { 329 | log.error('Error saving preferences:', error); 330 | console.error('Error saving preferences:', error); 331 | return { success: false, error: error.message }; 332 | } 333 | }); 334 | electron_1.ipcMain.handle('get-preferences', () => { 335 | return { 336 | preferredLanguage: store.get('preferredLanguage') || 'python' 337 | }; 338 | }); 339 | electron_1.ipcMain.handle('get-screenshots', () => { 340 | return screenshotQueue; 341 | }); 342 | electron_1.ipcMain.handle('remove-screenshot', (_event, index) => { 343 | try { 344 | if (index >= 0 && index < screenshotQueue.length) { 345 | const screenshotPath = screenshotQueue[index]; 346 | screenshotQueue.splice(index, 1); 347 | if (fs.existsSync(screenshotPath)) { 348 | fs.unlinkSync(screenshotPath); 349 | } 350 | return { success: true }; 351 | } 352 | return { success: false, error: 'Invalid screenshot index' }; 353 | } 354 | catch (error) { 355 | log.error('Error removing screenshot:', error); 356 | console.error('Error removing screenshot:', error); 357 | return { success: false, error: error.message }; 358 | } 359 | }); 360 | // Window management handlers 361 | electron_1.ipcMain.on('close-window', () => { 362 | mainWindow?.close(); 363 | }); 364 | electron_1.ipcMain.on('hide-window', () => { 365 | mainWindow?.hide(); 366 | }); 367 | electron_1.ipcMain.on('show-window', () => { 368 | mainWindow?.show(); 369 | }); 370 | electron_1.ipcMain.on('move-window', (_event, direction) => { 371 | if (!mainWindow) 372 | return; 373 | const position = mainWindow.getPosition(); 374 | const step = 200; // pixels to move 375 | let newX = position[0]; 376 | let newY = position[1]; 377 | switch (direction) { 378 | case 'up': 379 | newY -= step; 380 | break; 381 | case 'down': 382 | newY += step; 383 | break; 384 | case 'left': 385 | newX -= step; 386 | break; 387 | case 'right': 388 | newX += step; 389 | break; 390 | } 391 | mainWindow.setPosition(newX, newY); 392 | }); 393 | // Application initialization 394 | electron_1.app.whenReady().then(() => { 395 | createWindow(); 396 | log.info('Application started'); 397 | console.log('Application started'); 398 | console.log('CMD/Control+Shift+A for showing up'); 399 | // Register global shortcuts 400 | // Toggle window visibility: Ctrl+Shift+A 401 | electron_1.globalShortcut.register('CommandOrControl+Shift+A', () => { 402 | if (!mainWindow) { 403 | createWindow(); 404 | } 405 | else if (mainWindow.isVisible()) { 406 | mainWindow.hide(); 407 | } 408 | else { 409 | mainWindow.show(); 410 | } 411 | }); 412 | // Take screenshot: Ctrl+Shift+S 413 | electron_1.globalShortcut.register('CommandOrControl+Shift+S', async () => { 414 | try { 415 | const screenshotPath = await takeScreenshot(); 416 | screenshotQueue.push(screenshotPath); 417 | // Keep only the last 5 screenshots 418 | if (screenshotQueue.length > 5) { 419 | const oldScreenshot = screenshotQueue.shift(); 420 | if (oldScreenshot && fs.existsSync(oldScreenshot)) { 421 | fs.unlinkSync(oldScreenshot); 422 | } 423 | } 424 | // Notify renderer 425 | if (mainWindow) { 426 | mainWindow.webContents.send('screenshot-taken', { path: screenshotPath }); 427 | } 428 | } 429 | catch (error) { 430 | log.error('Error taking screenshot via shortcut:', error); 431 | console.error('Error taking screenshot via shortcut:', error); 432 | } 433 | }); 434 | // Move window: Ctrl+Shift+Arrow keys 435 | electron_1.globalShortcut.register('CommandOrControl+Shift+Up', () => { 436 | electron_1.ipcMain.emit('move-window', null, 'up'); 437 | }); 438 | electron_1.globalShortcut.register('CommandOrControl+Shift+Down', () => { 439 | electron_1.ipcMain.emit('move-window', null, 'down'); 440 | }); 441 | electron_1.globalShortcut.register('CommandOrControl+Shift+Left', () => { 442 | electron_1.ipcMain.emit('move-window', null, 'left'); 443 | }); 444 | electron_1.globalShortcut.register('CommandOrControl+Shift+Right', () => { 445 | electron_1.ipcMain.emit('move-window', null, 'right'); 446 | }); 447 | // Scroll window: Ctrl+Arrow keys 448 | electron_1.globalShortcut.register('CommandOrControl+Up', () => { 449 | if (mainWindow) { 450 | mainWindow.webContents.send('scroll-content', { direction: 'up' }); 451 | } 452 | }); 453 | electron_1.globalShortcut.register('CommandOrControl+Down', () => { 454 | if (mainWindow) { 455 | mainWindow.webContents.send('scroll-content', { direction: 'down' }); 456 | } 457 | }); 458 | // Process screenshots: Ctrl+Shift+P 459 | electron_1.globalShortcut.register('CommandOrControl+Shift+P', () => { 460 | if (mainWindow) { 461 | mainWindow.webContents.send('process-screenshots'); 462 | } 463 | }); 464 | // On macOS, re-create a window when clicking the dock icon if none open 465 | electron_1.app.on('activate', () => { 466 | if (electron_1.BrowserWindow.getAllWindows().length === 0) 467 | createWindow(); 468 | }); 469 | }); 470 | // Quit when all windows are closed, except on macOS 471 | electron_1.app.on('window-all-closed', () => { 472 | if (process.platform !== 'darwin') 473 | electron_1.app.quit(); 474 | }); 475 | // Clean up before quitting 476 | electron_1.app.on('will-quit', () => { 477 | electron_1.globalShortcut.unregisterAll(); 478 | // Clean up temp screenshots 479 | try { 480 | for (const screenshot of screenshotQueue) { 481 | if (fs.existsSync(screenshot)) { 482 | fs.unlinkSync(screenshot); 483 | } 484 | } 485 | } 486 | catch (error) { 487 | log.error('Error cleaning up screenshots:', error); 488 | console.error('Error cleaning up screenshots:', error); 489 | } 490 | }); 491 | -------------------------------------------------------------------------------- /dist/preload.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const electron_1 = require("electron"); 4 | // Expose protected methods that allow the renderer process to use 5 | // the ipcRenderer without exposing the entire object 6 | electron_1.contextBridge.exposeInMainWorld('electronAPI', { 7 | // Send prompt to ChatGPT 8 | sendPrompt: (prompt) => electron_1.ipcRenderer.invoke('chatgpt-request', prompt), 9 | // Window management 10 | closeWindow: () => electron_1.ipcRenderer.send('close-window'), 11 | hideWindow: () => electron_1.ipcRenderer.send('hide-window'), 12 | showWindow: () => electron_1.ipcRenderer.send('show-window'), 13 | moveWindow: (direction) => electron_1.ipcRenderer.send('move-window', direction), 14 | // Screenshot functionality 15 | takeScreenshot: () => electron_1.ipcRenderer.invoke('take-screenshot'), 16 | analyzeScreenshots: (options) => electron_1.ipcRenderer.invoke('analyze-screenshots', options), 17 | // API Key and Preferences management 18 | saveApiKey: (apiKey) => electron_1.ipcRenderer.invoke('save-api-key', apiKey), 19 | getApiKey: () => electron_1.ipcRenderer.invoke('get-api-key'), 20 | savePreferences: (preferences) => electron_1.ipcRenderer.invoke('save-preferences', preferences), 21 | getPreferences: () => electron_1.ipcRenderer.invoke('get-preferences'), 22 | // Screenshot management 23 | getScreenshots: () => electron_1.ipcRenderer.invoke('get-screenshots'), 24 | removeScreenshot: (index) => electron_1.ipcRenderer.invoke('remove-screenshot', index), 25 | // Event listeners 26 | onScreenshotTaken: (callback) => { 27 | electron_1.ipcRenderer.on('screenshot-taken', (_event, data) => callback(data)); 28 | // Return a function to remove the listener 29 | return () => { 30 | electron_1.ipcRenderer.removeAllListeners('screenshot-taken'); 31 | }; 32 | }, 33 | onProcessScreenshots: (callback) => { 34 | electron_1.ipcRenderer.on('process-screenshots', () => callback()); 35 | // Return a function to remove the listener 36 | return () => { 37 | electron_1.ipcRenderer.removeAllListeners('process-screenshots'); 38 | }; 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /dist/renderer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | -------------------------------------------------------------------------------- /docs/ADDITIONAL_DOCS.md: -------------------------------------------------------------------------------- 1 | # Open Interview Coder - Additional Documentation 2 | 3 | ## Table of Contents 4 | 1. [Installation Guide](#installation-guide) 5 | 2. [API Documentation](#api-documentation) 6 | 3. [Keyboard Shortcuts Reference](#keyboard-shortcuts-reference) 7 | 4. [Troubleshooting Guide](#troubleshooting-guide) 8 | 5. [Changelog](#changelog) 9 | 6. [License Information](#license-information) 10 | 11 | ## Installation Guide 12 | 13 | ### System Requirements 14 | - **Operating System**: Windows 10/11, macOS 10.15+, or Linux (Ubuntu 20.04+ recommended) 15 | - **Processor**: 1.6 GHz or faster, dual-core 16 | - **Memory**: 4 GB RAM minimum, 8 GB recommended 17 | - **Disk Space**: 200 MB available space 18 | - **Internet Connection**: Required for AI functionality 19 | - **Node.js**: Version 20.19.0 or higher 20 | 21 | ### Installation Steps by Platform 22 | 23 | #### Windows 24 | 1. Download the latest Windows installer (.exe) from the releases page 25 | 2. Run the installer and follow the on-screen instructions 26 | 3. Create a `.env` file in the installation directory with your OpenAI API key 27 | 4. Launch the application from the Start menu or desktop shortcut 28 | 29 | #### macOS 30 | 1. Download the latest macOS disk image (.dmg) from the releases page 31 | 2. Open the disk image and drag the application to your Applications folder 32 | 3. Create a `.env` file in the application contents folder with your OpenAI API key 33 | 4. Launch the application from the Applications folder or Dock 34 | 35 | #### Linux 36 | 1. Download the latest AppImage from the releases page 37 | 2. Make the AppImage executable: `chmod +x OpenInterviewCoder-*.AppImage` 38 | 3. Create a `.env` file in the same directory as the AppImage with your OpenAI API key 39 | 4. Run the AppImage: `./OpenInterviewCoder-*.AppImage` 40 | 41 | ### From Source 42 | 1. Clone the repository: `git clone https://github.com/yourusername/open-interview-coder.git` 43 | 2. Navigate to the project directory: `cd open-interview-coder` 44 | 3. Install dependencies: `npm install` 45 | 4. Create a `.env` file with your OpenAI API key 46 | 5. Build the application: `npm run build` 47 | 6. Start the application: `npm start` 48 | 49 | ## API Documentation 50 | 51 | The Open Interview Coder exposes several APIs through the Electron IPC system. These APIs are primarily for internal use but can be useful for developers extending the application. 52 | 53 | ### Main Process APIs 54 | 55 | #### Screenshot Management 56 | 57 | ```typescript 58 | // Take a screenshot 59 | ipcMain.handle('take-screenshot', async () => { 60 | // Returns: { success: boolean, path?: string, error?: string } 61 | }); 62 | 63 | // Analyze screenshots 64 | ipcMain.handle('analyze-screenshots', async (_event, options: { language?: string }) => { 65 | // Returns: { success: boolean, analysis?: string, screenshots?: string[], error?: string } 66 | }); 67 | ``` 68 | 69 | #### OpenAI Integration 70 | 71 | ```typescript 72 | // Send a prompt to OpenAI 73 | ipcMain.handle('chatgpt-request', async (_event, prompt: string) => { 74 | // Returns: string (the AI response) 75 | }); 76 | ``` 77 | 78 | #### Window Management 79 | 80 | ```typescript 81 | // Close the window 82 | ipcMain.on('close-window', () => {}); 83 | 84 | // Hide the window 85 | ipcMain.on('hide-window', () => {}); 86 | 87 | // Show the window 88 | ipcMain.on('show-window', () => {}); 89 | 90 | // Move the window 91 | ipcMain.on('move-window', (_event, direction: 'up' | 'down' | 'left' | 'right') => {}); 92 | ``` 93 | 94 | ### Renderer Process APIs 95 | 96 | The following APIs are exposed to the renderer process through the preload script: 97 | 98 | ```typescript 99 | window.electronAPI = { 100 | // Send prompt to ChatGPT 101 | sendPrompt: (prompt: string) => Promise, 102 | 103 | // Window management 104 | closeWindow: () => void, 105 | hideWindow: () => void, 106 | showWindow: () => void, 107 | moveWindow: (direction: 'up' | 'down' | 'left' | 'right') => void, 108 | 109 | // Screenshot functionality 110 | takeScreenshot: () => Promise<{ success: boolean, path?: string, error?: string }>, 111 | analyzeScreenshots: (options: { language?: string }) => Promise<{ success: boolean, analysis?: string, screenshots?: string[], error?: string }>, 112 | 113 | // Event listeners 114 | onScreenshotTaken: (callback: (data: any) => void) => Function, 115 | onProcessScreenshots: (callback: () => void) => Function 116 | } 117 | ``` 118 | 119 | ## Keyboard Shortcuts Reference 120 | 121 | | Shortcut | Platform | Action | 122 | |----------|----------|--------| 123 | | Ctrl+Shift+A | Windows/Linux | Toggle window visibility | 124 | | Cmd+Shift+A | macOS | Toggle window visibility | 125 | | Ctrl+Shift+S | Windows/Linux | Take screenshot | 126 | | Cmd+Shift+S | macOS | Take screenshot | 127 | | Ctrl+Shift+P | Windows/Linux | Process screenshots | 128 | | Cmd+Shift+P | macOS | Process screenshots | 129 | | Ctrl+Shift+↑ | Windows/Linux | Move window up | 130 | | Cmd+Shift+↑ | macOS | Move window up | 131 | | Ctrl+Shift+↓ | Windows/Linux | Move window down | 132 | | Cmd+Shift+↓ | macOS | Move window down | 133 | | Ctrl+Shift+← | Windows/Linux | Move window left | 134 | | Cmd+Shift+← | macOS | Move window left | 135 | | Ctrl+Shift+→ | Windows/Linux | Move window right | 136 | | Cmd+Shift+→ | macOS | Move window right | 137 | 138 | ## Troubleshooting Guide 139 | 140 | ### Common Issues and Solutions 141 | 142 | #### Application Won't Start 143 | 144 | **Symptoms**: Application fails to launch or crashes immediately after launch. 145 | 146 | **Possible Causes and Solutions**: 147 | 1. **Missing Node.js**: Ensure Node.js v20.19.0 or higher is installed. 148 | 2. **Missing Dependencies**: Run `npm install` to ensure all dependencies are installed. 149 | 3. **Corrupted Installation**: Try reinstalling the application. 150 | 4. **Port Conflict**: Check if another application is using the same port. 151 | 152 | #### Screenshots Not Working 153 | 154 | **Symptoms**: Unable to take screenshots or screenshots are blank. 155 | 156 | **Possible Causes and Solutions**: 157 | 1. **Missing Permissions**: Ensure the application has screen recording permissions. 158 | - macOS: System Preferences > Security & Privacy > Privacy > Screen Recording 159 | - Windows: No special permissions needed 160 | - Linux: May require `xhost` access 161 | 2. **Graphics Driver Issues**: Update your graphics drivers. 162 | 3. **Multiple Displays**: Try taking screenshots on the primary display. 163 | 164 | #### OpenAI API Errors 165 | 166 | **Symptoms**: AI analysis fails or returns errors. 167 | 168 | **Possible Causes and Solutions**: 169 | 1. **Missing API Key**: Ensure your OpenAI API key is correctly set in the `.env` file. 170 | 2. **API Key Format**: Check that the API key is in the correct format. 171 | 3. **API Quota**: Verify that your OpenAI account has available credits. 172 | 4. **Network Issues**: Check your internet connection. 173 | 174 | #### Window Not Invisible 175 | 176 | **Symptoms**: The application window is visible in screen sharing software. 177 | 178 | **Possible Causes and Solutions**: 179 | 1. **Incompatible Software**: Some newer versions of screen sharing software can detect the window. 180 | 2. **Window Position**: Try positioning the window in a less noticeable area. 181 | 3. **Alternative Method**: Use the keyboard shortcut to hide the window when not in use. 182 | 183 | ### Logging 184 | 185 | The application uses electron-log for logging. Logs are stored in: 186 | - **Windows**: `%USERPROFILE%\AppData\Roaming\open-interview-coder\logs\` 187 | - **macOS**: `~/Library/Logs/open-interview-coder/` 188 | - **Linux**: `~/.config/open-interview-coder/logs/` 189 | 190 | These logs can be helpful for diagnosing issues. 191 | 192 | ## Changelog 193 | 194 | ### Version 1.0.0 (Current) 195 | - Initial release of the improved Open Interview Coder 196 | - Added screenshot capture functionality 197 | - Added AI-powered analysis of screenshots 198 | - Implemented window management features 199 | - Added global keyboard shortcuts 200 | - Created modern, tabbed UI interface 201 | - Removed login/logout functionality 202 | - Enhanced error handling and logging 203 | 204 | ### Version 0.1.0 (Original) 205 | - Basic invisible window functionality 206 | - Simple ChatGPT integration 207 | - Minimal UI 208 | 209 | ## License Information 210 | 211 | ### Open Interview Coder License (MIT) 212 | 213 | ``` 214 | MIT License 215 | 216 | Copyright (c) 2025 Open Interview Coder Contributors 217 | 218 | Permission is hereby granted, free of charge, to any person obtaining a copy 219 | of this software and associated documentation files (the "Software"), to deal 220 | in the Software without restriction, including without limitation the rights 221 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 222 | copies of the Software, and to permit persons to whom the Software is 223 | furnished to do so, subject to the following conditions: 224 | 225 | The above copyright notice and this permission notice shall be included in all 226 | copies or substantial portions of the Software. 227 | 228 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 229 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 230 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 231 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 232 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 233 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 234 | SOFTWARE. 235 | ``` 236 | 237 | ### Third-Party Licenses 238 | 239 | The Open Interview Coder uses several third-party libraries, each with its own license: 240 | 241 | - **Electron**: MIT License 242 | - **TypeScript**: Apache License 2.0 243 | - **Node.js**: MIT License 244 | - **electron-store**: MIT License 245 | - **screenshot-desktop**: MIT License 246 | - **electron-log**: MIT License 247 | - **dotenv**: BSD 2-Clause License 248 | - **node-fetch**: MIT License 249 | 250 | Full license texts for these dependencies can be found in the `node_modules` directory of the project. 251 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Open Interview Coder - Development Guide 2 | 3 | ## Table of Contents 4 | 1. [Introduction](#introduction) 5 | 2. [Project Structure](#project-structure) 6 | 3. [Technology Stack](#technology-stack) 7 | 4. [Development Environment Setup](#development-environment-setup) 8 | 5. [Building and Running](#building-and-running) 9 | 6. [Architecture Overview](#architecture-overview) 10 | 7. [Key Components](#key-components) 11 | 8. [Adding Features](#adding-features) 12 | 9. [Testing](#testing) 13 | 10. [Packaging and Distribution](#packaging-and-distribution) 14 | 11. [Contributing Guidelines](#contributing-guidelines) 15 | 16 | ## Introduction 17 | 18 | This development guide provides information for developers who want to understand, modify, or contribute to the Open Interview Coder project. The application is an invisible desktop tool designed to help with technical coding interviews by providing screenshot capture, AI-powered analysis, and solution generation. 19 | 20 | ## Project Structure 21 | 22 | ``` 23 | open-interview-coder/ 24 | ├── assets/ # Application assets 25 | │ └── icons/ # Application icons for different platforms 26 | ├── dist/ # Compiled JavaScript files (generated) 27 | ├── docs/ # Documentation 28 | │ ├── USER_GUIDE.md # User guide 29 | │ └── DEVELOPMENT_GUIDE.md # This development guide 30 | ├── node_modules/ # Node.js dependencies (generated) 31 | ├── src/ # Source code 32 | │ ├── index.html # Main HTML file 33 | │ ├── main.ts # Main Electron process 34 | │ └── preload.ts # Preload script for renderer process 35 | ├── .env # Environment variables (create this file) 36 | ├── .gitignore # Git ignore file 37 | ├── package.json # Project metadata and dependencies 38 | ├── package-lock.json # Locked dependencies 39 | ├── README.md # Project overview 40 | └── tsconfig.json # TypeScript configuration 41 | ``` 42 | 43 | ## Technology Stack 44 | 45 | The Open Interview Coder is built using the following technologies: 46 | 47 | - **Electron**: Cross-platform desktop application framework 48 | - **TypeScript**: Typed superset of JavaScript 49 | - **Node.js**: JavaScript runtime 50 | - **HTML/CSS**: Frontend UI 51 | - **OpenAI API**: AI-powered analysis and solution generation 52 | - **Electron Store**: Local storage for application settings 53 | - **Screenshot Desktop**: Screen capture functionality 54 | 55 | ## Development Environment Setup 56 | 57 | ### Prerequisites 58 | 59 | - Node.js (v18) 60 | - npm (included with Node.js) 61 | - Git 62 | - OpenAI API Key 63 | 64 | ### Setup Steps 65 | 66 | 1. Clone the repository: 67 | ```bash 68 | git clone https://github.com/yourusername/open-interview-coder.git 69 | cd open-interview-coder 70 | ``` 71 | 72 | 2. Install dependencies: 73 | ```bash 74 | npm install 75 | ``` 76 | 77 | 3. Create a `.env` file in the root directory with your OpenAI API key: 78 | ``` 79 | OPENAI_API_KEY=your_api_key_here 80 | ``` 81 | 82 | 4. Set up your IDE: 83 | - We recommend Visual Studio Code with the following extensions: 84 | - ESLint 85 | - Prettier 86 | - TypeScript and JavaScript Language Features 87 | - Electron Debug 88 | 89 | ## Building and Running 90 | 91 | ### Development Mode 92 | 93 | To run the application in development mode: 94 | 95 | ```bash 96 | npm run dev 97 | ``` 98 | 99 | This will: 100 | - Compile TypeScript files 101 | - Start the Electron application 102 | 103 | ### Production Build 104 | 105 | To build the application for production: 106 | 107 | ```bash 108 | npm run build 109 | ``` 110 | 111 | This will: 112 | - Clean the output directories 113 | - Compile TypeScript files 114 | - Package the application using electron-builder 115 | 116 | ### Running the Production Build 117 | 118 | To run the production build: 119 | 120 | ```bash 121 | npm start 122 | ``` 123 | 124 | ## Architecture Overview 125 | 126 | The Open Interview Coder follows the standard Electron architecture with two main processes: 127 | 128 | 1. **Main Process** (`main.ts`): 129 | - Controls the application lifecycle 130 | - Creates and manages browser windows 131 | - Handles IPC (Inter-Process Communication) 132 | - Manages screenshot capture and storage 133 | - Communicates with the OpenAI API 134 | - Registers global shortcuts 135 | 136 | 2. **Renderer Process** (`index.html` and associated scripts): 137 | - Handles the user interface 138 | - Sends requests to the main process via IPC 139 | - Displays results from the main process 140 | 141 | Communication between these processes is facilitated by the **Preload Script** (`preload.ts`), which exposes a limited API to the renderer process using Electron's contextBridge. 142 | 143 | ## Key Components 144 | 145 | ### Main Process Components 146 | 147 | #### Window Management 148 | The main process creates and manages the application window, setting properties for invisibility and screen capture resistance. 149 | 150 | ```typescript 151 | function createWindow() { 152 | mainWindow = new BrowserWindow({ 153 | // Window configuration 154 | transparent: true, 155 | backgroundColor: '#00000000', 156 | frame: false, 157 | // Other properties 158 | }); 159 | 160 | // Enhanced screen capture resistance 161 | mainWindow.setContentProtection(true); 162 | // Other window settings 163 | } 164 | ``` 165 | 166 | #### Screenshot Capture 167 | The application uses the screenshot-desktop package to capture screenshots. 168 | 169 | ```typescript 170 | async function takeScreenshot(): Promise { 171 | try { 172 | const timestamp = new Date().getTime(); 173 | const screenshotPath = path.join(tempDir, `screenshot-${timestamp}.png`); 174 | 175 | // Take screenshot 176 | const imgBuffer = await screenshot(); 177 | fs.writeFileSync(screenshotPath, imgBuffer); 178 | 179 | return screenshotPath; 180 | } catch (error) { 181 | // Error handling 182 | } 183 | } 184 | ``` 185 | 186 | #### OpenAI Integration 187 | The application communicates with the OpenAI API to analyze screenshots and generate solutions. 188 | 189 | ```typescript 190 | ipcMain.handle('chatgpt-request', async (_event: IpcMainInvokeEvent, prompt: string) => { 191 | // Ensure API key is loaded 192 | if (!process.env.OPENAI_API_KEY) { 193 | throw new Error('Missing OPENAI_API_KEY in .env'); 194 | } 195 | 196 | try { 197 | // Make a request to OpenAI 198 | const response = await fetch('https://api.openai.com/v1/chat/completions', { 199 | // Request configuration 200 | }); 201 | 202 | // Process response 203 | } catch (error) { 204 | // Error handling 205 | } 206 | }); 207 | ``` 208 | 209 | #### Global Shortcuts 210 | The application registers global keyboard shortcuts for various functions. 211 | 212 | ```typescript 213 | app.whenReady().then(() => { 214 | // Register global shortcuts 215 | globalShortcut.register('CommandOrControl+Shift+A', () => { 216 | // Toggle window visibility 217 | }); 218 | 219 | // Other shortcuts 220 | }); 221 | ``` 222 | 223 | ### Renderer Process Components 224 | 225 | #### User Interface 226 | The renderer process handles the user interface, including tabs, buttons, and input fields. 227 | 228 | #### IPC Communication 229 | The renderer process communicates with the main process using the exposed API. 230 | 231 | ```javascript 232 | // Send prompt to ChatGPT 233 | sendBtn.addEventListener('click', async () => { 234 | const prompt = userInput.value.trim(); 235 | if (!prompt) return; 236 | 237 | responseContainer.textContent = 'Loading...'; 238 | try { 239 | const reply = await window.electronAPI.sendPrompt(prompt); 240 | responseContainer.textContent = reply; 241 | } catch (error) { 242 | responseContainer.textContent = `Error: ${error.message}`; 243 | } 244 | }); 245 | ``` 246 | 247 | ### Preload Script 248 | 249 | The preload script exposes a limited API to the renderer process using Electron's contextBridge. 250 | 251 | ```typescript 252 | contextBridge.exposeInMainWorld('electronAPI', { 253 | // Send prompt to ChatGPT 254 | sendPrompt: (prompt: string) => ipcRenderer.invoke('chatgpt-request', prompt), 255 | 256 | // Window management 257 | closeWindow: () => ipcRenderer.send('close-window'), 258 | // Other methods 259 | 260 | // Event listeners 261 | onScreenshotTaken: (callback: (data: any) => void) => { 262 | ipcRenderer.on('screenshot-taken', (_event, data) => callback(data)); 263 | // Return cleanup function 264 | }, 265 | // Other event listeners 266 | }); 267 | ``` 268 | 269 | ## Adding Features 270 | 271 | ### General Guidelines 272 | 273 | 1. **Maintain Process Separation**: Keep main process logic in main.ts and renderer process logic in the appropriate HTML/JS files. 274 | 2. **Use TypeScript**: Write all new code in TypeScript to maintain type safety. 275 | 3. **Follow Existing Patterns**: Maintain consistency with the existing codebase. 276 | 4. **Document Your Code**: Add comments to explain complex logic. 277 | 278 | ### Adding a New Feature 279 | 280 | To add a new feature to the application: 281 | 282 | 1. **Identify the Process**: Determine whether the feature belongs in the main process, renderer process, or both. 283 | 2. **Update the Main Process** (if needed): 284 | - Add new functionality to main.ts 285 | - Register new IPC handlers if needed 286 | 3. **Update the Preload Script** (if needed): 287 | - Expose new functionality to the renderer process 288 | 4. **Update the Renderer Process** (if needed): 289 | - Add new UI elements to index.html 290 | - Add event handlers for the new functionality 291 | 5. **Test the Feature**: Ensure it works as expected in both development and production builds. 292 | 293 | ### Example: Adding a New Shortcut 294 | 295 | ```typescript 296 | // In main.ts 297 | globalShortcut.register('CommandOrControl+Shift+R', () => { 298 | // New shortcut functionality 299 | }); 300 | 301 | // In preload.ts (if needed) 302 | contextBridge.exposeInMainWorld('electronAPI', { 303 | // Existing methods 304 | newFunction: () => ipcRenderer.invoke('new-function'), 305 | }); 306 | 307 | // In renderer process (if needed) 308 | document.getElementById('newButton').addEventListener('click', () => { 309 | window.electronAPI.newFunction(); 310 | }); 311 | ``` 312 | 313 | ## Testing 314 | 315 | ### Manual Testing 316 | 317 | The application does not currently have automated tests. Manual testing should focus on: 318 | 319 | 1. **Functionality Testing**: Ensure all features work as expected. 320 | 2. **Invisibility Testing**: Verify the application remains invisible in screen sharing applications. 321 | 3. **Performance Testing**: Check for any performance issues, especially with screenshot capture and AI analysis. 322 | 4. **Cross-Platform Testing**: Test on different operating systems (Windows, macOS, Linux). 323 | 324 | ### Testing Checklist 325 | 326 | - [ ] Application starts correctly 327 | - [ ] Window visibility toggle works 328 | - [ ] Screenshot capture works 329 | - [ ] AI analysis works 330 | - [ ] Window movement shortcuts work 331 | - [ ] UI elements function correctly 332 | - [ ] Application remains invisible in screen sharing 333 | - [ ] Application works on all target platforms 334 | 335 | ## Packaging and Distribution 336 | 337 | ### Building for Distribution 338 | 339 | The application uses electron-builder for packaging. The configuration is in package.json: 340 | 341 | ```json 342 | "build": { 343 | "appId": "com.openinterviewcoder.app", 344 | "productName": "Open Interview Coder", 345 | "files": [ 346 | "dist/**/*", 347 | "package.json" 348 | ], 349 | "directories": { 350 | "output": "release" 351 | }, 352 | "mac": { 353 | // Mac-specific configuration 354 | }, 355 | "win": { 356 | // Windows-specific configuration 357 | }, 358 | "linux": { 359 | // Linux-specific configuration 360 | } 361 | } 362 | ``` 363 | 364 | To build for all platforms: 365 | 366 | ```bash 367 | npm run build 368 | ``` 369 | 370 | This will create distribution packages in the `release` directory. 371 | 372 | ### Platform-Specific Considerations 373 | 374 | #### macOS 375 | - Ensure the application has proper entitlements for screen recording 376 | - Consider notarization for distribution outside the App Store 377 | 378 | #### Windows 379 | - No special considerations required 380 | 381 | #### Linux 382 | - May require additional dependencies depending on the distribution 383 | - Consider packaging as AppImage, deb, or rpm 384 | 385 | ## Contributing Guidelines 386 | 387 | ### Code Style 388 | 389 | - Follow the existing code style 390 | - Use TypeScript for all new code 391 | - Use meaningful variable and function names 392 | - Keep functions small and focused 393 | - Add comments for complex logic 394 | 395 | ### Pull Request Process 396 | 397 | 1. Fork the repository 398 | 2. Create a feature branch 399 | 3. Make your changes 400 | 4. Test your changes 401 | 5. Submit a pull request with a clear description of the changes 402 | 403 | ### Commit Message Format 404 | 405 | Use clear, descriptive commit messages: 406 | 407 | ``` 408 | feat: Add new feature X 409 | fix: Fix bug in feature Y 410 | docs: Update documentation for feature Z 411 | refactor: Refactor feature W 412 | ``` 413 | 414 | ### Code Review 415 | 416 | All pull requests will be reviewed for: 417 | - Code quality 418 | - Adherence to the project's architecture 419 | - Potential security issues 420 | - Performance implications 421 | - Documentation 422 | 423 | ## Conclusion 424 | 425 | This development guide provides an overview of the Open Interview Coder project structure, architecture, and development processes. By following these guidelines, you can contribute to the project effectively and maintain its quality and consistency. 426 | 427 | For any questions or issues not covered in this guide, please open an issue on the project's GitHub repository. 428 | -------------------------------------------------------------------------------- /docs/USER_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Open Interview Coder - User Guide 2 | 3 | ## Table of Contents 4 | 1. [Introduction](#introduction) 5 | 2. [Installation](#installation) 6 | 3. [Getting Started](#getting-started) 7 | 4. [Features](#features) 8 | 5. [User Interface](#user-interface) 9 | 6. [Keyboard Shortcuts](#keyboard-shortcuts) 10 | 7. [Taking Screenshots](#taking-screenshots) 11 | 8. [Analyzing Code Problems](#analyzing-code-problems) 12 | 9. [Window Management](#window-management) 13 | 10. [Troubleshooting](#troubleshooting) 14 | 15 | ## Introduction 16 | 17 | Open Interview Coder is an invisible desktop application designed to help you succeed in technical coding interviews. It provides a discreet way to capture, analyze, and solve coding problems during interviews without being detected by most screen sharing and recording software. 18 | 19 | The application works by creating an invisible window that can be toggled on and off with keyboard shortcuts. When visible, you can take screenshots of coding problems, get AI-powered analysis and solutions, and manage the application window. 20 | 21 | ## Installation 22 | 23 | ### Prerequisites 24 | - Node.js (v18.19.0 or higher) 25 | - OpenAI API Key 26 | 27 | ### Installation Steps 28 | 29 | 1. Download the latest release from the GitHub repository or clone the repository: 30 | ```bash 31 | git clone https://github.com/yourusername/open-interview-coder.git 32 | cd open-interview-coder 33 | ``` 34 | 35 | 2. Install dependencies: 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | 3. Create a `.env` file in the root directory with your OpenAI API key: 41 | ``` 42 | OPENAI_API_KEY=your_api_key_here 43 | ``` 44 | 45 | 4. Build the application: 46 | ```bash 47 | npm run build 48 | ``` 49 | 50 | 5. Start the application: 51 | ```bash 52 | npm start 53 | ``` 54 | 55 | ## Getting Started 56 | 57 | 1. After starting the application, the window will be invisible by default. 58 | 2. Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) to toggle the window visibility. 59 | 3. When the window appears, you'll see the main interface with tabs for Prompt, Screenshots, and Shortcuts. 60 | 4. You can now use the application to take screenshots, analyze coding problems, and get solutions. 61 | 62 | ## Features 63 | 64 | ### Invisibility 65 | The application window is invisible to most screen sharing and recording software, including: 66 | - Google Meet 67 | - Discord 68 | - Most browser-based screen recording tools 69 | - Screenshot tools 70 | 71 | ### Screenshot Capture 72 | Capture screenshots of coding problems directly from your screen with a simple keyboard shortcut. 73 | 74 | ### AI-Powered Analysis 75 | Get AI-powered analysis of coding problems, including: 76 | - Problem understanding 77 | - Solution strategies 78 | - Code examples 79 | - Debugging assistance 80 | 81 | ### Window Management 82 | Easily move and position the window anywhere on your screen using keyboard shortcuts. 83 | 84 | ## User Interface 85 | 86 | The application has a tabbed interface with three main sections: 87 | 88 | ### Prompt Tab 89 | - Text input field for entering custom prompts 90 | - Send button to submit prompts to the AI 91 | - Response area displaying AI-generated solutions and explanations 92 | 93 | ### Screenshots Tab 94 | - Button to take screenshots 95 | - Button to process screenshots 96 | - List of captured screenshots 97 | - Option to remove screenshots 98 | 99 | ### Shortcuts Tab 100 | - List of all available keyboard shortcuts 101 | - Descriptions of what each shortcut does 102 | 103 | ## Keyboard Shortcuts 104 | 105 | | Shortcut | Action | 106 | |----------|--------| 107 | | Ctrl/Cmd+Shift+A | Toggle window visibility | 108 | | Ctrl/Cmd+Shift+S | Take screenshot | 109 | | Ctrl/Cmd+Shift+P | Process screenshots | 110 | | Ctrl/Cmd+Shift+↑ | Move window up | 111 | | Ctrl/Cmd+Shift+↓ | Move window down | 112 | | Ctrl/Cmd+Shift+← | Move window left | 113 | | Ctrl/Cmd+Shift+→ | Move window right | 114 | 115 | ## Taking Screenshots 116 | 117 | There are two ways to take screenshots: 118 | 119 | 1. **Using keyboard shortcut**: 120 | - Press `Ctrl+Shift+S` (or `Cmd+Shift+S` on macOS) to capture a screenshot 121 | - The screenshot will be automatically added to the queue 122 | 123 | 2. **Using the UI**: 124 | - Navigate to the Screenshots tab 125 | - Click the "Take Screenshot" button 126 | - The screenshot will be added to the list below 127 | 128 | The application stores up to 5 recent screenshots. When you exceed this limit, the oldest screenshot will be automatically removed. 129 | 130 | ## Analyzing Code Problems 131 | 132 | To analyze coding problems from your screenshots: 133 | 134 | 1. Take one or more screenshots of the problem 135 | 2. Process the screenshots using one of these methods: 136 | - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) 137 | - Click the "Process Screenshots" button in the Screenshots tab 138 | 139 | 3. The AI will analyze the screenshots and generate a solution 140 | 4. The solution will be displayed in the Prompt tab, including: 141 | - Problem explanation 142 | - Solution approach 143 | - Code implementation 144 | - Explanation of the code 145 | 146 | You can also enter custom prompts in the Prompt tab for specific questions about the problem. 147 | 148 | ## Window Management 149 | 150 | ### Moving the Window 151 | You can move the window using keyboard shortcuts: 152 | - `Ctrl+Shift+↑` (or `Cmd+Shift+↑` on macOS): Move window up 153 | - `Ctrl+Shift+↓` (or `Cmd+Shift+↓` on macOS): Move window down 154 | - `Ctrl+Shift+←` (or `Cmd+Shift+←` on macOS): Move window left 155 | - `Ctrl+Shift+→` (or `Cmd+Shift+→` on macOS): Move window right 156 | 157 | ### Hiding the Window 158 | To hide the window: 159 | - Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) 160 | - Click the minimize button in the top-right corner 161 | 162 | ### Showing the Window 163 | To show the window after it's been hidden: 164 | - Press `Ctrl+Shift+A` (or `Cmd+Shift+A` on macOS) 165 | 166 | ## Troubleshooting 167 | 168 | ### Application Not Starting 169 | - Ensure Node.js is installed and is version 18.19.0 170 | - Check that all dependencies are installed with `npm install` 171 | - Verify that the `.env` file exists with a valid OpenAI API key 172 | 173 | ### Screenshots Not Working 174 | - Ensure the application has screen recording permissions 175 | - On macOS: System Preferences > Security & Privacy > Privacy > Screen Recording 176 | - On Windows: No special permissions needed 177 | - On Linux: May require `xhost` access depending on your distribution 178 | 179 | ### OpenAI API Errors 180 | - Verify your API key is correct in the `.env` file 181 | - Check your internet connection 182 | - Ensure your OpenAI account has available credits 183 | 184 | ### Window Not Invisible in Screen Sharing 185 | - Some newer versions of screen sharing software may detect the window 186 | - Try positioning the window in a less noticeable area of the screen 187 | - Use the keyboard shortcuts to hide the window when not in use 188 | 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-interview-coder", 3 | "version": "1.0.0", 4 | "description": "An invisible desktop application to help you pass your technical interviews.", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "start": "electron dist/main.js", 8 | "dev": "tsc && cp src/index.html dist/ && electron dist/main.js", 9 | "build": "tsc && cp src/index.html dist/ && electron-builder", 10 | "clean": "rimraf dist" 11 | }, 12 | "dependencies": { 13 | "dotenv": "^16.4.7", 14 | "electron-log": "^5.2.4", 15 | "electron-store": "^8.1.0", 16 | "node-fetch": "^2.6.11", 17 | "openai": "^4.28.0", 18 | "screenshot-desktop": "^1.15.0", 19 | "tailwindcss": "^3.4.15" 20 | }, 21 | "devDependencies": { 22 | "@types/node-fetch": "^2.6.2", 23 | "@types/screenshot-desktop": "^1.12.3", 24 | "autoprefixer": "^10.4.20", 25 | "electron": "^29.0.0", 26 | "electron-builder": "^24.13.3", 27 | "postcss": "^8.4.49", 28 | "rimraf": "^6.0.1", 29 | "typescript": "^5.0.4" 30 | }, 31 | "build": { 32 | "appId": "com.openinterviewcoder.app", 33 | "productName": "Open Interview Coder", 34 | "files": [ 35 | "dist/**/*", 36 | "package.json" 37 | ], 38 | "extraResources": [ 39 | { 40 | "from": "src", 41 | "to": "dist", 42 | "filter": ["index.html"] 43 | } 44 | ], 45 | "directories": { 46 | "output": "release" 47 | }, 48 | "mac": { 49 | "category": "public.app-category.developer-tools", 50 | "target": [ 51 | { 52 | "target": "dmg", 53 | "arch": [ 54 | "x64", 55 | "arm64" 56 | ] 57 | } 58 | ], 59 | "icon": "assets/icons/mac/icon.icns" 60 | }, 61 | "win": { 62 | "target": [ 63 | "nsis" 64 | ], 65 | "icon": "assets/icons/win/icon.ico" 66 | }, 67 | "linux": { 68 | "target": [ 69 | "AppImage" 70 | ], 71 | "icon": "assets/icons/png/icon-256x256.png" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Open Interview Coder 7 | 8 | 9 | 10 | 11 | 12 | 355 | 356 | 357 |
358 |
359 |

Open Interview Coder

360 |
361 | 362 | 363 |
364 |
365 | 366 |
367 |
Prompt
368 |
Screenshots
369 |
Settings
370 |
Shortcuts
371 |
372 | 373 |
374 |
375 |
376 | 377 | 378 |
379 |
380 |
381 | 382 |
383 |
384 |
385 | 386 | 387 |
388 |
389 |
390 |
391 | No screenshots 392 |
393 |
394 |
395 |
396 |
397 | 398 |
399 |
400 |
401 |

API Settings

402 |
403 | 404 | 405 | Your API key is stored locally and never sent to our servers. 406 |
407 |
408 | 409 |
410 |
411 | 412 |
413 |

Preferences

414 |
415 | 416 | 423 |
424 |
425 | 426 |
427 |
428 |
429 |
430 | 431 |
432 |
433 |
434 | Toggle Window Visibility 435 |
436 | Ctrl/Cmd+Shift+A 437 |
438 |
439 |
440 | Toggle Mouse Events 441 |
442 | Ctrl/Cmd+Shift+W 443 |
444 |
445 |
446 | Take Screenshot 447 |
448 | Ctrl/Cmd+Shift+S 449 |
450 |
451 |
452 | Process Screenshots 453 |
454 | Ctrl/Cmd+Shift+P 455 |
456 |
457 |
458 | Move Window Up 459 |
460 | Ctrl/Cmd+Shift+ 461 |
462 |
463 |
464 | Move Window Down 465 |
466 | Ctrl/Cmd+Shift+ 467 |
468 |
469 |
470 | Move Window Left 471 |
472 | Ctrl/Cmd+Shift+ 473 |
474 |
475 |
476 | Move Window Right 477 |
478 | Ctrl/Cmd+Shift+ 479 |
480 |
481 |
482 |
483 |
484 | 485 | 492 |
493 | 494 |
495 | 496 | 744 | 745 | 746 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, IpcMainInvokeEvent, globalShortcut, screen } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | import * as dotenv from 'dotenv'; 5 | import fetch from 'node-fetch'; 6 | import screenshot from 'screenshot-desktop'; 7 | import * as fs from 'fs'; 8 | import * as os from 'os'; 9 | import * as electronLog from 'electron-log'; 10 | // Use CommonJS require for electron-store with Node 18 11 | const Store = require('electron-store'); 12 | // Use CommonJS require for OpenAI with Node 18 13 | const { OpenAI } = require('openai'); 14 | 15 | // Configure logging 16 | electronLog.initialize(); 17 | electronLog.transports.file.level = 'info'; 18 | const log = electronLog; 19 | 20 | // Load environment variables 21 | dotenv.config(); 22 | 23 | // Initialize store for settings 24 | // Define schema type for TypeScript 25 | interface StoreSchema { 26 | windowPosition: { x: number, y: number }; 27 | windowSize: { width: number, height: number }; 28 | preferredLanguage: string; 29 | apiKey?: string; 30 | } 31 | 32 | // Create store with schema 33 | const store = new Store({ 34 | defaults: { 35 | windowPosition: { x: 100, y: 100 }, 36 | windowSize: { width: 1600, height: 1200 }, 37 | preferredLanguage: 'python' 38 | } 39 | }); 40 | 41 | // Global variables 42 | let mainWindow: BrowserWindow | null = null; 43 | let screenshotQueue: string[] = []; 44 | const tempDir = path.join(os.tmpdir(), 'open-interview-coder'); 45 | 46 | // Ensure temp directory exists 47 | if (!fs.existsSync(tempDir)) { 48 | fs.mkdirSync(tempDir, { recursive: true }); 49 | } 50 | 51 | function createWindow() { 52 | // Get saved position and size or use defaults 53 | const savedPosition = store.get('windowPosition'); 54 | const savedSize = store.get('windowSize'); 55 | 56 | 57 | // Get screen dimensions 58 | const primaryDisplay = screen.getPrimaryDisplay(); 59 | const { width, height } = primaryDisplay.workAreaSize; 60 | 61 | // Ensure window is within screen bounds 62 | const x = Math.min(Math.max(savedPosition.x, 0), width - savedSize.width); 63 | const y = Math.min(Math.max(savedPosition.y, 0), height - savedSize.height); 64 | 65 | mainWindow = new BrowserWindow({ 66 | width: savedSize.width, 67 | height: savedSize.height, 68 | x: x, 69 | y: y, 70 | show: false, // Window is initially hidden 71 | 72 | // Enable transparent window 73 | transparent: true, 74 | backgroundColor: '#00000000', 75 | frame: false, 76 | titleBarStyle: 'customButtonsOnHover', 77 | 78 | // WebPreferences 79 | webPreferences: { 80 | preload: path.join(__dirname, 'preload.js'), 81 | contextIsolation: true, 82 | nodeIntegration: false, 83 | } 84 | }); 85 | 86 | // Keep track of the current ignore state 87 | let isIgnoringMouseEvents = true; 88 | 89 | // Register a new global shortcut for toggling 90 | globalShortcut.register('CommandOrControl+Shift+W', () => { 91 | // Flip the ignore state 92 | isIgnoringMouseEvents = !isIgnoringMouseEvents; 93 | 94 | // Apply the updated ignore state to the main window 95 | if (mainWindow) { 96 | mainWindow.setIgnoreMouseEvents(isIgnoringMouseEvents, { forward: true }); 97 | } 98 | 99 | console.log('Toggled mouse events ignoring:', isIgnoringMouseEvents); 100 | }); 101 | 102 | // Enhanced screen capture resistance 103 | mainWindow.setContentProtection(true); 104 | mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); 105 | mainWindow.setAlwaysOnTop(true, 'screen-saver', 1); 106 | 107 | if (process.platform === 'darwin') { 108 | mainWindow.setHiddenInMissionControl(true); 109 | mainWindow.setWindowButtonVisibility(false); 110 | mainWindow.setSkipTaskbar(true); 111 | mainWindow.setHasShadow(false); 112 | } 113 | 114 | mainWindow.webContents.setBackgroundThrottling(false); 115 | mainWindow.webContents.setFrameRate(60); 116 | 117 | // Load index.html 118 | mainWindow.loadURL( 119 | url.format({ 120 | pathname: path.join(__dirname, 'index.html'), 121 | protocol: 'file:', 122 | slashes: true 123 | }) 124 | ); 125 | 126 | // Save window position when moved 127 | mainWindow.on('moved', () => { 128 | if (mainWindow) { 129 | const position = mainWindow.getPosition(); 130 | store.set('windowPosition', { x: position[0], y: position[1] }); 131 | } 132 | }); 133 | 134 | // Save window size when resized 135 | mainWindow.on('resized', () => { 136 | if (mainWindow) { 137 | const size = mainWindow.getSize(); 138 | store.set('windowSize', { width: size[0], height: size[1] }); 139 | } 140 | }); 141 | 142 | // Cleanup when closed 143 | mainWindow.on('closed', () => { 144 | mainWindow = null; 145 | }); 146 | } 147 | 148 | // Take a screenshot and save it to temp directory 149 | async function takeScreenshot(): Promise { 150 | try { 151 | const timestamp = new Date().getTime(); 152 | const screenshotPath = path.join(tempDir, `screenshot-${timestamp}.png`); 153 | 154 | // Take screenshot 155 | const imgBuffer = await screenshot(); 156 | fs.writeFileSync(screenshotPath, imgBuffer); 157 | 158 | log.info(`Screenshot saved to ${screenshotPath}`); 159 | return screenshotPath; 160 | } catch (error) { 161 | log.error('Failed to take screenshot:', error); 162 | console.error('Failed to take screenshot:', error); 163 | throw new Error(`Failed to take screenshot: ${(error as Error).message}`); 164 | } 165 | } 166 | 167 | // Convert image to base64 168 | function imageToBase64(imagePath: string): string { 169 | try { 170 | const imageBuffer = fs.readFileSync(imagePath); 171 | return imageBuffer.toString('base64'); 172 | } catch (error) { 173 | log.error('Failed to convert image to base64:', error); 174 | console.error('Failed to convert image to base64:', error); 175 | throw new Error(`Failed to convert image to base64: ${(error as Error).message}`); 176 | } 177 | } 178 | 179 | // Handle calls from the renderer to ChatGPT 180 | ipcMain.handle('chatgpt-request', async (_event: IpcMainInvokeEvent, prompt: string) => { 181 | // Get API key from store 182 | const apiKey = store.get('apiKey'); 183 | 184 | if (!apiKey) { 185 | const errorMsg = 'Missing OpenAI API Key. Please add your API key in the Settings tab.'; 186 | log.error(errorMsg); 187 | console.error(errorMsg); 188 | throw new Error(errorMsg); 189 | } 190 | 191 | try { 192 | log.info('Sending request to OpenAI API'); 193 | 194 | // Initialize OpenAI client 195 | const openai = new OpenAI({ 196 | apiKey: apiKey 197 | }); 198 | 199 | // Make a request to OpenAI 200 | const completion = await openai.chat.completions.create({ 201 | model: "gpt-4o-mini", 202 | messages: [{ role: 'user', content: prompt }] 203 | }); 204 | 205 | // Extract the response 206 | const assistantReply = completion.choices[0].message.content || ''; 207 | log.info('Received response from OpenAI API'); 208 | return assistantReply; 209 | } catch (error) { 210 | log.error('Failed to fetch from OpenAI:', error); 211 | console.error('Failed to fetch from OpenAI:', error); 212 | throw new Error(`Failed to fetch from OpenAI: ${(error as Error).message}`); 213 | } 214 | }); 215 | 216 | // Handle screenshot request 217 | ipcMain.handle('take-screenshot', async () => { 218 | try { 219 | const screenshotPath = await takeScreenshot(); 220 | screenshotQueue.push(screenshotPath); 221 | 222 | // Keep only the last 5 screenshots 223 | if (screenshotQueue.length > 5) { 224 | const oldScreenshot = screenshotQueue.shift(); 225 | if (oldScreenshot && fs.existsSync(oldScreenshot)) { 226 | fs.unlinkSync(oldScreenshot); 227 | } 228 | } 229 | 230 | return { success: true, path: screenshotPath }; 231 | } catch (error) { 232 | log.error('Error taking screenshot:', error); 233 | console.error('Error taking screenshot:', error); 234 | return { success: false, error: (error as Error).message }; 235 | } 236 | }); 237 | 238 | // Define the type for message content 239 | type MessageContent = string | Array<{ 240 | type: string; 241 | text?: string; 242 | image_url?: { url: string }; 243 | }>; 244 | 245 | // Handle screenshot analysis request with image upload using modern OpenAI SDK 246 | ipcMain.handle('analyze-screenshots', async (_event: IpcMainInvokeEvent, options: { language?: string }) => { 247 | if (screenshotQueue.length === 0) { 248 | const errorMsg = 'No screenshots available to analyze'; 249 | log.error(errorMsg); 250 | console.error(errorMsg); 251 | return { success: false, error: errorMsg }; 252 | } 253 | 254 | try { 255 | // Get API key from store 256 | const apiKey = store.get('apiKey'); 257 | 258 | if (!apiKey) { 259 | const errorMsg = 'Missing OpenAI API Key. Please add your API key in the Settings tab.'; 260 | log.error(errorMsg); 261 | console.error(errorMsg); 262 | return { success: false, error: errorMsg }; 263 | } 264 | 265 | // Initialize OpenAI client 266 | const openai = new OpenAI({ 267 | apiKey: apiKey 268 | }); 269 | 270 | // Prepare screenshots for analysis 271 | const screenshots = [...screenshotQueue]; 272 | const language = options.language || store.get('preferredLanguage') || 'python'; 273 | 274 | // Build prompt for OpenAI 275 | const promptText = `I'm taking a coding interview and need help with the following problem. Please analyze these screenshots and provide a solution in ${language}. First explain the problem, then provide a step-by-step solution with code examples. But make it short and condense`; 276 | 277 | // Prepare message content array 278 | const messageContent: MessageContent = [ 279 | { type: 'text', text: promptText } 280 | ]; 281 | 282 | // Add images to the message content 283 | for (const screenshotPath of screenshots) { 284 | try { 285 | // Convert image to base64 286 | const base64Image = imageToBase64(screenshotPath); 287 | 288 | // Add image content 289 | (messageContent as Array).push({ 290 | type: 'image_url', 291 | image_url: { 292 | url: `data:image/png;base64,${base64Image}` 293 | } 294 | }); 295 | } catch (error) { 296 | log.error(`Error processing image ${screenshotPath}:`, error); 297 | console.error(`Error processing image ${screenshotPath}:`, error); 298 | } 299 | } 300 | 301 | log.info('Sending request to OpenAI API with images'); 302 | console.log('Sending request to OpenAI API with images'); 303 | 304 | // Make a request to OpenAI with images using the SDK 305 | const completion = await openai.chat.completions.create({ 306 | model: "gpt-4o-mini", 307 | messages: [{ 308 | role: "user", 309 | content: messageContent as any 310 | }], 311 | max_tokens: 2000 312 | }); 313 | 314 | // Extract the response 315 | const analysis = completion.choices[0].message.content || 'Analysis completed, but no specific solution was generated.'; 316 | 317 | log.info('Received analysis from OpenAI API'); 318 | console.log('Received analysis from OpenAI API'); 319 | 320 | return { 321 | success: true, 322 | analysis: analysis, 323 | screenshots: screenshots 324 | }; 325 | } catch (error) { 326 | log.error('Error analyzing screenshots:', error); 327 | console.error('Error analyzing screenshots:', error); 328 | return { success: false, error: (error as Error).message }; 329 | } 330 | }); 331 | 332 | // API Key and Preferences handlers 333 | ipcMain.handle('save-api-key', (_event: IpcMainInvokeEvent, apiKey: string) => { 334 | try { 335 | store.set('apiKey', apiKey); 336 | return { success: true }; 337 | } catch (error) { 338 | log.error('Error saving API key:', error); 339 | console.error('Error saving API key:', error); 340 | return { success: false, error: (error as Error).message }; 341 | } 342 | }); 343 | 344 | ipcMain.handle('get-api-key', () => { 345 | return store.get('apiKey') || ''; 346 | }); 347 | 348 | ipcMain.handle('save-preferences', (_event: IpcMainInvokeEvent, preferences: { preferredLanguage: string }) => { 349 | try { 350 | if (preferences.preferredLanguage) { 351 | store.set('preferredLanguage', preferences.preferredLanguage); 352 | } 353 | return { success: true }; 354 | } catch (error) { 355 | log.error('Error saving preferences:', error); 356 | console.error('Error saving preferences:', error); 357 | return { success: false, error: (error as Error).message }; 358 | } 359 | }); 360 | 361 | ipcMain.handle('get-preferences', () => { 362 | return { 363 | preferredLanguage: store.get('preferredLanguage') || 'python' 364 | }; 365 | }); 366 | 367 | ipcMain.handle('get-screenshots', () => { 368 | return screenshotQueue; 369 | }); 370 | 371 | ipcMain.handle('remove-screenshot', (_event: IpcMainInvokeEvent, index: number) => { 372 | try { 373 | if (index >= 0 && index < screenshotQueue.length) { 374 | const screenshotPath = screenshotQueue[index]; 375 | screenshotQueue.splice(index, 1); 376 | 377 | if (fs.existsSync(screenshotPath)) { 378 | fs.unlinkSync(screenshotPath); 379 | } 380 | 381 | return { success: true }; 382 | } 383 | return { success: false, error: 'Invalid screenshot index' }; 384 | } catch (error) { 385 | log.error('Error removing screenshot:', error); 386 | console.error('Error removing screenshot:', error); 387 | return { success: false, error: (error as Error).message }; 388 | } 389 | }); 390 | 391 | // Window management handlers 392 | ipcMain.on('close-window', () => { 393 | mainWindow?.close(); 394 | }); 395 | 396 | ipcMain.on('hide-window', () => { 397 | mainWindow?.hide(); 398 | }); 399 | 400 | ipcMain.on('show-window', () => { 401 | mainWindow?.show(); 402 | }); 403 | 404 | ipcMain.on('move-window', (_event, direction) => { 405 | if (!mainWindow) return; 406 | 407 | const position = mainWindow.getPosition(); 408 | const step = 200; // pixels to move 409 | 410 | let newX = position[0]; 411 | let newY = position[1]; 412 | 413 | switch (direction) { 414 | case 'up': 415 | newY -= step; 416 | break; 417 | case 'down': 418 | newY += step; 419 | break; 420 | case 'left': 421 | newX -= step; 422 | break; 423 | case 'right': 424 | newX += step; 425 | break; 426 | } 427 | 428 | mainWindow.setPosition(newX, newY); 429 | }); 430 | 431 | // Application initialization 432 | app.whenReady().then(() => { 433 | createWindow(); 434 | log.info('Application started'); 435 | console.log('Application started'); 436 | console.log('CMD/Control+Shift+A for showing up'); 437 | 438 | // Register global shortcuts 439 | 440 | // Toggle window visibility: Ctrl+Shift+A 441 | globalShortcut.register('CommandOrControl+Shift+A', () => { 442 | if (!mainWindow) { 443 | createWindow(); 444 | } else if (mainWindow.isVisible()) { 445 | mainWindow.hide(); 446 | } else { 447 | mainWindow.show(); 448 | } 449 | }); 450 | 451 | // Take screenshot: Ctrl+Shift+S 452 | globalShortcut.register('CommandOrControl+Shift+S', async () => { 453 | try { 454 | const screenshotPath = await takeScreenshot(); 455 | screenshotQueue.push(screenshotPath); 456 | 457 | // Keep only the last 5 screenshots 458 | if (screenshotQueue.length > 5) { 459 | const oldScreenshot = screenshotQueue.shift(); 460 | if (oldScreenshot && fs.existsSync(oldScreenshot)) { 461 | fs.unlinkSync(oldScreenshot); 462 | } 463 | } 464 | 465 | // Notify renderer 466 | if (mainWindow) { 467 | mainWindow.webContents.send('screenshot-taken', { path: screenshotPath }); 468 | } 469 | } catch (error) { 470 | log.error('Error taking screenshot via shortcut:', error); 471 | console.error('Error taking screenshot via shortcut:', error); 472 | } 473 | }); 474 | 475 | // Move window: Ctrl+Shift+Arrow keys 476 | globalShortcut.register('CommandOrControl+Shift+Up', () => { 477 | ipcMain.emit('move-window', null, 'up'); 478 | }); 479 | 480 | globalShortcut.register('CommandOrControl+Shift+Down', () => { 481 | ipcMain.emit('move-window', null, 'down'); 482 | }); 483 | 484 | globalShortcut.register('CommandOrControl+Shift+Left', () => { 485 | ipcMain.emit('move-window', null, 'left'); 486 | }); 487 | 488 | globalShortcut.register('CommandOrControl+Shift+Right', () => { 489 | ipcMain.emit('move-window', null, 'right'); 490 | }); 491 | 492 | // Scroll window: Ctrl+Arrow keys 493 | globalShortcut.register('CommandOrControl+Up', () => { 494 | if (mainWindow) { 495 | mainWindow.webContents.send('scroll-content', { direction: 'up' }); 496 | } 497 | }); 498 | 499 | globalShortcut.register('CommandOrControl+Down', () => { 500 | if (mainWindow) { 501 | mainWindow.webContents.send('scroll-content', { direction: 'down' }); 502 | } 503 | }); 504 | 505 | // Process screenshots: Ctrl+Shift+P 506 | globalShortcut.register('CommandOrControl+Shift+P', () => { 507 | if (mainWindow) { 508 | mainWindow.webContents.send('process-screenshots'); 509 | } 510 | }); 511 | 512 | // On macOS, re-create a window when clicking the dock icon if none open 513 | app.on('activate', () => { 514 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 515 | }); 516 | }); 517 | 518 | // Quit when all windows are closed, except on macOS 519 | app.on('window-all-closed', () => { 520 | if (process.platform !== 'darwin') app.quit(); 521 | }); 522 | 523 | // Clean up before quitting 524 | app.on('will-quit', () => { 525 | globalShortcut.unregisterAll(); 526 | 527 | // Clean up temp screenshots 528 | try { 529 | for (const screenshot of screenshotQueue) { 530 | if (fs.existsSync(screenshot)) { 531 | fs.unlinkSync(screenshot); 532 | } 533 | } 534 | } catch (error) { 535 | log.error('Error cleaning up screenshots:', error); 536 | console.error('Error cleaning up screenshots:', error); 537 | } 538 | }); 539 | -------------------------------------------------------------------------------- /src/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | 3 | // Expose protected methods that allow the renderer process to use 4 | // the ipcRenderer without exposing the entire object 5 | contextBridge.exposeInMainWorld('electronAPI', { 6 | // Send prompt to ChatGPT 7 | sendPrompt: (prompt: string) => ipcRenderer.invoke('chatgpt-request', prompt), 8 | 9 | // Window management 10 | closeWindow: () => ipcRenderer.send('close-window'), 11 | hideWindow: () => ipcRenderer.send('hide-window'), 12 | showWindow: () => ipcRenderer.send('show-window'), 13 | moveWindow: (direction: 'up' | 'down' | 'left' | 'right') => 14 | ipcRenderer.send('move-window', direction), 15 | 16 | // Screenshot functionality 17 | takeScreenshot: () => ipcRenderer.invoke('take-screenshot'), 18 | analyzeScreenshots: (options: { language?: string }) => 19 | ipcRenderer.invoke('analyze-screenshots', options), 20 | 21 | // API Key and Preferences management 22 | saveApiKey: (apiKey: string) => ipcRenderer.invoke('save-api-key', apiKey), 23 | getApiKey: () => ipcRenderer.invoke('get-api-key'), 24 | savePreferences: (preferences: { preferredLanguage: string }) => 25 | ipcRenderer.invoke('save-preferences', preferences), 26 | getPreferences: () => ipcRenderer.invoke('get-preferences'), 27 | 28 | // Screenshot management 29 | getScreenshots: () => ipcRenderer.invoke('get-screenshots'), 30 | removeScreenshot: (index: number) => ipcRenderer.invoke('remove-screenshot', index), 31 | 32 | // Event listeners 33 | onScreenshotTaken: (callback: (data: any) => void) => { 34 | ipcRenderer.on('screenshot-taken', (_event, data) => callback(data)); 35 | 36 | // Return a function to remove the listener 37 | return () => { 38 | ipcRenderer.removeAllListeners('screenshot-taken'); 39 | }; 40 | }, 41 | 42 | onProcessScreenshots: (callback: () => void) => { 43 | ipcRenderer.on('process-screenshots', () => callback()); 44 | 45 | // Return a function to remove the listener 46 | return () => { 47 | ipcRenderer.removeAllListeners('process-screenshots'); 48 | }; 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phulelouch/open-interview-coder/32bafea9ddb29473beba14917dbd754ec74a6db8/src/renderer.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2020", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src/**/*"] 12 | } --------------------------------------------------------------------------------