├── .gitattributes ├── .gitignore ├── README.md ├── assets └── icons │ ├── mac │ └── icon.icns │ └── win │ └── icon.ico ├── build └── entitlements.mac.plist ├── bun.lockb ├── electron ├── ProcessingHelper.ts ├── ScreenshotHelper.ts ├── autoUpdater.ts ├── ipcHandlers.ts ├── main.ts ├── preload.ts ├── shortcuts.ts ├── store.ts └── tsconfig.json ├── env.d.ts ├── index.html ├── package.json ├── postcss.config.js ├── renderer ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts └── tsconfig.json ├── src ├── App.tsx ├── _pages │ ├── Debug.tsx │ ├── Queue.tsx │ ├── Solutions.tsx │ ├── SubscribePage.tsx │ └── SubscribedApp.tsx ├── components │ ├── Queue │ │ ├── QueueCommands.tsx │ │ ├── ScreenshotItem.tsx │ │ └── ScreenshotQueue.tsx │ ├── Solutions │ │ └── SolutionCommands.tsx │ ├── UpdateNotification.tsx │ ├── shared │ │ └── LanguageSelector.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── toast.tsx ├── contexts │ └── toast.tsx ├── env.d.ts ├── index.css ├── lib │ ├── supabase.ts │ └── utils.ts ├── main.tsx ├── types │ ├── electron.d.ts │ ├── global.d.ts │ ├── index.tsx │ ├── screenshots.ts │ └── solutions.ts ├── utils │ └── platform.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.electron.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | release/Interview[[:space:]]Coder-1.0.0-arm64-mac.zip filter=lfs diff=lfs merge=lfs -text 2 | release/Interview[[:space:]]Coder-1.0.0-arm64.dmg filter=lfs diff=lfs merge=lfs -text 3 | release/mac-arm64/Interview[[:space:]]Coder.app/Contents/Frameworks/Electron[[:space:]]Framework.framework/Versions/A/Electron[[:space:]]Framework filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist-electron 3 | release 4 | dist 5 | .env 6 | .env.* 7 | **/.DS_Store 8 | **/.vscode 9 | **/.idea 10 | **/package-lock.json 11 | scripts/ 12 | **/scripts/ 13 | scripts/manual-notarize.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interview Coder 2 | 3 | An invisible desktop application that will help you pass your technical interviews. 4 | 5 | https://www.interviewcoder.co 6 | 7 | 8 | https://github.com/user-attachments/assets/0615b110-2670-4b0e-bc69-3c32a2d8a996 9 | 10 | ## Invisibility Compatibility 11 | 12 | The application is invisible to: 13 | 14 | - Zoom versions below 6.1.6 (inclusive) 15 | - All browser-based screen recording software 16 | - All versions of Discord 17 | - Mac OS _screenshot_ functionality (Command + Shift + 3/4) 18 | 19 | Note: The application is **NOT** invisible to: 20 | 21 | - Zoom versions 6.1.6 and above 22 | - https://zoom.en.uptodown.com/mac/versions (link to downgrade Zoom if needed) 23 | - Mac OS native screen _recording_ (Command + Shift + 5) 24 | 25 | ## Features 26 | 27 | - 🎯 99% Invisibility: Undetectable window that bypasses most screen capture methods 28 | - 📸 Smart Screenshot Capture: Capture both question text and code separately for better analysis 29 | - 🤖 AI-Powered Analysis: Automatically extracts and analyzes coding problems 30 | - 💡 Solution Generation: Get detailed explanations and solutions 31 | - 🔧 Real-time Debugging: Debug your code with AI assistance 32 | - 🎨 Window Management: Freely move and position the window anywhere on screen 33 | 34 | ## Global Commands 35 | 36 | The application uses unidentifiable global keyboard shortcuts that won't be detected by browsers or other applications: 37 | 38 | - Toggle Window Visibility: [Control or Cmd + b] 39 | - Move Window: [Control or Cmd + arrows] 40 | - Take Screenshot: [Control or Cmd + H] 41 | - Process Screenshots: [Control or Cmd + Enter] 42 | - Reset View: [Control or Cmd + R] 43 | - Quit: [Control or Cmd + Q] 44 | 45 | ## Usage 46 | 47 | 1. **Initial Setup** 48 | 49 | - Launch the invisible window 50 | - Login and subscribe 51 | 52 | 2. **Capturing Problem** 53 | 54 | - Use global shortcut [Control or Cmd + H] to take screenshots 55 | - Screenshots are automatically added to the queue of up to 5. 56 | 57 | 3. **Processing** 58 | 59 | - AI analyzes the screenshots to extract: 60 | - Problem requirements 61 | - Code context 62 | - System generates optimal solution strategy 63 | 64 | 4. **Solution & Debugging** 65 | 66 | - View generated solutions 67 | - Use debugging feature to: 68 | - Test different approaches 69 | - Fix errors in your code 70 | - Get line-by-line explanations 71 | - Toggle between solutions and queue views 72 | 73 | 5. **Window Management** 74 | - Move window freely using global shortcut 75 | - Toggle visibility as needed 76 | - Window remains invisible to specified applications 77 | - Reset view using Command + R 78 | 79 | ## Prerequisites 80 | 81 | - Node.js (v16 or higher) 82 | - npm or bun package manager 83 | - Subscription on https://www.interviewcoder.co/settings 84 | - Screen Recording Permission for Terminal/IDE 85 | - On macOS: 86 | 1. Go to System Preferences > Security & Privacy > Privacy > Screen Recording 87 | 2. Ensure that Interview Coder has screen recording permission enabled 88 | 3. Restart Interview Coder after enabling permissions 89 | - On Windows: 90 | - No additional permissions needed 91 | - On Linux: 92 | - May require `xhost` access depending on your distribution 93 | 94 | ## Installation 95 | 96 | 1. Clone the repository: 97 | 98 | ```bash 99 | git clone https://github.com/ibttf/interview-coder-v1.git 100 | cd interview-coder-v1 101 | ``` 102 | 103 | 2. Install dependencies: 104 | 105 | ```bash 106 | npm install 107 | # or if using bun 108 | bun install 109 | ``` 110 | 111 | ## Running Locally 112 | 113 | 1. Start the development server: 114 | 115 | ```bash 116 | npm run dev 117 | ``` 118 | 119 | This will: 120 | 121 | - Start the Vite development server 122 | - Launch the Electron application 123 | - Enable hot-reloading for development 124 | 125 | ## Tech Stack 126 | 127 | - Electron 128 | - React 129 | - TypeScript 130 | - Vite 131 | - Tailwind CSS 132 | - Radix UI Components 133 | - OpenAI API 134 | 135 | ## Configuration 136 | 137 | 1. On first launch, you'll need to provide your OpenAI API key 138 | 2. The application will store your settings locally using electron-store 139 | 140 | ## Building (for Roy) 141 | 142 | after npm run build, hit: 143 | 144 | ``` 145 | node scripts/manual-notarize.js "release/Interview-Coder-x64.dmg" && xcrun stapler staple "release/Interview-Coder-x64.dmg" 146 | node scripts/manual-notarize.js "release/Interview-Coder-arm64.dmg" && xcrun stapler staple "release/Interview-Coder-arm64.dmg" 147 | ``` 148 | 149 | ## Contributing 150 | 151 | Contributions are welcome! Please feel free to submit a Pull Request. 152 | -------------------------------------------------------------------------------- /assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.debugger 10 | 11 | com.apple.security.device.camera 12 | 13 | com.apple.security.device.microphone 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | com.apple.security.network.client 18 | 19 | 20 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/bun.lockb -------------------------------------------------------------------------------- /electron/ProcessingHelper.ts: -------------------------------------------------------------------------------- 1 | // ProcessingHelper.ts 2 | import fs from "node:fs" 3 | import { ScreenshotHelper } from "./ScreenshotHelper" 4 | import { IProcessingHelperDeps } from "./main" 5 | import axios from "axios" 6 | import { app } from "electron" 7 | import { BrowserWindow } from "electron" 8 | 9 | const isDev = !app.isPackaged 10 | const API_BASE_URL = isDev 11 | ? "http://localhost:3000" 12 | : "https://www.interviewcoder.co" 13 | 14 | export class ProcessingHelper { 15 | private deps: IProcessingHelperDeps 16 | private screenshotHelper: ScreenshotHelper 17 | 18 | // AbortControllers for API requests 19 | private currentProcessingAbortController: AbortController | null = null 20 | private currentExtraProcessingAbortController: AbortController | null = null 21 | 22 | constructor(deps: IProcessingHelperDeps) { 23 | this.deps = deps 24 | this.screenshotHelper = deps.getScreenshotHelper() 25 | } 26 | 27 | private async waitForInitialization( 28 | mainWindow: BrowserWindow 29 | ): Promise { 30 | let attempts = 0 31 | const maxAttempts = 50 // 5 seconds total 32 | 33 | while (attempts < maxAttempts) { 34 | const isInitialized = await mainWindow.webContents.executeJavaScript( 35 | "window.__IS_INITIALIZED__" 36 | ) 37 | if (isInitialized) return 38 | await new Promise((resolve) => setTimeout(resolve, 100)) 39 | attempts++ 40 | } 41 | throw new Error("App failed to initialize after 5 seconds") 42 | } 43 | 44 | private async getCredits(): Promise { 45 | const mainWindow = this.deps.getMainWindow() 46 | if (!mainWindow) return 0 47 | 48 | try { 49 | await this.waitForInitialization(mainWindow) 50 | const credits = await mainWindow.webContents.executeJavaScript( 51 | "window.__CREDITS__" 52 | ) 53 | 54 | if ( 55 | typeof credits !== "number" || 56 | credits === undefined || 57 | credits === null 58 | ) { 59 | console.warn("Credits not properly initialized") 60 | return 0 61 | } 62 | 63 | return credits 64 | } catch (error) { 65 | console.error("Error getting credits:", error) 66 | return 0 67 | } 68 | } 69 | 70 | private async getLanguage(): Promise { 71 | const mainWindow = this.deps.getMainWindow() 72 | if (!mainWindow) return "python" 73 | 74 | try { 75 | await this.waitForInitialization(mainWindow) 76 | const language = await mainWindow.webContents.executeJavaScript( 77 | "window.__LANGUAGE__" 78 | ) 79 | 80 | if ( 81 | typeof language !== "string" || 82 | language === undefined || 83 | language === null 84 | ) { 85 | console.warn("Language not properly initialized") 86 | return "python" 87 | } 88 | 89 | return language 90 | } catch (error) { 91 | console.error("Error getting language:", error) 92 | return "python" 93 | } 94 | } 95 | 96 | public async processScreenshots(): Promise { 97 | const mainWindow = this.deps.getMainWindow() 98 | if (!mainWindow) return 99 | 100 | // Check if we have any credits left 101 | const credits = await this.getCredits() 102 | if (credits < 1) { 103 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS) 104 | return 105 | } 106 | 107 | const view = this.deps.getView() 108 | console.log("Processing screenshots in view:", view) 109 | 110 | if (view === "queue") { 111 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.INITIAL_START) 112 | const screenshotQueue = this.screenshotHelper.getScreenshotQueue() 113 | console.log("Processing main queue screenshots:", screenshotQueue) 114 | if (screenshotQueue.length === 0) { 115 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 116 | return 117 | } 118 | 119 | try { 120 | // Initialize AbortController 121 | this.currentProcessingAbortController = new AbortController() 122 | const { signal } = this.currentProcessingAbortController 123 | 124 | const screenshots = await Promise.all( 125 | screenshotQueue.map(async (path) => ({ 126 | path, 127 | preview: await this.screenshotHelper.getImagePreview(path), 128 | data: fs.readFileSync(path).toString("base64") 129 | })) 130 | ) 131 | 132 | const result = await this.processScreenshotsHelper(screenshots, signal) 133 | 134 | if (!result.success) { 135 | console.log("Processing failed:", result.error) 136 | if (result.error?.includes("API Key out of credits")) { 137 | mainWindow.webContents.send( 138 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 139 | ) 140 | } else if (result.error?.includes("OpenAI API key not found")) { 141 | mainWindow.webContents.send( 142 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 143 | "OpenAI API key not found in environment variables. Please set the OPEN_AI_API_KEY environment variable." 144 | ) 145 | } else { 146 | mainWindow.webContents.send( 147 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 148 | result.error 149 | ) 150 | } 151 | // Reset view back to queue on error 152 | console.log("Resetting view to queue due to error") 153 | this.deps.setView("queue") 154 | return 155 | } 156 | 157 | // Only set view to solutions if processing succeeded 158 | console.log("Setting view to solutions after successful processing") 159 | mainWindow.webContents.send( 160 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 161 | result.data 162 | ) 163 | this.deps.setView("solutions") 164 | } catch (error: any) { 165 | mainWindow.webContents.send( 166 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 167 | error 168 | ) 169 | console.error("Processing error:", error) 170 | if (axios.isCancel(error)) { 171 | mainWindow.webContents.send( 172 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 173 | "Processing was canceled by the user." 174 | ) 175 | } else { 176 | mainWindow.webContents.send( 177 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 178 | error.message || "Server error. Please try again." 179 | ) 180 | } 181 | // Reset view back to queue on error 182 | console.log("Resetting view to queue due to error") 183 | this.deps.setView("queue") 184 | } finally { 185 | this.currentProcessingAbortController = null 186 | } 187 | } else { 188 | // view == 'solutions' 189 | const extraScreenshotQueue = 190 | this.screenshotHelper.getExtraScreenshotQueue() 191 | console.log("Processing extra queue screenshots:", extraScreenshotQueue) 192 | if (extraScreenshotQueue.length === 0) { 193 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 194 | return 195 | } 196 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.DEBUG_START) 197 | 198 | // Initialize AbortController 199 | this.currentExtraProcessingAbortController = new AbortController() 200 | const { signal } = this.currentExtraProcessingAbortController 201 | 202 | try { 203 | const screenshots = await Promise.all( 204 | [ 205 | ...this.screenshotHelper.getScreenshotQueue(), 206 | ...extraScreenshotQueue 207 | ].map(async (path) => ({ 208 | path, 209 | preview: await this.screenshotHelper.getImagePreview(path), 210 | data: fs.readFileSync(path).toString("base64") 211 | })) 212 | ) 213 | console.log( 214 | "Combined screenshots for processing:", 215 | screenshots.map((s) => s.path) 216 | ) 217 | 218 | const result = await this.processExtraScreenshotsHelper( 219 | screenshots, 220 | signal 221 | ) 222 | 223 | if (result.success) { 224 | this.deps.setHasDebugged(true) 225 | mainWindow.webContents.send( 226 | this.deps.PROCESSING_EVENTS.DEBUG_SUCCESS, 227 | result.data 228 | ) 229 | } else { 230 | mainWindow.webContents.send( 231 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 232 | result.error 233 | ) 234 | } 235 | } catch (error: any) { 236 | if (axios.isCancel(error)) { 237 | mainWindow.webContents.send( 238 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 239 | "Extra processing was canceled by the user." 240 | ) 241 | } else { 242 | mainWindow.webContents.send( 243 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 244 | error.message 245 | ) 246 | } 247 | } finally { 248 | this.currentExtraProcessingAbortController = null 249 | } 250 | } 251 | } 252 | 253 | private async processScreenshotsHelper( 254 | screenshots: Array<{ path: string; data: string }>, 255 | signal: AbortSignal 256 | ) { 257 | const MAX_RETRIES = 0 258 | let retryCount = 0 259 | 260 | while (retryCount <= MAX_RETRIES) { 261 | try { 262 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 263 | const mainWindow = this.deps.getMainWindow() 264 | const language = await this.getLanguage() 265 | let problemInfo 266 | 267 | // First API call - extract problem info 268 | try { 269 | const extractResponse = await axios.post( 270 | `${API_BASE_URL}/api/extract`, 271 | { imageDataList, language }, 272 | { 273 | signal, 274 | timeout: 300000, 275 | validateStatus: function (status) { 276 | return status < 500 277 | }, 278 | maxRedirects: 5, 279 | headers: { 280 | "Content-Type": "application/json" 281 | } 282 | } 283 | ) 284 | 285 | problemInfo = extractResponse.data 286 | 287 | // Store problem info in AppState 288 | this.deps.setProblemInfo(problemInfo) 289 | 290 | // Send first success event 291 | if (mainWindow) { 292 | mainWindow.webContents.send( 293 | this.deps.PROCESSING_EVENTS.PROBLEM_EXTRACTED, 294 | problemInfo 295 | ) 296 | 297 | // Generate solutions after successful extraction 298 | const solutionsResult = await this.generateSolutionsHelper(signal) 299 | if (solutionsResult.success) { 300 | // Clear any existing extra screenshots before transitioning to solutions view 301 | this.screenshotHelper.clearExtraScreenshotQueue() 302 | mainWindow.webContents.send( 303 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 304 | solutionsResult.data 305 | ) 306 | return { success: true, data: solutionsResult.data } 307 | } else { 308 | throw new Error( 309 | solutionsResult.error || "Failed to generate solutions" 310 | ) 311 | } 312 | } 313 | } catch (error: any) { 314 | // If the request was cancelled, don't retry 315 | if (axios.isCancel(error)) { 316 | return { 317 | success: false, 318 | error: "Processing was canceled by the user." 319 | } 320 | } 321 | 322 | console.error("API Error Details:", { 323 | status: error.response?.status, 324 | data: error.response?.data, 325 | message: error.message, 326 | code: error.code 327 | }) 328 | 329 | // Handle API-specific errors 330 | if ( 331 | error.response?.data?.error && 332 | typeof error.response.data.error === "string" 333 | ) { 334 | if (error.response.data.error.includes("Operation timed out")) { 335 | throw new Error( 336 | "Operation timed out after 1 minute. Please try again." 337 | ) 338 | } 339 | if (error.response.data.error.includes("API Key out of credits")) { 340 | throw new Error(error.response.data.error) 341 | } 342 | throw new Error(error.response.data.error) 343 | } 344 | 345 | // If we get here, it's an unknown error 346 | throw new Error(error.message || "Server error. Please try again.") 347 | } 348 | } catch (error: any) { 349 | // Log the full error for debugging 350 | console.error("Processing error details:", { 351 | message: error.message, 352 | code: error.code, 353 | response: error.response?.data, 354 | retryCount 355 | }) 356 | 357 | // If it's a cancellation or we've exhausted retries, return the error 358 | if (axios.isCancel(error) || retryCount >= MAX_RETRIES) { 359 | return { success: false, error: error.message } 360 | } 361 | 362 | // Increment retry count and continue 363 | retryCount++ 364 | } 365 | } 366 | 367 | // If we get here, all retries failed 368 | return { 369 | success: false, 370 | error: "Failed to process after multiple attempts. Please try again." 371 | } 372 | } 373 | 374 | private async generateSolutionsHelper(signal: AbortSignal) { 375 | try { 376 | const problemInfo = this.deps.getProblemInfo() 377 | const language = await this.getLanguage() 378 | 379 | if (!problemInfo) { 380 | throw new Error("No problem info available") 381 | } 382 | 383 | const response = await axios.post( 384 | `${API_BASE_URL}/api/generate`, 385 | { ...problemInfo, language }, 386 | { 387 | signal, 388 | timeout: 300000, 389 | validateStatus: function (status) { 390 | return status < 500 391 | }, 392 | maxRedirects: 5, 393 | headers: { 394 | "Content-Type": "application/json" 395 | } 396 | } 397 | ) 398 | 399 | return { success: true, data: response.data } 400 | } catch (error: any) { 401 | const mainWindow = this.deps.getMainWindow() 402 | 403 | // Handle timeout errors (both 504 and axios timeout) 404 | if (error.code === "ECONNABORTED" || error.response?.status === 504) { 405 | // Cancel ongoing API requests 406 | this.cancelOngoingRequests() 407 | // Clear both screenshot queues 408 | this.deps.clearQueues() 409 | // Update view state to queue 410 | this.deps.setView("queue") 411 | // Notify renderer to switch view 412 | if (mainWindow && !mainWindow.isDestroyed()) { 413 | mainWindow.webContents.send("reset-view") 414 | mainWindow.webContents.send( 415 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 416 | "Request timed out. The server took too long to respond. Please try again." 417 | ) 418 | } 419 | return { 420 | success: false, 421 | error: "Request timed out. Please try again." 422 | } 423 | } 424 | 425 | if (error.response?.data?.error?.includes("API Key out of credits")) { 426 | if (mainWindow) { 427 | mainWindow.webContents.send( 428 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 429 | ) 430 | } 431 | return { success: false, error: error.response.data.error } 432 | } 433 | 434 | if ( 435 | error.response?.data?.error?.includes( 436 | "Please close this window and re-enter a valid Open AI API key." 437 | ) 438 | ) { 439 | if (mainWindow) { 440 | mainWindow.webContents.send( 441 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 442 | ) 443 | } 444 | return { success: false, error: error.response.data.error } 445 | } 446 | 447 | return { success: false, error: error.message } 448 | } 449 | } 450 | 451 | private async processExtraScreenshotsHelper( 452 | screenshots: Array<{ path: string; data: string }>, 453 | signal: AbortSignal 454 | ) { 455 | try { 456 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 457 | const problemInfo = this.deps.getProblemInfo() 458 | const language = await this.getLanguage() 459 | 460 | if (!problemInfo) { 461 | throw new Error("No problem info available") 462 | } 463 | 464 | const response = await axios.post( 465 | `${API_BASE_URL}/api/debug`, 466 | { imageDataList, problemInfo, language }, 467 | { 468 | signal, 469 | timeout: 300000, 470 | validateStatus: function (status) { 471 | return status < 500 472 | }, 473 | maxRedirects: 5, 474 | headers: { 475 | "Content-Type": "application/json" 476 | } 477 | } 478 | ) 479 | 480 | return { success: true, data: response.data } 481 | } catch (error: any) { 482 | const mainWindow = this.deps.getMainWindow() 483 | 484 | // Handle cancellation first 485 | if (axios.isCancel(error)) { 486 | return { 487 | success: false, 488 | error: "Processing was canceled by the user." 489 | } 490 | } 491 | 492 | if (error.response?.data?.error?.includes("Operation timed out")) { 493 | // Cancel ongoing API requests 494 | this.cancelOngoingRequests() 495 | // Clear both screenshot queues 496 | this.deps.clearQueues() 497 | // Update view state to queue 498 | this.deps.setView("queue") 499 | // Notify renderer to switch view 500 | if (mainWindow && !mainWindow.isDestroyed()) { 501 | mainWindow.webContents.send("reset-view") 502 | mainWindow.webContents.send( 503 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 504 | "Operation timed out after 1 minute. Please try again." 505 | ) 506 | } 507 | return { 508 | success: false, 509 | error: "Operation timed out after 1 minute. Please try again." 510 | } 511 | } 512 | 513 | if (error.response?.data?.error?.includes("API Key out of credits")) { 514 | if (mainWindow) { 515 | mainWindow.webContents.send( 516 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 517 | ) 518 | } 519 | return { success: false, error: error.response.data.error } 520 | } 521 | 522 | if ( 523 | error.response?.data?.error?.includes( 524 | "Please close this window and re-enter a valid Open AI API key." 525 | ) 526 | ) { 527 | if (mainWindow) { 528 | mainWindow.webContents.send( 529 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 530 | ) 531 | } 532 | return { success: false, error: error.response.data.error } 533 | } 534 | 535 | return { success: false, error: error.message } 536 | } 537 | } 538 | 539 | public cancelOngoingRequests(): void { 540 | let wasCancelled = false 541 | 542 | if (this.currentProcessingAbortController) { 543 | this.currentProcessingAbortController.abort() 544 | this.currentProcessingAbortController = null 545 | wasCancelled = true 546 | } 547 | 548 | if (this.currentExtraProcessingAbortController) { 549 | this.currentExtraProcessingAbortController.abort() 550 | this.currentExtraProcessingAbortController = null 551 | wasCancelled = true 552 | } 553 | 554 | // Reset hasDebugged flag 555 | this.deps.setHasDebugged(false) 556 | 557 | // Clear any pending state 558 | this.deps.setProblemInfo(null) 559 | 560 | const mainWindow = this.deps.getMainWindow() 561 | if (wasCancelled && mainWindow && !mainWindow.isDestroyed()) { 562 | // Send a clear message that processing was cancelled 563 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /electron/ScreenshotHelper.ts: -------------------------------------------------------------------------------- 1 | // ScreenshotHelper.ts 2 | 3 | import path from "node:path" 4 | import fs from "node:fs" 5 | import { app } from "electron" 6 | import { v4 as uuidv4 } from "uuid" 7 | import { execFile } from "child_process" 8 | import { promisify } from "util" 9 | 10 | const execFileAsync = promisify(execFile) 11 | 12 | export class ScreenshotHelper { 13 | private screenshotQueue: string[] = [] 14 | private extraScreenshotQueue: string[] = [] 15 | private readonly MAX_SCREENSHOTS = 2 16 | 17 | private readonly screenshotDir: string 18 | private readonly extraScreenshotDir: string 19 | 20 | private view: "queue" | "solutions" | "debug" = "queue" 21 | 22 | constructor(view: "queue" | "solutions" | "debug" = "queue") { 23 | this.view = view 24 | 25 | // Initialize directories 26 | this.screenshotDir = path.join(app.getPath("userData"), "screenshots") 27 | this.extraScreenshotDir = path.join( 28 | app.getPath("userData"), 29 | "extra_screenshots" 30 | ) 31 | 32 | // Create directories if they don't exist 33 | if (!fs.existsSync(this.screenshotDir)) { 34 | fs.mkdirSync(this.screenshotDir) 35 | } 36 | if (!fs.existsSync(this.extraScreenshotDir)) { 37 | fs.mkdirSync(this.extraScreenshotDir) 38 | } 39 | } 40 | 41 | public getView(): "queue" | "solutions" | "debug" { 42 | return this.view 43 | } 44 | 45 | public setView(view: "queue" | "solutions" | "debug"): void { 46 | console.log("Setting view in ScreenshotHelper:", view) 47 | console.log( 48 | "Current queues - Main:", 49 | this.screenshotQueue, 50 | "Extra:", 51 | this.extraScreenshotQueue 52 | ) 53 | this.view = view 54 | } 55 | 56 | public getScreenshotQueue(): string[] { 57 | return this.screenshotQueue 58 | } 59 | 60 | public getExtraScreenshotQueue(): string[] { 61 | console.log("Getting extra screenshot queue:", this.extraScreenshotQueue) 62 | return this.extraScreenshotQueue 63 | } 64 | 65 | public clearQueues(): void { 66 | // Clear screenshotQueue 67 | this.screenshotQueue.forEach((screenshotPath) => { 68 | fs.unlink(screenshotPath, (err) => { 69 | if (err) 70 | console.error(`Error deleting screenshot at ${screenshotPath}:`, err) 71 | }) 72 | }) 73 | this.screenshotQueue = [] 74 | 75 | // Clear extraScreenshotQueue 76 | this.extraScreenshotQueue.forEach((screenshotPath) => { 77 | fs.unlink(screenshotPath, (err) => { 78 | if (err) 79 | console.error( 80 | `Error deleting extra screenshot at ${screenshotPath}:`, 81 | err 82 | ) 83 | }) 84 | }) 85 | this.extraScreenshotQueue = [] 86 | } 87 | 88 | private async captureScreenshotMac(): Promise { 89 | const tmpPath = path.join(app.getPath("temp"), `${uuidv4()}.png`) 90 | await execFileAsync("screencapture", ["-x", tmpPath]) 91 | const buffer = await fs.promises.readFile(tmpPath) 92 | await fs.promises.unlink(tmpPath) 93 | return buffer 94 | } 95 | 96 | private async captureScreenshotWindows(): Promise { 97 | // Using PowerShell's native screenshot capability 98 | const tmpPath = path.join(app.getPath("temp"), `${uuidv4()}.png`) 99 | const script = ` 100 | Add-Type -AssemblyName System.Windows.Forms 101 | Add-Type -AssemblyName System.Drawing 102 | $screen = [System.Windows.Forms.Screen]::PrimaryScreen 103 | $bitmap = New-Object System.Drawing.Bitmap $screen.Bounds.Width, $screen.Bounds.Height 104 | $graphics = [System.Drawing.Graphics]::FromImage($bitmap) 105 | $graphics.CopyFromScreen($screen.Bounds.X, $screen.Bounds.Y, 0, 0, $bitmap.Size) 106 | $bitmap.Save('${tmpPath.replace(/\\/g, "\\\\")}') 107 | $graphics.Dispose() 108 | $bitmap.Dispose() 109 | ` 110 | await execFileAsync("powershell", ["-command", script]) 111 | const buffer = await fs.promises.readFile(tmpPath) 112 | await fs.promises.unlink(tmpPath) 113 | return buffer 114 | } 115 | 116 | public async takeScreenshot( 117 | hideMainWindow: () => void, 118 | showMainWindow: () => void 119 | ): Promise { 120 | console.log("Taking screenshot in view:", this.view) 121 | hideMainWindow() 122 | await new Promise((resolve) => setTimeout(resolve, 100)) 123 | 124 | let screenshotPath = "" 125 | try { 126 | // Get screenshot buffer using native methods 127 | const screenshotBuffer = 128 | process.platform === "darwin" 129 | ? await this.captureScreenshotMac() 130 | : await this.captureScreenshotWindows() 131 | 132 | // Save and manage the screenshot based on current view 133 | if (this.view === "queue") { 134 | screenshotPath = path.join(this.screenshotDir, `${uuidv4()}.png`) 135 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 136 | console.log("Adding screenshot to main queue:", screenshotPath) 137 | this.screenshotQueue.push(screenshotPath) 138 | if (this.screenshotQueue.length > this.MAX_SCREENSHOTS) { 139 | const removedPath = this.screenshotQueue.shift() 140 | if (removedPath) { 141 | try { 142 | await fs.promises.unlink(removedPath) 143 | console.log( 144 | "Removed old screenshot from main queue:", 145 | removedPath 146 | ) 147 | } catch (error) { 148 | console.error("Error removing old screenshot:", error) 149 | } 150 | } 151 | } 152 | } else { 153 | // In solutions view, only add to extra queue 154 | screenshotPath = path.join(this.extraScreenshotDir, `${uuidv4()}.png`) 155 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 156 | console.log("Adding screenshot to extra queue:", screenshotPath) 157 | this.extraScreenshotQueue.push(screenshotPath) 158 | if (this.extraScreenshotQueue.length > this.MAX_SCREENSHOTS) { 159 | const removedPath = this.extraScreenshotQueue.shift() 160 | if (removedPath) { 161 | try { 162 | await fs.promises.unlink(removedPath) 163 | console.log( 164 | "Removed old screenshot from extra queue:", 165 | removedPath 166 | ) 167 | } catch (error) { 168 | console.error("Error removing old screenshot:", error) 169 | } 170 | } 171 | } 172 | } 173 | } catch (error) { 174 | console.error("Screenshot error:", error) 175 | throw error 176 | } finally { 177 | await new Promise((resolve) => setTimeout(resolve, 50)) 178 | showMainWindow() 179 | } 180 | 181 | return screenshotPath 182 | } 183 | 184 | public async getImagePreview(filepath: string): Promise { 185 | try { 186 | const data = await fs.promises.readFile(filepath) 187 | return `data:image/png;base64,${data.toString("base64")}` 188 | } catch (error) { 189 | console.error("Error reading image:", error) 190 | throw error 191 | } 192 | } 193 | 194 | public async deleteScreenshot( 195 | path: string 196 | ): Promise<{ success: boolean; error?: string }> { 197 | try { 198 | await fs.promises.unlink(path) 199 | if (this.view === "queue") { 200 | this.screenshotQueue = this.screenshotQueue.filter( 201 | (filePath) => filePath !== path 202 | ) 203 | } else { 204 | this.extraScreenshotQueue = this.extraScreenshotQueue.filter( 205 | (filePath) => filePath !== path 206 | ) 207 | } 208 | return { success: true } 209 | } catch (error) { 210 | console.error("Error deleting file:", error) 211 | return { success: false, error: error.message } 212 | } 213 | } 214 | 215 | public clearExtraScreenshotQueue(): void { 216 | // Clear extraScreenshotQueue 217 | this.extraScreenshotQueue.forEach((screenshotPath) => { 218 | fs.unlink(screenshotPath, (err) => { 219 | if (err) 220 | console.error( 221 | `Error deleting extra screenshot at ${screenshotPath}:`, 222 | err 223 | ) 224 | }) 225 | }) 226 | this.extraScreenshotQueue = [] 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /electron/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from "electron-updater" 2 | import { BrowserWindow, ipcMain, app } from "electron" 3 | import log from "electron-log" 4 | 5 | export function initAutoUpdater() { 6 | console.log("Initializing auto-updater...") 7 | 8 | // Skip update checks in development 9 | if (!app.isPackaged) { 10 | console.log("Skipping auto-updater in development mode") 11 | return 12 | } 13 | 14 | if (!process.env.GH_TOKEN) { 15 | console.error("GH_TOKEN environment variable is not set") 16 | return 17 | } 18 | 19 | // Configure auto updater 20 | autoUpdater.autoDownload = true 21 | autoUpdater.autoInstallOnAppQuit = true 22 | autoUpdater.allowDowngrade = true 23 | autoUpdater.allowPrerelease = true 24 | 25 | // Enable more verbose logging 26 | autoUpdater.logger = log 27 | log.transports.file.level = "debug" 28 | console.log( 29 | "Auto-updater logger configured with level:", 30 | log.transports.file.level 31 | ) 32 | 33 | // Log all update events 34 | autoUpdater.on("checking-for-update", () => { 35 | console.log("Checking for updates...") 36 | }) 37 | 38 | autoUpdater.on("update-available", (info) => { 39 | console.log("Update available:", info) 40 | // Notify renderer process about available update 41 | BrowserWindow.getAllWindows().forEach((window) => { 42 | console.log("Sending update-available to window") 43 | window.webContents.send("update-available", info) 44 | }) 45 | }) 46 | 47 | autoUpdater.on("update-not-available", (info) => { 48 | console.log("Update not available:", info) 49 | }) 50 | 51 | autoUpdater.on("download-progress", (progressObj) => { 52 | console.log("Download progress:", progressObj) 53 | }) 54 | 55 | autoUpdater.on("update-downloaded", (info) => { 56 | console.log("Update downloaded:", info) 57 | // Notify renderer process that update is ready to install 58 | BrowserWindow.getAllWindows().forEach((window) => { 59 | console.log("Sending update-downloaded to window") 60 | window.webContents.send("update-downloaded", info) 61 | }) 62 | }) 63 | 64 | autoUpdater.on("error", (err) => { 65 | console.error("Auto updater error:", err) 66 | }) 67 | 68 | // Check for updates immediately 69 | console.log("Checking for updates...") 70 | autoUpdater 71 | .checkForUpdates() 72 | .then((result) => { 73 | console.log("Update check result:", result) 74 | }) 75 | .catch((err) => { 76 | console.error("Error checking for updates:", err) 77 | }) 78 | 79 | // Set up update checking interval (every 1 hour) 80 | setInterval(() => { 81 | console.log("Checking for updates (interval)...") 82 | autoUpdater 83 | .checkForUpdates() 84 | .then((result) => { 85 | console.log("Update check result (interval):", result) 86 | }) 87 | .catch((err) => { 88 | console.error("Error checking for updates (interval):", err) 89 | }) 90 | }, 60 * 60 * 1000) 91 | 92 | // Handle IPC messages from renderer 93 | ipcMain.handle("start-update", async () => { 94 | console.log("Start update requested") 95 | try { 96 | await autoUpdater.downloadUpdate() 97 | console.log("Update download completed") 98 | return { success: true } 99 | } catch (error) { 100 | console.error("Failed to start update:", error) 101 | return { success: false, error: error.message } 102 | } 103 | }) 104 | 105 | ipcMain.handle("install-update", () => { 106 | console.log("Install update requested") 107 | autoUpdater.quitAndInstall() 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /electron/ipcHandlers.ts: -------------------------------------------------------------------------------- 1 | // ipcHandlers.ts 2 | 3 | import { ipcMain, shell } from "electron" 4 | import { createClient } from "@supabase/supabase-js" 5 | import { randomBytes } from "crypto" 6 | import { IIpcHandlerDeps } from "./main" 7 | 8 | export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { 9 | console.log("Initializing IPC handlers") 10 | 11 | // Credits handlers 12 | ipcMain.handle("set-initial-credits", async (_event, credits: number) => { 13 | const mainWindow = deps.getMainWindow() 14 | if (!mainWindow) return 15 | 16 | try { 17 | // Set the credits in a way that ensures atomicity 18 | await mainWindow.webContents.executeJavaScript( 19 | `window.__CREDITS__ = ${credits}` 20 | ) 21 | mainWindow.webContents.send("credits-updated", credits) 22 | } catch (error) { 23 | console.error("Error setting initial credits:", error) 24 | throw error 25 | } 26 | }) 27 | 28 | ipcMain.handle("decrement-credits", async () => { 29 | const mainWindow = deps.getMainWindow() 30 | if (!mainWindow) return 31 | 32 | try { 33 | const currentCredits = await mainWindow.webContents.executeJavaScript( 34 | "window.__CREDITS__" 35 | ) 36 | if (currentCredits > 0) { 37 | const newCredits = currentCredits - 1 38 | await mainWindow.webContents.executeJavaScript( 39 | `window.__CREDITS__ = ${newCredits}` 40 | ) 41 | mainWindow.webContents.send("credits-updated", newCredits) 42 | } 43 | } catch (error) { 44 | console.error("Error decrementing credits:", error) 45 | } 46 | }) 47 | 48 | // Screenshot queue handlers 49 | ipcMain.handle("get-screenshot-queue", () => { 50 | return deps.getScreenshotQueue() 51 | }) 52 | 53 | ipcMain.handle("get-extra-screenshot-queue", () => { 54 | return deps.getExtraScreenshotQueue() 55 | }) 56 | 57 | ipcMain.handle("delete-screenshot", async (event, path: string) => { 58 | return deps.deleteScreenshot(path) 59 | }) 60 | 61 | ipcMain.handle("get-image-preview", async (event, path: string) => { 62 | return deps.getImagePreview(path) 63 | }) 64 | 65 | // Screenshot processing handlers 66 | ipcMain.handle("process-screenshots", async () => { 67 | await deps.processingHelper?.processScreenshots() 68 | }) 69 | 70 | // Window dimension handlers 71 | ipcMain.handle( 72 | "update-content-dimensions", 73 | async (event, { width, height }: { width: number; height: number }) => { 74 | if (width && height) { 75 | deps.setWindowDimensions(width, height) 76 | } 77 | } 78 | ) 79 | 80 | ipcMain.handle( 81 | "set-window-dimensions", 82 | (event, width: number, height: number) => { 83 | deps.setWindowDimensions(width, height) 84 | } 85 | ) 86 | 87 | // Screenshot management handlers 88 | ipcMain.handle("get-screenshots", async () => { 89 | try { 90 | let previews = [] 91 | const currentView = deps.getView() 92 | 93 | if (currentView === "queue") { 94 | const queue = deps.getScreenshotQueue() 95 | previews = await Promise.all( 96 | queue.map(async (path) => ({ 97 | path, 98 | preview: await deps.getImagePreview(path) 99 | })) 100 | ) 101 | } else { 102 | const extraQueue = deps.getExtraScreenshotQueue() 103 | previews = await Promise.all( 104 | extraQueue.map(async (path) => ({ 105 | path, 106 | preview: await deps.getImagePreview(path) 107 | })) 108 | ) 109 | } 110 | 111 | return previews 112 | } catch (error) { 113 | console.error("Error getting screenshots:", error) 114 | throw error 115 | } 116 | }) 117 | 118 | // Screenshot trigger handlers 119 | ipcMain.handle("trigger-screenshot", async () => { 120 | const mainWindow = deps.getMainWindow() 121 | if (mainWindow) { 122 | try { 123 | const screenshotPath = await deps.takeScreenshot() 124 | const preview = await deps.getImagePreview(screenshotPath) 125 | mainWindow.webContents.send("screenshot-taken", { 126 | path: screenshotPath, 127 | preview 128 | }) 129 | return { success: true } 130 | } catch (error) { 131 | console.error("Error triggering screenshot:", error) 132 | return { error: "Failed to trigger screenshot" } 133 | } 134 | } 135 | return { error: "No main window available" } 136 | }) 137 | 138 | ipcMain.handle("take-screenshot", async () => { 139 | try { 140 | const screenshotPath = await deps.takeScreenshot() 141 | const preview = await deps.getImagePreview(screenshotPath) 142 | return { path: screenshotPath, preview } 143 | } catch (error) { 144 | console.error("Error taking screenshot:", error) 145 | return { error: "Failed to take screenshot" } 146 | } 147 | }) 148 | 149 | // Auth related handlers 150 | ipcMain.handle("get-pkce-verifier", () => { 151 | return randomBytes(32).toString("base64url") 152 | }) 153 | 154 | ipcMain.handle("open-external-url", (event, url: string) => { 155 | shell.openExternal(url) 156 | }) 157 | 158 | // Subscription handlers 159 | ipcMain.handle("open-settings-portal", () => { 160 | shell.openExternal("https://www.interviewcoder.co/settings") 161 | }) 162 | ipcMain.handle("open-subscription-portal", async (_event, authData) => { 163 | try { 164 | const url = "https://www.interviewcoder.co/checkout" 165 | await shell.openExternal(url) 166 | return { success: true } 167 | } catch (error) { 168 | console.error("Error opening checkout page:", error) 169 | return { 170 | success: false, 171 | error: 172 | error instanceof Error 173 | ? error.message 174 | : "Failed to open checkout page" 175 | } 176 | } 177 | }) 178 | 179 | // Window management handlers 180 | ipcMain.handle("toggle-window", () => { 181 | try { 182 | deps.toggleMainWindow() 183 | return { success: true } 184 | } catch (error) { 185 | console.error("Error toggling window:", error) 186 | return { error: "Failed to toggle window" } 187 | } 188 | }) 189 | 190 | ipcMain.handle("reset-queues", async () => { 191 | try { 192 | deps.clearQueues() 193 | return { success: true } 194 | } catch (error) { 195 | console.error("Error resetting queues:", error) 196 | return { error: "Failed to reset queues" } 197 | } 198 | }) 199 | 200 | // Process screenshot handlers 201 | ipcMain.handle("trigger-process-screenshots", async () => { 202 | try { 203 | await deps.processingHelper?.processScreenshots() 204 | return { success: true } 205 | } catch (error) { 206 | console.error("Error processing screenshots:", error) 207 | return { error: "Failed to process screenshots" } 208 | } 209 | }) 210 | 211 | // Reset handlers 212 | ipcMain.handle("trigger-reset", () => { 213 | try { 214 | // First cancel any ongoing requests 215 | deps.processingHelper?.cancelOngoingRequests() 216 | 217 | // Clear all queues immediately 218 | deps.clearQueues() 219 | 220 | // Reset view to queue 221 | deps.setView("queue") 222 | 223 | // Get main window and send reset events 224 | const mainWindow = deps.getMainWindow() 225 | if (mainWindow && !mainWindow.isDestroyed()) { 226 | // Send reset events in sequence 227 | mainWindow.webContents.send("reset-view") 228 | mainWindow.webContents.send("reset") 229 | } 230 | 231 | return { success: true } 232 | } catch (error) { 233 | console.error("Error triggering reset:", error) 234 | return { error: "Failed to trigger reset" } 235 | } 236 | }) 237 | 238 | // Window movement handlers 239 | ipcMain.handle("trigger-move-left", () => { 240 | try { 241 | deps.moveWindowLeft() 242 | return { success: true } 243 | } catch (error) { 244 | console.error("Error moving window left:", error) 245 | return { error: "Failed to move window left" } 246 | } 247 | }) 248 | 249 | ipcMain.handle("trigger-move-right", () => { 250 | try { 251 | deps.moveWindowRight() 252 | return { success: true } 253 | } catch (error) { 254 | console.error("Error moving window right:", error) 255 | return { error: "Failed to move window right" } 256 | } 257 | }) 258 | 259 | ipcMain.handle("trigger-move-up", () => { 260 | try { 261 | deps.moveWindowUp() 262 | return { success: true } 263 | } catch (error) { 264 | console.error("Error moving window up:", error) 265 | return { error: "Failed to move window up" } 266 | } 267 | }) 268 | 269 | ipcMain.handle("trigger-move-down", () => { 270 | try { 271 | deps.moveWindowDown() 272 | return { success: true } 273 | } catch (error) { 274 | console.error("Error moving window down:", error) 275 | return { error: "Failed to move window down" } 276 | } 277 | }) 278 | } 279 | -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, screen, shell, ipcMain } from "electron" 2 | import path from "path" 3 | import { initializeIpcHandlers } from "./ipcHandlers" 4 | import { ProcessingHelper } from "./ProcessingHelper" 5 | import { ScreenshotHelper } from "./ScreenshotHelper" 6 | import { ShortcutsHelper } from "./shortcuts" 7 | import { initAutoUpdater } from "./autoUpdater" 8 | import * as dotenv from "dotenv" 9 | 10 | // Constants 11 | const isDev = !app.isPackaged 12 | 13 | // Application State 14 | const state = { 15 | // Window management properties 16 | mainWindow: null as BrowserWindow | null, 17 | isWindowVisible: false, 18 | windowPosition: null as { x: number; y: number } | null, 19 | windowSize: null as { width: number; height: number } | null, 20 | screenWidth: 0, 21 | screenHeight: 0, 22 | step: 0, 23 | currentX: 0, 24 | currentY: 0, 25 | 26 | // Application helpers 27 | screenshotHelper: null as ScreenshotHelper | null, 28 | shortcutsHelper: null as ShortcutsHelper | null, 29 | processingHelper: null as ProcessingHelper | null, 30 | 31 | // View and state management 32 | view: "queue" as "queue" | "solutions" | "debug", 33 | problemInfo: null as any, 34 | hasDebugged: false, 35 | 36 | // Processing events 37 | PROCESSING_EVENTS: { 38 | UNAUTHORIZED: "processing-unauthorized", 39 | NO_SCREENSHOTS: "processing-no-screenshots", 40 | OUT_OF_CREDITS: "out-of-credits", 41 | API_KEY_INVALID: "processing-api-key-invalid", 42 | INITIAL_START: "initial-start", 43 | PROBLEM_EXTRACTED: "problem-extracted", 44 | SOLUTION_SUCCESS: "solution-success", 45 | INITIAL_SOLUTION_ERROR: "solution-error", 46 | DEBUG_START: "debug-start", 47 | DEBUG_SUCCESS: "debug-success", 48 | DEBUG_ERROR: "debug-error" 49 | } as const 50 | } 51 | 52 | // Add interfaces for helper classes 53 | export interface IProcessingHelperDeps { 54 | getScreenshotHelper: () => ScreenshotHelper | null 55 | getMainWindow: () => BrowserWindow | null 56 | getView: () => "queue" | "solutions" | "debug" 57 | setView: (view: "queue" | "solutions" | "debug") => void 58 | getProblemInfo: () => any 59 | setProblemInfo: (info: any) => void 60 | getScreenshotQueue: () => string[] 61 | getExtraScreenshotQueue: () => string[] 62 | clearQueues: () => void 63 | takeScreenshot: () => Promise 64 | getImagePreview: (filepath: string) => Promise 65 | deleteScreenshot: ( 66 | path: string 67 | ) => Promise<{ success: boolean; error?: string }> 68 | setHasDebugged: (value: boolean) => void 69 | getHasDebugged: () => boolean 70 | PROCESSING_EVENTS: typeof state.PROCESSING_EVENTS 71 | } 72 | 73 | export interface IShortcutsHelperDeps { 74 | getMainWindow: () => BrowserWindow | null 75 | takeScreenshot: () => Promise 76 | getImagePreview: (filepath: string) => Promise 77 | processingHelper: ProcessingHelper | null 78 | clearQueues: () => void 79 | setView: (view: "queue" | "solutions" | "debug") => void 80 | isVisible: () => boolean 81 | toggleMainWindow: () => void 82 | moveWindowLeft: () => void 83 | moveWindowRight: () => void 84 | moveWindowUp: () => void 85 | moveWindowDown: () => void 86 | } 87 | 88 | export interface IIpcHandlerDeps { 89 | getMainWindow: () => BrowserWindow | null 90 | setWindowDimensions: (width: number, height: number) => void 91 | getScreenshotQueue: () => string[] 92 | getExtraScreenshotQueue: () => string[] 93 | deleteScreenshot: ( 94 | path: string 95 | ) => Promise<{ success: boolean; error?: string }> 96 | getImagePreview: (filepath: string) => Promise 97 | processingHelper: ProcessingHelper | null 98 | PROCESSING_EVENTS: typeof state.PROCESSING_EVENTS 99 | takeScreenshot: () => Promise 100 | getView: () => "queue" | "solutions" | "debug" 101 | toggleMainWindow: () => void 102 | clearQueues: () => void 103 | setView: (view: "queue" | "solutions" | "debug") => void 104 | moveWindowLeft: () => void 105 | moveWindowRight: () => void 106 | moveWindowUp: () => void 107 | moveWindowDown: () => void 108 | } 109 | 110 | // Initialize helpers 111 | function initializeHelpers() { 112 | state.screenshotHelper = new ScreenshotHelper(state.view) 113 | state.processingHelper = new ProcessingHelper({ 114 | getScreenshotHelper, 115 | getMainWindow, 116 | getView, 117 | setView, 118 | getProblemInfo, 119 | setProblemInfo, 120 | getScreenshotQueue, 121 | getExtraScreenshotQueue, 122 | clearQueues, 123 | takeScreenshot, 124 | getImagePreview, 125 | deleteScreenshot, 126 | setHasDebugged, 127 | getHasDebugged, 128 | PROCESSING_EVENTS: state.PROCESSING_EVENTS 129 | } as IProcessingHelperDeps) 130 | state.shortcutsHelper = new ShortcutsHelper({ 131 | getMainWindow, 132 | takeScreenshot, 133 | getImagePreview, 134 | processingHelper: state.processingHelper, 135 | clearQueues, 136 | setView, 137 | isVisible: () => state.isWindowVisible, 138 | toggleMainWindow, 139 | moveWindowLeft: () => 140 | moveWindowHorizontal((x) => 141 | Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) 142 | ), 143 | moveWindowRight: () => 144 | moveWindowHorizontal((x) => 145 | Math.min( 146 | state.screenWidth - (state.windowSize?.width || 0) / 2, 147 | x + state.step 148 | ) 149 | ), 150 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 151 | moveWindowDown: () => moveWindowVertical((y) => y + state.step) 152 | } as IShortcutsHelperDeps) 153 | } 154 | 155 | // Auth callback handler 156 | 157 | // Register the interview-coder protocol 158 | if (process.platform === "darwin") { 159 | app.setAsDefaultProtocolClient("interview-coder") 160 | } else { 161 | app.setAsDefaultProtocolClient("interview-coder", process.execPath, [ 162 | path.resolve(process.argv[1] || "") 163 | ]) 164 | } 165 | 166 | // Handle the protocol. In this case, we choose to show an Error Box. 167 | if (process.defaultApp && process.argv.length >= 2) { 168 | app.setAsDefaultProtocolClient("interview-coder", process.execPath, [ 169 | path.resolve(process.argv[1]) 170 | ]) 171 | } 172 | 173 | // Force Single Instance Lock 174 | const gotTheLock = app.requestSingleInstanceLock() 175 | 176 | if (!gotTheLock) { 177 | app.quit() 178 | } else { 179 | app.on("second-instance", (event, commandLine) => { 180 | // Someone tried to run a second instance, we should focus our window. 181 | if (state.mainWindow) { 182 | if (state.mainWindow.isMinimized()) state.mainWindow.restore() 183 | state.mainWindow.focus() 184 | 185 | // Protocol handler for state.mainWindow32 186 | // argv: An array of the second instance's (command line / deep linked) arguments 187 | if (process.platform === "win32") { 188 | // Keep only command line / deep linked arguments 189 | const deeplinkingUrl = commandLine.pop() 190 | if (deeplinkingUrl) { 191 | handleAuthCallback(deeplinkingUrl, state.mainWindow) 192 | } 193 | } 194 | } 195 | }) 196 | } 197 | 198 | async function handleAuthCallback(url: string, win: BrowserWindow | null) { 199 | try { 200 | console.log("Auth callback received:", url) 201 | const urlObj = new URL(url) 202 | const code = urlObj.searchParams.get("code") 203 | 204 | if (!code) { 205 | console.error("Missing code in callback URL") 206 | return 207 | } 208 | 209 | if (win) { 210 | // Send the code to the renderer for PKCE exchange 211 | win.webContents.send("auth-callback", { code }) 212 | } 213 | } catch (error) { 214 | console.error("Error handling auth callback:", error) 215 | } 216 | } 217 | 218 | // Window management functions 219 | async function createWindow(): Promise { 220 | if (state.mainWindow) { 221 | if (state.mainWindow.isMinimized()) state.mainWindow.restore() 222 | state.mainWindow.focus() 223 | return 224 | } 225 | 226 | const primaryDisplay = screen.getPrimaryDisplay() 227 | const workArea = primaryDisplay.workAreaSize 228 | state.screenWidth = workArea.width 229 | state.screenHeight = workArea.height 230 | state.step = 60 231 | state.currentY = 50 232 | 233 | const windowSettings: Electron.BrowserWindowConstructorOptions = { 234 | height: 600, 235 | 236 | x: state.currentX, 237 | y: 50, 238 | alwaysOnTop: true, 239 | webPreferences: { 240 | nodeIntegration: false, 241 | contextIsolation: true, 242 | preload: isDev 243 | ? path.join(__dirname, "../dist-electron/preload.js") 244 | : path.join(__dirname, "preload.js"), 245 | scrollBounce: true 246 | }, 247 | show: true, 248 | frame: false, 249 | transparent: true, 250 | fullscreenable: false, 251 | hasShadow: false, 252 | backgroundColor: "#00000000", 253 | focusable: true, 254 | skipTaskbar: true, 255 | type: "panel", 256 | paintWhenInitiallyHidden: true, 257 | titleBarStyle: "hidden", 258 | enableLargerThanScreen: true, 259 | movable: true 260 | } 261 | 262 | state.mainWindow = new BrowserWindow(windowSettings) 263 | 264 | // Add more detailed logging for window events 265 | state.mainWindow.webContents.on("did-finish-load", () => { 266 | console.log("Window finished loading") 267 | }) 268 | state.mainWindow.webContents.on( 269 | "did-fail-load", 270 | async (event, errorCode, errorDescription) => { 271 | console.error("Window failed to load:", errorCode, errorDescription) 272 | if (isDev) { 273 | // In development, retry loading after a short delay 274 | console.log("Retrying to load development server...") 275 | setTimeout(() => { 276 | state.mainWindow?.loadURL("http://localhost:54321").catch((error) => { 277 | console.error("Failed to load dev server on retry:", error) 278 | }) 279 | }, 1000) 280 | } 281 | } 282 | ) 283 | 284 | if (isDev) { 285 | // In development, load from the dev server 286 | state.mainWindow.loadURL("http://localhost:54321").catch((error) => { 287 | console.error("Failed to load dev server:", error) 288 | }) 289 | } else { 290 | // In production, load from the built files 291 | console.log( 292 | "Loading production build:", 293 | path.join(__dirname, "../dist/index.html") 294 | ) 295 | state.mainWindow.loadFile(path.join(__dirname, "../dist/index.html")) 296 | } 297 | 298 | // Configure window behavior 299 | state.mainWindow.webContents.setZoomFactor(1) 300 | if (isDev) { 301 | state.mainWindow.webContents.openDevTools() 302 | } 303 | state.mainWindow.webContents.setWindowOpenHandler(({ url }) => { 304 | console.log("Attempting to open URL:", url) 305 | if (url.includes("google.com") || url.includes("supabase.co")) { 306 | shell.openExternal(url) 307 | return { action: "deny" } 308 | } 309 | return { action: "allow" } 310 | }) 311 | 312 | // Enhanced screen capture resistance 313 | state.mainWindow.setContentProtection(true) 314 | 315 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 316 | visibleOnFullScreen: true 317 | }) 318 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 319 | 320 | // Additional screen capture resistance settings 321 | if (process.platform === "darwin") { 322 | // Prevent window from being captured in screenshots 323 | state.mainWindow.setHiddenInMissionControl(true) 324 | state.mainWindow.setWindowButtonVisibility(false) 325 | state.mainWindow.setBackgroundColor("#00000000") 326 | 327 | // Prevent window from being included in window switcher 328 | state.mainWindow.setSkipTaskbar(true) 329 | 330 | // Disable window shadow 331 | state.mainWindow.setHasShadow(false) 332 | } 333 | 334 | // Prevent the window from being captured by screen recording 335 | state.mainWindow.webContents.setBackgroundThrottling(false) 336 | state.mainWindow.webContents.setFrameRate(60) 337 | 338 | // Set up window listeners 339 | state.mainWindow.on("move", handleWindowMove) 340 | state.mainWindow.on("resize", handleWindowResize) 341 | state.mainWindow.on("closed", handleWindowClosed) 342 | 343 | // Initialize window state 344 | const bounds = state.mainWindow.getBounds() 345 | state.windowPosition = { x: bounds.x, y: bounds.y } 346 | state.windowSize = { width: bounds.width, height: bounds.height } 347 | state.currentX = bounds.x 348 | state.currentY = bounds.y 349 | state.isWindowVisible = true 350 | } 351 | 352 | function handleWindowMove(): void { 353 | if (!state.mainWindow) return 354 | const bounds = state.mainWindow.getBounds() 355 | state.windowPosition = { x: bounds.x, y: bounds.y } 356 | state.currentX = bounds.x 357 | state.currentY = bounds.y 358 | } 359 | 360 | function handleWindowResize(): void { 361 | if (!state.mainWindow) return 362 | const bounds = state.mainWindow.getBounds() 363 | state.windowSize = { width: bounds.width, height: bounds.height } 364 | } 365 | 366 | function handleWindowClosed(): void { 367 | state.mainWindow = null 368 | state.isWindowVisible = false 369 | state.windowPosition = null 370 | state.windowSize = null 371 | } 372 | 373 | // Window visibility functions 374 | function hideMainWindow(): void { 375 | if (!state.mainWindow?.isDestroyed()) { 376 | const bounds = state.mainWindow.getBounds() 377 | state.windowPosition = { x: bounds.x, y: bounds.y } 378 | state.windowSize = { width: bounds.width, height: bounds.height } 379 | state.mainWindow.setIgnoreMouseEvents(true, { forward: true }) 380 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 381 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 382 | visibleOnFullScreen: true 383 | }) 384 | state.mainWindow.setOpacity(0) 385 | state.mainWindow.hide() 386 | state.isWindowVisible = false 387 | } 388 | } 389 | 390 | function showMainWindow(): void { 391 | if (!state.mainWindow?.isDestroyed()) { 392 | if (state.windowPosition && state.windowSize) { 393 | state.mainWindow.setBounds({ 394 | ...state.windowPosition, 395 | ...state.windowSize 396 | }) 397 | } 398 | state.mainWindow.setIgnoreMouseEvents(false) 399 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 400 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 401 | visibleOnFullScreen: true 402 | }) 403 | state.mainWindow.setContentProtection(true) 404 | state.mainWindow.setOpacity(0) 405 | state.mainWindow.showInactive() 406 | state.mainWindow.setOpacity(1) 407 | state.isWindowVisible = true 408 | } 409 | } 410 | 411 | function toggleMainWindow(): void { 412 | state.isWindowVisible ? hideMainWindow() : showMainWindow() 413 | } 414 | 415 | // Window movement functions 416 | function moveWindowHorizontal(updateFn: (x: number) => number): void { 417 | if (!state.mainWindow) return 418 | state.currentX = updateFn(state.currentX) 419 | state.mainWindow.setPosition( 420 | Math.round(state.currentX), 421 | Math.round(state.currentY) 422 | ) 423 | } 424 | 425 | function moveWindowVertical(updateFn: (y: number) => number): void { 426 | if (!state.mainWindow) return 427 | 428 | const newY = updateFn(state.currentY) 429 | // Allow window to go 2/3 off screen in either direction 430 | const maxUpLimit = (-(state.windowSize?.height || 0) * 2) / 3 431 | const maxDownLimit = 432 | state.screenHeight + ((state.windowSize?.height || 0) * 2) / 3 433 | 434 | // Log the current state and limits 435 | console.log({ 436 | newY, 437 | maxUpLimit, 438 | maxDownLimit, 439 | screenHeight: state.screenHeight, 440 | windowHeight: state.windowSize?.height, 441 | currentY: state.currentY 442 | }) 443 | 444 | // Only update if within bounds 445 | if (newY >= maxUpLimit && newY <= maxDownLimit) { 446 | state.currentY = newY 447 | state.mainWindow.setPosition( 448 | Math.round(state.currentX), 449 | Math.round(state.currentY) 450 | ) 451 | } 452 | } 453 | 454 | // Window dimension functions 455 | function setWindowDimensions(width: number, height: number): void { 456 | if (!state.mainWindow?.isDestroyed()) { 457 | const [currentX, currentY] = state.mainWindow.getPosition() 458 | const primaryDisplay = screen.getPrimaryDisplay() 459 | const workArea = primaryDisplay.workAreaSize 460 | const maxWidth = Math.floor(workArea.width * 0.5) 461 | 462 | state.mainWindow.setBounds({ 463 | x: Math.min(currentX, workArea.width - maxWidth), 464 | y: currentY, 465 | width: Math.min(width + 32, maxWidth), 466 | height: Math.ceil(height) 467 | }) 468 | } 469 | } 470 | 471 | // Environment setup 472 | function loadEnvVariables() { 473 | if (isDev) { 474 | console.log("Loading env variables from:", path.join(process.cwd(), ".env")) 475 | dotenv.config({ path: path.join(process.cwd(), ".env") }) 476 | } else { 477 | console.log( 478 | "Loading env variables from:", 479 | path.join(process.resourcesPath, ".env") 480 | ) 481 | dotenv.config({ path: path.join(process.resourcesPath, ".env") }) 482 | } 483 | console.log("Loaded environment variables:", { 484 | VITE_SUPABASE_URL: process.env.VITE_SUPABASE_URL ? "exists" : "missing", 485 | VITE_SUPABASE_ANON_KEY: process.env.VITE_SUPABASE_ANON_KEY 486 | ? "exists" 487 | : "missing" 488 | }) 489 | } 490 | 491 | // Initialize application 492 | async function initializeApp() { 493 | try { 494 | loadEnvVariables() 495 | initializeHelpers() 496 | initializeIpcHandlers({ 497 | getMainWindow, 498 | setWindowDimensions, 499 | getScreenshotQueue, 500 | getExtraScreenshotQueue, 501 | deleteScreenshot, 502 | getImagePreview, 503 | processingHelper: state.processingHelper, 504 | PROCESSING_EVENTS: state.PROCESSING_EVENTS, 505 | takeScreenshot, 506 | getView, 507 | toggleMainWindow, 508 | clearQueues, 509 | setView, 510 | moveWindowLeft: () => 511 | moveWindowHorizontal((x) => 512 | Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) 513 | ), 514 | moveWindowRight: () => 515 | moveWindowHorizontal((x) => 516 | Math.min( 517 | state.screenWidth - (state.windowSize?.width || 0) / 2, 518 | x + state.step 519 | ) 520 | ), 521 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 522 | moveWindowDown: () => moveWindowVertical((y) => y + state.step) 523 | }) 524 | await createWindow() 525 | state.shortcutsHelper?.registerGlobalShortcuts() 526 | 527 | // Initialize auto-updater regardless of environment 528 | initAutoUpdater() 529 | console.log( 530 | "Auto-updater initialized in", 531 | isDev ? "development" : "production", 532 | "mode" 533 | ) 534 | } catch (error) { 535 | console.error("Failed to initialize application:", error) 536 | app.quit() 537 | } 538 | } 539 | 540 | // Handle the auth callback in development 541 | app.on("open-url", (event, url) => { 542 | console.log("open-url event received:", url) 543 | event.preventDefault() 544 | if (url.startsWith("interview-coder://")) { 545 | handleAuthCallback(url, state.mainWindow) 546 | } 547 | }) 548 | 549 | // Handle the auth callback in production (Windows/Linux) 550 | app.on("second-instance", (event, commandLine) => { 551 | console.log("second-instance event received:", commandLine) 552 | const url = commandLine.find((arg) => arg.startsWith("interview-coder://")) 553 | if (url) { 554 | handleAuthCallback(url, state.mainWindow) 555 | } 556 | 557 | // Focus or create the main window 558 | if (!state.mainWindow) { 559 | createWindow() 560 | } else { 561 | if (state.mainWindow.isMinimized()) state.mainWindow.restore() 562 | state.mainWindow.focus() 563 | } 564 | }) 565 | 566 | // Prevent multiple instances of the app 567 | if (!app.requestSingleInstanceLock()) { 568 | app.quit() 569 | } else { 570 | app.on("window-all-closed", () => { 571 | if (process.platform !== "darwin") { 572 | app.quit() 573 | state.mainWindow = null 574 | } 575 | }) 576 | } 577 | 578 | app.on("activate", () => { 579 | if (BrowserWindow.getAllWindows().length === 0) { 580 | createWindow() 581 | } 582 | }) 583 | 584 | // State getter/setter functions 585 | function getMainWindow(): BrowserWindow | null { 586 | return state.mainWindow 587 | } 588 | 589 | function getView(): "queue" | "solutions" | "debug" { 590 | return state.view 591 | } 592 | 593 | function setView(view: "queue" | "solutions" | "debug"): void { 594 | state.view = view 595 | state.screenshotHelper?.setView(view) 596 | } 597 | 598 | function getScreenshotHelper(): ScreenshotHelper | null { 599 | return state.screenshotHelper 600 | } 601 | 602 | function getProblemInfo(): any { 603 | return state.problemInfo 604 | } 605 | 606 | function setProblemInfo(problemInfo: any): void { 607 | state.problemInfo = problemInfo 608 | } 609 | 610 | function getScreenshotQueue(): string[] { 611 | return state.screenshotHelper?.getScreenshotQueue() || [] 612 | } 613 | 614 | function getExtraScreenshotQueue(): string[] { 615 | return state.screenshotHelper?.getExtraScreenshotQueue() || [] 616 | } 617 | 618 | function clearQueues(): void { 619 | state.screenshotHelper?.clearQueues() 620 | state.problemInfo = null 621 | setView("queue") 622 | } 623 | 624 | async function takeScreenshot(): Promise { 625 | if (!state.mainWindow) throw new Error("No main window available") 626 | return ( 627 | state.screenshotHelper?.takeScreenshot( 628 | () => hideMainWindow(), 629 | () => showMainWindow() 630 | ) || "" 631 | ) 632 | } 633 | 634 | async function getImagePreview(filepath: string): Promise { 635 | return state.screenshotHelper?.getImagePreview(filepath) || "" 636 | } 637 | 638 | async function deleteScreenshot( 639 | path: string 640 | ): Promise<{ success: boolean; error?: string }> { 641 | return ( 642 | state.screenshotHelper?.deleteScreenshot(path) || { 643 | success: false, 644 | error: "Screenshot helper not initialized" 645 | } 646 | ) 647 | } 648 | 649 | function setHasDebugged(value: boolean): void { 650 | state.hasDebugged = value 651 | } 652 | 653 | function getHasDebugged(): boolean { 654 | return state.hasDebugged 655 | } 656 | 657 | // Export state and functions for other modules 658 | export { 659 | state, 660 | createWindow, 661 | hideMainWindow, 662 | showMainWindow, 663 | toggleMainWindow, 664 | setWindowDimensions, 665 | moveWindowHorizontal, 666 | moveWindowVertical, 667 | handleAuthCallback, 668 | getMainWindow, 669 | getView, 670 | setView, 671 | getScreenshotHelper, 672 | getProblemInfo, 673 | setProblemInfo, 674 | getScreenshotQueue, 675 | getExtraScreenshotQueue, 676 | clearQueues, 677 | takeScreenshot, 678 | getImagePreview, 679 | deleteScreenshot, 680 | setHasDebugged, 681 | getHasDebugged 682 | } 683 | 684 | app.whenReady().then(initializeApp) 685 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | console.log("Preload script starting...") 2 | import { contextBridge, ipcRenderer } from "electron" 3 | const { shell } = require("electron") 4 | 5 | // Types for the exposed Electron API 6 | interface ElectronAPI { 7 | openSubscriptionPortal: (authData: { 8 | id: string 9 | email: string 10 | }) => Promise<{ success: boolean; error?: string }> 11 | updateContentDimensions: (dimensions: { 12 | width: number 13 | height: number 14 | }) => Promise 15 | clearStore: () => Promise<{ success: boolean; error?: string }> 16 | getScreenshots: () => Promise<{ 17 | success: boolean 18 | previews?: Array<{ path: string; preview: string }> | null 19 | error?: string 20 | }> 21 | deleteScreenshot: ( 22 | path: string 23 | ) => Promise<{ success: boolean; error?: string }> 24 | onScreenshotTaken: ( 25 | callback: (data: { path: string; preview: string }) => void 26 | ) => () => void 27 | onResetView: (callback: () => void) => () => void 28 | onSolutionStart: (callback: () => void) => () => void 29 | onDebugStart: (callback: () => void) => () => void 30 | onDebugSuccess: (callback: (data: any) => void) => () => void 31 | onSolutionError: (callback: (error: string) => void) => () => void 32 | onProcessingNoScreenshots: (callback: () => void) => () => void 33 | onProblemExtracted: (callback: (data: any) => void) => () => void 34 | onSolutionSuccess: (callback: (data: any) => void) => () => void 35 | onUnauthorized: (callback: () => void) => () => void 36 | onDebugError: (callback: (error: string) => void) => () => void 37 | openExternal: (url: string) => void 38 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 39 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 40 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 41 | triggerReset: () => Promise<{ success: boolean; error?: string }> 42 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 43 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 44 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 45 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 46 | onSubscriptionUpdated: (callback: () => void) => () => void 47 | onSubscriptionPortalClosed: (callback: () => void) => () => void 48 | startUpdate: () => Promise<{ success: boolean; error?: string }> 49 | installUpdate: () => void 50 | onUpdateAvailable: (callback: (info: any) => void) => () => void 51 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 52 | decrementCredits: () => Promise 53 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 54 | onOutOfCredits: (callback: () => void) => () => void 55 | getPlatform: () => string 56 | } 57 | 58 | export const PROCESSING_EVENTS = { 59 | //global states 60 | UNAUTHORIZED: "procesing-unauthorized", 61 | NO_SCREENSHOTS: "processing-no-screenshots", 62 | OUT_OF_CREDITS: "out-of-credits", 63 | 64 | //states for generating the initial solution 65 | INITIAL_START: "initial-start", 66 | PROBLEM_EXTRACTED: "problem-extracted", 67 | SOLUTION_SUCCESS: "solution-success", 68 | INITIAL_SOLUTION_ERROR: "solution-error", 69 | RESET: "reset", 70 | 71 | //states for processing the debugging 72 | DEBUG_START: "debug-start", 73 | DEBUG_SUCCESS: "debug-success", 74 | DEBUG_ERROR: "debug-error" 75 | } as const 76 | 77 | // At the top of the file 78 | console.log("Preload script is running") 79 | 80 | const electronAPI = { 81 | openSubscriptionPortal: async (authData: { id: string; email: string }) => { 82 | return ipcRenderer.invoke("open-subscription-portal", authData) 83 | }, 84 | openSettingsPortal: () => ipcRenderer.invoke("open-settings-portal"), 85 | updateContentDimensions: (dimensions: { width: number; height: number }) => 86 | ipcRenderer.invoke("update-content-dimensions", dimensions), 87 | clearStore: () => ipcRenderer.invoke("clear-store"), 88 | getScreenshots: () => ipcRenderer.invoke("get-screenshots"), 89 | deleteScreenshot: (path: string) => 90 | ipcRenderer.invoke("delete-screenshot", path), 91 | toggleMainWindow: async () => { 92 | console.log("toggleMainWindow called from preload") 93 | try { 94 | const result = await ipcRenderer.invoke("toggle-window") 95 | console.log("toggle-window result:", result) 96 | return result 97 | } catch (error) { 98 | console.error("Error in toggleMainWindow:", error) 99 | throw error 100 | } 101 | }, 102 | // Event listeners 103 | onScreenshotTaken: ( 104 | callback: (data: { path: string; preview: string }) => void 105 | ) => { 106 | const subscription = (_: any, data: { path: string; preview: string }) => 107 | callback(data) 108 | ipcRenderer.on("screenshot-taken", subscription) 109 | return () => { 110 | ipcRenderer.removeListener("screenshot-taken", subscription) 111 | } 112 | }, 113 | onResetView: (callback: () => void) => { 114 | const subscription = () => callback() 115 | ipcRenderer.on("reset-view", subscription) 116 | return () => { 117 | ipcRenderer.removeListener("reset-view", subscription) 118 | } 119 | }, 120 | onSolutionStart: (callback: () => void) => { 121 | const subscription = () => callback() 122 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 123 | return () => { 124 | ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 125 | } 126 | }, 127 | onDebugStart: (callback: () => void) => { 128 | const subscription = () => callback() 129 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 130 | return () => { 131 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 132 | } 133 | }, 134 | onDebugSuccess: (callback: (data: any) => void) => { 135 | ipcRenderer.on("debug-success", (_event, data) => callback(data)) 136 | return () => { 137 | ipcRenderer.removeListener("debug-success", (_event, data) => 138 | callback(data) 139 | ) 140 | } 141 | }, 142 | onDebugError: (callback: (error: string) => void) => { 143 | const subscription = (_: any, error: string) => callback(error) 144 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 145 | return () => { 146 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 147 | } 148 | }, 149 | onSolutionError: (callback: (error: string) => void) => { 150 | const subscription = (_: any, error: string) => callback(error) 151 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 152 | return () => { 153 | ipcRenderer.removeListener( 154 | PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 155 | subscription 156 | ) 157 | } 158 | }, 159 | onProcessingNoScreenshots: (callback: () => void) => { 160 | const subscription = () => callback() 161 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 162 | return () => { 163 | ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 164 | } 165 | }, 166 | onOutOfCredits: (callback: () => void) => { 167 | const subscription = () => callback() 168 | ipcRenderer.on(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 169 | return () => { 170 | ipcRenderer.removeListener(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 171 | } 172 | }, 173 | onProblemExtracted: (callback: (data: any) => void) => { 174 | const subscription = (_: any, data: any) => callback(data) 175 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 176 | return () => { 177 | ipcRenderer.removeListener( 178 | PROCESSING_EVENTS.PROBLEM_EXTRACTED, 179 | subscription 180 | ) 181 | } 182 | }, 183 | onSolutionSuccess: (callback: (data: any) => void) => { 184 | const subscription = (_: any, data: any) => callback(data) 185 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 186 | return () => { 187 | ipcRenderer.removeListener( 188 | PROCESSING_EVENTS.SOLUTION_SUCCESS, 189 | subscription 190 | ) 191 | } 192 | }, 193 | onUnauthorized: (callback: () => void) => { 194 | const subscription = () => callback() 195 | ipcRenderer.on(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 196 | return () => { 197 | ipcRenderer.removeListener(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 198 | } 199 | }, 200 | openExternal: (url: string) => shell.openExternal(url), 201 | triggerScreenshot: () => ipcRenderer.invoke("trigger-screenshot"), 202 | triggerProcessScreenshots: () => 203 | ipcRenderer.invoke("trigger-process-screenshots"), 204 | triggerReset: () => ipcRenderer.invoke("trigger-reset"), 205 | triggerMoveLeft: () => ipcRenderer.invoke("trigger-move-left"), 206 | triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), 207 | triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), 208 | triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), 209 | onSubscriptionUpdated: (callback: () => void) => { 210 | const subscription = () => callback() 211 | ipcRenderer.on("subscription-updated", subscription) 212 | return () => { 213 | ipcRenderer.removeListener("subscription-updated", subscription) 214 | } 215 | }, 216 | onSubscriptionPortalClosed: (callback: () => void) => { 217 | const subscription = () => callback() 218 | ipcRenderer.on("subscription-portal-closed", subscription) 219 | return () => { 220 | ipcRenderer.removeListener("subscription-portal-closed", subscription) 221 | } 222 | }, 223 | onReset: (callback: () => void) => { 224 | const subscription = () => callback() 225 | ipcRenderer.on(PROCESSING_EVENTS.RESET, subscription) 226 | return () => { 227 | ipcRenderer.removeListener(PROCESSING_EVENTS.RESET, subscription) 228 | } 229 | }, 230 | startUpdate: () => ipcRenderer.invoke("start-update"), 231 | installUpdate: () => ipcRenderer.invoke("install-update"), 232 | onUpdateAvailable: (callback: (info: any) => void) => { 233 | const subscription = (_: any, info: any) => callback(info) 234 | ipcRenderer.on("update-available", subscription) 235 | return () => { 236 | ipcRenderer.removeListener("update-available", subscription) 237 | } 238 | }, 239 | onUpdateDownloaded: (callback: (info: any) => void) => { 240 | const subscription = (_: any, info: any) => callback(info) 241 | ipcRenderer.on("update-downloaded", subscription) 242 | return () => { 243 | ipcRenderer.removeListener("update-downloaded", subscription) 244 | } 245 | }, 246 | decrementCredits: () => ipcRenderer.invoke("decrement-credits"), 247 | onCreditsUpdated: (callback: (credits: number) => void) => { 248 | const subscription = (_event: any, credits: number) => callback(credits) 249 | ipcRenderer.on("credits-updated", subscription) 250 | return () => { 251 | ipcRenderer.removeListener("credits-updated", subscription) 252 | } 253 | }, 254 | getPlatform: () => process.platform 255 | } as ElectronAPI 256 | 257 | // Before exposing the API 258 | console.log( 259 | "About to expose electronAPI with methods:", 260 | Object.keys(electronAPI) 261 | ) 262 | 263 | // Expose the API 264 | contextBridge.exposeInMainWorld("electronAPI", electronAPI) 265 | 266 | console.log("electronAPI exposed to window") 267 | 268 | // Add this focus restoration handler 269 | ipcRenderer.on("restore-focus", () => { 270 | // Try to focus the active element if it exists 271 | const activeElement = document.activeElement as HTMLElement 272 | if (activeElement && typeof activeElement.focus === "function") { 273 | activeElement.focus() 274 | } 275 | }) 276 | 277 | // Expose protected methods that allow the renderer process to use 278 | // the ipcRenderer without exposing the entire object 279 | contextBridge.exposeInMainWorld("electron", { 280 | ipcRenderer: { 281 | on: (channel: string, func: (...args: any[]) => void) => { 282 | if (channel === "auth-callback") { 283 | ipcRenderer.on(channel, (event, ...args) => func(...args)) 284 | } 285 | }, 286 | removeListener: (channel: string, func: (...args: any[]) => void) => { 287 | if (channel === "auth-callback") { 288 | ipcRenderer.removeListener(channel, (event, ...args) => func(...args)) 289 | } 290 | } 291 | } 292 | }) 293 | -------------------------------------------------------------------------------- /electron/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut, app } from "electron" 2 | import { IShortcutsHelperDeps } from "./main" 3 | 4 | export class ShortcutsHelper { 5 | private deps: IShortcutsHelperDeps 6 | 7 | constructor(deps: IShortcutsHelperDeps) { 8 | this.deps = deps 9 | } 10 | 11 | public registerGlobalShortcuts(): void { 12 | globalShortcut.register("CommandOrControl+H", async () => { 13 | const mainWindow = this.deps.getMainWindow() 14 | if (mainWindow) { 15 | console.log("Taking screenshot...") 16 | try { 17 | const screenshotPath = await this.deps.takeScreenshot() 18 | const preview = await this.deps.getImagePreview(screenshotPath) 19 | mainWindow.webContents.send("screenshot-taken", { 20 | path: screenshotPath, 21 | preview 22 | }) 23 | } catch (error) { 24 | console.error("Error capturing screenshot:", error) 25 | } 26 | } 27 | }) 28 | 29 | globalShortcut.register("CommandOrControl+Enter", async () => { 30 | await this.deps.processingHelper?.processScreenshots() 31 | }) 32 | 33 | globalShortcut.register("CommandOrControl+R", () => { 34 | console.log( 35 | "Command + R pressed. Canceling requests and resetting queues..." 36 | ) 37 | 38 | // Cancel ongoing API requests 39 | this.deps.processingHelper?.cancelOngoingRequests() 40 | 41 | // Clear both screenshot queues 42 | this.deps.clearQueues() 43 | 44 | console.log("Cleared queues.") 45 | 46 | // Update the view state to 'queue' 47 | this.deps.setView("queue") 48 | 49 | // Notify renderer process to switch view to 'queue' 50 | const mainWindow = this.deps.getMainWindow() 51 | if (mainWindow && !mainWindow.isDestroyed()) { 52 | mainWindow.webContents.send("reset-view") 53 | mainWindow.webContents.send("reset") 54 | } 55 | }) 56 | 57 | // New shortcuts for moving the window 58 | globalShortcut.register("CommandOrControl+Left", () => { 59 | console.log("Command/Ctrl + Left pressed. Moving window left.") 60 | this.deps.moveWindowLeft() 61 | }) 62 | 63 | globalShortcut.register("CommandOrControl+Right", () => { 64 | console.log("Command/Ctrl + Right pressed. Moving window right.") 65 | this.deps.moveWindowRight() 66 | }) 67 | 68 | globalShortcut.register("CommandOrControl+Down", () => { 69 | console.log("Command/Ctrl + down pressed. Moving window down.") 70 | this.deps.moveWindowDown() 71 | }) 72 | 73 | globalShortcut.register("CommandOrControl+Up", () => { 74 | console.log("Command/Ctrl + Up pressed. Moving window Up.") 75 | this.deps.moveWindowUp() 76 | }) 77 | 78 | globalShortcut.register("CommandOrControl+B", () => { 79 | this.deps.toggleMainWindow() 80 | }) 81 | 82 | // Unregister shortcuts when quitting 83 | app.on("will-quit", () => { 84 | globalShortcut.unregisterAll() 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /electron/store.ts: -------------------------------------------------------------------------------- 1 | import Store from "electron-store" 2 | 3 | interface StoreSchema { 4 | // Empty for now, we can add other store items here later 5 | } 6 | 7 | const store = new Store({ 8 | defaults: {}, 9 | encryptionKey: "your-encryption-key" 10 | }) as Store & { 11 | store: StoreSchema 12 | get: (key: K) => StoreSchema[K] 13 | set: (key: K, value: StoreSchema[K]) => void 14 | } 15 | 16 | export { store } 17 | -------------------------------------------------------------------------------- /electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "jsx": "react-jsx", 11 | "baseUrl": ".", 12 | "outDir": "../dist-electron", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | readonly VITE_SUPABASE_ANON_KEY: string 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Interview Coder 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interview-coder-v1", 3 | "version": "1.0.18", 4 | "main": "dist-electron/main.js", 5 | "scripts": { 6 | "clean": "rimraf dist dist-electron", 7 | "dev": "cross-env NODE_ENV=development npm run clean && concurrently \"tsc -w -p tsconfig.electron.json\" \"vite\" \"wait-on http://localhost:54321 && electron .\"", 8 | "build": "cross-env NODE_ENV=production rimraf dist dist-electron && vite build && tsc -p tsconfig.electron.json && electron-builder" 9 | }, 10 | "build": { 11 | "appId": "com.chunginlee.interviewcoder", 12 | "productName": "Interview Coder", 13 | "files": [ 14 | "dist/**/*", 15 | "dist-electron/**/*", 16 | "package.json", 17 | "electron/**/*" 18 | ], 19 | "directories": { 20 | "output": "release", 21 | "buildResources": "assets" 22 | }, 23 | "asar": true, 24 | "compression": "maximum", 25 | "generateUpdatesFilesForAllChannels": true, 26 | "mac": { 27 | "category": "public.app-category.developer-tools", 28 | "target": [ 29 | { 30 | "target": "dmg", 31 | "arch": [ 32 | "x64", 33 | "arm64" 34 | ] 35 | }, 36 | { 37 | "target": "zip", 38 | "arch": [ 39 | "x64", 40 | "arm64" 41 | ] 42 | } 43 | ], 44 | "artifactName": "Interview-Coder-${arch}.${ext}", 45 | "icon": "assets/icons/mac/icon.icns", 46 | "hardenedRuntime": true, 47 | "gatekeeperAssess": false, 48 | "entitlements": "build/entitlements.mac.plist", 49 | "entitlementsInherit": "build/entitlements.mac.plist", 50 | "identity": "Developer ID Application", 51 | "notarize": true, 52 | "protocols": { 53 | "name": "interview-coder-protocol", 54 | "schemes": [ 55 | "interview-coder" 56 | ] 57 | } 58 | }, 59 | "win": { 60 | "target": [ 61 | "nsis" 62 | ], 63 | "icon": "assets/icons/win/icon.ico", 64 | "artifactName": "${productName}-Windows-${version}.${ext}", 65 | "protocols": { 66 | "name": "interview-coder-protocol", 67 | "schemes": [ 68 | "interview-coder" 69 | ] 70 | } 71 | }, 72 | "linux": { 73 | "target": [ 74 | "AppImage" 75 | ], 76 | "icon": "assets/icons/png/icon-256x256.png", 77 | "artifactName": "${productName}-Linux-${version}.${ext}", 78 | "protocols": { 79 | "name": "interview-coder-protocol", 80 | "schemes": [ 81 | "interview-coder" 82 | ] 83 | } 84 | }, 85 | "publish": [ 86 | { 87 | "provider": "github", 88 | "owner": "ibttf", 89 | "repo": "interview-coder", 90 | "private": false, 91 | "releaseType": "release" 92 | } 93 | ], 94 | "extraResources": [ 95 | { 96 | "from": ".env", 97 | "to": ".env", 98 | "filter": [ 99 | "**/*" 100 | ] 101 | } 102 | ], 103 | "extraMetadata": { 104 | "main": "dist-electron/main.js" 105 | } 106 | }, 107 | "keywords": [], 108 | "author": "", 109 | "license": "ISC", 110 | "description": "An invisible desktop application to help you pass your technical interviews.", 111 | "dependencies": { 112 | "@electron/notarize": "^2.3.0", 113 | "@emotion/react": "^11.11.0", 114 | "@emotion/styled": "^11.11.0", 115 | "@radix-ui/react-dialog": "^1.1.2", 116 | "@radix-ui/react-label": "^2.1.0", 117 | "@radix-ui/react-slot": "^1.1.0", 118 | "@radix-ui/react-toast": "^1.2.2", 119 | "@supabase/supabase-js": "^2.39.0", 120 | "@tanstack/react-query": "^5.64.0", 121 | "axios": "^1.7.7", 122 | "class-variance-authority": "^0.7.1", 123 | "clsx": "^2.1.1", 124 | "diff": "^7.0.0", 125 | "dotenv": "^16.4.7", 126 | "electron-log": "^5.2.4", 127 | "electron-store": "^10.0.0", 128 | "electron-updater": "^6.3.9", 129 | "form-data": "^4.0.1", 130 | "lucide-react": "^0.460.0", 131 | "react": "^18.2.0", 132 | "react-code-blocks": "^0.1.6", 133 | "react-dom": "^18.2.0", 134 | "react-router-dom": "^6.28.1", 135 | "react-syntax-highlighter": "^15.6.1", 136 | "screenshot-desktop": "^1.15.0", 137 | "tailwind-merge": "^2.5.5", 138 | "uuid": "^11.0.3" 139 | }, 140 | "devDependencies": { 141 | "@electron/typescript-definitions": "^8.14.0", 142 | "@types/color": "^4.2.0", 143 | "@types/diff": "^6.0.0", 144 | "@types/electron-store": "^1.3.1", 145 | "@types/node": "^20.11.30", 146 | "@types/react": "^18.2.67", 147 | "@types/react-dom": "^18.2.22", 148 | "@types/react-syntax-highlighter": "^15.5.13", 149 | "@types/screenshot-desktop": "^1.12.3", 150 | "@types/uuid": "^9.0.8", 151 | "@typescript-eslint/eslint-plugin": "^7.3.1", 152 | "@typescript-eslint/parser": "^7.3.1", 153 | "@vitejs/plugin-react": "^4.2.1", 154 | "autoprefixer": "^10.4.20", 155 | "concurrently": "^8.2.2", 156 | "cross-env": "^7.0.3", 157 | "electron": "^29.1.4", 158 | "electron-builder": "^24.13.3", 159 | "electron-is-dev": "^3.0.1", 160 | "eslint": "^8.57.0", 161 | "eslint-plugin-react-hooks": "^4.6.0", 162 | "eslint-plugin-react-refresh": "^0.4.6", 163 | "postcss": "^8.4.49", 164 | "rimraf": "^6.0.1", 165 | "tailwindcss": "^3.4.15", 166 | "typescript": "^5.4.2", 167 | "vite": "^5.1.6", 168 | "vite-plugin-electron": "^0.28.4", 169 | "vite-plugin-electron-renderer": "^0.14.6", 170 | "wait-on": "^7.2.0" 171 | }, 172 | "browserslist": { 173 | "production": [ 174 | ">0.2%", 175 | "not dead", 176 | "not op_mini all" 177 | ], 178 | "development": [ 179 | "last 1 chrome version", 180 | "last 1 firefox version", 181 | "last 1 safari version" 182 | ] 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /renderer/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /renderer/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renderer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.119", 11 | "@types/react": "^18.3.12", 12 | "@types/react-dom": "^18.3.1", 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "react-scripts": "5.0.1", 16 | "typescript": "^4.9.5", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /renderer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/renderer/public/favicon.ico -------------------------------------------------------------------------------- /renderer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /renderer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/renderer/public/logo192.png -------------------------------------------------------------------------------- /renderer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrkeyiano/interview-coder-clone/d42a594fa6796e9e5354e1e7116df42a1de82be4/renderer/public/logo512.png -------------------------------------------------------------------------------- /renderer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Interview Coder", 3 | "name": "Interview Coder", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /renderer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /renderer/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /renderer/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.tsx and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /renderer/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /renderer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /renderer/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renderer/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /renderer/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /renderer/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/_pages/Debug.tsx: -------------------------------------------------------------------------------- 1 | // Debug.tsx 2 | import { useQuery, useQueryClient } from "@tanstack/react-query" 3 | import React, { useEffect, useRef, useState } from "react" 4 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" 5 | import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" 6 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 7 | import SolutionCommands from "../components/Solutions/SolutionCommands" 8 | import { Screenshot } from "../types/screenshots" 9 | import { ComplexitySection, ContentSection } from "./Solutions" 10 | import { useToast } from "../contexts/toast" 11 | 12 | const CodeSection = ({ 13 | title, 14 | code, 15 | isLoading, 16 | currentLanguage 17 | }: { 18 | title: string 19 | code: React.ReactNode 20 | isLoading: boolean 21 | currentLanguage: string 22 | }) => ( 23 |
24 |

