├── .env.example ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.html ├── main.ts ├── preload.ts ├── renderer │ ├── App.css │ ├── App.tsx │ ├── ConfigScreen.css │ ├── ConfigScreen.tsx │ ├── index.html │ └── index.tsx └── services │ └── openai.ts ├── tsconfig.json └── webpack.config.js /.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key - Required for AI functionality 2 | OPENAI_API_KEY="your-api-key-here" 3 | 4 | # Programming Language Setting 5 | # Supported languages: Java, Python, JavaScript, C++, etc. 6 | APP_LANGUAGE="Java" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CrackCode - Invisible AI-Powered Interview Assistant 2 | 3 | A powerful, completely invisible AI tool for solving Coding questions during technical interviews. The tool runs 100% undetectably in the background - no screen recording or monitoring software can identify its presence. 4 | 5 | Open-source Alternative to Interview Coder 6 | 7 | ## Demo 8 | https://github.com/user-attachments/assets/179701eb-0fcf-4e33-86f3-c92688f508a5 9 | 10 | 11 | 12 | ## Features 13 | 14 | - 🔒 100% Undetectable - Completely invisible to all screen recording and monitoring software 15 | - 🤖 Real-time AI assistance for solving Coding problems 16 | - 🌐 Support for multiple programming languages 17 | - 🎯 Precise, contextual coding suggestions 18 | - ⚙️ Easy configuration setup 19 | 20 | 21 | ### Local Setup 22 | 23 | 1. Clone the repository: 24 | ```bash 25 | git clone https://github.com/yourusername/crackcode.git 26 | cd crackcode 27 | ``` 28 | 29 | 2. Install dependencies: 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | 3. Configure environment variables:(Or set these in the Settings ⌘/Ctrl + P ) 35 | - Copy `.env.example` to `.env` 36 | - Add your OpenAI API key 37 | - Set your preferred programming language 38 | 39 | 4. Start the Application: 40 | ```bash 41 | npm start 42 | ``` 43 | 44 | 45 | ## Prerequisites 46 | 47 | - Node.js (v14 or higher) - only for local setup 48 | - npm (Node Package Manager) - only for local setup 49 | - OpenAI API key 50 | 51 | ## Configuration 52 | 53 | Create a `.env` file in the root directory with the following settings: ( or Just press ⌘/Ctrl + P and set it up in Settings/Config page) 54 | ```env 55 | OPENAI_API_KEY="your-api-key-here" 56 | APP_LANGUAGE="Java" # Or Python, JavaScript, C++, etc. 57 | ``` 58 | 59 | ## Usage 60 | 61 | Start the application: 62 | ```bash 63 | npm start # For local setup 64 | ``` 65 | 66 | ## Shortcuts 67 | 68 | ### General Shortcuts 69 | 70 | - **Screenshot**: ⌘/Ctrl + H 71 | - **Solution**: ⌘/Ctrl + ↵/Enter 72 | - **Reset**: ⌘/Ctrl + R 73 | - **Show/Hide**: ⌘/Ctrl + B 74 | - **Settings/Config (Configure your preferred coding language and OpenAI API key)**: ⌘/Ctrl + P 75 | - **Quit**: ⌘/Ctrl + Q 76 | - **Move Around**: ⌘/Ctrl + Arrow Keys 77 | 78 | ## Contributing 79 | We welcome contributions! Please feel free to submit a Pull Request. 80 | 81 | ## Support 82 | If you find this tool helpful, please consider giving it a star ⭐️ 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crackcoder", 3 | "version": "1.0.0", 4 | "description": "Invisible AI tool for solving Coding Questions during technical interviews", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "start": "npm run build && electron .", 8 | "build": "tsc && webpack --config webpack.config.js", 9 | "watch": "concurrently \"tsc -w\" \"webpack --config webpack.config.js --watch\"", 10 | "dev": "concurrently \"npm run watch\" \"electron .\"", 11 | "pack": "npm run build && electron-builder --dir", 12 | "dist": "npm run build && electron-builder", 13 | "dist:mac": "npm run build && electron-builder --mac", 14 | "dist:win": "npm run build && electron-builder --win" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@babel/core": "^7.23.0", 20 | "@babel/preset-react": "^7.23.0", 21 | "@babel/preset-typescript": "^7.23.0", 22 | "@types/node": "^20.10.0", 23 | "@types/react": "^18.2.0", 24 | "@types/react-dom": "^18.2.0", 25 | "babel-loader": "^9.1.3", 26 | "concurrently": "^8.2.2", 27 | "css-loader": "^6.8.1", 28 | "electron": "^28.0.0", 29 | "electron-builder": "^24.13.3", 30 | "html-webpack-plugin": "^5.5.3", 31 | "style-loader": "^3.3.3", 32 | "typescript": "^5.3.0", 33 | "webpack": "^5.89.0", 34 | "webpack-cli": "^5.1.4" 35 | }, 36 | "dependencies": { 37 | "dotenv": "^16.4.7", 38 | "openai": "^4.87.3", 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0" 41 | }, 42 | "build": { 43 | "appId": "com.crackcoder.app", 44 | "productName": "CrackCoder", 45 | "directories": { 46 | "output": "release" 47 | }, 48 | "files": [ 49 | "dist/**/*", 50 | "package.json" 51 | ], 52 | "mac": { 53 | "category": "public.app-category.developer-tools", 54 | "target": [ 55 | "dmg", 56 | "zip" 57 | ], 58 | "icon": "build/icon.icns" 59 | }, 60 | "win": { 61 | "target": [ 62 | "nsis", 63 | "portable" 64 | ], 65 | "icon": "build/icon.ico" 66 | }, 67 | "linux": { 68 | "target": [ 69 | "AppImage", 70 | "deb" 71 | ], 72 | "category": "Development" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron App 6 | 26 | 27 | 28 |
29 |

Welcome to Your Electron App!

30 |

This is a basic Electron application. You can start building your app from here.

