├── .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 │ └── 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 | Interview Coder is a desktop application designed to help users with technical coding interviews. It allows users to take screenshots of coding problems, process them using AI, and get solutions. 4 | 5 | 6 | ## Features 7 | 8 | - Take screenshots of coding problems 9 | - Process screenshots to extract problem statements 10 | - Generate solutions in your preferred programming language 11 | - View time and space complexity analysis 12 | - Toggle window visibility with keyboard shortcuts 13 | - Move the window around the screen with keyboard shortcuts 14 | 15 | ## Keyboard Shortcuts 16 | 17 | - **Cmd/Ctrl + B**: Toggle window visibility 18 | - **Cmd/Ctrl + Q**: Quit the application 19 | - **Cmd/Ctrl + H**: Take a screenshot 20 | - **Cmd/Ctrl + Enter**: Process screenshots 21 | - **Arrow keys with Cmd/Ctrl**: Move window around the screen 22 | 23 | ## Running the Application 24 | 25 | ### Prerequisites 26 | 27 | - Node.js (v16 or higher) 28 | - npm or yarn 29 | 30 | ### Installation 31 | 32 | 1. Clone the repository: 33 | ``` 34 | git clone https://github.com/yourusername/interview-coder.git 35 | cd interview-coder 36 | ``` 37 | 38 | 2. Install dependencies: 39 | ``` 40 | npm install 41 | # or 42 | yarn 43 | ``` 44 | 45 | 3. Run the application in development mode: 46 | ``` 47 | npm run dev 48 | # or 49 | yarn dev 50 | ``` 51 | 52 | 4. Build the application for production: 53 | ``` 54 | npm run build 55 | # or 56 | yarn build 57 | ``` 58 | 59 | ## API Integration 60 | 61 | This version of the application still requires an API connection to process screenshots and generate solutions. You'll need to set up your own API service or modify the code to use a different solution generation method. 62 | 63 | ## Disclaimer 64 | 65 | This modified version is for educational purposes only. The original Interview Coder application is a commercial product with subscription requirements. 66 | 67 | ## License 68 | 69 | This project is licensed under the ISC License. 70 | -------------------------------------------------------------------------------- /assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/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/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/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 | // Always return a high number of credits 46 | return 999 47 | } 48 | 49 | private async getLanguage(): Promise { 50 | const mainWindow = this.deps.getMainWindow() 51 | if (!mainWindow) return "python" 52 | 53 | try { 54 | await this.waitForInitialization(mainWindow) 55 | const language = await mainWindow.webContents.executeJavaScript( 56 | "window.__LANGUAGE__" 57 | ) 58 | 59 | if ( 60 | typeof language !== "string" || 61 | language === undefined || 62 | language === null 63 | ) { 64 | console.warn("Language not properly initialized") 65 | return "python" 66 | } 67 | 68 | return language 69 | } catch (error) { 70 | console.error("Error getting language:", error) 71 | return "python" 72 | } 73 | } 74 | 75 | public async processScreenshots(): Promise { 76 | const mainWindow = this.deps.getMainWindow() 77 | if (!mainWindow) return 78 | 79 | // Credits check is bypassed - we always have enough credits 80 | 81 | const view = this.deps.getView() 82 | console.log("Processing screenshots in view:", view) 83 | 84 | if (view === "queue") { 85 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.INITIAL_START) 86 | const screenshotQueue = this.screenshotHelper.getScreenshotQueue() 87 | console.log("Processing main queue screenshots:", screenshotQueue) 88 | if (screenshotQueue.length === 0) { 89 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 90 | return 91 | } 92 | 93 | try { 94 | // Initialize AbortController 95 | this.currentProcessingAbortController = new AbortController() 96 | const { signal } = this.currentProcessingAbortController 97 | 98 | const screenshots = await Promise.all( 99 | screenshotQueue.map(async (path) => ({ 100 | path, 101 | preview: await this.screenshotHelper.getImagePreview(path), 102 | data: fs.readFileSync(path).toString("base64") 103 | })) 104 | ) 105 | 106 | const result = await this.processScreenshotsHelper(screenshots, signal) 107 | 108 | if (!result.success) { 109 | console.log("Processing failed:", result.error) 110 | if (result.error?.includes("API Key out of credits")) { 111 | mainWindow.webContents.send( 112 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 113 | ) 114 | } else if (result.error?.includes("OpenAI API key not found")) { 115 | mainWindow.webContents.send( 116 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 117 | "OpenAI API key not found in environment variables. Please set the OPEN_AI_API_KEY environment variable." 118 | ) 119 | } else { 120 | mainWindow.webContents.send( 121 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 122 | result.error 123 | ) 124 | } 125 | // Reset view back to queue on error 126 | console.log("Resetting view to queue due to error") 127 | this.deps.setView("queue") 128 | return 129 | } 130 | 131 | // Only set view to solutions if processing succeeded 132 | console.log("Setting view to solutions after successful processing") 133 | mainWindow.webContents.send( 134 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 135 | result.data 136 | ) 137 | this.deps.setView("solutions") 138 | } catch (error: any) { 139 | mainWindow.webContents.send( 140 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 141 | error 142 | ) 143 | console.error("Processing error:", error) 144 | if (axios.isCancel(error)) { 145 | mainWindow.webContents.send( 146 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 147 | "Processing was canceled by the user." 148 | ) 149 | } else { 150 | mainWindow.webContents.send( 151 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 152 | error.message || "Server error. Please try again." 153 | ) 154 | } 155 | // Reset view back to queue on error 156 | console.log("Resetting view to queue due to error") 157 | this.deps.setView("queue") 158 | } finally { 159 | this.currentProcessingAbortController = null 160 | } 161 | } else { 162 | // view == 'solutions' 163 | const extraScreenshotQueue = 164 | this.screenshotHelper.getExtraScreenshotQueue() 165 | console.log("Processing extra queue screenshots:", extraScreenshotQueue) 166 | if (extraScreenshotQueue.length === 0) { 167 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 168 | return 169 | } 170 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.DEBUG_START) 171 | 172 | // Initialize AbortController 173 | this.currentExtraProcessingAbortController = new AbortController() 174 | const { signal } = this.currentExtraProcessingAbortController 175 | 176 | try { 177 | const screenshots = await Promise.all( 178 | [ 179 | ...this.screenshotHelper.getScreenshotQueue(), 180 | ...extraScreenshotQueue 181 | ].map(async (path) => ({ 182 | path, 183 | preview: await this.screenshotHelper.getImagePreview(path), 184 | data: fs.readFileSync(path).toString("base64") 185 | })) 186 | ) 187 | console.log( 188 | "Combined screenshots for processing:", 189 | screenshots.map((s) => s.path) 190 | ) 191 | 192 | const result = await this.processExtraScreenshotsHelper( 193 | screenshots, 194 | signal 195 | ) 196 | 197 | if (result.success) { 198 | this.deps.setHasDebugged(true) 199 | mainWindow.webContents.send( 200 | this.deps.PROCESSING_EVENTS.DEBUG_SUCCESS, 201 | result.data 202 | ) 203 | } else { 204 | mainWindow.webContents.send( 205 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 206 | result.error 207 | ) 208 | } 209 | } catch (error: any) { 210 | if (axios.isCancel(error)) { 211 | mainWindow.webContents.send( 212 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 213 | "Extra processing was canceled by the user." 214 | ) 215 | } else { 216 | mainWindow.webContents.send( 217 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 218 | error.message 219 | ) 220 | } 221 | } finally { 222 | this.currentExtraProcessingAbortController = null 223 | } 224 | } 225 | } 226 | 227 | private async processScreenshotsHelper( 228 | screenshots: Array<{ path: string; data: string }>, 229 | signal: AbortSignal 230 | ) { 231 | const MAX_RETRIES = 0 232 | let retryCount = 0 233 | 234 | while (retryCount <= MAX_RETRIES) { 235 | try { 236 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 237 | const mainWindow = this.deps.getMainWindow() 238 | const language = await this.getLanguage() 239 | let problemInfo 240 | 241 | // First API call - extract problem info 242 | try { 243 | const extractResponse = await axios.post( 244 | `${API_BASE_URL}/api/extract`, 245 | { imageDataList, language }, 246 | { 247 | signal, 248 | timeout: 300000, 249 | validateStatus: function (status) { 250 | return status < 500 251 | }, 252 | maxRedirects: 5, 253 | headers: { 254 | "Content-Type": "application/json" 255 | } 256 | } 257 | ) 258 | 259 | problemInfo = extractResponse.data 260 | 261 | // Store problem info in AppState 262 | this.deps.setProblemInfo(problemInfo) 263 | 264 | // Send first success event 265 | if (mainWindow) { 266 | mainWindow.webContents.send( 267 | this.deps.PROCESSING_EVENTS.PROBLEM_EXTRACTED, 268 | problemInfo 269 | ) 270 | 271 | // Generate solutions after successful extraction 272 | const solutionsResult = await this.generateSolutionsHelper(signal) 273 | if (solutionsResult.success) { 274 | // Clear any existing extra screenshots before transitioning to solutions view 275 | this.screenshotHelper.clearExtraScreenshotQueue() 276 | mainWindow.webContents.send( 277 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 278 | solutionsResult.data 279 | ) 280 | return { success: true, data: solutionsResult.data } 281 | } else { 282 | throw new Error( 283 | solutionsResult.error || "Failed to generate solutions" 284 | ) 285 | } 286 | } 287 | } catch (error: any) { 288 | // If the request was cancelled, don't retry 289 | if (axios.isCancel(error)) { 290 | return { 291 | success: false, 292 | error: "Processing was canceled by the user." 293 | } 294 | } 295 | 296 | console.error("API Error Details:", { 297 | status: error.response?.status, 298 | data: error.response?.data, 299 | message: error.message, 300 | code: error.code 301 | }) 302 | 303 | // Handle API-specific errors 304 | if ( 305 | error.response?.data?.error && 306 | typeof error.response.data.error === "string" 307 | ) { 308 | if (error.response.data.error.includes("Operation timed out")) { 309 | throw new Error( 310 | "Operation timed out after 1 minute. Please try again." 311 | ) 312 | } 313 | if (error.response.data.error.includes("API Key out of credits")) { 314 | throw new Error(error.response.data.error) 315 | } 316 | throw new Error(error.response.data.error) 317 | } 318 | 319 | // If we get here, it's an unknown error 320 | throw new Error(error.message || "Server error. Please try again.") 321 | } 322 | } catch (error: any) { 323 | // Log the full error for debugging 324 | console.error("Processing error details:", { 325 | message: error.message, 326 | code: error.code, 327 | response: error.response?.data, 328 | retryCount 329 | }) 330 | 331 | // If it's a cancellation or we've exhausted retries, return the error 332 | if (axios.isCancel(error) || retryCount >= MAX_RETRIES) { 333 | return { success: false, error: error.message } 334 | } 335 | 336 | // Increment retry count and continue 337 | retryCount++ 338 | } 339 | } 340 | 341 | // If we get here, all retries failed 342 | return { 343 | success: false, 344 | error: "Failed to process after multiple attempts. Please try again." 345 | } 346 | } 347 | 348 | private async generateSolutionsHelper(signal: AbortSignal) { 349 | try { 350 | const problemInfo = this.deps.getProblemInfo() 351 | const language = await this.getLanguage() 352 | 353 | if (!problemInfo) { 354 | throw new Error("No problem info available") 355 | } 356 | 357 | const response = await axios.post( 358 | `${API_BASE_URL}/api/generate`, 359 | { ...problemInfo, language }, 360 | { 361 | signal, 362 | timeout: 300000, 363 | validateStatus: function (status) { 364 | return status < 500 365 | }, 366 | maxRedirects: 5, 367 | headers: { 368 | "Content-Type": "application/json" 369 | } 370 | } 371 | ) 372 | 373 | return { success: true, data: response.data } 374 | } catch (error: any) { 375 | const mainWindow = this.deps.getMainWindow() 376 | 377 | // Handle timeout errors (both 504 and axios timeout) 378 | if (error.code === "ECONNABORTED" || error.response?.status === 504) { 379 | // Cancel ongoing API requests 380 | this.cancelOngoingRequests() 381 | // Clear both screenshot queues 382 | this.deps.clearQueues() 383 | // Update view state to queue 384 | this.deps.setView("queue") 385 | // Notify renderer to switch view 386 | if (mainWindow && !mainWindow.isDestroyed()) { 387 | mainWindow.webContents.send("reset-view") 388 | mainWindow.webContents.send( 389 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 390 | "Request timed out. The server took too long to respond. Please try again." 391 | ) 392 | } 393 | return { 394 | success: false, 395 | error: "Request timed out. Please try again." 396 | } 397 | } 398 | 399 | if (error.response?.data?.error?.includes("API Key out of credits")) { 400 | if (mainWindow) { 401 | mainWindow.webContents.send( 402 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 403 | ) 404 | } 405 | return { success: false, error: error.response.data.error } 406 | } 407 | 408 | if ( 409 | error.response?.data?.error?.includes( 410 | "Please close this window and re-enter a valid Open AI API key." 411 | ) 412 | ) { 413 | if (mainWindow) { 414 | mainWindow.webContents.send( 415 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 416 | ) 417 | } 418 | return { success: false, error: error.response.data.error } 419 | } 420 | 421 | return { success: false, error: error.message } 422 | } 423 | } 424 | 425 | private async processExtraScreenshotsHelper( 426 | screenshots: Array<{ path: string; data: string }>, 427 | signal: AbortSignal 428 | ) { 429 | try { 430 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 431 | const problemInfo = this.deps.getProblemInfo() 432 | const language = await this.getLanguage() 433 | 434 | if (!problemInfo) { 435 | throw new Error("No problem info available") 436 | } 437 | 438 | const response = await axios.post( 439 | `${API_BASE_URL}/api/debug`, 440 | { imageDataList, problemInfo, language }, 441 | { 442 | signal, 443 | timeout: 300000, 444 | validateStatus: function (status) { 445 | return status < 500 446 | }, 447 | maxRedirects: 5, 448 | headers: { 449 | "Content-Type": "application/json" 450 | } 451 | } 452 | ) 453 | 454 | return { success: true, data: response.data } 455 | } catch (error: any) { 456 | const mainWindow = this.deps.getMainWindow() 457 | 458 | // Handle cancellation first 459 | if (axios.isCancel(error)) { 460 | return { 461 | success: false, 462 | error: "Processing was canceled by the user." 463 | } 464 | } 465 | 466 | if (error.response?.data?.error?.includes("Operation timed out")) { 467 | // Cancel ongoing API requests 468 | this.cancelOngoingRequests() 469 | // Clear both screenshot queues 470 | this.deps.clearQueues() 471 | // Update view state to queue 472 | this.deps.setView("queue") 473 | // Notify renderer to switch view 474 | if (mainWindow && !mainWindow.isDestroyed()) { 475 | mainWindow.webContents.send("reset-view") 476 | mainWindow.webContents.send( 477 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 478 | "Operation timed out after 1 minute. Please try again." 479 | ) 480 | } 481 | return { 482 | success: false, 483 | error: "Operation timed out after 1 minute. Please try again." 484 | } 485 | } 486 | 487 | if (error.response?.data?.error?.includes("API Key out of credits")) { 488 | if (mainWindow) { 489 | mainWindow.webContents.send( 490 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 491 | ) 492 | } 493 | return { success: false, error: error.response.data.error } 494 | } 495 | 496 | if ( 497 | error.response?.data?.error?.includes( 498 | "Please close this window and re-enter a valid Open AI API key." 499 | ) 500 | ) { 501 | if (mainWindow) { 502 | mainWindow.webContents.send( 503 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 504 | ) 505 | } 506 | return { success: false, error: error.response.data.error } 507 | } 508 | 509 | return { success: false, error: error.message } 510 | } 511 | } 512 | 513 | public cancelOngoingRequests(): void { 514 | let wasCancelled = false 515 | 516 | if (this.currentProcessingAbortController) { 517 | this.currentProcessingAbortController.abort() 518 | this.currentProcessingAbortController = null 519 | wasCancelled = true 520 | } 521 | 522 | if (this.currentExtraProcessingAbortController) { 523 | this.currentExtraProcessingAbortController.abort() 524 | this.currentExtraProcessingAbortController = null 525 | wasCancelled = true 526 | } 527 | 528 | // Reset hasDebugged flag 529 | this.deps.setHasDebugged(false) 530 | 531 | // Clear any pending state 532 | this.deps.setProblemInfo(null) 533 | 534 | const mainWindow = this.deps.getMainWindow() 535 | if (wasCancelled && mainWindow && !mainWindow.isDestroyed()) { 536 | // Send a clear message that processing was cancelled 537 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 538 | } 539 | } 540 | 541 | public cancelProcessing(): void { 542 | if (this.currentProcessingAbortController) { 543 | this.currentProcessingAbortController.abort(); 544 | this.currentProcessingAbortController = null; 545 | } 546 | 547 | if (this.currentExtraProcessingAbortController) { 548 | this.currentExtraProcessingAbortController.abort(); 549 | this.currentExtraProcessingAbortController = null; 550 | } 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /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 | // For testing purposes, we'll allow the auto-updater to run in development mode 9 | // but log a warning 10 | if (!app.isPackaged) { 11 | console.log("Auto-updater running in development mode (for testing)") 12 | } 13 | 14 | // If we're in development mode, simulate update events for testing 15 | if (!app.isPackaged || !process.env.GH_TOKEN) { 16 | console.log("Auto updater: Development mode, update notifications disabled") 17 | 18 | // Disable all update events 19 | ipcMain.handle("start-update", async () => { 20 | console.log("Update download requested, but updates are disabled") 21 | return { success: true } 22 | }) 23 | 24 | ipcMain.handle("install-update", () => { 25 | console.log("Update installation requested, but updates are disabled") 26 | }) 27 | 28 | // No simulated update events will be sent 29 | 30 | return 31 | } 32 | 33 | // Configure auto updater for production 34 | autoUpdater.autoDownload = true 35 | autoUpdater.autoInstallOnAppQuit = true 36 | autoUpdater.allowDowngrade = true 37 | autoUpdater.allowPrerelease = true 38 | 39 | // Enable more verbose logging 40 | autoUpdater.logger = log 41 | log.transports.file.level = "debug" 42 | console.log( 43 | "Auto-updater logger configured with level:", 44 | log.transports.file.level 45 | ) 46 | 47 | // Log all update events 48 | autoUpdater.on("checking-for-update", () => { 49 | console.log("Checking for updates...") 50 | }) 51 | 52 | autoUpdater.on("update-available", (info) => { 53 | console.log("Update available:", info) 54 | // Notify renderer process about available update 55 | BrowserWindow.getAllWindows().forEach((window) => { 56 | console.log("Sending update-available to window") 57 | window.webContents.send("update-available", info) 58 | }) 59 | }) 60 | 61 | autoUpdater.on("update-not-available", (info) => { 62 | console.log("Update not available:", info) 63 | }) 64 | 65 | autoUpdater.on("download-progress", (progressObj) => { 66 | console.log("Download progress:", progressObj) 67 | }) 68 | 69 | autoUpdater.on("update-downloaded", (info) => { 70 | console.log("Update downloaded:", info) 71 | // Notify renderer process that update is ready to install 72 | BrowserWindow.getAllWindows().forEach((window) => { 73 | console.log("Sending update-downloaded to window") 74 | window.webContents.send("update-downloaded", info) 75 | }) 76 | }) 77 | 78 | autoUpdater.on("error", (err) => { 79 | console.error("Auto updater error:", err) 80 | }) 81 | 82 | // Check for updates immediately 83 | console.log("Checking for updates...") 84 | autoUpdater 85 | .checkForUpdates() 86 | .then((result) => { 87 | console.log("Update check result:", result) 88 | }) 89 | .catch((err) => { 90 | console.error("Error checking for updates:", err) 91 | }) 92 | 93 | // Set up update checking interval (every 1 hour) 94 | setInterval(() => { 95 | console.log("Checking for updates (interval)...") 96 | autoUpdater 97 | .checkForUpdates() 98 | .then((result) => { 99 | console.log("Update check result (interval):", result) 100 | }) 101 | .catch((err) => { 102 | console.error("Error checking for updates (interval):", err) 103 | }) 104 | }, 60 * 60 * 1000) 105 | 106 | // Handle IPC messages from renderer 107 | ipcMain.handle("start-update", async () => { 108 | console.log("Start update requested") 109 | try { 110 | await autoUpdater.downloadUpdate() 111 | console.log("Update download completed") 112 | return { success: true } 113 | } catch (error) { 114 | console.error("Failed to start update:", error) 115 | return { success: false, error: error.message } 116 | } 117 | }) 118 | 119 | ipcMain.handle("install-update", () => { 120 | console.log("Install update requested") 121 | autoUpdater.quitAndInstall() 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /electron/ipcHandlers.ts: -------------------------------------------------------------------------------- 1 | // ipcHandlers.ts 2 | 3 | import { ipcMain, shell } from "electron" 4 | import { randomBytes } from "crypto" 5 | import { IIpcHandlerDeps } from "./main" 6 | 7 | export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { 8 | console.log("Initializing IPC handlers") 9 | 10 | // Credits handlers 11 | ipcMain.handle("set-initial-credits", async (_event, credits: number) => { 12 | const mainWindow = deps.getMainWindow() 13 | if (!mainWindow) return 14 | 15 | try { 16 | // Set the credits in a way that ensures atomicity 17 | await mainWindow.webContents.executeJavaScript( 18 | `window.__CREDITS__ = ${credits}` 19 | ) 20 | mainWindow.webContents.send("credits-updated", credits) 21 | } catch (error) { 22 | console.error("Error setting initial credits:", error) 23 | throw error 24 | } 25 | }) 26 | 27 | ipcMain.handle("decrement-credits", async () => { 28 | // No need to decrement credits since we're bypassing the credit system 29 | return 30 | }) 31 | 32 | // Screenshot queue handlers 33 | ipcMain.handle("get-screenshot-queue", () => { 34 | return deps.getScreenshotQueue() 35 | }) 36 | 37 | ipcMain.handle("get-extra-screenshot-queue", () => { 38 | return deps.getExtraScreenshotQueue() 39 | }) 40 | 41 | ipcMain.handle("delete-screenshot", async (event, path: string) => { 42 | return deps.deleteScreenshot(path) 43 | }) 44 | 45 | ipcMain.handle("get-image-preview", async (event, path: string) => { 46 | return deps.getImagePreview(path) 47 | }) 48 | 49 | // Screenshot processing handlers 50 | ipcMain.handle("process-screenshots", async () => { 51 | await deps.processingHelper?.processScreenshots() 52 | }) 53 | 54 | // Window dimension handlers 55 | ipcMain.handle( 56 | "update-content-dimensions", 57 | async (event, { width, height }: { width: number; height: number }) => { 58 | if (width && height) { 59 | deps.setWindowDimensions(width, height) 60 | } 61 | } 62 | ) 63 | 64 | ipcMain.handle( 65 | "set-window-dimensions", 66 | (event, width: number, height: number) => { 67 | deps.setWindowDimensions(width, height) 68 | } 69 | ) 70 | 71 | // Screenshot management handlers 72 | ipcMain.handle("get-screenshots", async () => { 73 | try { 74 | let previews = [] 75 | const currentView = deps.getView() 76 | 77 | if (currentView === "queue") { 78 | const queue = deps.getScreenshotQueue() 79 | previews = await Promise.all( 80 | queue.map(async (path) => ({ 81 | path, 82 | preview: await deps.getImagePreview(path) 83 | })) 84 | ) 85 | } else { 86 | const extraQueue = deps.getExtraScreenshotQueue() 87 | previews = await Promise.all( 88 | extraQueue.map(async (path) => ({ 89 | path, 90 | preview: await deps.getImagePreview(path) 91 | })) 92 | ) 93 | } 94 | 95 | return previews 96 | } catch (error) { 97 | console.error("Error getting screenshots:", error) 98 | throw error 99 | } 100 | }) 101 | 102 | // Screenshot trigger handlers 103 | ipcMain.handle("trigger-screenshot", async () => { 104 | const mainWindow = deps.getMainWindow() 105 | if (mainWindow) { 106 | try { 107 | const screenshotPath = await deps.takeScreenshot() 108 | const preview = await deps.getImagePreview(screenshotPath) 109 | mainWindow.webContents.send("screenshot-taken", { 110 | path: screenshotPath, 111 | preview 112 | }) 113 | return { success: true } 114 | } catch (error) { 115 | console.error("Error triggering screenshot:", error) 116 | return { error: "Failed to trigger screenshot" } 117 | } 118 | } 119 | return { error: "No main window available" } 120 | }) 121 | 122 | ipcMain.handle("take-screenshot", async () => { 123 | try { 124 | const screenshotPath = await deps.takeScreenshot() 125 | return { success: true, path: screenshotPath } 126 | } catch (error) { 127 | console.error("Error taking screenshot:", error) 128 | return { success: false, error: String(error) } 129 | } 130 | }) 131 | 132 | // Cancel processing handler 133 | ipcMain.handle("cancel-processing", () => { 134 | deps.processingHelper?.cancelProcessing() 135 | return { success: true } 136 | }) 137 | 138 | // External link handler 139 | ipcMain.handle("open-external-link", async (event, url: string) => { 140 | try { 141 | await shell.openExternal(url) 142 | return { success: true } 143 | } catch (error) { 144 | console.error("Error opening external link:", error) 145 | return { success: false, error: String(error) } 146 | } 147 | }) 148 | 149 | // Window management handlers 150 | ipcMain.handle("toggle-window", () => { 151 | try { 152 | deps.toggleMainWindow() 153 | return { success: true } 154 | } catch (error) { 155 | console.error("Error toggling window:", error) 156 | return { error: "Failed to toggle window" } 157 | } 158 | }) 159 | 160 | ipcMain.handle("reset-queues", async () => { 161 | try { 162 | deps.clearQueues() 163 | return { success: true } 164 | } catch (error) { 165 | console.error("Error resetting queues:", error) 166 | return { error: "Failed to reset queues" } 167 | } 168 | }) 169 | 170 | // Process screenshot handlers 171 | ipcMain.handle("trigger-process-screenshots", async () => { 172 | try { 173 | await deps.processingHelper?.processScreenshots() 174 | return { success: true } 175 | } catch (error) { 176 | console.error("Error processing screenshots:", error) 177 | return { error: "Failed to process screenshots" } 178 | } 179 | }) 180 | 181 | // Reset handlers 182 | ipcMain.handle("trigger-reset", () => { 183 | try { 184 | // First cancel any ongoing requests 185 | deps.processingHelper?.cancelOngoingRequests() 186 | 187 | // Clear all queues immediately 188 | deps.clearQueues() 189 | 190 | // Reset view to queue 191 | deps.setView("queue") 192 | 193 | // Get main window and send reset events 194 | const mainWindow = deps.getMainWindow() 195 | if (mainWindow && !mainWindow.isDestroyed()) { 196 | // Send reset events in sequence 197 | mainWindow.webContents.send("reset-view") 198 | mainWindow.webContents.send("reset") 199 | } 200 | 201 | return { success: true } 202 | } catch (error) { 203 | console.error("Error triggering reset:", error) 204 | return { error: "Failed to trigger reset" } 205 | } 206 | }) 207 | 208 | // Window movement handlers 209 | ipcMain.handle("trigger-move-left", () => { 210 | try { 211 | deps.moveWindowLeft() 212 | return { success: true } 213 | } catch (error) { 214 | console.error("Error moving window left:", error) 215 | return { error: "Failed to move window left" } 216 | } 217 | }) 218 | 219 | ipcMain.handle("trigger-move-right", () => { 220 | try { 221 | deps.moveWindowRight() 222 | return { success: true } 223 | } catch (error) { 224 | console.error("Error moving window right:", error) 225 | return { error: "Failed to move window right" } 226 | } 227 | }) 228 | 229 | ipcMain.handle("trigger-move-up", () => { 230 | try { 231 | deps.moveWindowUp() 232 | return { success: true } 233 | } catch (error) { 234 | console.error("Error moving window up:", error) 235 | return { error: "Failed to move window up" } 236 | } 237 | }) 238 | 239 | ipcMain.handle("trigger-move-down", () => { 240 | try { 241 | deps.moveWindowDown() 242 | return { success: true } 243 | } catch (error) { 244 | console.error("Error moving window down:", error) 245 | return { error: "Failed to move window down" } 246 | } 247 | }) 248 | } 249 | -------------------------------------------------------------------------------- /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 = process.env.NODE_ENV === "development" || !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: (hasDebugged: 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 | setHasDebugged: (value: boolean) => void 105 | moveWindowLeft: () => void 106 | moveWindowRight: () => void 107 | moveWindowUp: () => void 108 | moveWindowDown: () => void 109 | } 110 | 111 | // Initialize helpers 112 | function initializeHelpers() { 113 | state.screenshotHelper = new ScreenshotHelper(state.view) 114 | state.processingHelper = new ProcessingHelper({ 115 | getScreenshotHelper, 116 | getMainWindow, 117 | getView, 118 | setView, 119 | getProblemInfo, 120 | setProblemInfo, 121 | getScreenshotQueue, 122 | getExtraScreenshotQueue, 123 | clearQueues, 124 | takeScreenshot, 125 | getImagePreview, 126 | deleteScreenshot, 127 | setHasDebugged, 128 | getHasDebugged, 129 | PROCESSING_EVENTS: state.PROCESSING_EVENTS 130 | } as IProcessingHelperDeps) 131 | state.shortcutsHelper = new ShortcutsHelper({ 132 | getMainWindow, 133 | takeScreenshot, 134 | getImagePreview, 135 | processingHelper: state.processingHelper, 136 | clearQueues, 137 | setView, 138 | isVisible: () => state.isWindowVisible, 139 | toggleMainWindow, 140 | moveWindowLeft: () => 141 | moveWindowHorizontal((x) => 142 | Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) 143 | ), 144 | moveWindowRight: () => 145 | moveWindowHorizontal((x) => 146 | Math.min( 147 | state.screenWidth - (state.windowSize?.width || 0) / 2, 148 | x + state.step 149 | ) 150 | ), 151 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 152 | moveWindowDown: () => moveWindowVertical((y) => y + state.step) 153 | } as IShortcutsHelperDeps) 154 | } 155 | 156 | // Window management functions 157 | async function createWindow(): Promise { 158 | if (state.mainWindow) { 159 | if (state.mainWindow.isMinimized()) state.mainWindow.restore() 160 | state.mainWindow.focus() 161 | return 162 | } 163 | 164 | const primaryDisplay = screen.getPrimaryDisplay() 165 | const workArea = primaryDisplay.workAreaSize 166 | state.screenWidth = workArea.width 167 | state.screenHeight = workArea.height 168 | state.step = 60 169 | state.currentY = 50 170 | 171 | const windowSettings: Electron.BrowserWindowConstructorOptions = { 172 | height: 600, 173 | 174 | x: state.currentX, 175 | y: 50, 176 | alwaysOnTop: true, 177 | webPreferences: { 178 | nodeIntegration: false, 179 | contextIsolation: true, 180 | preload: isDev 181 | ? path.join(__dirname, "../dist-electron/preload.js") 182 | : path.join(__dirname, "preload.js"), 183 | scrollBounce: true 184 | }, 185 | show: true, 186 | frame: false, 187 | transparent: true, 188 | fullscreenable: false, 189 | hasShadow: false, 190 | backgroundColor: "#00000000", 191 | focusable: true, 192 | skipTaskbar: true, 193 | type: "panel", 194 | paintWhenInitiallyHidden: true, 195 | titleBarStyle: "hidden", 196 | enableLargerThanScreen: true, 197 | movable: true 198 | } 199 | 200 | state.mainWindow = new BrowserWindow(windowSettings) 201 | 202 | // Add more detailed logging for window events 203 | state.mainWindow.webContents.on("did-finish-load", () => { 204 | console.log("Window finished loading") 205 | }) 206 | state.mainWindow.webContents.on( 207 | "did-fail-load", 208 | async (event, errorCode, errorDescription) => { 209 | console.error("Window failed to load:", errorCode, errorDescription) 210 | // Always try to load the built files on failure 211 | console.log("Attempting to load built files...") 212 | setTimeout(() => { 213 | state.mainWindow?.loadFile(path.join(__dirname, "../dist/index.html")).catch((error) => { 214 | console.error("Failed to load built files on retry:", error) 215 | }) 216 | }, 1000) 217 | } 218 | ) 219 | 220 | // Load the app - always load from built files 221 | console.log("Loading application from built files...") 222 | state.mainWindow?.loadFile(path.join(__dirname, "../dist/index.html")).catch((error) => { 223 | console.error("Failed to load built files:", error) 224 | }) 225 | 226 | // Configure window behavior 227 | state.mainWindow.webContents.setZoomFactor(1) 228 | if (isDev) { 229 | state.mainWindow.webContents.openDevTools() 230 | } 231 | state.mainWindow.webContents.setWindowOpenHandler(({ url }) => { 232 | // Allow opening URLs in external browser 233 | shell.openExternal(url) 234 | return { action: "deny" } 235 | }) 236 | 237 | // Enhanced screen capture resistance 238 | state.mainWindow.setContentProtection(true) 239 | 240 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 241 | visibleOnFullScreen: true 242 | }) 243 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 244 | 245 | // Additional screen capture resistance settings 246 | if (process.platform === "darwin") { 247 | // Prevent window from being captured in screenshots 248 | state.mainWindow.setHiddenInMissionControl(true) 249 | state.mainWindow.setWindowButtonVisibility(false) 250 | state.mainWindow.setBackgroundColor("#00000000") 251 | 252 | // Prevent window from being included in window switcher 253 | state.mainWindow.setSkipTaskbar(true) 254 | 255 | // Disable window shadow 256 | state.mainWindow.setHasShadow(false) 257 | } 258 | 259 | // Prevent the window from being captured by screen recording 260 | state.mainWindow.webContents.setBackgroundThrottling(false) 261 | state.mainWindow.webContents.setFrameRate(60) 262 | 263 | // Set up window listeners 264 | state.mainWindow.on("move", handleWindowMove) 265 | state.mainWindow.on("resize", handleWindowResize) 266 | state.mainWindow.on("closed", handleWindowClosed) 267 | 268 | // Initialize window state 269 | const bounds = state.mainWindow.getBounds() 270 | state.windowPosition = { x: bounds.x, y: bounds.y } 271 | state.windowSize = { width: bounds.width, height: bounds.height } 272 | state.currentX = bounds.x 273 | state.currentY = bounds.y 274 | state.isWindowVisible = true 275 | } 276 | 277 | function handleWindowMove(): void { 278 | if (!state.mainWindow) return 279 | const bounds = state.mainWindow.getBounds() 280 | state.windowPosition = { x: bounds.x, y: bounds.y } 281 | state.currentX = bounds.x 282 | state.currentY = bounds.y 283 | } 284 | 285 | function handleWindowResize(): void { 286 | if (!state.mainWindow) return 287 | const bounds = state.mainWindow.getBounds() 288 | state.windowSize = { width: bounds.width, height: bounds.height } 289 | } 290 | 291 | function handleWindowClosed(): void { 292 | state.mainWindow = null 293 | state.isWindowVisible = false 294 | state.windowPosition = null 295 | state.windowSize = null 296 | } 297 | 298 | // Window visibility functions 299 | function hideMainWindow(): void { 300 | if (!state.mainWindow?.isDestroyed()) { 301 | const bounds = state.mainWindow.getBounds() 302 | state.windowPosition = { x: bounds.x, y: bounds.y } 303 | state.windowSize = { width: bounds.width, height: bounds.height } 304 | state.mainWindow.setIgnoreMouseEvents(true, { forward: true }) 305 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 306 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 307 | visibleOnFullScreen: true 308 | }) 309 | state.mainWindow.setOpacity(0) 310 | state.mainWindow.hide() 311 | state.isWindowVisible = false 312 | } 313 | } 314 | 315 | function showMainWindow(): void { 316 | if (!state.mainWindow?.isDestroyed()) { 317 | if (state.windowPosition && state.windowSize) { 318 | state.mainWindow.setBounds({ 319 | ...state.windowPosition, 320 | ...state.windowSize 321 | }) 322 | } 323 | state.mainWindow.setIgnoreMouseEvents(false) 324 | state.mainWindow.setAlwaysOnTop(true, "screen-saver", 1) 325 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 326 | visibleOnFullScreen: true 327 | }) 328 | state.mainWindow.setContentProtection(true) 329 | state.mainWindow.setOpacity(0) 330 | state.mainWindow.showInactive() 331 | state.mainWindow.setOpacity(1) 332 | state.isWindowVisible = true 333 | } 334 | } 335 | 336 | function toggleMainWindow(): void { 337 | state.isWindowVisible ? hideMainWindow() : showMainWindow() 338 | } 339 | 340 | // Window movement functions 341 | function moveWindowHorizontal(updateFn: (x: number) => number): void { 342 | if (!state.mainWindow) return 343 | state.currentX = updateFn(state.currentX) 344 | state.mainWindow.setPosition( 345 | Math.round(state.currentX), 346 | Math.round(state.currentY) 347 | ) 348 | } 349 | 350 | function moveWindowVertical(updateFn: (y: number) => number): void { 351 | if (!state.mainWindow) return 352 | 353 | const newY = updateFn(state.currentY) 354 | // Allow window to go 2/3 off screen in either direction 355 | const maxUpLimit = (-(state.windowSize?.height || 0) * 2) / 3 356 | const maxDownLimit = 357 | state.screenHeight + ((state.windowSize?.height || 0) * 2) / 3 358 | 359 | // Log the current state and limits 360 | console.log({ 361 | newY, 362 | maxUpLimit, 363 | maxDownLimit, 364 | screenHeight: state.screenHeight, 365 | windowHeight: state.windowSize?.height, 366 | currentY: state.currentY 367 | }) 368 | 369 | // Only update if within bounds 370 | if (newY >= maxUpLimit && newY <= maxDownLimit) { 371 | state.currentY = newY 372 | state.mainWindow.setPosition( 373 | Math.round(state.currentX), 374 | Math.round(state.currentY) 375 | ) 376 | } 377 | } 378 | 379 | // Window dimension functions 380 | function setWindowDimensions(width: number, height: number): void { 381 | if (!state.mainWindow?.isDestroyed()) { 382 | const [currentX, currentY] = state.mainWindow.getPosition() 383 | const primaryDisplay = screen.getPrimaryDisplay() 384 | const workArea = primaryDisplay.workAreaSize 385 | const maxWidth = Math.floor(workArea.width * 0.5) 386 | 387 | state.mainWindow.setBounds({ 388 | x: Math.min(currentX, workArea.width - maxWidth), 389 | y: currentY, 390 | width: Math.min(width + 32, maxWidth), 391 | height: Math.ceil(height) 392 | }) 393 | } 394 | } 395 | 396 | // Environment setup 397 | function loadEnvVariables() { 398 | try { 399 | dotenv.config() 400 | console.log("Environment variables loaded:", { 401 | NODE_ENV: process.env.NODE_ENV, 402 | // Remove Supabase references 403 | OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY ? "exists" : "missing" 404 | }) 405 | } catch (error) { 406 | console.error("Error loading environment variables:", error) 407 | } 408 | } 409 | 410 | // Initialize application 411 | async function initializeApp() { 412 | try { 413 | loadEnvVariables() 414 | initializeHelpers() 415 | initializeIpcHandlers({ 416 | getMainWindow, 417 | setWindowDimensions, 418 | getScreenshotQueue, 419 | getExtraScreenshotQueue, 420 | deleteScreenshot, 421 | getImagePreview, 422 | processingHelper: state.processingHelper, 423 | PROCESSING_EVENTS: state.PROCESSING_EVENTS, 424 | takeScreenshot, 425 | getView, 426 | toggleMainWindow, 427 | clearQueues, 428 | setView, 429 | setHasDebugged, 430 | moveWindowLeft: () => 431 | moveWindowHorizontal((x) => 432 | Math.max(-(state.windowSize?.width || 0) / 2, x - state.step) 433 | ), 434 | moveWindowRight: () => 435 | moveWindowHorizontal((x) => 436 | Math.min( 437 | state.screenWidth - (state.windowSize?.width || 0) / 2, 438 | x + state.step 439 | ) 440 | ), 441 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 442 | moveWindowDown: () => moveWindowVertical((y) => y + state.step) 443 | }) 444 | await createWindow() 445 | state.shortcutsHelper?.registerGlobalShortcuts() 446 | 447 | // Initialize auto-updater regardless of environment 448 | initAutoUpdater() 449 | console.log( 450 | "Auto-updater initialized in", 451 | isDev ? "development" : "production", 452 | "mode" 453 | ) 454 | } catch (error) { 455 | console.error("Failed to initialize application:", error) 456 | app.quit() 457 | } 458 | } 459 | 460 | // State getter/setter functions 461 | function getMainWindow(): BrowserWindow | null { 462 | return state.mainWindow 463 | } 464 | 465 | function getView(): "queue" | "solutions" | "debug" { 466 | return state.view 467 | } 468 | 469 | function setView(view: "queue" | "solutions" | "debug"): void { 470 | state.view = view 471 | state.screenshotHelper?.setView(view) 472 | } 473 | 474 | function getScreenshotHelper(): ScreenshotHelper | null { 475 | return state.screenshotHelper 476 | } 477 | 478 | function getProblemInfo(): any { 479 | return state.problemInfo 480 | } 481 | 482 | function setProblemInfo(problemInfo: any): void { 483 | state.problemInfo = problemInfo 484 | } 485 | 486 | function getScreenshotQueue(): string[] { 487 | return state.screenshotHelper?.getScreenshotQueue() || [] 488 | } 489 | 490 | function getExtraScreenshotQueue(): string[] { 491 | return state.screenshotHelper?.getExtraScreenshotQueue() || [] 492 | } 493 | 494 | function clearQueues(): void { 495 | state.screenshotHelper?.clearQueues() 496 | state.problemInfo = null 497 | setView("queue") 498 | } 499 | 500 | async function takeScreenshot(): Promise { 501 | if (!state.mainWindow) throw new Error("No main window available") 502 | return ( 503 | state.screenshotHelper?.takeScreenshot( 504 | () => hideMainWindow(), 505 | () => showMainWindow() 506 | ) || "" 507 | ) 508 | } 509 | 510 | async function getImagePreview(filepath: string): Promise { 511 | return state.screenshotHelper?.getImagePreview(filepath) || "" 512 | } 513 | 514 | async function deleteScreenshot( 515 | path: string 516 | ): Promise<{ success: boolean; error?: string }> { 517 | return ( 518 | state.screenshotHelper?.deleteScreenshot(path) || { 519 | success: false, 520 | error: "Screenshot helper not initialized" 521 | } 522 | ) 523 | } 524 | 525 | function setHasDebugged(value: boolean): void { 526 | state.hasDebugged = value 527 | } 528 | 529 | function getHasDebugged(): boolean { 530 | return state.hasDebugged 531 | } 532 | 533 | // Export state and functions for other modules 534 | export { 535 | state, 536 | createWindow, 537 | hideMainWindow, 538 | showMainWindow, 539 | toggleMainWindow, 540 | setWindowDimensions, 541 | moveWindowHorizontal, 542 | moveWindowVertical, 543 | getMainWindow, 544 | getView, 545 | setView, 546 | getScreenshotHelper, 547 | getProblemInfo, 548 | setProblemInfo, 549 | getScreenshotQueue, 550 | getExtraScreenshotQueue, 551 | clearQueues, 552 | takeScreenshot, 553 | getImagePreview, 554 | deleteScreenshot, 555 | setHasDebugged, 556 | getHasDebugged 557 | } 558 | 559 | app.whenReady().then(initializeApp) 560 | -------------------------------------------------------------------------------- /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 | updateContentDimensions: (dimensions: { 8 | width: number 9 | height: number 10 | }) => Promise 11 | clearStore: () => Promise<{ success: boolean; error?: string }> 12 | getScreenshots: () => Promise<{ 13 | success: boolean 14 | previews?: Array<{ path: string; preview: string }> | null 15 | error?: string 16 | }> 17 | deleteScreenshot: ( 18 | path: string 19 | ) => Promise<{ success: boolean; error?: string }> 20 | onScreenshotTaken: ( 21 | callback: (data: { path: string; preview: string }) => void 22 | ) => () => void 23 | onResetView: (callback: () => void) => () => void 24 | onSolutionStart: (callback: () => void) => () => void 25 | onDebugStart: (callback: () => void) => () => void 26 | onDebugSuccess: (callback: (data: any) => void) => () => void 27 | onSolutionError: (callback: (error: string) => void) => () => void 28 | onProcessingNoScreenshots: (callback: () => void) => () => void 29 | onProblemExtracted: (callback: (data: any) => void) => () => void 30 | onSolutionSuccess: (callback: (data: any) => void) => () => void 31 | onUnauthorized: (callback: () => void) => () => void 32 | onDebugError: (callback: (error: string) => void) => () => void 33 | openExternal: (url: string) => void 34 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 35 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 36 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 37 | triggerReset: () => Promise<{ success: boolean; error?: string }> 38 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 39 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 40 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 41 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 42 | startUpdate: () => Promise<{ success: boolean; error?: string }> 43 | installUpdate: () => void 44 | onUpdateAvailable: (callback: (info: any) => void) => () => void 45 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 46 | decrementCredits: () => Promise 47 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 48 | onOutOfCredits: (callback: () => void) => () => void 49 | getPlatform: () => string 50 | } 51 | 52 | export const PROCESSING_EVENTS = { 53 | //global states 54 | UNAUTHORIZED: "procesing-unauthorized", 55 | NO_SCREENSHOTS: "processing-no-screenshots", 56 | OUT_OF_CREDITS: "out-of-credits", 57 | 58 | //states for generating the initial solution 59 | INITIAL_START: "initial-start", 60 | PROBLEM_EXTRACTED: "problem-extracted", 61 | SOLUTION_SUCCESS: "solution-success", 62 | INITIAL_SOLUTION_ERROR: "solution-error", 63 | RESET: "reset", 64 | 65 | //states for processing the debugging 66 | DEBUG_START: "debug-start", 67 | DEBUG_SUCCESS: "debug-success", 68 | DEBUG_ERROR: "debug-error" 69 | } as const 70 | 71 | // At the top of the file 72 | console.log("Preload script is running") 73 | 74 | const electronAPI = { 75 | updateContentDimensions: (dimensions: { width: number; height: number }) => 76 | ipcRenderer.invoke("update-content-dimensions", dimensions), 77 | clearStore: () => ipcRenderer.invoke("clear-store"), 78 | getScreenshots: () => ipcRenderer.invoke("get-screenshots"), 79 | deleteScreenshot: (path: string) => 80 | ipcRenderer.invoke("delete-screenshot", path), 81 | toggleMainWindow: async () => { 82 | console.log("toggleMainWindow called from preload") 83 | try { 84 | const result = await ipcRenderer.invoke("toggle-window") 85 | console.log("toggle-window result:", result) 86 | return result 87 | } catch (error) { 88 | console.error("Error in toggleMainWindow:", error) 89 | throw error 90 | } 91 | }, 92 | // Event listeners 93 | onScreenshotTaken: ( 94 | callback: (data: { path: string; preview: string }) => void 95 | ) => { 96 | const subscription = (_: any, data: { path: string; preview: string }) => 97 | callback(data) 98 | ipcRenderer.on("screenshot-taken", subscription) 99 | return () => { 100 | ipcRenderer.removeListener("screenshot-taken", subscription) 101 | } 102 | }, 103 | onResetView: (callback: () => void) => { 104 | const subscription = () => callback() 105 | ipcRenderer.on("reset-view", subscription) 106 | return () => { 107 | ipcRenderer.removeListener("reset-view", subscription) 108 | } 109 | }, 110 | onSolutionStart: (callback: () => void) => { 111 | const subscription = () => callback() 112 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 113 | return () => { 114 | ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 115 | } 116 | }, 117 | onDebugStart: (callback: () => void) => { 118 | const subscription = () => callback() 119 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 120 | return () => { 121 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 122 | } 123 | }, 124 | onDebugSuccess: (callback: (data: any) => void) => { 125 | ipcRenderer.on("debug-success", (_event, data) => callback(data)) 126 | return () => { 127 | ipcRenderer.removeListener("debug-success", (_event, data) => 128 | callback(data) 129 | ) 130 | } 131 | }, 132 | onDebugError: (callback: (error: string) => void) => { 133 | const subscription = (_: any, error: string) => callback(error) 134 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 135 | return () => { 136 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 137 | } 138 | }, 139 | onSolutionError: (callback: (error: string) => void) => { 140 | const subscription = (_: any, error: string) => callback(error) 141 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 142 | return () => { 143 | ipcRenderer.removeListener( 144 | PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 145 | subscription 146 | ) 147 | } 148 | }, 149 | onProcessingNoScreenshots: (callback: () => void) => { 150 | const subscription = () => callback() 151 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 152 | return () => { 153 | ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 154 | } 155 | }, 156 | onOutOfCredits: (callback: () => void) => { 157 | const subscription = () => callback() 158 | ipcRenderer.on(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 159 | return () => { 160 | ipcRenderer.removeListener(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 161 | } 162 | }, 163 | onProblemExtracted: (callback: (data: any) => void) => { 164 | const subscription = (_: any, data: any) => callback(data) 165 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 166 | return () => { 167 | ipcRenderer.removeListener( 168 | PROCESSING_EVENTS.PROBLEM_EXTRACTED, 169 | subscription 170 | ) 171 | } 172 | }, 173 | onSolutionSuccess: (callback: (data: any) => void) => { 174 | const subscription = (_: any, data: any) => callback(data) 175 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 176 | return () => { 177 | ipcRenderer.removeListener( 178 | PROCESSING_EVENTS.SOLUTION_SUCCESS, 179 | subscription 180 | ) 181 | } 182 | }, 183 | onUnauthorized: (callback: () => void) => { 184 | const subscription = () => callback() 185 | ipcRenderer.on(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 186 | return () => { 187 | ipcRenderer.removeListener(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 188 | } 189 | }, 190 | openExternal: (url: string) => shell.openExternal(url), 191 | triggerScreenshot: () => ipcRenderer.invoke("trigger-screenshot"), 192 | triggerProcessScreenshots: () => 193 | ipcRenderer.invoke("trigger-process-screenshots"), 194 | triggerReset: () => ipcRenderer.invoke("trigger-reset"), 195 | triggerMoveLeft: () => ipcRenderer.invoke("trigger-move-left"), 196 | triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), 197 | triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), 198 | triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), 199 | startUpdate: () => ipcRenderer.invoke("start-update"), 200 | installUpdate: () => ipcRenderer.invoke("install-update"), 201 | onUpdateAvailable: (callback: (info: any) => void) => { 202 | const subscription = (_: any, info: any) => callback(info) 203 | ipcRenderer.on("update-available", subscription) 204 | return () => { 205 | ipcRenderer.removeListener("update-available", subscription) 206 | } 207 | }, 208 | onUpdateDownloaded: (callback: (info: any) => void) => { 209 | const subscription = (_: any, info: any) => callback(info) 210 | ipcRenderer.on("update-downloaded", subscription) 211 | return () => { 212 | ipcRenderer.removeListener("update-downloaded", subscription) 213 | } 214 | }, 215 | decrementCredits: () => ipcRenderer.invoke("decrement-credits"), 216 | onCreditsUpdated: (callback: (credits: number) => void) => { 217 | const subscription = (_event: any, credits: number) => callback(credits) 218 | ipcRenderer.on("credits-updated", subscription) 219 | return () => { 220 | ipcRenderer.removeListener("credits-updated", subscription) 221 | } 222 | }, 223 | getPlatform: () => process.platform 224 | } as ElectronAPI 225 | 226 | // Before exposing the API 227 | console.log( 228 | "About to expose electronAPI with methods:", 229 | Object.keys(electronAPI) 230 | ) 231 | 232 | // Add this focus restoration handler 233 | window.addEventListener("focus", () => { 234 | console.log("Window focused") 235 | }) 236 | 237 | // Expose the API to the renderer process 238 | contextBridge.exposeInMainWorld("electronAPI", electronAPI) 239 | 240 | // Expose platform info 241 | contextBridge.exposeInMainWorld("platform", process.platform) 242 | 243 | // Log that preload is complete 244 | console.log("Preload script completed") 245 | -------------------------------------------------------------------------------- /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 | // Remove Supabase environment variables 5 | readonly VITE_OPEN_AI_API_KEY: string 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | 12 | // Extend the Window interface 13 | interface Window { 14 | __CREDITS__: number 15 | __LANGUAGE__: string 16 | __IS_INITIALIZED__: boolean 17 | electronAPI: { 18 | // Remove subscription portal 19 | updateContentDimensions: (dimensions: { 20 | width: number 21 | height: number 22 | }) => Promise 23 | clearStore: () => Promise<{ success: boolean; error?: string }> 24 | getScreenshots: () => Promise 25 | deleteScreenshot: ( 26 | path: string 27 | ) => Promise<{ success: boolean; error?: string }> 28 | onScreenshotTaken: ( 29 | callback: (data: { path: string; preview: string }) => void 30 | ) => () => void 31 | onResetView: (callback: () => void) => () => void 32 | onSolutionStart: (callback: () => void) => () => void 33 | onDebugStart: (callback: () => void) => () => void 34 | onDebugSuccess: (callback: (data: any) => void) => () => void 35 | onSolutionError: (callback: (error: string) => void) => () => void 36 | onProcessingNoScreenshots: (callback: () => void) => () => void 37 | onProblemExtracted: (callback: (data: any) => void) => () => void 38 | onSolutionSuccess: (callback: (data: any) => void) => () => void 39 | onUnauthorized: (callback: () => void) => () => void 40 | onDebugError: (callback: (error: string) => void) => () => void 41 | openExternal: (url: string) => void 42 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 43 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 44 | triggerProcessScreenshots: () => Promise<{ 45 | success: boolean 46 | error?: string 47 | }> 48 | triggerReset: () => Promise<{ success: boolean; error?: string }> 49 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 50 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 51 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 52 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 53 | // Remove subscription callbacks 54 | startUpdate: () => Promise<{ success: boolean; error?: string }> 55 | installUpdate: () => void 56 | onUpdateAvailable: (callback: (info: any) => void) => () => void 57 | decrementCredits: () => Promise 58 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 59 | onOutOfCredits: (callback: () => void) => () => void 60 | getPlatform: () => string 61 | } 62 | electron?: { 63 | ipcRenderer: { 64 | on: (channel: string, func: (...args: any[]) => void) => void 65 | removeListener: (channel: string, func: (...args: any[]) => void) => void 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Interview Coder 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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=production npm run clean && vite build && tsc -p tsconfig.electron.json && electron .", 8 | "dev:prod": "cross-env NODE_ENV=production npm run clean && vite build && tsc -p tsconfig.electron.json && electron .", 9 | "build": "cross-env NODE_ENV=production rimraf dist dist-electron && vite build && tsc -p tsconfig.electron.json && electron-builder" 10 | }, 11 | "build": { 12 | "appId": "com.chunginlee.interviewcoder", 13 | "productName": "Interview Coder", 14 | "files": [ 15 | "dist/**/*", 16 | "dist-electron/**/*", 17 | "package.json", 18 | "electron/**/*" 19 | ], 20 | "directories": { 21 | "output": "release", 22 | "buildResources": "assets" 23 | }, 24 | "asar": true, 25 | "compression": "maximum", 26 | "generateUpdatesFilesForAllChannels": true, 27 | "mac": { 28 | "category": "public.app-category.developer-tools", 29 | "target": [ 30 | { 31 | "target": "dmg", 32 | "arch": [ 33 | "x64", 34 | "arm64" 35 | ] 36 | }, 37 | { 38 | "target": "zip", 39 | "arch": [ 40 | "x64", 41 | "arm64" 42 | ] 43 | } 44 | ], 45 | "artifactName": "Interview-Coder-${arch}.${ext}", 46 | "icon": "assets/icons/mac/icon.icns", 47 | "hardenedRuntime": true, 48 | "gatekeeperAssess": false, 49 | "entitlements": "build/entitlements.mac.plist", 50 | "entitlementsInherit": "build/entitlements.mac.plist", 51 | "identity": "Developer ID Application", 52 | "notarize": true, 53 | "protocols": { 54 | "name": "interview-coder-protocol", 55 | "schemes": [ 56 | "interview-coder" 57 | ] 58 | } 59 | }, 60 | "win": { 61 | "target": [ 62 | "nsis" 63 | ], 64 | "icon": "assets/icons/win/icon.ico", 65 | "artifactName": "${productName}-Windows-${version}.${ext}", 66 | "protocols": { 67 | "name": "interview-coder-protocol", 68 | "schemes": [ 69 | "interview-coder" 70 | ] 71 | } 72 | }, 73 | "linux": { 74 | "target": [ 75 | "AppImage" 76 | ], 77 | "icon": "assets/icons/png/icon-256x256.png", 78 | "artifactName": "${productName}-Linux-${version}.${ext}", 79 | "protocols": { 80 | "name": "interview-coder-protocol", 81 | "schemes": [ 82 | "interview-coder" 83 | ] 84 | } 85 | }, 86 | "publish": [ 87 | { 88 | "provider": "github", 89 | "owner": "ibttf", 90 | "repo": "interview-coder", 91 | "private": false, 92 | "releaseType": "release" 93 | } 94 | ], 95 | "extraResources": [ 96 | { 97 | "from": ".env", 98 | "to": ".env", 99 | "filter": [ 100 | "**/*" 101 | ] 102 | } 103 | ], 104 | "extraMetadata": { 105 | "main": "dist-electron/main.js" 106 | } 107 | }, 108 | "keywords": [], 109 | "author": "", 110 | "license": "ISC", 111 | "description": "An invisible desktop application to help you pass your technical interviews.", 112 | "dependencies": { 113 | "@electron/notarize": "^2.3.0", 114 | "@emotion/react": "^11.11.0", 115 | "@emotion/styled": "^11.11.0", 116 | "@radix-ui/react-dialog": "^1.1.2", 117 | "@radix-ui/react-label": "^2.1.0", 118 | "@radix-ui/react-slot": "^1.1.0", 119 | "@radix-ui/react-toast": "^1.2.2", 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.22.0", 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/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/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/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/renderer/public/logo192.png -------------------------------------------------------------------------------- /renderer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daydiff/interview-coder-no-sub/601b7172d179de3f9f1cab4cd24ff6f266354f8d/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/App.tsx: -------------------------------------------------------------------------------- 1 | import SubscribedApp from "./_pages/SubscribedApp" 2 | import { 3 | QueryClient, 4 | QueryClientProvider, 5 | useQueryClient 6 | } from "@tanstack/react-query" 7 | import { useEffect, useState, useCallback } from "react" 8 | import { 9 | Toast, 10 | ToastDescription, 11 | ToastProvider, 12 | ToastTitle, 13 | ToastViewport 14 | } from "./components/ui/toast" 15 | import { ToastContext } from "./contexts/toast" 16 | 17 | // Create a React Query client 18 | const queryClient = new QueryClient({ 19 | defaultOptions: { 20 | queries: { 21 | staleTime: 0, 22 | gcTime: Infinity, 23 | retry: 1, 24 | refetchOnWindowFocus: false 25 | }, 26 | mutations: { 27 | retry: 1 28 | } 29 | } 30 | }) 31 | 32 | // Root component that provides the QueryClient 33 | function App() { 34 | const [toastState, setToastState] = useState({ 35 | open: false, 36 | title: "", 37 | description: "", 38 | variant: "neutral" as const 39 | }) 40 | const [credits, setCredits] = useState(999) // Set a high default value 41 | const [currentLanguage, setCurrentLanguage] = useState("python") 42 | const [isInitialized, setIsInitialized] = useState(false) 43 | 44 | // Helper function to safely update credits 45 | const updateCredits = useCallback((newCredits: number) => { 46 | setCredits(newCredits) 47 | window.__CREDITS__ = newCredits 48 | }, []) 49 | 50 | // Helper function to safely update language 51 | const updateLanguage = useCallback((newLanguage: string) => { 52 | setCurrentLanguage(newLanguage) 53 | window.__LANGUAGE__ = newLanguage 54 | }, []) 55 | 56 | // Helper function to mark initialization complete 57 | const markInitialized = useCallback(() => { 58 | setIsInitialized(true) 59 | window.__IS_INITIALIZED__ = true 60 | }, []) 61 | 62 | // Show toast method 63 | const showToast = useCallback( 64 | ( 65 | title: string, 66 | description: string, 67 | variant: "neutral" | "success" | "error" 68 | ) => { 69 | setToastState({ 70 | open: true, 71 | title, 72 | description, 73 | variant 74 | }) 75 | }, 76 | [] 77 | ) 78 | 79 | // Initialize app with default values 80 | useEffect(() => { 81 | // Set default values 82 | updateCredits(999) // High number of credits 83 | updateLanguage("python") 84 | markInitialized() 85 | }, [updateCredits, updateLanguage, markInitialized]) 86 | 87 | // Close toast after delay 88 | useEffect(() => { 89 | if (toastState.open) { 90 | const timer = setTimeout(() => { 91 | setToastState((prev) => ({ ...prev, open: false })) 92 | }, 5000) 93 | return () => clearTimeout(timer) 94 | } 95 | }, [toastState.open]) 96 | 97 | // Render the main app directly without authentication check 98 | return ( 99 | 100 | 101 | 102 |
103 | 108 | 111 | setToastState((prev) => ({ ...prev, open })) 112 | } 113 | variant={toastState.variant} 114 | > 115 |
116 | {toastState.title && {toastState.title}} 117 | {toastState.description && ( 118 | {toastState.description} 119 | )} 120 |
121 |
122 | 123 |
124 |
125 |
126 |
127 | ) 128 | } 129 | 130 | export default App 131 | -------------------------------------------------------------------------------- /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/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 { useToast } from "../../contexts/toast" 4 | import { LanguageSelector } from "../shared/LanguageSelector" 5 | import { COMMAND_KEY } from "../../utils/platform" 6 | 7 | interface QueueCommandsProps { 8 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 9 | screenshotCount?: number 10 | credits: number 11 | currentLanguage: string 12 | setLanguage: (language: string) => void 13 | } 14 | 15 | const QueueCommands: React.FC = ({ 16 | onTooltipVisibilityChange, 17 | screenshotCount = 0, 18 | credits, 19 | currentLanguage, 20 | setLanguage 21 | }) => { 22 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 23 | const tooltipRef = useRef(null) 24 | const { showToast } = useToast() 25 | 26 | useEffect(() => { 27 | let tooltipHeight = 0 28 | if (tooltipRef.current && isTooltipVisible) { 29 | tooltipHeight = tooltipRef.current.offsetHeight + 10 30 | } 31 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 32 | }, [isTooltipVisible]) 33 | 34 | const handleMouseEnter = () => { 35 | setIsTooltipVisible(true) 36 | } 37 | 38 | const handleMouseLeave = () => { 39 | setIsTooltipVisible(false) 40 | } 41 | 42 | return ( 43 |
44 |
45 |
46 | {/* Screenshot */} 47 |
{ 50 | try { 51 | const result = await window.electronAPI.triggerScreenshot() 52 | if (!result.success) { 53 | console.error("Failed to take screenshot:", result.error) 54 | showToast("Error", "Failed to take screenshot", "error") 55 | } 56 | } catch (error) { 57 | console.error("Error taking screenshot:", error) 58 | showToast("Error", "Failed to take screenshot", "error") 59 | } 60 | }} 61 | > 62 | 63 | {screenshotCount === 0 64 | ? "Take first screenshot" 65 | : screenshotCount === 1 66 | ? "Take second screenshot" 67 | : "Reset first screenshot"} 68 | 69 |
70 | 73 | 76 |
77 |
78 | 79 | {/* Solve Command */} 80 | {screenshotCount > 0 && ( 81 |
{ 86 | if (credits <= 0) { 87 | showToast( 88 | "Out of Credits", 89 | "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", 90 | "error" 91 | ) 92 | return 93 | } 94 | 95 | try { 96 | const result = 97 | await window.electronAPI.triggerProcessScreenshots() 98 | if (!result.success) { 99 | console.error( 100 | "Failed to process screenshots:", 101 | result.error 102 | ) 103 | showToast("Error", "Failed to process screenshots", "error") 104 | } 105 | } catch (error) { 106 | console.error("Error processing screenshots:", error) 107 | showToast("Error", "Failed to process screenshots", "error") 108 | } 109 | }} 110 | > 111 |
112 | Solve 113 |
114 | 117 | 120 |
121 |
122 |
123 | )} 124 | 125 | {/* Separator */} 126 |
127 | 128 | {/* Settings with Tooltip */} 129 |
134 | {/* Gear icon */} 135 |
136 | 146 | 147 | 148 | 149 |
150 | 151 | {/* Tooltip Content */} 152 | {isTooltipVisible && ( 153 |
158 | {/* Add transparent bridge */} 159 |
160 |
161 |
162 |

Keyboard Shortcuts

163 |
164 | {/* Toggle Command */} 165 |
{ 168 | try { 169 | const result = 170 | await window.electronAPI.toggleMainWindow() 171 | if (!result.success) { 172 | console.error( 173 | "Failed to toggle window:", 174 | result.error 175 | ) 176 | showToast( 177 | "Error", 178 | "Failed to toggle window", 179 | "error" 180 | ) 181 | } 182 | } catch (error) { 183 | console.error("Error toggling window:", error) 184 | showToast( 185 | "Error", 186 | "Failed to toggle window", 187 | "error" 188 | ) 189 | } 190 | }} 191 | > 192 |
193 | Toggle Window 194 |
195 | 196 | {COMMAND_KEY} 197 | 198 | 199 | B 200 | 201 |
202 |
203 |

204 | Show or hide this window. 205 |

206 |
207 | 208 | {/* Screenshot Command */} 209 |
{ 212 | try { 213 | const result = 214 | await window.electronAPI.triggerScreenshot() 215 | if (!result.success) { 216 | console.error( 217 | "Failed to take screenshot:", 218 | result.error 219 | ) 220 | showToast( 221 | "Error", 222 | "Failed to take screenshot", 223 | "error" 224 | ) 225 | } 226 | } catch (error) { 227 | console.error("Error taking screenshot:", error) 228 | showToast( 229 | "Error", 230 | "Failed to take screenshot", 231 | "error" 232 | ) 233 | } 234 | }} 235 | > 236 |
237 | Take Screenshot 238 |
239 | 240 | {COMMAND_KEY} 241 | 242 | 243 | H 244 | 245 |
246 |
247 |