25 | {isLoading ? ( 26 |
27 |
28 |

29 | Loading solutions... 30 |

31 |
32 |
33 | ) : ( 34 |
35 | 49 | {code as string} 50 | 51 |
52 | )} 53 |
54 | ) 55 | 56 | async function fetchScreenshots(): Promise { 57 | try { 58 | const existing = await window.electronAPI.getScreenshots() 59 | console.log("Raw screenshot data in Debug:", existing) 60 | return (Array.isArray(existing) ? existing : []).map((p) => ({ 61 | id: p.path, 62 | path: p.path, 63 | preview: p.preview, 64 | timestamp: Date.now() 65 | })) 66 | } catch (error) { 67 | console.error("Error loading screenshots:", error) 68 | throw error 69 | } 70 | } 71 | 72 | interface DebugProps { 73 | isProcessing: boolean 74 | setIsProcessing: (isProcessing: boolean) => void 75 | currentLanguage: string 76 | setLanguage: (language: string) => void 77 | } 78 | 79 | const Debug: React.FC = ({ 80 | isProcessing, 81 | setIsProcessing, 82 | currentLanguage, 83 | setLanguage 84 | }) => { 85 | const [tooltipVisible, setTooltipVisible] = useState(false) 86 | const [tooltipHeight, setTooltipHeight] = useState(0) 87 | const { showToast } = useToast() 88 | 89 | const { data: screenshots = [], refetch } = useQuery({ 90 | queryKey: ["screenshots"], 91 | queryFn: fetchScreenshots, 92 | staleTime: Infinity, 93 | gcTime: Infinity, 94 | refetchOnWindowFocus: false 95 | }) 96 | 97 | const [newCode, setNewCode] = useState(null) 98 | const [thoughtsData, setThoughtsData] = useState(null) 99 | const [timeComplexityData, setTimeComplexityData] = useState( 100 | null 101 | ) 102 | const [spaceComplexityData, setSpaceComplexityData] = useState( 103 | null 104 | ) 105 | 106 | const queryClient = useQueryClient() 107 | const contentRef = useRef(null) 108 | 109 | useEffect(() => { 110 | // Try to get the new solution data from cache first 111 | const newSolution = queryClient.getQueryData(["new_solution"]) as { 112 | new_code: string 113 | thoughts: string[] 114 | time_complexity: string 115 | space_complexity: string 116 | } | null 117 | 118 | // If we have cached data, set all state variables to the cached data 119 | if (newSolution) { 120 | setNewCode(newSolution.new_code || null) 121 | setThoughtsData(newSolution.thoughts || null) 122 | setTimeComplexityData(newSolution.time_complexity || null) 123 | setSpaceComplexityData(newSolution.space_complexity || null) 124 | setIsProcessing(false) 125 | } 126 | 127 | // Set up event listeners 128 | const cleanupFunctions = [ 129 | window.electronAPI.onScreenshotTaken(() => refetch()), 130 | window.electronAPI.onResetView(() => refetch()), 131 | window.electronAPI.onDebugSuccess(() => { 132 | setIsProcessing(false) 133 | }), 134 | window.electronAPI.onDebugStart(() => { 135 | setIsProcessing(true) 136 | }), 137 | window.electronAPI.onDebugError((error: string) => { 138 | showToast( 139 | "Processing Failed", 140 | "There was an error debugging your code.", 141 | "error" 142 | ) 143 | setIsProcessing(false) 144 | console.error("Processing error:", error) 145 | }) 146 | ] 147 | 148 | // Set up resize observer 149 | const updateDimensions = () => { 150 | if (contentRef.current) { 151 | let contentHeight = contentRef.current.scrollHeight 152 | const contentWidth = contentRef.current.scrollWidth 153 | if (tooltipVisible) { 154 | contentHeight += tooltipHeight 155 | } 156 | window.electronAPI.updateContentDimensions({ 157 | width: contentWidth, 158 | height: contentHeight 159 | }) 160 | } 161 | } 162 | 163 | const resizeObserver = new ResizeObserver(updateDimensions) 164 | if (contentRef.current) { 165 | resizeObserver.observe(contentRef.current) 166 | } 167 | updateDimensions() 168 | 169 | return () => { 170 | resizeObserver.disconnect() 171 | cleanupFunctions.forEach((cleanup) => cleanup()) 172 | } 173 | }, [queryClient, setIsProcessing]) 174 | 175 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 176 | setTooltipVisible(visible) 177 | setTooltipHeight(height) 178 | } 179 | 180 | const handleDeleteExtraScreenshot = async (index: number) => { 181 | const screenshotToDelete = screenshots[index] 182 | 183 | try { 184 | const response = await window.electronAPI.deleteScreenshot( 185 | screenshotToDelete.path 186 | ) 187 | 188 | if (response.success) { 189 | refetch() 190 | } else { 191 | console.error("Failed to delete extra screenshot:", response.error) 192 | } 193 | } catch (error) { 194 | console.error("Error deleting extra screenshot:", error) 195 | } 196 | } 197 | 198 | return ( 199 |
200 | {/* Conditionally render the screenshot queue */} 201 |
202 |
203 |
204 | 209 |
210 |
211 |
212 | 213 | {/* Navbar of commands with the tooltip */} 214 | 223 | 224 | {/* Main Content */} 225 |
226 |
227 |
228 | {/* Thoughts Section */} 229 | 234 |
235 | {thoughtsData.map((thought, index) => ( 236 |
237 |
238 |
{thought}
239 |
240 | ))} 241 |
242 |
243 | ) 244 | } 245 | isLoading={!thoughtsData} 246 | /> 247 | 248 | {/* Code Section */} 249 | 255 | 256 | {/* Complexity Section */} 257 | 262 |
263 |
264 |
265 |
266 | ) 267 | } 268 | 269 | export default Debug 270 | -------------------------------------------------------------------------------- /src/_pages/Queue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { useQuery } from "@tanstack/react-query" 3 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 4 | import QueueCommands from "../components/Queue/QueueCommands" 5 | 6 | import { useToast } from "../contexts/toast" 7 | import { Screenshot } from "../types/screenshots" 8 | 9 | async function fetchScreenshots(): Promise { 10 | try { 11 | const existing = await window.electronAPI.getScreenshots() 12 | return existing 13 | } catch (error) { 14 | console.error("Error loading screenshots:", error) 15 | throw error 16 | } 17 | } 18 | 19 | interface QueueProps { 20 | setView: (view: "queue" | "solutions" | "debug") => void 21 | credits: number 22 | currentLanguage: string 23 | setLanguage: (language: string) => void 24 | } 25 | 26 | const Queue: React.FC = ({ 27 | setView, 28 | credits, 29 | currentLanguage, 30 | setLanguage 31 | }) => { 32 | const { showToast } = useToast() 33 | 34 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 35 | const [tooltipHeight, setTooltipHeight] = useState(0) 36 | const contentRef = useRef(null) 37 | 38 | const { 39 | data: screenshots = [], 40 | isLoading, 41 | refetch 42 | } = useQuery({ 43 | queryKey: ["screenshots"], 44 | queryFn: fetchScreenshots, 45 | staleTime: Infinity, 46 | gcTime: Infinity, 47 | refetchOnWindowFocus: false 48 | }) 49 | 50 | const handleDeleteScreenshot = async (index: number) => { 51 | const screenshotToDelete = screenshots[index] 52 | 53 | try { 54 | const response = await window.electronAPI.deleteScreenshot( 55 | screenshotToDelete.path 56 | ) 57 | 58 | if (response.success) { 59 | refetch() // Refetch screenshots instead of managing state directly 60 | } else { 61 | console.error("Failed to delete screenshot:", response.error) 62 | showToast("Error", "Failed to delete the screenshot file", "error") 63 | } 64 | } catch (error) { 65 | console.error("Error deleting screenshot:", error) 66 | } 67 | } 68 | 69 | useEffect(() => { 70 | // Height update logic 71 | const updateDimensions = () => { 72 | if (contentRef.current) { 73 | let contentHeight = contentRef.current.scrollHeight 74 | const contentWidth = contentRef.current.scrollWidth 75 | if (isTooltipVisible) { 76 | contentHeight += tooltipHeight 77 | } 78 | window.electronAPI.updateContentDimensions({ 79 | width: contentWidth, 80 | height: contentHeight 81 | }) 82 | } 83 | } 84 | 85 | // Initialize resize observer 86 | const resizeObserver = new ResizeObserver(updateDimensions) 87 | if (contentRef.current) { 88 | resizeObserver.observe(contentRef.current) 89 | } 90 | updateDimensions() 91 | 92 | // Set up event listeners 93 | const cleanupFunctions = [ 94 | window.electronAPI.onScreenshotTaken(() => refetch()), 95 | window.electronAPI.onResetView(() => refetch()), 96 | 97 | window.electronAPI.onSolutionError((error: string) => { 98 | showToast( 99 | "Processing Failed", 100 | "There was an error processing your screenshots.", 101 | "error" 102 | ) 103 | setView("queue") // Revert to queue if processing fails 104 | console.error("Processing error:", error) 105 | }), 106 | window.electronAPI.onProcessingNoScreenshots(() => { 107 | showToast( 108 | "No Screenshots", 109 | "There are no screenshots to process.", 110 | "neutral" 111 | ) 112 | }), 113 | window.electronAPI.onOutOfCredits(() => { 114 | showToast( 115 | "Out of Credits", 116 | "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", 117 | "error" 118 | ) 119 | }) 120 | ] 121 | 122 | return () => { 123 | resizeObserver.disconnect() 124 | cleanupFunctions.forEach((cleanup) => cleanup()) 125 | } 126 | }, [isTooltipVisible, tooltipHeight]) 127 | 128 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 129 | setIsTooltipVisible(visible) 130 | setTooltipHeight(height) 131 | } 132 | 133 | return ( 134 |
135 |
136 |
137 | 142 | 143 | 150 |
151 |
152 |
153 | ) 154 | } 155 | 156 | export default Queue 157 | -------------------------------------------------------------------------------- /src/_pages/Solutions.tsx: -------------------------------------------------------------------------------- 1 | // Solutions.tsx 2 | import React, { useState, useEffect, useRef } from "react" 3 | import { useQuery, useQueryClient } from "@tanstack/react-query" 4 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" 5 | import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" 6 | 7 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 8 | 9 | import { ProblemStatementData } from "../types/solutions" 10 | import SolutionCommands from "../components/Solutions/SolutionCommands" 11 | import Debug from "./Debug" 12 | import { useToast } from "../contexts/toast" 13 | import { COMMAND_KEY } from "../utils/platform" 14 | 15 | export const ContentSection = ({ 16 | title, 17 | content, 18 | isLoading 19 | }: { 20 | title: string 21 | content: React.ReactNode 22 | isLoading: boolean 23 | }) => ( 24 |
25 |

26 | {title} 27 |

28 | {isLoading ? ( 29 |
30 |

31 | Extracting problem statement... 32 |

33 |
34 | ) : ( 35 |
36 | {content} 37 |
38 | )} 39 |
40 | ) 41 | const SolutionSection = ({ 42 | title, 43 | content, 44 | isLoading, 45 | currentLanguage 46 | }: { 47 | title: string 48 | content: React.ReactNode 49 | isLoading: boolean 50 | currentLanguage: string 51 | }) => ( 52 |
53 |

54 | {title} 55 |

56 | {isLoading ? ( 57 |
58 |
59 |

60 | Loading solutions... 61 |

62 |
63 |
64 | ) : ( 65 |
66 | 80 | {content as string} 81 | 82 |
83 | )} 84 |
85 | ) 86 | 87 | export const ComplexitySection = ({ 88 | timeComplexity, 89 | spaceComplexity, 90 | isLoading 91 | }: { 92 | timeComplexity: string | null 93 | spaceComplexity: string | null 94 | isLoading: boolean 95 | }) => ( 96 |
97 |

98 | Complexity 99 |

100 | {isLoading ? ( 101 |

102 | Calculating complexity... 103 |

104 | ) : ( 105 |
106 |
107 |
108 |
109 | Time: {timeComplexity} 110 |
111 |
112 |
113 |
114 |
115 | Space: {spaceComplexity} 116 |
117 |
118 |
119 | )} 120 |
121 | ) 122 | 123 | export interface SolutionsProps { 124 | setView: (view: "queue" | "solutions" | "debug") => void 125 | credits: number 126 | currentLanguage: string 127 | setLanguage: (language: string) => void 128 | } 129 | const Solutions: React.FC = ({ 130 | setView, 131 | credits, 132 | currentLanguage, 133 | setLanguage 134 | }) => { 135 | const queryClient = useQueryClient() 136 | const contentRef = useRef(null) 137 | 138 | const [debugProcessing, setDebugProcessing] = useState(false) 139 | const [problemStatementData, setProblemStatementData] = 140 | useState(null) 141 | const [solutionData, setSolutionData] = useState(null) 142 | const [thoughtsData, setThoughtsData] = useState(null) 143 | const [timeComplexityData, setTimeComplexityData] = useState( 144 | null 145 | ) 146 | const [spaceComplexityData, setSpaceComplexityData] = useState( 147 | null 148 | ) 149 | 150 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 151 | const [tooltipHeight, setTooltipHeight] = useState(0) 152 | 153 | const [isResetting, setIsResetting] = useState(false) 154 | 155 | interface Screenshot { 156 | id: string 157 | path: string 158 | preview: string 159 | timestamp: number 160 | } 161 | 162 | const [extraScreenshots, setExtraScreenshots] = useState([]) 163 | 164 | useEffect(() => { 165 | const fetchScreenshots = async () => { 166 | try { 167 | const existing = await window.electronAPI.getScreenshots() 168 | console.log("Raw screenshot data:", existing) 169 | const screenshots = (Array.isArray(existing) ? existing : []).map( 170 | (p) => ({ 171 | id: p.path, 172 | path: p.path, 173 | preview: p.preview, 174 | timestamp: Date.now() 175 | }) 176 | ) 177 | console.log("Processed screenshots:", screenshots) 178 | setExtraScreenshots(screenshots) 179 | } catch (error) { 180 | console.error("Error loading extra screenshots:", error) 181 | setExtraScreenshots([]) 182 | } 183 | } 184 | 185 | fetchScreenshots() 186 | }, [solutionData]) 187 | 188 | const { showToast } = useToast() 189 | 190 | useEffect(() => { 191 | // Height update logic 192 | const updateDimensions = () => { 193 | if (contentRef.current) { 194 | let contentHeight = contentRef.current.scrollHeight 195 | const contentWidth = contentRef.current.scrollWidth 196 | if (isTooltipVisible) { 197 | contentHeight += tooltipHeight 198 | } 199 | window.electronAPI.updateContentDimensions({ 200 | width: contentWidth, 201 | height: contentHeight 202 | }) 203 | } 204 | } 205 | 206 | // Initialize resize observer 207 | const resizeObserver = new ResizeObserver(updateDimensions) 208 | if (contentRef.current) { 209 | resizeObserver.observe(contentRef.current) 210 | } 211 | updateDimensions() 212 | 213 | // Set up event listeners 214 | const cleanupFunctions = [ 215 | window.electronAPI.onScreenshotTaken(async () => { 216 | try { 217 | const existing = await window.electronAPI.getScreenshots() 218 | const screenshots = (Array.isArray(existing) ? existing : []).map( 219 | (p) => ({ 220 | id: p.path, 221 | path: p.path, 222 | preview: p.preview, 223 | timestamp: Date.now() 224 | }) 225 | ) 226 | setExtraScreenshots(screenshots) 227 | } catch (error) { 228 | console.error("Error loading extra screenshots:", error) 229 | } 230 | }), 231 | window.electronAPI.onResetView(() => { 232 | // Set resetting state first 233 | setIsResetting(true) 234 | 235 | // Remove queries 236 | queryClient.removeQueries({ 237 | queryKey: ["solution"] 238 | }) 239 | queryClient.removeQueries({ 240 | queryKey: ["new_solution"] 241 | }) 242 | 243 | // Reset screenshots 244 | setExtraScreenshots([]) 245 | 246 | // After a small delay, clear the resetting state 247 | setTimeout(() => { 248 | setIsResetting(false) 249 | }, 0) 250 | }), 251 | window.electronAPI.onSolutionStart(() => { 252 | // Every time processing starts, reset relevant states 253 | setSolutionData(null) 254 | setThoughtsData(null) 255 | setTimeComplexityData(null) 256 | setSpaceComplexityData(null) 257 | }), 258 | window.electronAPI.onProblemExtracted((data) => { 259 | queryClient.setQueryData(["problem_statement"], data) 260 | }), 261 | //if there was an error processing the initial solution 262 | window.electronAPI.onSolutionError((error: string) => { 263 | showToast("Processing Failed", error, "error") 264 | // Reset solutions in the cache (even though this shouldn't ever happen) and complexities to previous states 265 | const solution = queryClient.getQueryData(["solution"]) as { 266 | code: string 267 | thoughts: string[] 268 | time_complexity: string 269 | space_complexity: string 270 | } | null 271 | if (!solution) { 272 | setView("queue") 273 | } 274 | setSolutionData(solution?.code || null) 275 | setThoughtsData(solution?.thoughts || null) 276 | setTimeComplexityData(solution?.time_complexity || null) 277 | setSpaceComplexityData(solution?.space_complexity || null) 278 | console.error("Processing error:", error) 279 | }), 280 | //when the initial solution is generated, we'll set the solution data to that 281 | window.electronAPI.onSolutionSuccess((data) => { 282 | if (!data) { 283 | console.warn("Received empty or invalid solution data") 284 | return 285 | } 286 | console.log({ data }) 287 | const solutionData = { 288 | code: data.code, 289 | thoughts: data.thoughts, 290 | time_complexity: data.time_complexity, 291 | space_complexity: data.space_complexity 292 | } 293 | 294 | queryClient.setQueryData(["solution"], solutionData) 295 | setSolutionData(solutionData.code || null) 296 | setThoughtsData(solutionData.thoughts || null) 297 | setTimeComplexityData(solutionData.time_complexity || null) 298 | setSpaceComplexityData(solutionData.space_complexity || null) 299 | 300 | // Fetch latest screenshots when solution is successful 301 | const fetchScreenshots = async () => { 302 | try { 303 | const existing = await window.electronAPI.getScreenshots() 304 | const screenshots = 305 | existing.previews?.map((p) => ({ 306 | id: p.path, 307 | path: p.path, 308 | preview: p.preview, 309 | timestamp: Date.now() 310 | })) || [] 311 | setExtraScreenshots(screenshots) 312 | } catch (error) { 313 | console.error("Error loading extra screenshots:", error) 314 | setExtraScreenshots([]) 315 | } 316 | } 317 | fetchScreenshots() 318 | }), 319 | 320 | //######################################################## 321 | //DEBUG EVENTS 322 | //######################################################## 323 | window.electronAPI.onDebugStart(() => { 324 | //we'll set the debug processing state to true and use that to render a little loader 325 | setDebugProcessing(true) 326 | }), 327 | //the first time debugging works, we'll set the view to debug and populate the cache with the data 328 | window.electronAPI.onDebugSuccess((data) => { 329 | queryClient.setQueryData(["new_solution"], data) 330 | setDebugProcessing(false) 331 | }), 332 | //when there was an error in the initial debugging, we'll show a toast and stop the little generating pulsing thing. 333 | window.electronAPI.onDebugError(() => { 334 | showToast( 335 | "Processing Failed", 336 | "There was an error debugging your code.", 337 | "error" 338 | ) 339 | setDebugProcessing(false) 340 | }), 341 | window.electronAPI.onProcessingNoScreenshots(() => { 342 | showToast( 343 | "No Screenshots", 344 | "There are no extra screenshots to process.", 345 | "neutral" 346 | ) 347 | }), 348 | window.electronAPI.onOutOfCredits(() => { 349 | showToast( 350 | "Out of Credits", 351 | "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", 352 | "error" 353 | ) 354 | }) 355 | ] 356 | 357 | return () => { 358 | resizeObserver.disconnect() 359 | cleanupFunctions.forEach((cleanup) => cleanup()) 360 | } 361 | }, [isTooltipVisible, tooltipHeight]) 362 | 363 | useEffect(() => { 364 | setProblemStatementData( 365 | queryClient.getQueryData(["problem_statement"]) || null 366 | ) 367 | setSolutionData(queryClient.getQueryData(["solution"]) || null) 368 | 369 | const unsubscribe = queryClient.getQueryCache().subscribe((event) => { 370 | if (event?.query.queryKey[0] === "problem_statement") { 371 | setProblemStatementData( 372 | queryClient.getQueryData(["problem_statement"]) || null 373 | ) 374 | } 375 | if (event?.query.queryKey[0] === "solution") { 376 | const solution = queryClient.getQueryData(["solution"]) as { 377 | code: string 378 | thoughts: string[] 379 | time_complexity: string 380 | space_complexity: string 381 | } | null 382 | 383 | setSolutionData(solution?.code ?? null) 384 | setThoughtsData(solution?.thoughts ?? null) 385 | setTimeComplexityData(solution?.time_complexity ?? null) 386 | setSpaceComplexityData(solution?.space_complexity ?? null) 387 | } 388 | }) 389 | return () => unsubscribe() 390 | }, [queryClient]) 391 | 392 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 393 | setIsTooltipVisible(visible) 394 | setTooltipHeight(height) 395 | } 396 | 397 | const handleDeleteExtraScreenshot = async (index: number) => { 398 | const screenshotToDelete = extraScreenshots[index] 399 | 400 | try { 401 | const response = await window.electronAPI.deleteScreenshot( 402 | screenshotToDelete.path 403 | ) 404 | 405 | if (response.success) { 406 | // Fetch and update screenshots after successful deletion 407 | const existing = await window.electronAPI.getScreenshots() 408 | const screenshots = (Array.isArray(existing) ? existing : []).map( 409 | (p) => ({ 410 | id: p.path, 411 | path: p.path, 412 | preview: p.preview, 413 | timestamp: Date.now() 414 | }) 415 | ) 416 | setExtraScreenshots(screenshots) 417 | } else { 418 | console.error("Failed to delete extra screenshot:", response.error) 419 | showToast("Error", "Failed to delete the screenshot", "error") 420 | } 421 | } catch (error) { 422 | console.error("Error deleting extra screenshot:", error) 423 | showToast("Error", "Failed to delete the screenshot", "error") 424 | } 425 | } 426 | 427 | return ( 428 | <> 429 | {!isResetting && queryClient.getQueryData(["new_solution"]) ? ( 430 | 436 | ) : ( 437 |
438 | {/* Conditionally render the screenshot queue if solutionData is available */} 439 | {solutionData && ( 440 |
441 |
442 |
443 | 448 |
449 |
450 |
451 | )} 452 | 453 | {/* Navbar of commands with the SolutionsHelper */} 454 | 462 | 463 | {/* Main Content - Modified width constraints */} 464 |
465 |
466 |
467 | {!solutionData && ( 468 | <> 469 | 474 | {problemStatementData && ( 475 |
476 |

477 | Generating solutions... 478 |

479 |
480 | )} 481 | 482 | )} 483 | 484 | {solutionData && ( 485 | <> 486 | 491 |
492 | {thoughtsData.map((thought, index) => ( 493 |
497 |
498 |
{thought}
499 |
500 | ))} 501 |
502 |
503 | ) 504 | } 505 | isLoading={!thoughtsData} 506 | /> 507 | 508 | 514 | 515 | 520 | 521 | )} 522 |
523 |
524 |
525 |
526 | )} 527 | 528 | ) 529 | } 530 | 531 | export default Solutions 532 | -------------------------------------------------------------------------------- /src/_pages/SubscribePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react" 2 | import { supabase } from "../lib/supabase" 3 | import { User } from "@supabase/supabase-js" 4 | 5 | interface SubscribePageProps { 6 | user: User 7 | } 8 | 9 | export default function SubscribePage({ user }: SubscribePageProps) { 10 | const [error, setError] = useState(null) 11 | const containerRef = useRef(null) 12 | 13 | useEffect(() => { 14 | const updateDimensions = () => { 15 | if (containerRef.current) { 16 | window.electronAPI.updateContentDimensions({ 17 | width: 400, // Fixed width 18 | height: 400 // Fixed height 19 | }) 20 | } 21 | } 22 | 23 | updateDimensions() 24 | }, []) 25 | 26 | const handleSignOut = async () => { 27 | try { 28 | const { error: signOutError } = await supabase.auth.signOut() 29 | if (signOutError) throw signOutError 30 | } catch (err) { 31 | console.error("Error signing out:", err) 32 | setError("Failed to sign out. Please try again.") 33 | setTimeout(() => setError(null), 3000) 34 | } 35 | } 36 | 37 | const handleSubscribe = async () => { 38 | if (!user) return 39 | 40 | try { 41 | const result = await window.electronAPI.openSubscriptionPortal({ 42 | id: user.id, 43 | email: user.email! 44 | }) 45 | 46 | if (!result.success) { 47 | throw new Error(result.error || "Failed to open subscription portal") 48 | } 49 | } catch (err) { 50 | console.error("Error opening subscription portal:", err) 51 | setError("Failed to open subscription portal. Please try again.") 52 | setTimeout(() => setError(null), 3000) 53 | } 54 | } 55 | 56 | return ( 57 |
61 |
62 |
63 |