31 |
32 | 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, globalShortcut } from 'electron'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs/promises'; 4 | import { execFile } from 'child_process'; 5 | import { promisify } from 'util'; 6 | import openaiService from './services/openai'; 7 | 8 | const execFileAsync = promisify(execFile); 9 | 10 | interface Screenshot { 11 | id: number; 12 | preview: string; 13 | path: string; 14 | } 15 | 16 | const CONFIG_FILE = path.join(app.getPath('userData'), 'config.json'); 17 | console.log(CONFIG_FILE); 18 | 19 | interface Config { 20 | apiKey: string; 21 | language: string; 22 | } 23 | 24 | let config: Config | null = null; 25 | 26 | let mainWindow: BrowserWindow | null = null; 27 | let screenshotQueue: Screenshot[] = []; 28 | let isProcessing = false; 29 | const MAX_SCREENSHOTS = 4; 30 | const SCREENSHOT_DIR = path.join(app.getPath('temp'), 'screenshots'); 31 | 32 | async function ensureScreenshotDir() { 33 | try { 34 | await fs.mkdir(SCREENSHOT_DIR, { recursive: true }); 35 | } catch (error) { 36 | console.error('Error creating screenshot directory:', error); 37 | } 38 | } 39 | 40 | async function loadConfig(): Promise { 41 | try { 42 | // First try loading from environment variables 43 | const envApiKey = process.env.OPENAI_API_KEY; 44 | const envLanguage = process.env.APP_LANGUAGE; 45 | 46 | if (envApiKey && envLanguage) { 47 | const envConfig = { 48 | apiKey: envApiKey, 49 | language: envLanguage 50 | }; 51 | openaiService.updateConfig(envConfig); 52 | return envConfig; 53 | } 54 | 55 | // If env vars not found, try loading from config file 56 | const data = await fs.readFile(CONFIG_FILE, 'utf-8'); 57 | const loadedConfig = JSON.parse(data); 58 | if (loadedConfig && loadedConfig.apiKey && loadedConfig.language) { 59 | openaiService.updateConfig(loadedConfig); 60 | return loadedConfig; 61 | } 62 | return null; 63 | } catch (error) { 64 | console.error('Error loading config:', error); 65 | return null; 66 | } 67 | } 68 | 69 | async function saveConfig(newConfig: Config): Promise { 70 | try { 71 | if (!newConfig.apiKey || !newConfig.language) { 72 | throw new Error('Invalid configuration'); 73 | } 74 | await fs.writeFile(CONFIG_FILE, JSON.stringify(newConfig, null, 2)); 75 | config = newConfig; 76 | // Update OpenAI service with new config 77 | openaiService.updateConfig(newConfig); 78 | } catch (error) { 79 | console.error('Error saving config:', error); 80 | throw error; 81 | } 82 | } 83 | 84 | function createWindow() { 85 | mainWindow = new BrowserWindow({ 86 | width: 800, 87 | height: 600, 88 | frame: false, 89 | transparent: true, 90 | backgroundColor: "#00000000", 91 | hasShadow: false, 92 | alwaysOnTop: true, 93 | webPreferences: { 94 | nodeIntegration: true, 95 | contextIsolation: true, 96 | preload: path.join(__dirname, 'preload.js') 97 | } 98 | }); 99 | 100 | // Open DevTools by default in development 101 | if (process.env.NODE_ENV === 'development') { 102 | mainWindow.webContents.openDevTools({ mode: 'detach' }); 103 | } 104 | 105 | // Register DevTools shortcut 106 | globalShortcut.register('CommandOrControl+Shift+I', () => { 107 | if (mainWindow) { 108 | mainWindow.webContents.toggleDevTools(); 109 | } 110 | }); 111 | 112 | // Enable content protection to prevent screen capture 113 | mainWindow.setContentProtection(true); 114 | 115 | // Platform specific enhancements for macOS 116 | if (process.platform === 'darwin') { 117 | mainWindow.setHiddenInMissionControl(true); 118 | mainWindow.setVisibleOnAllWorkspaces(true, { 119 | visibleOnFullScreen: true 120 | }); 121 | mainWindow.setAlwaysOnTop(true, "floating"); 122 | } 123 | 124 | // Load the index.html file from the dist directory 125 | mainWindow.loadFile(path.join(__dirname, '../dist/renderer/index.html')); 126 | 127 | // Register global shortcuts 128 | registerShortcuts(); 129 | } 130 | 131 | function registerShortcuts() { 132 | // Screenshot & Processing shortcuts 133 | globalShortcut.register('CommandOrControl+H', handleTakeScreenshot); 134 | globalShortcut.register('CommandOrControl+Enter', handleProcessScreenshots); 135 | globalShortcut.register('CommandOrControl+R', handleResetQueue); 136 | globalShortcut.register('CommandOrControl+Q', () => app.quit()); 137 | 138 | // Window visibility 139 | globalShortcut.register('CommandOrControl+B', handleToggleVisibility); 140 | 141 | // Window movement 142 | globalShortcut.register('CommandOrControl+Left', () => moveWindow('left')); 143 | globalShortcut.register('CommandOrControl+Right', () => moveWindow('right')); 144 | globalShortcut.register('CommandOrControl+Up', () => moveWindow('up')); 145 | globalShortcut.register('CommandOrControl+Down', () => moveWindow('down')); 146 | 147 | // Config shortcut 148 | globalShortcut.register('CommandOrControl+P', () => { 149 | mainWindow?.webContents.send('show-config'); 150 | }); 151 | } 152 | 153 | async function captureScreenshot(): Promise { 154 | if (process.platform === 'darwin') { 155 | const tmpPath = path.join(SCREENSHOT_DIR, `${Date.now()}.png`); 156 | await execFileAsync('screencapture', ['-x', tmpPath]); 157 | const buffer = await fs.readFile(tmpPath); 158 | await fs.unlink(tmpPath); 159 | return buffer; 160 | } else { 161 | // Windows implementation 162 | const tmpPath = path.join(SCREENSHOT_DIR, `${Date.now()}.png`); 163 | const script = ` 164 | Add-Type -AssemblyName System.Windows.Forms 165 | Add-Type -AssemblyName System.Drawing 166 | $screen = [System.Windows.Forms.Screen]::PrimaryScreen 167 | $bitmap = New-Object System.Drawing.Bitmap $screen.Bounds.Width, $screen.Bounds.Height 168 | $graphics = [System.Drawing.Graphics]::FromImage($bitmap) 169 | $graphics.CopyFromScreen($screen.Bounds.X, $screen.Bounds.Y, 0, 0, $bitmap.Size) 170 | $bitmap.Save('${tmpPath.replace(/\\/g, "\\\\")}') 171 | $graphics.Dispose() 172 | $bitmap.Dispose() 173 | `; 174 | await execFileAsync('powershell', ['-command', script]); 175 | const buffer = await fs.readFile(tmpPath); 176 | await fs.unlink(tmpPath); 177 | return buffer; 178 | } 179 | } 180 | 181 | async function handleTakeScreenshot() { 182 | if (screenshotQueue.length >= MAX_SCREENSHOTS) return; 183 | 184 | try { 185 | // Hide window before taking screenshot 186 | mainWindow?.hide(); 187 | await new Promise(resolve => setTimeout(resolve, 100)); 188 | 189 | const buffer = await captureScreenshot(); 190 | const id = Date.now(); 191 | const screenshotPath = path.join(SCREENSHOT_DIR, `${id}.png`); 192 | 193 | await fs.writeFile(screenshotPath, buffer); 194 | const preview = `data:image/png;base64,${buffer.toString('base64')}`; 195 | 196 | const screenshot = { id, preview, path: screenshotPath }; 197 | screenshotQueue.push(screenshot); 198 | 199 | mainWindow?.show(); 200 | mainWindow?.webContents.send('screenshot-taken', screenshot); 201 | } catch (error) { 202 | console.error('Error taking screenshot:', error); 203 | mainWindow?.show(); 204 | } 205 | } 206 | 207 | async function handleProcessScreenshots() { 208 | if (isProcessing || screenshotQueue.length === 0) return; 209 | 210 | isProcessing = true; 211 | mainWindow?.webContents.send('processing-started'); 212 | 213 | try { 214 | const result = await openaiService.processScreenshots(screenshotQueue); 215 | // Check if processing was cancelled 216 | if (!isProcessing) return; 217 | mainWindow?.webContents.send('processing-complete', JSON.stringify(result)); 218 | } catch (error: any) { 219 | console.error('Error processing screenshots:', error); 220 | // Check if processing was cancelled 221 | if (!isProcessing) return; 222 | 223 | // Extract the most relevant error message 224 | let errorMessage = 'Error processing screenshots'; 225 | if (error?.error?.message) { 226 | errorMessage = error.error.message; 227 | } else if (error?.message) { 228 | errorMessage = error.message; 229 | } 230 | 231 | mainWindow?.webContents.send('processing-complete', JSON.stringify({ 232 | error: errorMessage, 233 | approach: 'Error occurred while processing', 234 | code: 'Error: ' + errorMessage, 235 | timeComplexity: 'N/A', 236 | spaceComplexity: 'N/A' 237 | })); 238 | } finally { 239 | isProcessing = false; 240 | } 241 | } 242 | 243 | async function handleResetQueue() { 244 | // Cancel any ongoing processing 245 | if (isProcessing) { 246 | isProcessing = false; 247 | mainWindow?.webContents.send('processing-complete', JSON.stringify({ 248 | approach: 'Processing cancelled', 249 | code: '', 250 | timeComplexity: '', 251 | spaceComplexity: '' 252 | })); 253 | } 254 | 255 | // Delete all screenshot files 256 | for (const screenshot of screenshotQueue) { 257 | try { 258 | await fs.unlink(screenshot.path); 259 | } catch (error) { 260 | console.error('Error deleting screenshot:', error); 261 | } 262 | } 263 | 264 | screenshotQueue = []; 265 | mainWindow?.webContents.send('queue-reset'); 266 | } 267 | 268 | function handleToggleVisibility() { 269 | if (!mainWindow) return; 270 | if (mainWindow.isVisible()) { 271 | mainWindow.hide(); 272 | } else { 273 | mainWindow.show(); 274 | } 275 | } 276 | 277 | function moveWindow(direction: 'left' | 'right' | 'up' | 'down') { 278 | if (!mainWindow) return; 279 | 280 | const [x, y] = mainWindow.getPosition(); 281 | const moveAmount = 50; 282 | 283 | switch (direction) { 284 | case 'left': 285 | mainWindow.setPosition(x - moveAmount, y); 286 | break; 287 | case 'right': 288 | mainWindow.setPosition(x + moveAmount, y); 289 | break; 290 | case 'up': 291 | mainWindow.setPosition(x, y - moveAmount); 292 | break; 293 | case 'down': 294 | mainWindow.setPosition(x, y + moveAmount); 295 | break; 296 | } 297 | } 298 | 299 | // This method will be called when Electron has finished initialization 300 | app.whenReady().then(async () => { 301 | await ensureScreenshotDir(); 302 | // Load config before creating window 303 | config = await loadConfig(); 304 | createWindow(); 305 | 306 | app.on('activate', function () { 307 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 308 | }); 309 | }); 310 | 311 | app.on('will-quit', () => { 312 | globalShortcut.unregisterAll(); 313 | handleResetQueue(); 314 | }); 315 | 316 | app.on('window-all-closed', function () { 317 | if (process.platform !== 'darwin') app.quit(); 318 | }); 319 | 320 | // IPC Handlers 321 | ipcMain.handle('take-screenshot', handleTakeScreenshot); 322 | ipcMain.handle('process-screenshots', handleProcessScreenshots); 323 | ipcMain.handle('reset-queue', handleResetQueue); 324 | 325 | // Window control events 326 | ipcMain.on('minimize-window', () => { 327 | mainWindow?.minimize(); 328 | }); 329 | 330 | ipcMain.on('maximize-window', () => { 331 | if (mainWindow?.isMaximized()) { 332 | mainWindow?.unmaximize(); 333 | } else { 334 | mainWindow?.maximize(); 335 | } 336 | }); 337 | 338 | ipcMain.on('close-window', () => { 339 | mainWindow?.close(); 340 | }); 341 | 342 | ipcMain.on('quit-app', () => { 343 | app.quit(); 344 | }); 345 | 346 | ipcMain.on('toggle-visibility', handleToggleVisibility); 347 | 348 | // Add these IPC handlers before app.whenReady() 349 | ipcMain.handle('get-config', async () => { 350 | try { 351 | if (!config) { 352 | config = await loadConfig(); 353 | } 354 | return config; 355 | } catch (error) { 356 | console.error('Error getting config:', error); 357 | return null; 358 | } 359 | }); 360 | 361 | ipcMain.handle('save-config', async (_, newConfig: Config) => { 362 | try { 363 | await saveConfig(newConfig); 364 | return true; 365 | } catch (error) { 366 | console.error('Error in save-config handler:', error); 367 | return false; 368 | } 369 | }); -------------------------------------------------------------------------------- /src/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | 3 | contextBridge.exposeInMainWorld('electron', { 4 | minimize: () => ipcRenderer.send('minimize-window'), 5 | maximize: () => ipcRenderer.send('maximize-window'), 6 | close: () => ipcRenderer.send('close-window'), 7 | quit: () => ipcRenderer.send('quit-app'), 8 | 9 | takeScreenshot: () => ipcRenderer.invoke('take-screenshot'), 10 | processScreenshots: () => ipcRenderer.invoke('process-screenshots'), 11 | resetQueue: () => ipcRenderer.invoke('reset-queue'), 12 | getConfig: () => ipcRenderer.invoke('get-config'), 13 | saveConfig: (config: any) => ipcRenderer.invoke('save-config', config), 14 | 15 | toggleVisibility: () => ipcRenderer.send('toggle-visibility'), 16 | 17 | onProcessingComplete: (callback: (result: string) => void) => { 18 | ipcRenderer.on('processing-complete', (_, result) => callback(result)); 19 | }, 20 | onScreenshotTaken: (callback: (data: any) => void) => { 21 | ipcRenderer.on('screenshot-taken', (_, data) => callback(data)); 22 | }, 23 | onProcessingStarted: (callback: () => void) => { 24 | ipcRenderer.on('processing-started', () => callback()); 25 | }, 26 | onQueueReset: (callback: () => void) => { 27 | ipcRenderer.on('queue-reset', () => callback()); 28 | }, 29 | onShowConfig: (callback: () => void) => { 30 | ipcRenderer.on('show-config', () => callback()); 31 | } 32 | }); -------------------------------------------------------------------------------- /src/renderer/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 10px; 4 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 5 | background: transparent; 6 | color: #e0e0e0; 7 | } 8 | 9 | .app { 10 | max-width: 100%; 11 | margin: 0 auto; 12 | padding: 1rem; 13 | background-color: rgba(70, 73, 78, 0.65); 14 | backdrop-filter: blur(8px); 15 | -webkit-backdrop-filter: blur(8px); 16 | border-radius: 8px; 17 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); 18 | display: flex; 19 | flex-direction: column; 20 | gap: 1rem; 21 | } 22 | 23 | .preview-row { 24 | display: flex; 25 | gap: 0.5rem; 26 | padding: 0.5rem; 27 | background-color: rgba(7, 8, 9, 0.4); 28 | border-radius: 4px; 29 | min-height: 60px; 30 | align-items: center; 31 | } 32 | 33 | .preview-item { 34 | position: relative; 35 | width: 90px; 36 | height: 60px; 37 | border-radius: 4px; 38 | overflow: hidden; 39 | background-color: rgba(0, 0, 0, 0.2); 40 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 41 | flex-shrink: 0; 42 | } 43 | 44 | .preview-item img { 45 | width: 100%; 46 | height: 100%; 47 | object-fit: cover; 48 | border-radius: 4px; 49 | } 50 | 51 | .status-row { 52 | display: flex; 53 | flex-direction: column; 54 | padding: 1rem; 55 | background-color: rgba(7, 8, 9, 0.4); 56 | border-radius: 4px; 57 | min-height: 24px; 58 | } 59 | 60 | .processing { 61 | color: #abb2bf; 62 | font-weight: 500; 63 | font-size: 0.7rem; 64 | text-align: center; 65 | align-items: center; 66 | } 67 | 68 | .result { 69 | color: #98c379; 70 | text-align: left; 71 | display: flex; 72 | flex-direction: column; 73 | gap: 0.25rem; 74 | } 75 | 76 | .solution-section { 77 | margin: 0.25rem 0; 78 | padding: 1rem; 79 | background: rgba(255, 255, 255, 0.1); 80 | border-radius: 8px; 81 | } 82 | 83 | .solution-section h3 { 84 | color: #edf1f5; 85 | font-size: 0.8rem; 86 | margin: 0 0 0.5rem 0; 87 | font-weight: normal; 88 | } 89 | 90 | .solution-section pre { 91 | margin: 0; 92 | padding: 1rem; 93 | background: rgba(0, 0, 0, 0.2); 94 | border-radius: 4px; 95 | overflow-x: auto; 96 | } 97 | 98 | .solution-section code { 99 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 100 | font-size: 0.9rem; 101 | color: #abb2bf; 102 | } 103 | 104 | .solution-section p { 105 | margin: 0.5rem 0; 106 | line-height: 1.5; 107 | color: #c6c9cc; 108 | font-size: 0.9rem; 109 | background: rgba(0, 0, 0, 0.3); 110 | padding: 1rem; 111 | border-radius: 4px; 112 | } 113 | 114 | .empty-status { 115 | color: #abb2bf; 116 | font-size: 0.6rem; 117 | align-items: center; 118 | justify-content: center; 119 | display: flex; 120 | flex-direction: column; 121 | } 122 | 123 | .shortcuts-row { 124 | display: flex; 125 | gap: 1rem; 126 | align-items: center; 127 | justify-content: center; 128 | padding: 0.5rem; 129 | background-color: rgba(7, 8, 9, 0.4); 130 | border-radius: 4px; 131 | font-size: 0.6rem; 132 | position: relative; 133 | } 134 | 135 | .hover-shortcuts { 136 | position: absolute; 137 | right: 0.5rem; 138 | top: 50%; 139 | transform: translateY(-50%); 140 | } 141 | 142 | .hover-shortcuts::before { 143 | content: "?"; 144 | display: flex; 145 | align-items: center; 146 | justify-content: center; 147 | width: 20px; 148 | height: 20px; 149 | background-color: rgba(97, 175, 239, 0.2); 150 | border-radius: 50%; 151 | color: #61afef; 152 | cursor: help; 153 | } 154 | 155 | .hover-shortcuts-content { 156 | display: none; 157 | position: absolute; 158 | right: 100%; 159 | top: 0; 160 | transform: translateY(0); 161 | background-color: rgba(7, 8, 9, 0.9); 162 | padding: 1rem 1.5rem; 163 | border-radius: 4px; 164 | margin-right: 10px; 165 | margin-top: -10px; 166 | flex-direction: column; 167 | gap: 0.5rem; 168 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 169 | min-width: 180px; 170 | white-space: nowrap; 171 | } 172 | 173 | .hover-shortcuts:hover .hover-shortcuts-content { 174 | display: flex; 175 | } 176 | 177 | .hover-shortcuts-content::after { 178 | content: ""; 179 | position: absolute; 180 | right: -5px; 181 | top: 20px; 182 | transform: translateY(0); 183 | border-left: 5px solid rgba(7, 8, 9, 0.9); 184 | border-top: 5px solid transparent; 185 | border-bottom: 5px solid transparent; 186 | } 187 | 188 | .shortcut { 189 | display: flex; 190 | align-items: center; 191 | gap: 0.25rem; 192 | color: #abb2bf; 193 | font-size: 0.6rem; 194 | } 195 | 196 | .shortcut code { 197 | background-color: rgba(0, 0, 0, 0.2); 198 | padding: 0.2em 0.4em; 199 | border-radius: 3px; 200 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 201 | color: #61afef; 202 | font-size: 0.6rem; 203 | } 204 | 205 | /* Syntax highlighting colors for code */ 206 | .keyword { color: #c678dd; } 207 | .comment { color: #5c6370; } 208 | .string { color: #98c379; } 209 | .number { color: #d19a66; } 210 | .function { color: #61afef; } 211 | 212 | .hint { 213 | font-size: 0.8rem; 214 | color: #dbdce0; 215 | margin-top: 0.5rem; 216 | text-align: center; 217 | align-items: center; 218 | justify-content: center; 219 | } 220 | 221 | /* Add a draggable region for window dragging since we removed the frame */ 222 | .app::before { 223 | content: ''; 224 | position: fixed; 225 | top: 0; 226 | left: 0; 227 | right: 0; 228 | height: 30px; 229 | -webkit-app-region: drag; 230 | } 231 | 232 | .window-controls { 233 | position: fixed; 234 | top: 8px; 235 | right: 8px; 236 | display: flex; 237 | gap: 8px; 238 | z-index: 1000; 239 | } 240 | 241 | .control { 242 | width: 24px; 243 | height: 24px; 244 | padding: 0; 245 | display: flex; 246 | align-items: center; 247 | justify-content: center; 248 | font-size: 18px; 249 | border-radius: 50%; 250 | background-color: rgba(255, 255, 255, 0.2); 251 | color: rgba(0, 0, 0, 0.7); 252 | transition: all 0.2s ease; 253 | } 254 | 255 | .control:hover { 256 | transform: none; 257 | } 258 | 259 | .control.minimize:hover { 260 | background-color: rgba(255, 255, 255, 0.3); 261 | } 262 | 263 | .control.close:hover { 264 | background-color: rgba(255, 0, 0, 0.8); 265 | color: white; 266 | } 267 | 268 | .preview-grid { 269 | display: grid; 270 | grid-template-columns: repeat(2, 1fr); 271 | gap: 1rem; 272 | margin: 2rem 0; 273 | padding: 1rem; 274 | background-color: rgba(0, 0, 0, 0.05); 275 | border-radius: 8px; 276 | } 277 | 278 | .preview-item { 279 | position: relative; 280 | aspect-ratio: 16/9; 281 | border-radius: 4px; 282 | overflow: hidden; 283 | background-color: rgba(0, 0, 0, 0.1); 284 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 285 | } 286 | 287 | .preview-item img { 288 | width: 100%; 289 | height: 100%; 290 | object-fit: cover; 291 | border-radius: 4px; 292 | } 293 | 294 | .solution-section { 295 | margin: 1rem 0; 296 | padding: 1rem; 297 | background: rgba(255, 255, 255, 0.1); 298 | border-radius: 8px; 299 | } 300 | 301 | .solution-section h3 { 302 | margin: 0 0 0.5rem 0; 303 | color: #e8ebef; 304 | font-size: 1.0rem; 305 | } 306 | 307 | .solution-section pre { 308 | margin: 0; 309 | padding: 1rem; 310 | background: rgba(0, 0, 0, 0.3); 311 | border-radius: 4px; 312 | overflow-x: auto; 313 | } 314 | 315 | .solution-section code { 316 | font-family: 'Fira Code', monospace; 317 | font-size: 0.9rem; 318 | } 319 | 320 | .solution-section p { 321 | margin: 0.5rem 0; 322 | line-height: 1.5; 323 | color: #c6c9cc; 324 | } 325 | 326 | .code-line { 327 | display: flex; 328 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 329 | line-height: 1.5; 330 | white-space: pre; 331 | } 332 | 333 | .line-number { 334 | color: #7c8089; 335 | text-align: right; 336 | padding-right: 1em; 337 | user-select: none; 338 | min-width: 2em; 339 | } 340 | 341 | pre { 342 | margin: 0; 343 | padding: 1rem; 344 | background: rgba(0, 0, 0, 0.3); 345 | border-radius: 4px; 346 | overflow-x: auto; 347 | } 348 | 349 | code { 350 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 351 | font-size: 0.9rem; 352 | color: #abb2bf; 353 | } 354 | 355 | .error-bar { 356 | position: fixed; 357 | top: 20px; 358 | left: 50%; 359 | transform: translateX(-50%); 360 | background-color: rgba(255, 59, 48, 0.9); 361 | color: white; 362 | padding: 12px 20px; 363 | border-radius: 8px; 364 | z-index: 2000; 365 | display: flex; 366 | align-items: center; 367 | gap: 12px; 368 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 369 | animation: slideIn 0.3s ease-out; 370 | } 371 | 372 | .error-bar span { 373 | font-size: 14px; 374 | } 375 | 376 | .error-bar button { 377 | background: none; 378 | border: none; 379 | color: white; 380 | font-size: 18px; 381 | cursor: pointer; 382 | padding: 0; 383 | margin-left: 8px; 384 | opacity: 0.8; 385 | transition: opacity 0.2s; 386 | } 387 | 388 | .error-bar button:hover { 389 | opacity: 1; 390 | } 391 | 392 | @keyframes slideIn { 393 | from { 394 | transform: translate(-50%, -100%); 395 | opacity: 0; 396 | } 397 | to { 398 | transform: translate(-50%, 0); 399 | opacity: 1; 400 | } 401 | } -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './App.css'; 3 | import ConfigScreen from './ConfigScreen'; 4 | 5 | interface Screenshot { 6 | id: number; 7 | preview: string; 8 | path: string; 9 | } 10 | 11 | interface ProcessedSolution { 12 | approach: string; 13 | code: string; 14 | timeComplexity: string; 15 | spaceComplexity: string; 16 | } 17 | 18 | interface Config { 19 | apiKey: string; 20 | language: string; 21 | } 22 | 23 | declare global { 24 | interface Window { 25 | electron: { 26 | minimize: () => void; 27 | maximize: () => void; 28 | close: () => void; 29 | quit: () => void; 30 | takeScreenshot: () => Promise; 31 | processScreenshots: () => Promise; 32 | resetQueue: () => Promise; 33 | getConfig: () => Promise; 34 | saveConfig: (config: Config) => Promise; 35 | onProcessingComplete: (callback: (result: string) => void) => void; 36 | onScreenshotTaken: (callback: (data: Screenshot) => void) => void; 37 | onProcessingStarted: (callback: () => void) => void; 38 | onQueueReset: (callback: () => void) => void; 39 | onShowConfig: (callback: () => void) => void; 40 | }; 41 | } 42 | } 43 | 44 | const App: React.FC = () => { 45 | const [isProcessing, setIsProcessing] = useState(false); 46 | const [result, setResult] = useState(null); 47 | const [screenshots, setScreenshots] = useState([]); 48 | const [showConfig, setShowConfig] = useState(false); 49 | const [config, setConfig] = useState(null); 50 | const [error, setError] = useState(null); 51 | 52 | useEffect(() => { 53 | const loadConfig = async () => { 54 | const savedConfig = await window.electron.getConfig(); 55 | setConfig(savedConfig); 56 | if (!savedConfig) { 57 | setShowConfig(true); 58 | } 59 | }; 60 | 61 | loadConfig(); 62 | }, []); 63 | 64 | useEffect(() => { 65 | console.log('Setting up event listeners...'); 66 | 67 | // Listen for show config events 68 | window.electron.onShowConfig(() => { 69 | setShowConfig(prev => !prev); 70 | }); 71 | 72 | // Listen for processing started events 73 | window.electron.onProcessingStarted(() => { 74 | console.log('Processing started'); 75 | setIsProcessing(true); 76 | setResult(null); 77 | }); 78 | 79 | // Keyboard event listener 80 | const handleKeyDown = async (event: KeyboardEvent) => { 81 | console.log('Key pressed:', event.key); 82 | 83 | // Check if Cmd/Ctrl is pressed 84 | const isCmdOrCtrl = event.metaKey || event.ctrlKey; 85 | 86 | switch (event.key.toLowerCase()) { 87 | case 'h': 88 | console.log('Screenshot hotkey pressed'); 89 | await handleTakeScreenshot(); 90 | break; 91 | case 'enter': 92 | console.log('Process hotkey pressed'); 93 | await handleProcess(); 94 | break; 95 | case 'r': 96 | console.log('Reset hotkey pressed'); 97 | await handleReset(); 98 | break; 99 | case 'p': 100 | if (isCmdOrCtrl) { 101 | console.log('Toggle config hotkey pressed'); 102 | setShowConfig(prev => !prev); 103 | } 104 | break; 105 | case 'b': 106 | if (isCmdOrCtrl) { 107 | console.log('Toggle visibility hotkey pressed'); 108 | // Toggle visibility logic here 109 | } 110 | break; 111 | case 'q': 112 | if (isCmdOrCtrl) { 113 | console.log('Quit hotkey pressed'); 114 | handleQuit(); 115 | } 116 | break; 117 | } 118 | }; 119 | 120 | // Add keyboard event listener 121 | window.addEventListener('keydown', handleKeyDown); 122 | 123 | // Listen for processing complete events 124 | window.electron.onProcessingComplete((resultStr) => { 125 | console.log('Processing complete. Result:', resultStr); 126 | try { 127 | const parsedResult = JSON.parse(resultStr) as ProcessedSolution; 128 | setResult(parsedResult); 129 | } catch (error) { 130 | console.error('Error parsing result:', error); 131 | } 132 | setIsProcessing(false); 133 | }); 134 | 135 | // Listen for new screenshots 136 | window.electron.onScreenshotTaken((screenshot) => { 137 | console.log('New screenshot taken:', screenshot); 138 | setScreenshots(prev => { 139 | const newScreenshots = [...prev, screenshot]; 140 | console.log('Updated screenshots array:', newScreenshots); 141 | return newScreenshots; 142 | }); 143 | }); 144 | 145 | // Listen for queue reset 146 | window.electron.onQueueReset(() => { 147 | console.log('Queue reset triggered'); 148 | setScreenshots([]); 149 | setResult(null); 150 | }); 151 | 152 | // Cleanup 153 | return () => { 154 | console.log('Cleaning up event listeners...'); 155 | window.removeEventListener('keydown', handleKeyDown); 156 | }; 157 | }, []); 158 | 159 | useEffect(() => { 160 | if (error) { 161 | const timer = setTimeout(() => { 162 | setError(null); 163 | }, 5000); // Hide error after 5 seconds 164 | return () => clearTimeout(timer); 165 | } 166 | }, [error]); 167 | 168 | const handleTakeScreenshot = async () => { 169 | console.log('Taking screenshot, current count:', screenshots.length); 170 | if (screenshots.length >= 4) { 171 | console.log('Maximum screenshots reached'); 172 | return; 173 | } 174 | try { 175 | await window.electron.takeScreenshot(); 176 | console.log('Screenshot taken successfully'); 177 | } catch (error) { 178 | console.error('Error taking screenshot:', error); 179 | } 180 | }; 181 | 182 | const handleProcess = async () => { 183 | console.log('Starting processing. Current screenshots:', screenshots); 184 | if (screenshots.length === 0) { 185 | console.log('No screenshots to process'); 186 | return; 187 | } 188 | setIsProcessing(true); 189 | setResult(null); 190 | setError(null); 191 | try { 192 | await window.electron.processScreenshots(); 193 | console.log('Process request sent successfully'); 194 | } catch (error: any) { 195 | console.error('Error processing screenshots:', error); 196 | setError(error?.message || 'Error processing screenshots'); 197 | setIsProcessing(false); 198 | } 199 | }; 200 | 201 | const handleReset = async () => { 202 | console.log('Resetting queue...'); 203 | await window.electron.resetQueue(); 204 | }; 205 | 206 | const handleQuit = () => { 207 | console.log('Quitting application...'); 208 | window.electron.quit(); 209 | }; 210 | 211 | const handleConfigSave = async (newConfig: Config) => { 212 | try { 213 | const success = await window.electron.saveConfig(newConfig); 214 | if (success) { 215 | setConfig(newConfig); 216 | setShowConfig(false); 217 | setError(null); 218 | } else { 219 | setError('Failed to save configuration'); 220 | } 221 | } catch (error: any) { 222 | console.error('Error saving configuration:', error); 223 | setError(error?.message || 'Error saving configuration'); 224 | } 225 | }; 226 | 227 | // Log state changes 228 | useEffect(() => { 229 | console.log('State update:', { 230 | isProcessing, 231 | result, 232 | screenshotCount: screenshots.length 233 | }); 234 | }, [isProcessing, result, screenshots]); 235 | 236 | const formatCode = (code: string) => { 237 | return code.split('\n').map((line, index) => ( 238 |
239 | {index + 1} 240 | {line} 241 |
242 | )); 243 | }; 244 | 245 | return ( 246 |
247 | {error && ( 248 |
249 | {error} 250 | 251 |
252 | )} 253 | {showConfig && ( 254 | 258 | )} 259 | 260 | {/* Preview Row */} 261 |
262 |
⌘/Ctrl + H Screenshot
263 |
⌘/Ctrl + ↵ Solution
264 |
⌘/Ctrl + R Reset
265 |
266 |
267 |
⌘/Ctrl + B Show/Hide
268 |
⌘/Ctrl + P Settings
269 |
⌘/Ctrl + Q Quit
270 |
⌘/Ctrl + Arrow Keys Move Around
271 |
272 |
273 |
274 |
275 | {screenshots.map(screenshot => ( 276 |
277 | Screenshot preview 278 |
279 | ))} 280 |
281 | 282 | {/* Status Row */} 283 |
284 | {isProcessing ? ( 285 |
Processing... ({screenshots.length} screenshots)
286 | ) : result ? ( 287 |
288 |
289 |

Approach

290 |

{result.approach}

291 |
292 |
293 |

Solution

294 |
295 |                 {formatCode(result.code)}
296 |               
297 |
298 |
299 |

Complexity

300 |

Time: {result.timeComplexity}

301 |

Space: {result.spaceComplexity}

302 |
303 |
(Press ⌘/Ctrl + R to reset)
304 |
305 | ) : ( 306 |
307 | {screenshots.length > 0 308 | ? `Press ⌘/Ctrl + ↵ to process ${screenshots.length} screenshot${screenshots.length > 1 ? 's' : ''}` 309 | : 'Press ⌘/Ctrl + H to take a screenshot'} 310 |
311 | )} 312 |
313 |
314 | ); 315 | }; 316 | 317 | export default App; -------------------------------------------------------------------------------- /src/renderer/ConfigScreen.css: -------------------------------------------------------------------------------- 1 | .config-screen { 2 | position: fixed; 3 | top: 0px; 4 | left: 50%; 5 | transform: translateX(-50%); 6 | z-index: 1000; 7 | } 8 | 9 | .config-container { 10 | background: rgba(0, 0, 0, 0.688); 11 | padding: 1rem; 12 | border-radius: 8px; 13 | width: 400px; 14 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.815); 15 | } 16 | 17 | .config-container h2 { 18 | color: #edf1f5; 19 | margin-bottom: 1rem; 20 | text-align: center; 21 | font-size: 0.9rem; 22 | font-weight: normal; 23 | } 24 | 25 | .form-group { 26 | margin-bottom: 0.75rem; 27 | } 28 | 29 | .form-group label { 30 | display: block; 31 | color: #edf1f5; 32 | margin-bottom: 0.25rem; 33 | font-size: 0.8rem; 34 | } 35 | 36 | .api-key-input { 37 | position: relative; 38 | display: flex; 39 | align-items: center; 40 | background: rgba(0, 0, 0, 0.3); 41 | border-radius: 4px; 42 | height: 32px; 43 | max-width: 100%; 44 | } 45 | 46 | .api-key-input input { 47 | flex: 1; 48 | padding: 0.4rem 0.75rem; 49 | padding-right: 60px; /* Make space for the button */ 50 | font-size: 0.85rem; 51 | border: none; 52 | border-radius: 4px; 53 | background: transparent; 54 | color: #c6c9cc; 55 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; 56 | letter-spacing: 0.5px; 57 | width: 100%; 58 | height: 100%; 59 | text-overflow: ellipsis; 60 | white-space: nowrap; 61 | overflow: hidden; 62 | } 63 | 64 | .api-key-input .toggle-visibility { 65 | position: absolute; 66 | right: 4px; 67 | background: none; 68 | border: none; 69 | color: #7c8089; 70 | cursor: pointer; 71 | opacity: 0.7; 72 | transition: opacity 0.2s; 73 | padding: 4px 8px; 74 | font-size: 0.8rem; 75 | border-radius: 4px; 76 | min-width: 50px; 77 | text-align: center; 78 | } 79 | 80 | .api-key-input .toggle-visibility:hover { 81 | opacity: 1; 82 | background: rgba(255, 255, 255, 0.1); 83 | } 84 | 85 | .api-key-help { 86 | margin-top: 0.5rem; 87 | font-size: 0.85rem; 88 | color: rgba(255, 255, 255, 0.6); 89 | } 90 | 91 | .api-key-help a { 92 | color: #61afef; 93 | text-decoration: none; 94 | transition: color 0.2s; 95 | } 96 | 97 | .api-key-help a:hover { 98 | color: #8ac7ff; 99 | text-decoration: underline; 100 | } 101 | 102 | .form-group select { 103 | width: 100%; 104 | padding: 0.4rem 0.75rem; 105 | font-size: 0.85rem; 106 | border: none; 107 | border-radius: 4px; 108 | background: rgba(0, 0, 0, 0.3); 109 | color: #c6c9cc; 110 | cursor: pointer; 111 | appearance: none; 112 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237c8089' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 113 | background-repeat: no-repeat; 114 | background-position: right 0.75rem center; 115 | background-size: 1em; 116 | height: 32px; 117 | } 118 | 119 | .form-group input:focus, 120 | .form-group select:focus { 121 | outline: none; 122 | box-shadow: 0 0 0 1px rgba(97, 175, 239, 0.3); 123 | } 124 | 125 | .form-actions { 126 | margin-top: 1rem; 127 | display: flex; 128 | justify-content: center; 129 | } 130 | 131 | .save-button { 132 | padding: 0.4rem 1.25rem; 133 | background: rgba(0, 0, 0, 0.3); 134 | color: #c6c9cc; 135 | border: 1px solid rgba(255, 255, 255, 0.1); 136 | border-radius: 4px; 137 | font-size: 0.85rem; 138 | cursor: pointer; 139 | transition: all 0.2s; 140 | min-width: 120px; 141 | height: 32px; 142 | } 143 | 144 | .save-button:hover { 145 | background: rgba(255, 255, 255, 0.1); 146 | border-color: rgba(255, 255, 255, 0.2); 147 | } 148 | 149 | .save-button:active { 150 | background: rgba(0, 0, 0, 0.4); 151 | transform: translateY(1px); 152 | } -------------------------------------------------------------------------------- /src/renderer/ConfigScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './ConfigScreen.css'; 3 | 4 | interface ConfigProps { 5 | onSave: (config: { apiKey: string; language: string }) => void; 6 | initialConfig?: { apiKey: string; language: string }; 7 | } 8 | 9 | const ConfigScreen: React.FC = ({ onSave, initialConfig }) => { 10 | const [apiKey, setApiKey] = useState(initialConfig?.apiKey || ''); 11 | const [language, setLanguage] = useState(initialConfig?.language || 'Python'); 12 | const [showApiKey, setShowApiKey] = useState(false); 13 | 14 | const handleSubmit = (e: React.FormEvent) => { 15 | e.preventDefault(); 16 | onSave({ apiKey: apiKey.trim(), language }); 17 | }; 18 | 19 | return ( 20 |
21 |
22 |

Configuration

23 |
24 |
25 | 26 |
27 | setApiKey(e.target.value)} 32 | required 33 | placeholder="sk-..." 34 | spellCheck="false" 35 | autoComplete="off" 36 | /> 37 | 44 |
45 |
46 |
47 | 48 | 63 |
64 |
65 | 68 |
69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default ConfigScreen; -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron React App 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const container = document.getElementById('root'); 6 | if (!container) throw new Error('Root element not found'); 7 | const root = createRoot(container); 8 | root.render(); -------------------------------------------------------------------------------- /src/services/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import dotenv from 'dotenv'; 3 | import fs from 'fs/promises'; 4 | 5 | dotenv.config(); 6 | 7 | let openai: OpenAI | null = null; 8 | let language = process.env.LANGUAGE || "Python"; 9 | 10 | interface Config { 11 | apiKey: string; 12 | language: string; 13 | } 14 | 15 | function updateConfig(config: Config) { 16 | if (!config.apiKey) { 17 | throw new Error('OpenAI API key is required'); 18 | } 19 | 20 | try { 21 | openai = new OpenAI({ 22 | apiKey: config.apiKey.trim(), 23 | }); 24 | language = config.language || 'Python'; 25 | // console.log('OpenAI client initialized with new config'); 26 | } catch (error) { 27 | console.error('Error initializing OpenAI client:', error); 28 | throw error; 29 | } 30 | } 31 | 32 | // Initialize with environment variables if available 33 | if (process.env.OPENAI_API_KEY) { 34 | try { 35 | updateConfig({ 36 | apiKey: process.env.OPENAI_API_KEY, 37 | language: process.env.LANGUAGE || 'Python' 38 | }); 39 | } catch (error) { 40 | console.error('Error initializing OpenAI with environment variables:', error); 41 | } 42 | } 43 | 44 | interface ProcessedSolution { 45 | approach: string; 46 | code: string; 47 | timeComplexity: string; 48 | spaceComplexity: string; 49 | } 50 | 51 | type MessageContent = 52 | | { type: "text"; text: string } 53 | | { type: "image_url"; image_url: { url: string } }; 54 | 55 | export async function processScreenshots(screenshots: { path: string }[]): Promise { 56 | if (!openai) { 57 | throw new Error('OpenAI client not initialized. Please configure API key first. Click CTRL/CMD + P to open settings and set the API key.'); 58 | } 59 | 60 | try { 61 | const messages = [ 62 | { 63 | role: "system" as const, 64 | content: `You are an expert coding interview assistant. Analyze the coding question from the screenshots and provide a solution in ${language}. 65 | Return the response in the following JSON format: 66 | { 67 | "approach": "Detailed approach to solve the problem on how are we solving the problem, that the interviewee will speak out loud and in easy explainatory words", 68 | "code": "The complete solution code", 69 | "timeComplexity": "Big O analysis of time complexity with the reason", 70 | "spaceComplexity": "Big O analysis of space complexity with the reason" 71 | }` 72 | }, 73 | { 74 | role: "user" as const, 75 | content: [ 76 | { type: "text", text: "Here is a coding interview question. Please analyze and provide a solution." } as MessageContent 77 | ] 78 | } 79 | ]; 80 | 81 | // Add screenshots as image URLs 82 | for (const screenshot of screenshots) { 83 | const base64Image = await fs.readFile(screenshot.path, { encoding: 'base64' }); 84 | messages.push({ 85 | role: "user" as const, 86 | content: [ 87 | { 88 | type: "image_url", 89 | image_url: { 90 | url: `data:image/png;base64,${base64Image}` 91 | } 92 | } as MessageContent 93 | ] 94 | }); 95 | } 96 | 97 | // Get response from OpenAI 98 | const response = await openai.chat.completions.create({ 99 | model: "gpt-4o", 100 | messages: messages as any, 101 | max_tokens: 2000, 102 | temperature: 0.7, 103 | response_format: { type: "json_object" } 104 | }); 105 | 106 | const content = response.choices[0].message.content || '{}'; 107 | return JSON.parse(content) as ProcessedSolution; 108 | } catch (error) { 109 | console.error('Error processing screenshots:', error); 110 | throw error; 111 | } 112 | } 113 | 114 | export default { 115 | processScreenshots, 116 | updateConfig 117 | }; 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "moduleResolution": "node", 12 | "jsx": "react", 13 | "lib": ["DOM", "ES2020"] 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/renderer/index.tsx', 7 | target: 'electron-renderer', 8 | devtool: 'source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | presets: [ 18 | '@babel/preset-react', 19 | '@babel/preset-typescript' 20 | ] 21 | } 22 | } 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: ['style-loader', 'css-loader'] 27 | } 28 | ] 29 | }, 30 | resolve: { 31 | extensions: ['.tsx', '.ts', '.js', '.jsx'] 32 | }, 33 | output: { 34 | filename: 'renderer.js', 35 | path: path.resolve(__dirname, 'dist/renderer') 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | template: './src/renderer/index.html' 40 | }) 41 | ] 42 | }; --------------------------------------------------------------------------------