248 | Take a screenshot of the problem description. 249 |

250 |
251 | 252 | {/* Solve Command */} 253 |
0 256 | ? "" 257 | : "opacity-50 cursor-not-allowed" 258 | }`} 259 | onClick={async () => { 260 | if (screenshotCount === 0) return 261 | 262 | try { 263 | const result = 264 | await window.electronAPI.triggerProcessScreenshots() 265 | if (!result.success) { 266 | console.error( 267 | "Failed to process screenshots:", 268 | result.error 269 | ) 270 | showToast( 271 | "Error", 272 | "Failed to process screenshots", 273 | "error" 274 | ) 275 | } 276 | } catch (error) { 277 | console.error( 278 | "Error processing screenshots:", 279 | error 280 | ) 281 | showToast( 282 | "Error", 283 | "Failed to process screenshots", 284 | "error" 285 | ) 286 | } 287 | }} 288 | > 289 |
290 | Solve 291 |
292 | 293 | {COMMAND_KEY} 294 | 295 | 296 | ↵ 297 | 298 |
299 |
300 |

301 | {screenshotCount > 0 302 | ? "Generate a solution based on the current problem." 303 | : "Take a screenshot first to generate a solution."} 304 |

305 |
306 |
307 | 308 | {/* Language Selector only */} 309 |
310 | 314 |
315 |
316 |
317 |
318 | )} 319 |
320 |
321 |
322 |
323 | ) 324 | } 325 | 326 | export default QueueCommands 327 | -------------------------------------------------------------------------------- /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/Solutions/SolutionCommands.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { useToast } from "../../contexts/toast" 3 | import { Screenshot } from "../../types/screenshots" 4 | import { LanguageSelector } from "../shared/LanguageSelector" 5 | import { COMMAND_KEY } from "../../utils/platform" 6 | 7 | export interface SolutionCommandsProps { 8 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 9 | isProcessing: boolean 10 | screenshots?: Screenshot[] 11 | extraScreenshots?: Screenshot[] 12 | credits: number 13 | currentLanguage: string 14 | setLanguage: (language: string) => void 15 | } 16 | 17 | const SolutionCommands: React.FC = ({ 18 | onTooltipVisibilityChange, 19 | isProcessing, 20 | extraScreenshots = [], 21 | credits, 22 | currentLanguage, 23 | setLanguage 24 | }) => { 25 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 26 | const tooltipRef = useRef(null) 27 | const { showToast } = useToast() 28 | 29 | useEffect(() => { 30 | let tooltipHeight = 0 31 | if (tooltipRef.current && isTooltipVisible) { 32 | tooltipHeight = tooltipRef.current.offsetHeight + 10 33 | } 34 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 35 | }, [isTooltipVisible]) 36 | 37 | const handleMouseEnter = () => { 38 | setIsTooltipVisible(true) 39 | } 40 | 41 | const handleMouseLeave = () => { 42 | setIsTooltipVisible(false) 43 | } 44 | 45 | return ( 46 |
47 |
48 |
49 | {/* Show/Hide - Always visible */} 50 |
{ 53 | try { 54 | const result = await window.electronAPI.toggleMainWindow() 55 | if (!result.success) { 56 | console.error("Failed to toggle window:", result.error) 57 | showToast("Error", "Failed to toggle window", "error") 58 | } 59 | } catch (error) { 60 | console.error("Error toggling window:", error) 61 | showToast("Error", "Failed to toggle window", "error") 62 | } 63 | }} 64 | > 65 | Show/Hide 66 |
67 | 70 | 73 |
74 |
75 | 76 | {/* Screenshot and Debug commands - Only show if not processing */} 77 | {!isProcessing && ( 78 | <> 79 |
{ 82 | try { 83 | const result = await window.electronAPI.triggerScreenshot() 84 | if (!result.success) { 85 | console.error("Failed to take screenshot:", result.error) 86 | showToast("Error", "Failed to take screenshot", "error") 87 | } 88 | } catch (error) { 89 | console.error("Error taking screenshot:", error) 90 | showToast("Error", "Failed to take screenshot", "error") 91 | } 92 | }} 93 | > 94 | 95 | {extraScreenshots.length === 0 96 | ? "Screenshot your code" 97 | : "Screenshot"} 98 | 99 |
100 | 103 | 106 |
107 |
108 | 109 | {extraScreenshots.length > 0 && ( 110 |
{ 113 | try { 114 | const result = 115 | await window.electronAPI.triggerProcessScreenshots() 116 | if (!result.success) { 117 | console.error( 118 | "Failed to process screenshots:", 119 | result.error 120 | ) 121 | showToast( 122 | "Error", 123 | "Failed to process screenshots", 124 | "error" 125 | ) 126 | } 127 | } catch (error) { 128 | console.error("Error processing screenshots:", error) 129 | showToast( 130 | "Error", 131 | "Failed to process screenshots", 132 | "error" 133 | ) 134 | } 135 | }} 136 | > 137 | Debug 138 |
139 | 142 | 145 |
146 |
147 | )} 148 | 149 | )} 150 | 151 | {/* Start Over - Always visible */} 152 |
{ 155 | try { 156 | const result = await window.electronAPI.triggerReset() 157 | if (!result.success) { 158 | console.error("Failed to reset:", result.error) 159 | showToast("Error", "Failed to reset", "error") 160 | } 161 | } catch (error) { 162 | console.error("Error resetting:", error) 163 | showToast("Error", "Failed to reset", "error") 164 | } 165 | }} 166 | > 167 | Start Over 168 |
169 | 172 | 175 |
176 |
177 | 178 | {/* Separator */} 179 |
180 | 181 | {/* Settings with Tooltip */} 182 |
187 | {/* Gear icon */} 188 |
189 | 199 | 200 | 201 | 202 |
203 | 204 | {/* Tooltip Content */} 205 | {isTooltipVisible && ( 206 |
211 | {/* Add transparent bridge */} 212 |
213 |
214 |
215 |

216 | Keyboard Shortcuts 217 |

218 |
219 | {/* Show/Hide - Always visible */} 220 |
{ 223 | try { 224 | const result = 225 | await window.electronAPI.toggleMainWindow() 226 | if (!result.success) { 227 | console.error( 228 | "Failed to toggle window:", 229 | result.error 230 | ) 231 | showToast( 232 | "Error", 233 | "Failed to toggle window", 234 | "error" 235 | ) 236 | } 237 | } catch (error) { 238 | console.error("Error toggling window:", error) 239 | showToast( 240 | "Error", 241 | "Failed to toggle window", 242 | "error" 243 | ) 244 | } 245 | }} 246 | > 247 |
248 | Toggle Window 249 |
250 | 251 | {COMMAND_KEY} 252 | 253 | 254 | B 255 | 256 |
257 |
258 |

259 | Show or hide this window. 260 |

261 |
262 | 263 | {/* Screenshot and Debug commands - Only show if not processing */} 264 | {!isProcessing && ( 265 | <> 266 |
{ 269 | try { 270 | const result = 271 | await window.electronAPI.triggerScreenshot() 272 | if (!result.success) { 273 | console.error( 274 | "Failed to take screenshot:", 275 | result.error 276 | ) 277 | showToast( 278 | "Error", 279 | "Failed to take screenshot", 280 | "error" 281 | ) 282 | } 283 | } catch (error) { 284 | console.error("Error taking screenshot:", error) 285 | showToast( 286 | "Error", 287 | "Failed to take screenshot", 288 | "error" 289 | ) 290 | } 291 | }} 292 | > 293 |
294 | Take Screenshot 295 |
296 | 297 | {COMMAND_KEY} 298 | 299 | 300 | H 301 | 302 |
303 |
304 |

305 | Capture additional parts of the question or your 306 | solution for debugging help. 307 |

308 |
309 | 310 | {extraScreenshots.length > 0 && ( 311 |
{ 314 | try { 315 | const result = 316 | await window.electronAPI.triggerProcessScreenshots() 317 | if (!result.success) { 318 | console.error( 319 | "Failed to process screenshots:", 320 | result.error 321 | ) 322 | showToast( 323 | "Error", 324 | "Failed to process screenshots", 325 | "error" 326 | ) 327 | } 328 | } catch (error) { 329 | console.error( 330 | "Error processing screenshots:", 331 | error 332 | ) 333 | showToast( 334 | "Error", 335 | "Failed to process screenshots", 336 | "error" 337 | ) 338 | } 339 | }} 340 | > 341 |
342 | Debug 343 |
344 | 345 | {COMMAND_KEY} 346 | 347 | 348 | ↵ 349 | 350 |
351 |
352 |

353 | Generate new solutions based on all previous and 354 | newly added screenshots. 355 |

356 |
357 | )} 358 | 359 | )} 360 | 361 | {/* Start Over - Always visible */} 362 |
{ 365 | try { 366 | const result = 367 | await window.electronAPI.triggerReset() 368 | if (!result.success) { 369 | console.error("Failed to reset:", result.error) 370 | showToast("Error", "Failed to reset", "error") 371 | } 372 | } catch (error) { 373 | console.error("Error resetting:", error) 374 | showToast("Error", "Failed to reset", "error") 375 | } 376 | }} 377 | > 378 |
379 | Start Over 380 |
381 | 382 | {COMMAND_KEY} 383 | 384 | 385 | R 386 | 387 |
388 |
389 |

390 | Start fresh with a new question. 391 |

392 |
393 |
394 | 395 | {/* Language Selector only */} 396 |
397 | 401 |
402 |
403 |
404 |
405 | )} 406 |
407 |
408 |
409 |
410 | ) 411 | } 412 | 413 | export default SolutionCommands 414 | -------------------------------------------------------------------------------- /src/components/UpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { Dialog, DialogContent, DialogTitle, DialogDescription } 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 | let unsubscribeAvailable = () => {} 16 | let unsubscribeDownloaded = () => {} 17 | 18 | try { 19 | if (window.electronAPI.onUpdateAvailable) { 20 | unsubscribeAvailable = window.electronAPI.onUpdateAvailable( 21 | (info) => { 22 | console.log("UpdateNotification: Update available received", info) 23 | setUpdateAvailable(true) 24 | } 25 | ) 26 | } 27 | 28 | if (window.electronAPI.onUpdateDownloaded) { 29 | unsubscribeDownloaded = window.electronAPI.onUpdateDownloaded( 30 | (info) => { 31 | console.log("UpdateNotification: Update downloaded received", info) 32 | setUpdateDownloaded(true) 33 | setIsDownloading(false) 34 | } 35 | ) 36 | } 37 | } catch (error) { 38 | console.error("Error setting up update listeners:", error) 39 | } 40 | 41 | return () => { 42 | console.log("UpdateNotification: Cleaning up event listeners") 43 | try { 44 | unsubscribeAvailable() 45 | unsubscribeDownloaded() 46 | } catch (error) { 47 | console.error("Error cleaning up update listeners:", error) 48 | } 49 | } 50 | }, []) 51 | 52 | const handleStartUpdate = async () => { 53 | console.log("UpdateNotification: Starting update download") 54 | setIsDownloading(true) 55 | try { 56 | const result = await window.electronAPI.startUpdate() 57 | console.log("UpdateNotification: Update download result", result) 58 | if (!result.success) { 59 | setIsDownloading(false) 60 | showToast("Error", "Failed to download update", "error") 61 | } 62 | } catch (error) { 63 | console.error("Error starting update:", error) 64 | setIsDownloading(false) 65 | showToast("Error", "Failed to download update", "error") 66 | } 67 | } 68 | 69 | const handleInstallUpdate = () => { 70 | console.log("UpdateNotification: Installing update") 71 | try { 72 | window.electronAPI.installUpdate() 73 | } catch (error) { 74 | console.error("Error installing update:", error) 75 | showToast("Error", "Failed to install update", "error") 76 | } 77 | } 78 | 79 | console.log("UpdateNotification: Render state", { 80 | updateAvailable, 81 | updateDownloaded, 82 | isDownloading 83 | }) 84 | if (!updateAvailable && !updateDownloaded) return null 85 | 86 | return ( 87 | 88 | e.preventDefault()} 91 | > 92 | 93 | {updateDownloaded 94 | ? "Update Ready to Install" 95 | : "A New Version is Available"} 96 | 97 | 98 | {updateDownloaded 99 | ? "The update has been downloaded and will be installed when you restart the app." 100 | : "A new version of Interview Coder is available. Please update to continue using the app."} 101 | 102 |
103 | {updateDownloaded ? ( 104 | 111 | ) : ( 112 | 120 | )} 121 |
122 |
123 |
124 | ) 125 | } 126 | -------------------------------------------------------------------------------- /src/components/shared/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | interface LanguageSelectorProps { 4 | currentLanguage: string 5 | setLanguage: (language: string) => void 6 | } 7 | 8 | export const LanguageSelector: React.FC = ({ 9 | currentLanguage, 10 | setLanguage 11 | }) => { 12 | const handleLanguageChange = async ( 13 | e: React.ChangeEvent 14 | ) => { 15 | const newLanguage = e.target.value 16 | try { 17 | // Just update the language locally 18 | setLanguage(newLanguage) 19 | window.__LANGUAGE__ = newLanguage 20 | } catch (error) { 21 | console.error("Error updating language preference:", error) 22 | } 23 | } 24 | 25 | return ( 26 |
27 |
28 | Language 29 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /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 DialogTitle = React.forwardRef< 45 | React.ElementRef, 46 | React.ComponentPropsWithoutRef 47 | >(({ className, ...props }, ref) => ( 48 | 53 | )) 54 | DialogTitle.displayName = DialogPrimitive.Title.displayName 55 | 56 | const DialogDescription = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 65 | )) 66 | DialogDescription.displayName = DialogPrimitive.Description.displayName 67 | 68 | const DialogClose = DialogPrimitive.Close 69 | 70 | export { Dialog, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogDescription } 71 | -------------------------------------------------------------------------------- /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 | // Mock Supabase client for the application to run without authentication 2 | 3 | // Create a mock client that doesn't do anything 4 | export const supabase = { 5 | auth: { 6 | getUser: async () => ({ data: { user: null }, error: null }), 7 | signInWithOAuth: async () => ({ data: null, error: null }), 8 | signInWithPassword: async () => ({ data: null, error: null }), 9 | signUp: async () => ({ data: null, error: null }), 10 | signOut: async () => ({ error: null }), 11 | onAuthStateChange: () => { 12 | return { data: { subscription: { unsubscribe: () => {} } } } 13 | }, 14 | exchangeCodeForSession: async () => ({ data: null, error: null }) 15 | }, 16 | from: () => ({ 17 | select: () => ({ 18 | eq: () => ({ 19 | single: async () => ({ data: null, error: null }), 20 | maybeSingle: async () => ({ data: null, error: null }) 21 | }), 22 | maybeSingle: async () => ({ data: null, error: null }) 23 | }), 24 | update: () => ({ 25 | eq: () => ({ 26 | select: () => ({ 27 | single: async () => ({ data: null, error: null }) 28 | }) 29 | }) 30 | }) 31 | }), 32 | channel: () => ({ 33 | on: () => ({ 34 | subscribe: () => ({ 35 | unsubscribe: () => {} 36 | }) 37 | }), 38 | subscribe: () => ({ 39 | unsubscribe: () => {} 40 | }), 41 | unsubscribe: () => {} 42 | }) 43 | } 44 | 45 | // Mock sign in function 46 | export const signInWithGoogle = async () => { 47 | console.log("Mock Google sign in...") 48 | return { url: "#" } 49 | } 50 | 51 | let channel: ReturnType | null = null 52 | 53 | // Monitor auth state changes and manage realtime connection 54 | supabase.auth.onAuthStateChange((event, session) => { 55 | console.log("Auth state changed:", event, session?.user?.id) 56 | console.log("Full session data:", session) 57 | 58 | if (event === "SIGNED_IN" && session) { 59 | // Only establish realtime connection after successful sign in 60 | console.log("Establishing realtime connection...") 61 | 62 | // Clean up existing channel if any 63 | if (channel) { 64 | channel.unsubscribe() 65 | } 66 | 67 | channel = supabase.channel("system", { 68 | config: { 69 | presence: { 70 | key: session.user.id 71 | } 72 | } 73 | }) 74 | 75 | channel 76 | .on("system", { event: "*" }, (payload) => { 77 | console.log("System event:", payload) 78 | }) 79 | .subscribe((status) => { 80 | console.log("Realtime subscription status:", status) 81 | if (status === "SUBSCRIBED") { 82 | console.log("Successfully connected to realtime system") 83 | } 84 | if (status === "CHANNEL_ERROR") { 85 | console.error("Realtime connection error - will retry in 5s") 86 | setTimeout(() => { 87 | channel?.subscribe() 88 | }, 5000) 89 | } 90 | }) 91 | } 92 | 93 | if (event === "SIGNED_OUT") { 94 | // Clean up realtime connection on sign out 95 | if (channel) { 96 | console.log("Cleaning up realtime connection") 97 | channel.unsubscribe() 98 | channel = null 99 | } 100 | } 101 | }) 102 | -------------------------------------------------------------------------------- /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 | import { HashRouter } from "react-router-dom" 6 | 7 | // Use HashRouter which is more compatible with Electron 8 | ReactDOM.createRoot(document.getElementById("root")!).render( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /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_OPEN_AI_API_KEY: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } 10 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------