64 | Welcome to Interview Coder 65 |

66 |

67 | To continue using Interview Coder, you'll need to subscribe 68 | ($60/month) 69 |

70 |

71 | * Undetectability may not work with some versions of MacOS. See our 72 | help center for more details 73 |

74 | 75 | {/* Keyboard Shortcuts */} 76 |
77 |
78 |
79 | Toggle Visibility 80 |
81 | 82 | ⌘ 83 | 84 | 85 | B 86 | 87 |
88 |
89 |
90 | Quit App 91 |
92 | 93 | ⌘ 94 | 95 | 96 | Q 97 | 98 |
99 |
100 |
101 |
102 | 103 | {/* Subscribe Button */} 104 | 125 | 126 | {/* Logout Section */} 127 |
128 | 150 |
151 | 152 | {error && ( 153 |
154 |

{error}

155 |
156 | )} 157 |
158 |
159 |
160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /src/_pages/SubscribedApp.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/SubscribedApp.tsx 2 | import { useQueryClient } from "@tanstack/react-query" 3 | import { useEffect, useRef, useState } from "react" 4 | import Queue from "../_pages/Queue" 5 | import Solutions from "../_pages/Solutions" 6 | import { useToast } from "../contexts/toast" 7 | 8 | interface SubscribedAppProps { 9 | credits: number 10 | currentLanguage: string 11 | setLanguage: (language: string) => void 12 | } 13 | 14 | const SubscribedApp: React.FC = ({ 15 | credits, 16 | currentLanguage, 17 | setLanguage 18 | }) => { 19 | const queryClient = useQueryClient() 20 | const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") 21 | const containerRef = useRef(null) 22 | const { showToast } = useToast() 23 | 24 | // Let's ensure we reset queries etc. if some electron signals happen 25 | useEffect(() => { 26 | const cleanup = window.electronAPI.onResetView(() => { 27 | queryClient.invalidateQueries({ 28 | queryKey: ["screenshots"] 29 | }) 30 | queryClient.invalidateQueries({ 31 | queryKey: ["problem_statement"] 32 | }) 33 | queryClient.invalidateQueries({ 34 | queryKey: ["solution"] 35 | }) 36 | queryClient.invalidateQueries({ 37 | queryKey: ["new_solution"] 38 | }) 39 | setView("queue") 40 | }) 41 | 42 | return () => { 43 | cleanup() 44 | } 45 | }, []) 46 | 47 | // Dynamically update the window size 48 | useEffect(() => { 49 | if (!containerRef.current) return 50 | 51 | const updateDimensions = () => { 52 | if (!containerRef.current) return 53 | const height = containerRef.current.scrollHeight 54 | const width = containerRef.current.scrollWidth 55 | window.electronAPI?.updateContentDimensions({ width, height }) 56 | } 57 | 58 | const resizeObserver = new ResizeObserver(updateDimensions) 59 | resizeObserver.observe(containerRef.current) 60 | 61 | // Also watch DOM changes 62 | const mutationObserver = new MutationObserver(updateDimensions) 63 | mutationObserver.observe(containerRef.current, { 64 | childList: true, 65 | subtree: true, 66 | attributes: true, 67 | characterData: true 68 | }) 69 | 70 | // Initial dimension update 71 | updateDimensions() 72 | 73 | return () => { 74 | resizeObserver.disconnect() 75 | mutationObserver.disconnect() 76 | } 77 | }, [view]) 78 | 79 | // Listen for events that might switch views or show errors 80 | useEffect(() => { 81 | const cleanupFunctions = [ 82 | window.electronAPI.onSolutionStart(() => { 83 | setView("solutions") 84 | }), 85 | window.electronAPI.onUnauthorized(() => { 86 | queryClient.removeQueries({ 87 | queryKey: ["screenshots"] 88 | }) 89 | queryClient.removeQueries({ 90 | queryKey: ["solution"] 91 | }) 92 | queryClient.removeQueries({ 93 | queryKey: ["problem_statement"] 94 | }) 95 | setView("queue") 96 | }), 97 | window.electronAPI.onResetView(() => { 98 | queryClient.removeQueries({ 99 | queryKey: ["screenshots"] 100 | }) 101 | queryClient.removeQueries({ 102 | queryKey: ["solution"] 103 | }) 104 | queryClient.removeQueries({ 105 | queryKey: ["problem_statement"] 106 | }) 107 | setView("queue") 108 | }), 109 | window.electronAPI.onResetView(() => { 110 | queryClient.setQueryData(["problem_statement"], null) 111 | }), 112 | window.electronAPI.onProblemExtracted((data: any) => { 113 | if (view === "queue") { 114 | queryClient.invalidateQueries({ 115 | queryKey: ["problem_statement"] 116 | }) 117 | queryClient.setQueryData(["problem_statement"], data) 118 | } 119 | }), 120 | window.electronAPI.onSolutionError((error: string) => { 121 | showToast("Error", error, "error") 122 | }) 123 | ] 124 | return () => cleanupFunctions.forEach((fn) => fn()) 125 | }, [view]) 126 | 127 | return ( 128 |
129 | {view === "queue" ? ( 130 | 136 | ) : view === "solutions" ? ( 137 | 143 | ) : null} 144 |
145 | ) 146 | } 147 | 148 | export default SubscribedApp 149 | -------------------------------------------------------------------------------- /src/components/Queue/QueueCommands.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | 3 | import { supabase } from "../../lib/supabase" 4 | import { useToast } from "../../contexts/toast" 5 | import { LanguageSelector } from "../shared/LanguageSelector" 6 | import { COMMAND_KEY } from "../../utils/platform" 7 | 8 | interface QueueCommandsProps { 9 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 10 | screenshotCount?: number 11 | credits: number 12 | currentLanguage: string 13 | setLanguage: (language: string) => void 14 | } 15 | 16 | const QueueCommands: React.FC = ({ 17 | onTooltipVisibilityChange, 18 | screenshotCount = 0, 19 | credits, 20 | currentLanguage, 21 | setLanguage 22 | }) => { 23 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 24 | const tooltipRef = useRef(null) 25 | const { showToast } = useToast() 26 | 27 | useEffect(() => { 28 | let tooltipHeight = 0 29 | if (tooltipRef.current && isTooltipVisible) { 30 | tooltipHeight = tooltipRef.current.offsetHeight + 10 31 | } 32 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 33 | }, [isTooltipVisible]) 34 | 35 | const handleSignOut = async () => { 36 | try { 37 | // Clear any local storage or electron-specific data first 38 | localStorage.clear() 39 | sessionStorage.clear() 40 | 41 | // Then sign out from Supabase 42 | const { error } = await supabase.auth.signOut() 43 | if (error) throw error 44 | } catch (err) { 45 | console.error("Error signing out:", err) 46 | } 47 | } 48 | 49 | const handleMouseEnter = () => { 50 | setIsTooltipVisible(true) 51 | } 52 | 53 | const handleMouseLeave = () => { 54 | setIsTooltipVisible(false) 55 | } 56 | 57 | return ( 58 |
59 |
60 |
61 | {/* Screenshot */} 62 |
{ 65 | try { 66 | const result = await window.electronAPI.triggerScreenshot() 67 | if (!result.success) { 68 | console.error("Failed to take screenshot:", result.error) 69 | showToast("Error", "Failed to take screenshot", "error") 70 | } 71 | } catch (error) { 72 | console.error("Error taking screenshot:", error) 73 | showToast("Error", "Failed to take screenshot", "error") 74 | } 75 | }} 76 | > 77 | 78 | {screenshotCount === 0 79 | ? "Take first screenshot" 80 | : screenshotCount === 1 81 | ? "Take second screenshot" 82 | : "Reset first screenshot"} 83 | 84 |
85 | 88 | 91 |
92 |
93 | 94 | {/* Solve Command */} 95 | {screenshotCount > 0 && ( 96 |
{ 101 | if (credits <= 0) { 102 | showToast( 103 | "Out of Credits", 104 | "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", 105 | "error" 106 | ) 107 | return 108 | } 109 | 110 | try { 111 | const result = 112 | await window.electronAPI.triggerProcessScreenshots() 113 | if (!result.success) { 114 | console.error( 115 | "Failed to process screenshots:", 116 | result.error 117 | ) 118 | showToast("Error", "Failed to process screenshots", "error") 119 | } 120 | } catch (error) { 121 | console.error("Error processing screenshots:", error) 122 | showToast("Error", "Failed to process screenshots", "error") 123 | } 124 | }} 125 | > 126 |
127 | Solve 128 |
129 | 132 | 135 |
136 |
137 |
138 | )} 139 | 140 | {/* Separator */} 141 |
142 | 143 | {/* Settings with Tooltip */} 144 |
149 | {/* Gear icon */} 150 |
151 | 161 | 162 | 163 | 164 |
165 | 166 | {/* Tooltip Content */} 167 | {isTooltipVisible && ( 168 |
173 | {/* Add transparent bridge */} 174 |
175 |
176 |
177 |

Keyboard Shortcuts

178 |
179 | {/* Toggle Command */} 180 |
{ 183 | try { 184 | const result = 185 | await window.electronAPI.toggleMainWindow() 186 | if (!result.success) { 187 | console.error( 188 | "Failed to toggle window:", 189 | result.error 190 | ) 191 | showToast( 192 | "Error", 193 | "Failed to toggle window", 194 | "error" 195 | ) 196 | } 197 | } catch (error) { 198 | console.error("Error toggling window:", error) 199 | showToast( 200 | "Error", 201 | "Failed to toggle window", 202 | "error" 203 | ) 204 | } 205 | }} 206 | > 207 |
208 | Toggle Window 209 |
210 | 211 | {COMMAND_KEY} 212 | 213 | 214 | B 215 | 216 |
217 |
218 |

219 | Show or hide this window. 220 |

221 |
222 | 223 | {/* Screenshot Command */} 224 |
{ 227 | try { 228 | const result = 229 | await window.electronAPI.triggerScreenshot() 230 | if (!result.success) { 231 | console.error( 232 | "Failed to take screenshot:", 233 | result.error 234 | ) 235 | showToast( 236 | "Error", 237 | "Failed to take screenshot", 238 | "error" 239 | ) 240 | } 241 | } catch (error) { 242 | console.error("Error taking screenshot:", error) 243 | showToast( 244 | "Error", 245 | "Failed to take screenshot", 246 | "error" 247 | ) 248 | } 249 | }} 250 | > 251 |
252 | Take Screenshot 253 |
254 | 255 | {COMMAND_KEY} 256 | 257 | 258 | H 259 | 260 |
261 |
262 |

263 | Take a screenshot of the problem description. 264 |

265 |
266 | 267 | {/* Solve Command */} 268 |
0 271 | ? "" 272 | : "opacity-50 cursor-not-allowed" 273 | }`} 274 | onClick={async () => { 275 | if (screenshotCount === 0) return 276 | 277 | try { 278 | const result = 279 | await window.electronAPI.triggerProcessScreenshots() 280 | if (!result.success) { 281 | console.error( 282 | "Failed to process screenshots:", 283 | result.error 284 | ) 285 | showToast( 286 | "Error", 287 | "Failed to process screenshots", 288 | "error" 289 | ) 290 | } 291 | } catch (error) { 292 | console.error( 293 | "Error processing screenshots:", 294 | error 295 | ) 296 | showToast( 297 | "Error", 298 | "Failed to process screenshots", 299 | "error" 300 | ) 301 | } 302 | }} 303 | > 304 |
305 | Solve 306 |
307 | 308 | {COMMAND_KEY} 309 | 310 | 311 | ↵ 312 | 313 |
314 |
315 |

316 | {screenshotCount > 0 317 | ? "Generate a solution based on the current problem." 318 | : "Take a screenshot first to generate a solution."} 319 |

320 |
321 |
322 | 323 | {/* Separator and Log Out */} 324 |
325 | 329 | 330 | {/* Credits Display */} 331 |
332 |
333 | Credits Remaining 334 | {credits} / 50 335 |
336 |
337 | Refill at{" "} 338 | 341 | window.electronAPI.openSettingsPortal() 342 | } 343 | > 344 | www.interviewcoder.co/settings 345 | 346 |
347 |
348 | 349 | 371 |
372 |
373 |
374 |
375 | )} 376 |
377 |
378 |
379 |
380 | ) 381 | } 382 | 383 | export default QueueCommands 384 | -------------------------------------------------------------------------------- /src/components/Queue/ScreenshotItem.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ScreenshotItem.tsx 2 | import React from "react" 3 | import { X } from "lucide-react" 4 | 5 | interface Screenshot { 6 | path: string 7 | preview: string 8 | } 9 | 10 | interface ScreenshotItemProps { 11 | screenshot: Screenshot 12 | onDelete: (index: number) => void 13 | index: number 14 | isLoading: boolean 15 | } 16 | 17 | const ScreenshotItem: React.FC = ({ 18 | screenshot, 19 | onDelete, 20 | index, 21 | isLoading 22 | }) => { 23 | const handleDelete = async () => { 24 | await onDelete(index) 25 | } 26 | 27 | return ( 28 | <> 29 |
34 |
35 | {isLoading && ( 36 |
37 |
38 |
39 | )} 40 | Screenshot 49 |
50 | {!isLoading && ( 51 | 61 | )} 62 |
63 | 64 | ) 65 | } 66 | 67 | export default ScreenshotItem 68 | -------------------------------------------------------------------------------- /src/components/Queue/ScreenshotQueue.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ScreenshotItem from "./ScreenshotItem" 3 | 4 | interface Screenshot { 5 | path: string 6 | preview: string 7 | } 8 | 9 | interface ScreenshotQueueProps { 10 | isLoading: boolean 11 | screenshots: Screenshot[] 12 | onDeleteScreenshot: (index: number) => void 13 | } 14 | const ScreenshotQueue: React.FC = ({ 15 | isLoading, 16 | screenshots, 17 | onDeleteScreenshot 18 | }) => { 19 | if (screenshots.length === 0) { 20 | return <> 21 | } 22 | 23 | const displayScreenshots = screenshots.slice(0, 5) 24 | 25 | return ( 26 |
27 | {displayScreenshots.map((screenshot, index) => ( 28 | 35 | ))} 36 |
37 | ) 38 | } 39 | 40 | export default ScreenshotQueue 41 | -------------------------------------------------------------------------------- /src/components/UpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { Dialog, DialogContent } from "./ui/dialog" 3 | import { Button } from "./ui/button" 4 | import { useToast } from "../contexts/toast" 5 | 6 | export const UpdateNotification: React.FC = () => { 7 | const [updateAvailable, setUpdateAvailable] = useState(false) 8 | const [updateDownloaded, setUpdateDownloaded] = useState(false) 9 | const [isDownloading, setIsDownloading] = useState(false) 10 | const { showToast } = useToast() 11 | 12 | useEffect(() => { 13 | console.log("UpdateNotification: Setting up event listeners") 14 | 15 | const unsubscribeAvailable = window.electronAPI.onUpdateAvailable( 16 | (info) => { 17 | console.log("UpdateNotification: Update available received", info) 18 | setUpdateAvailable(true) 19 | } 20 | ) 21 | 22 | const unsubscribeDownloaded = window.electronAPI.onUpdateDownloaded( 23 | (info) => { 24 | console.log("UpdateNotification: Update downloaded received", info) 25 | setUpdateDownloaded(true) 26 | setIsDownloading(false) 27 | } 28 | ) 29 | 30 | return () => { 31 | console.log("UpdateNotification: Cleaning up event listeners") 32 | unsubscribeAvailable() 33 | unsubscribeDownloaded() 34 | } 35 | }, []) 36 | 37 | const handleStartUpdate = async () => { 38 | console.log("UpdateNotification: Starting update download") 39 | setIsDownloading(true) 40 | const result = await window.electronAPI.startUpdate() 41 | console.log("UpdateNotification: Update download result", result) 42 | if (!result.success) { 43 | setIsDownloading(false) 44 | showToast("Error", "Failed to download update", "error") 45 | } 46 | } 47 | 48 | const handleInstallUpdate = () => { 49 | console.log("UpdateNotification: Installing update") 50 | window.electronAPI.installUpdate() 51 | } 52 | 53 | console.log("UpdateNotification: Render state", { 54 | updateAvailable, 55 | updateDownloaded, 56 | isDownloading 57 | }) 58 | if (!updateAvailable && !updateDownloaded) return null 59 | 60 | return ( 61 | 62 | e.preventDefault()} 65 | > 66 |
67 |

68 | {updateDownloaded 69 | ? "Update Ready to Install" 70 | : "A New Version is Available"} 71 |

72 |

73 | {updateDownloaded 74 | ? "The update has been downloaded and will be installed when you restart the app." 75 | : "A new version of Interview Coder is available. Please update to continue using the app."} 76 |

77 |
78 | {updateDownloaded ? ( 79 | 86 | ) : ( 87 | 95 | )} 96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/shared/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { supabase } from "../../lib/supabase" 3 | 4 | interface LanguageSelectorProps { 5 | currentLanguage: string 6 | setLanguage: (language: string) => void 7 | } 8 | 9 | export const LanguageSelector: React.FC = ({ 10 | currentLanguage, 11 | setLanguage 12 | }) => { 13 | const handleLanguageChange = async ( 14 | e: React.ChangeEvent 15 | ) => { 16 | const newLanguage = e.target.value 17 | const { 18 | data: { user } 19 | } = await supabase.auth.getUser() 20 | 21 | if (user) { 22 | const { error } = await supabase 23 | .from("subscriptions") 24 | .update({ preferred_language: newLanguage }) 25 | .eq("user_id", user.id) 26 | 27 | if (error) { 28 | console.error("Error updating language:", error) 29 | } else { 30 | window.__LANGUAGE__ = newLanguage 31 | setLanguage(newLanguage) 32 | } 33 | } 34 | } 35 | 36 | return ( 37 |
38 |
39 | Language 40 | 56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { cn } from "../../lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline" 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 rounded-md px-3 text-xs", 25 | lg: "h-10 rounded-md px-8", 26 | icon: "h-9 w-9" 27 | } 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default" 32 | } 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "../../lib/utils" 3 | 4 | const Card = React.forwardRef< 5 | HTMLDivElement, 6 | React.HTMLAttributes 7 | >(({ className, ...props }, ref) => ( 8 |
16 | )) 17 | Card.displayName = "Card" 18 | 19 | const CardHeader = React.forwardRef< 20 | HTMLDivElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 |
28 | )) 29 | CardHeader.displayName = "CardHeader" 30 | 31 | const CardTitle = React.forwardRef< 32 | HTMLParagraphElement, 33 | React.HTMLAttributes 34 | >(({ className, ...props }, ref) => ( 35 |

40 | )) 41 | CardTitle.displayName = "CardTitle" 42 | 43 | const CardDescription = React.forwardRef< 44 | HTMLParagraphElement, 45 | React.HTMLAttributes 46 | >(({ className, ...props }, ref) => ( 47 |

52 | )) 53 | CardDescription.displayName = "CardDescription" 54 | 55 | const CardContent = React.forwardRef< 56 | HTMLDivElement, 57 | React.HTMLAttributes 58 | >(({ className, ...props }, ref) => ( 59 |

60 | )) 61 | CardContent.displayName = "CardContent" 62 | 63 | const CardFooter = React.forwardRef< 64 | HTMLDivElement, 65 | React.HTMLAttributes 66 | >(({ className, ...props }, ref) => ( 67 |
72 | )) 73 | CardFooter.displayName = "CardFooter" 74 | 75 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 76 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ui/dialog.tsx 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { cn } from "../../lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | const DialogTrigger = DialogPrimitive.Trigger 9 | const DialogPortal = DialogPrimitive.Portal 10 | 11 | const DialogOverlay = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 22 | 23 | const DialogContent = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | 29 | 38 | {children} 39 | 40 | 41 | )) 42 | DialogContent.displayName = DialogPrimitive.Content.displayName 43 | 44 | const DialogClose = DialogPrimitive.Close 45 | 46 | export { Dialog, DialogTrigger, DialogContent, DialogClose } 47 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "../../lib/utils" 3 | 4 | export interface InputProps 5 | extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | Input.displayName = "Input" 23 | 24 | export { Input } 25 | -------------------------------------------------------------------------------- /src/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ToastPrimitive from "@radix-ui/react-toast" 3 | import { cn } from "../../lib/utils" 4 | import { AlertCircle, CheckCircle2, Info, X } from "lucide-react" 5 | 6 | const ToastProvider = ToastPrimitive.Provider 7 | 8 | export type ToastMessage = { 9 | title: string 10 | description: string 11 | variant: ToastVariant 12 | } 13 | 14 | const ToastViewport = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, ...props }, ref) => ( 18 | 26 | )) 27 | ToastViewport.displayName = ToastPrimitive.Viewport.displayName 28 | 29 | type ToastVariant = "neutral" | "success" | "error" 30 | 31 | interface ToastProps 32 | extends React.ComponentPropsWithoutRef { 33 | variant?: ToastVariant 34 | swipeDirection?: "right" | "left" | "up" | "down" 35 | } 36 | 37 | const toastVariants: Record< 38 | ToastVariant, 39 | { icon: React.ReactNode; bgColor: string } 40 | > = { 41 | neutral: { 42 | icon: , 43 | bgColor: "bg-amber-100" 44 | }, 45 | success: { 46 | icon: , 47 | bgColor: "bg-emerald-100" 48 | }, 49 | error: { 50 | icon: , 51 | bgColor: "bg-red-100" 52 | } 53 | } 54 | 55 | const Toast = React.forwardRef< 56 | React.ElementRef, 57 | ToastProps 58 | >(({ className, variant = "neutral", ...props }, ref) => ( 59 | 69 | {toastVariants[variant].icon} 70 |
{props.children}
71 | 72 | 73 | 74 |
75 | )) 76 | Toast.displayName = ToastPrimitive.Root.displayName 77 | 78 | const ToastAction = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | ToastAction.displayName = ToastPrimitive.Action.displayName 92 | 93 | const ToastTitle = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | ToastTitle.displayName = ToastPrimitive.Title.displayName 104 | 105 | const ToastDescription = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | ToastDescription.displayName = ToastPrimitive.Description.displayName 116 | 117 | export type { ToastProps, ToastVariant } 118 | export { 119 | ToastProvider, 120 | ToastViewport, 121 | Toast, 122 | ToastAction, 123 | ToastTitle, 124 | ToastDescription 125 | } 126 | -------------------------------------------------------------------------------- /src/contexts/toast.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | 3 | type ToastVariant = "neutral" | "success" | "error" 4 | 5 | interface ToastContextType { 6 | showToast: (title: string, description: string, variant: ToastVariant) => void 7 | } 8 | 9 | export const ToastContext = createContext( 10 | undefined 11 | ) 12 | 13 | export function useToast() { 14 | const context = useContext(ToastContext) 15 | if (!context) { 16 | throw new Error("useToast must be used within a ToastProvider") 17 | } 18 | return context 19 | } 20 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { ToastMessage } from "./components/ui/toast" 4 | 5 | interface ImportMetaEnv { 6 | readonly VITE_SUPABASE_URL: string 7 | readonly VITE_SUPABASE_ANON_KEY: string 8 | readonly NODE_ENV: string 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv 13 | } 14 | 15 | interface ElectronAPI { 16 | openSubscriptionPortal: (authData: { 17 | id: string 18 | email: string 19 | }) => Promise<{ success: boolean; error?: string }> 20 | updateContentDimensions: (dimensions: { 21 | width: number 22 | height: number 23 | }) => Promise 24 | clearStore: () => Promise<{ success: boolean; error?: string }> 25 | getScreenshots: () => Promise<{ 26 | success: boolean 27 | previews?: Array<{ path: string; preview: string }> | null 28 | error?: string 29 | }> 30 | deleteScreenshot: ( 31 | path: string 32 | ) => Promise<{ success: boolean; error?: string }> 33 | onScreenshotTaken: ( 34 | callback: (data: { path: string; preview: string }) => void 35 | ) => () => void 36 | onResetView: (callback: () => void) => () => void 37 | onSolutionStart: (callback: () => void) => () => void 38 | onDebugStart: (callback: () => void) => () => void 39 | onDebugSuccess: (callback: (data: any) => void) => () => void 40 | onSolutionError: (callback: (error: string) => void) => () => void 41 | onProcessingNoScreenshots: (callback: () => void) => () => void 42 | onProblemExtracted: (callback: (data: any) => void) => () => void 43 | onSolutionSuccess: (callback: (data: any) => void) => () => void 44 | onUnauthorized: (callback: () => void) => () => void 45 | onDebugError: (callback: (error: string) => void) => () => void 46 | openExternal: (url: string) => void 47 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 48 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 49 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 50 | triggerReset: () => Promise<{ success: boolean; error?: string }> 51 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 52 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 53 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 54 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 55 | onSubscriptionUpdated: (callback: () => void) => () => void 56 | onSubscriptionPortalClosed: (callback: () => void) => () => void 57 | // Add update-related methods 58 | startUpdate: () => Promise<{ success: boolean; error?: string }> 59 | installUpdate: () => void 60 | onUpdateAvailable: (callback: (info: any) => void) => () => void 61 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 62 | } 63 | 64 | interface Window { 65 | electronAPI: ElectronAPI 66 | electron: { 67 | ipcRenderer: { 68 | on(channel: string, func: (...args: any[]) => void): void 69 | removeListener(channel: string, func: (...args: any[]) => void): void 70 | } 71 | } 72 | __CREDITS__: number 73 | __LANGUAGE__: string 74 | __IS_INITIALIZED__: boolean 75 | } 76 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .frosted-glass { 6 | background: rgba(26, 26, 26, 0.8); 7 | backdrop-filter: blur(8px); 8 | } 9 | 10 | .auth-button { 11 | background: rgba(252, 252, 252, 0.98); 12 | color: rgba(60, 60, 60, 0.9); 13 | transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 14 | position: relative; 15 | z-index: 2; 16 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); 17 | } 18 | 19 | .auth-button:hover { 20 | background: rgba(255, 255, 255, 1); 21 | } 22 | 23 | .auth-button::before { 24 | content: ""; 25 | position: absolute; 26 | inset: -8px; 27 | background: linear-gradient(45deg, #ff000000, #0000ff00); 28 | z-index: -1; 29 | transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 30 | border-radius: inherit; 31 | filter: blur(24px); 32 | opacity: 0; 33 | } 34 | 35 | .auth-button:hover::before { 36 | background: linear-gradient( 37 | 45deg, 38 | rgba(255, 0, 0, 0.4), 39 | rgba(0, 0, 255, 0.4) 40 | ); 41 | filter: blur(48px); 42 | inset: -16px; 43 | opacity: 1; 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js" 2 | 3 | console.log("Supabase URL:", import.meta.env.VITE_SUPABASE_URL) 4 | console.log( 5 | "Supabase Anon Key:", 6 | import.meta.env.VITE_SUPABASE_ANON_KEY?.slice(0, 10) + "..." 7 | ) 8 | 9 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL 10 | const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY 11 | 12 | if (!supabaseUrl || !supabaseAnonKey) { 13 | throw new Error("Missing Supabase environment variables") 14 | } 15 | 16 | export const supabase = createClient(supabaseUrl, supabaseAnonKey, { 17 | auth: { 18 | flowType: "pkce", 19 | detectSessionInUrl: true, 20 | persistSession: true, 21 | autoRefreshToken: true, 22 | debug: true, 23 | storage: { 24 | getItem: (key) => { 25 | const item = localStorage.getItem(key) 26 | console.log("Auth storage - Getting key:", key, "Value exists:", !!item) 27 | return item 28 | }, 29 | setItem: (key, value) => { 30 | console.log("Auth storage - Setting key:", key) 31 | localStorage.setItem(key, value) 32 | }, 33 | removeItem: (key) => { 34 | console.log("Auth storage - Removing key:", key) 35 | localStorage.removeItem(key) 36 | } 37 | } 38 | }, 39 | realtime: { 40 | params: { 41 | eventsPerSecond: 10 42 | }, 43 | headers: { 44 | apikey: supabaseAnonKey 45 | } 46 | } 47 | }) 48 | 49 | export const signInWithGoogle = async () => { 50 | try { 51 | console.log("Initiating Google sign in...") 52 | const { data, error } = await supabase.auth.signInWithOAuth({ 53 | provider: "google", 54 | options: { 55 | redirectTo: window.location.origin 56 | } 57 | }) 58 | 59 | if (error) { 60 | console.error("Google sign in error:", error) 61 | throw error 62 | } 63 | 64 | console.log("Google sign in response:", data) 65 | return data 66 | } catch (error) { 67 | console.error("Unexpected error during Google sign in:", error) 68 | throw error 69 | } 70 | } 71 | 72 | let channel: ReturnType | null = null 73 | 74 | // Monitor auth state changes and manage realtime connection 75 | supabase.auth.onAuthStateChange((event, session) => { 76 | console.log("Auth state changed:", event, session?.user?.id) 77 | console.log("Full session data:", session) 78 | 79 | if (event === "SIGNED_IN" && session) { 80 | // Only establish realtime connection after successful sign in 81 | console.log("Establishing realtime connection...") 82 | 83 | // Clean up existing channel if any 84 | if (channel) { 85 | channel.unsubscribe() 86 | } 87 | 88 | channel = supabase.channel("system", { 89 | config: { 90 | presence: { 91 | key: session.user.id 92 | } 93 | } 94 | }) 95 | 96 | channel 97 | .on("system", { event: "*" }, (payload) => { 98 | console.log("System event:", payload) 99 | }) 100 | .subscribe((status) => { 101 | console.log("Realtime subscription status:", status) 102 | if (status === "SUBSCRIBED") { 103 | console.log("Successfully connected to realtime system") 104 | } 105 | if (status === "CHANNEL_ERROR") { 106 | console.error("Realtime connection error - will retry in 5s") 107 | setTimeout(() => { 108 | channel?.subscribe() 109 | }, 5000) 110 | } 111 | }) 112 | } 113 | 114 | if (event === "SIGNED_OUT") { 115 | // Clean up realtime connection on sign out 116 | if (channel) { 117 | console.log("Cleaning up realtime connection") 118 | channel.unsubscribe() 119 | channel = null 120 | } 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // src/lib/utils.ts 2 | 3 | import { type ClassValue, clsx } from "clsx" 4 | import { twMerge } from "tailwind-merge" 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)) 8 | } 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | export interface ElectronAPI { 2 | openSubscriptionPortal: (authData: { 3 | id: string 4 | email: string 5 | }) => Promise<{ success: boolean; error?: string }> 6 | updateContentDimensions: (dimensions: { 7 | width: number 8 | height: number 9 | }) => Promise 10 | clearStore: () => Promise<{ success: boolean; error?: string }> 11 | getScreenshots: () => Promise<{ 12 | success: boolean 13 | previews?: Array<{ path: string; preview: string }> | null 14 | error?: string 15 | }> 16 | deleteScreenshot: ( 17 | path: string 18 | ) => Promise<{ success: boolean; error?: string }> 19 | onScreenshotTaken: ( 20 | callback: (data: { path: string; preview: string }) => void 21 | ) => () => void 22 | onResetView: (callback: () => void) => () => void 23 | onSolutionStart: (callback: () => void) => () => void 24 | onDebugStart: (callback: () => void) => () => void 25 | onDebugSuccess: (callback: (data: any) => void) => () => void 26 | onSolutionError: (callback: (error: string) => void) => () => void 27 | onProcessingNoScreenshots: (callback: () => void) => () => void 28 | onProblemExtracted: (callback: (data: any) => void) => () => void 29 | onSolutionSuccess: (callback: (data: any) => void) => () => void 30 | onUnauthorized: (callback: () => void) => () => void 31 | onDebugError: (callback: (error: string) => void) => () => void 32 | openExternal: (url: string) => void 33 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 34 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 35 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 36 | triggerReset: () => Promise<{ success: boolean; error?: string }> 37 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 38 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 39 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 40 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 41 | onSubscriptionUpdated: (callback: () => void) => () => void 42 | onSubscriptionPortalClosed: (callback: () => void) => () => void 43 | startUpdate: () => Promise<{ success: boolean; error?: string }> 44 | installUpdate: () => void 45 | onUpdateAvailable: (callback: (info: any) => void) => () => void 46 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 47 | 48 | decrementCredits: () => Promise 49 | setInitialCredits: (credits: number) => Promise 50 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 51 | onOutOfCredits: (callback: () => void) => () => void 52 | openSettingsPortal: () => Promise 53 | getPlatform: () => string 54 | } 55 | 56 | declare global { 57 | interface Window { 58 | electronAPI: ElectronAPI 59 | electron: { 60 | ipcRenderer: { 61 | on: (channel: string, func: (...args: any[]) => void) => void 62 | removeListener: ( 63 | channel: string, 64 | func: (...args: any[]) => void 65 | ) => void 66 | } 67 | } 68 | __CREDITS__: number 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/types/index.tsx: -------------------------------------------------------------------------------- 1 | export interface Screenshot { 2 | id: string 3 | path: string 4 | timestamp: number 5 | thumbnail: string // Base64 thumbnail 6 | } 7 | 8 | export interface Solution { 9 | initial_thoughts: string[] 10 | thought_steps: string[] 11 | description: string 12 | code: string 13 | } 14 | -------------------------------------------------------------------------------- /src/types/screenshots.ts: -------------------------------------------------------------------------------- 1 | export interface Screenshot { 2 | path: string 3 | preview: string 4 | } 5 | -------------------------------------------------------------------------------- /src/types/solutions.ts: -------------------------------------------------------------------------------- 1 | export interface Solution { 2 | initial_thoughts: string[] 3 | thought_steps: string[] 4 | description: string 5 | code: string 6 | } 7 | 8 | export interface SolutionsResponse { 9 | [key: string]: Solution 10 | } 11 | 12 | export interface ProblemStatementData { 13 | problem_statement: string 14 | input_format: { 15 | description: string 16 | parameters: any[] 17 | } 18 | output_format: { 19 | description: string 20 | type: string 21 | subtype: string 22 | } 23 | complexity: { 24 | time: string 25 | space: string 26 | } 27 | test_cases: any[] 28 | validation_type: string 29 | difficulty: string 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/platform.ts: -------------------------------------------------------------------------------- 1 | // Get the platform safely 2 | const getPlatform = () => { 3 | try { 4 | return window.electronAPI?.getPlatform() || 'win32' // Default to win32 if API is not available 5 | } catch { 6 | return 'win32' // Default to win32 if there's an error 7 | } 8 | } 9 | 10 | // Platform-specific command key symbol 11 | export const COMMAND_KEY = getPlatform() === 'darwin' ? '⌘' : 'Ctrl' 12 | 13 | // Helper to check if we're on Windows 14 | export const isWindows = getPlatform() === 'win32' 15 | 16 | // Helper to check if we're on macOS 17 | export const isMacOS = getPlatform() === 'darwin' -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | 6 | readonly VITE_SUPABASE_ANON_KEY: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Inter", "system-ui", "sans-serif"] 8 | }, 9 | animation: { 10 | in: "in 0.2s ease-out", 11 | out: "out 0.2s ease-in", 12 | pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", 13 | shimmer: "shimmer 2s linear infinite", 14 | "text-gradient-wave": "textGradientWave 2s infinite ease-in-out" 15 | }, 16 | keyframes: { 17 | textGradientWave: { 18 | "0%": { backgroundPosition: "0% 50%" }, 19 | "100%": { backgroundPosition: "200% 50%" } 20 | }, 21 | shimmer: { 22 | "0%": { 23 | backgroundPosition: "200% 0" 24 | }, 25 | "100%": { 26 | backgroundPosition: "-200% 0" 27 | } 28 | }, 29 | in: { 30 | "0%": { transform: "translateY(100%)", opacity: 0 }, 31 | "100%": { transform: "translateY(0)", opacity: 1 } 32 | }, 33 | out: { 34 | "0%": { transform: "translateY(0)", opacity: 1 }, 35 | "100%": { transform: "translateY(100%)", opacity: 0 } 36 | }, 37 | pulse: { 38 | "0%, 100%": { 39 | opacity: 1 40 | }, 41 | "50%": { 42 | opacity: 0.5 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | plugins: [] 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.electron.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "outDir": "dist-electron", 11 | "esModuleInterop": true, 12 | "noImplicitAny": false, 13 | "strictNullChecks": false, 14 | "baseUrl": ".", 15 | "paths": { 16 | "main": ["electron/main.ts"] 17 | } 18 | }, 19 | "include": ["electron/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ES2020", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx", 13 | "strict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "allowJs": true, 18 | "esModuleInterop": true, 19 | "allowImportingTsExtensions": true, 20 | "types": ["vite/client"] 21 | }, 22 | "include": ["electron/**/*", "src/**/*"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "vite" 3 | import electron from "vite-plugin-electron" 4 | import react from "@vitejs/plugin-react" 5 | import path from "path" 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | electron([ 11 | { 12 | // main.ts 13 | entry: "electron/main.ts", 14 | vite: { 15 | build: { 16 | outDir: "dist-electron", 17 | sourcemap: true, 18 | minify: false, 19 | rollupOptions: { 20 | external: ["electron"] 21 | } 22 | } 23 | } 24 | }, 25 | { 26 | // preload.ts 27 | entry: "electron/preload.ts", 28 | vite: { 29 | build: { 30 | outDir: "dist-electron", 31 | sourcemap: true, 32 | rollupOptions: { 33 | external: ["electron"] 34 | } 35 | } 36 | } 37 | } 38 | ]) 39 | ], 40 | base: process.env.NODE_ENV === "production" ? "./" : "/", 41 | server: { 42 | port: 54321, 43 | strictPort: true, 44 | watch: { 45 | usePolling: true 46 | } 47 | }, 48 | build: { 49 | outDir: "dist", 50 | emptyOutDir: true, 51 | sourcemap: true 52 | }, 53 | resolve: { 54 | alias: { 55 | "@": path.resolve(__dirname, "./src") 56 | } 57 | } 58 | }) 59 | --------------------------------------------------------------------------------