├── .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 |

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 |
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 | };
--------------------------------------------------------------------------------