├── src ├── types │ ├── global.d.ts │ ├── screenshots.ts │ ├── index.tsx │ ├── solutions.ts │ └── electron.d.ts ├── lib │ ├── utils.ts │ └── supabase.ts ├── main.tsx ├── vite-env.d.ts ├── contexts │ └── toast.tsx ├── utils │ └── platform.ts ├── components │ ├── ui │ │ ├── input.tsx │ │ ├── dialog.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── toast.tsx │ ├── Queue │ │ ├── ScreenshotQueue.tsx │ │ ├── ScreenshotItem.tsx │ │ └── QueueCommands.tsx │ ├── shared │ │ └── LanguageSelector.tsx │ └── UpdateNotification.tsx ├── index.css ├── env.d.ts └── _pages │ ├── SubscribedApp.tsx │ ├── Queue.tsx │ ├── SubscribePage.tsx │ ├── Debug.tsx │ └── Solutions.tsx ├── backend ├── .gitignore ├── tsconfig.json ├── src │ ├── controllers │ │ ├── index.ts │ │ ├── extract.controller.ts │ │ ├── generate.controller.ts │ │ └── debug.controller.ts │ ├── configs │ │ ├── claude.ts │ │ └── openai.ts │ ├── types │ │ ├── solution.ts │ │ ├── debug.ts │ │ └── problem-info.ts │ └── index.ts └── README.md ├── renderer ├── src │ ├── react-app-env.d.ts │ ├── setupTests.ts │ ├── App.test.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── index.tsx │ ├── App.tsx │ ├── App.css │ └── logo.svg ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── .env.local.sample ├── assets └── icons │ ├── win │ └── icon.ico │ └── mac │ └── icon.icns ├── postcss.config.js ├── supabase ├── .gitignore ├── migrations │ └── 20250304092058_subscriptions_table.sql └── config.toml ├── .env.sample ├── .gitignore ├── tsconfig.node.json ├── env.d.ts ├── backend.Dockerfile ├── .gitattributes ├── electron ├── store.ts ├── tsconfig.json ├── shortcuts.ts ├── autoUpdater.ts ├── ScreenshotHelper.ts ├── ipcHandlers.ts ├── preload.ts └── ProcessingHelper.ts ├── index.html ├── tsconfig.electron.json ├── compose.yaml ├── tsconfig.json ├── vite.config.ts ├── tailwind.config.js ├── README.md └── package.json /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /renderer/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /renderer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/types/screenshots.ts: -------------------------------------------------------------------------------- 1 | export interface Screenshot { 2 | path: string 3 | preview: string 4 | } 5 | -------------------------------------------------------------------------------- /.env.local.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="your openai key" 2 | ANTHROPIC_API_KEY="your anthropic key" 3 | BACKEND_PORT=8000 -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woaitsAryan/interview-coder-self-host/HEAD/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woaitsAryan/interview-coder-self-host/HEAD/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /renderer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woaitsAryan/interview-coder-self-host/HEAD/renderer/public/favicon.ico -------------------------------------------------------------------------------- /renderer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woaitsAryan/interview-coder-self-host/HEAD/renderer/public/logo192.png -------------------------------------------------------------------------------- /renderer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woaitsAryan/interview-coder-self-host/HEAD/renderer/public/logo512.png -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | VITE_SUPABASE_URL="your supabase url" 2 | VITE_SUPABASE_ANON_KEY="your supabase anon key" 3 | VITE_BACKEND_URL="http://localhost:8000" -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "hono/jsx" 6 | } 7 | } -------------------------------------------------------------------------------- /backend/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug.controller' 2 | export * from './extract.controller' 3 | export * from './generate.controller' 4 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | To install dependencies: 2 | ```sh 3 | bun install 4 | ``` 5 | 6 | To run: 7 | ```sh 8 | bun run dev 9 | ``` 10 | 11 | open http://localhost:3000 12 | -------------------------------------------------------------------------------- /backend/src/configs/claude.ts: -------------------------------------------------------------------------------- 1 | import Anthropic from '@anthropic-ai/sdk'; 2 | 3 | export const anthropic = new Anthropic({ 4 | apiKey: process.env['ANTHROPIC_API_KEY'] 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /backend/src/configs/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | 3 | const openai = new OpenAI({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | }); 6 | 7 | export default openai; 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist-electron 3 | release 4 | dist 5 | .env 6 | .env.local 7 | **/.DS_Store 8 | **/.vscode 9 | **/.idea 10 | **/package-lock.json 11 | scripts/ 12 | **/scripts/ 13 | scripts/manual-notarize.js -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | readonly VITE_SUPABASE_ANON_KEY: string 6 | readonly VITE_BACKEND_URL: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | 6 | readonly VITE_SUPABASE_ANON_KEY: string 7 | readonly VITE_BACKEND_URL: string 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/types/solution.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const SolutionSchema = z.object({ 4 | code: z.string(), 5 | thoughts: z.array(z.string()), 6 | time_complexity: z.string(), 7 | space_complexity: z.string() 8 | }) 9 | 10 | export type Solution = z.infer 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1.2-alpine AS base 2 | 3 | FROM base AS builder 4 | 5 | WORKDIR /app 6 | 7 | COPY bun.lock package.json ./ 8 | 9 | RUN bun install 10 | 11 | COPY backend/ backend/ 12 | 13 | RUN bun run backend:build 14 | 15 | FROM base AS runner 16 | 17 | COPY --from=builder /app/backend/dist ./dist 18 | 19 | CMD ["bun", "run", "dist/index.js"] -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /supabase/migrations/20250304092058_subscriptions_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS subscriptions ( 2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | user_id UUID NOT NULL REFERENCES auth.users(id), 4 | credits INTEGER NOT NULL DEFAULT 100000, 5 | preferred_language TEXT NOT NULL DEFAULT 'python', 6 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), 7 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() 8 | ); -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Interview Coder 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { logger } from 'hono/logger' 3 | 4 | import { extractController, generateController, debugController } from './controllers' 5 | 6 | const app = new Hono() 7 | 8 | app.use(logger()) 9 | 10 | app.post('/api/debug', debugController) 11 | app.post('/api/generate', generateController) 12 | app.post('/api/extract', extractController) 13 | 14 | app.get('/ping', (c) => c.json({ status: 'ok' })) 15 | 16 | export default { 17 | port: process.env.BACKEND_PORT || 8000, 18 | fetch: app.fetch, 19 | }; -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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' -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | backend-interview-coder: 3 | build: 4 | context: . 5 | dockerfile: backend.Dockerfile 6 | container_name: backend-interview-coder 7 | restart: always 8 | env_file: 9 | - .env.local 10 | ports: 11 | - ${BACKEND_PORT}:${BACKEND_PORT} 12 | healthcheck: 13 | test: ["CMD", "wget", "--spider", "http://127.0.0.1:${BACKEND_PORT}/ping"] 14 | interval: 1m 15 | timeout: 5s 16 | retries: 3 17 | start_period: 10s 18 | logging: 19 | driver: "json-file" 20 | options: 21 | max-size: "200k" 22 | max-file: "10" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/types/debug.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { ProblemInfoSchema } from "./problem-info"; 3 | 4 | export const DebugInputSchema = z.object({ 5 | imageDataList: z.array(z.string()), 6 | problemInfo: z.object({ 7 | problemInfo: ProblemInfoSchema, 8 | }), 9 | language: z.string() 10 | }) 11 | 12 | export const DebugOutputSchema = z.object({ 13 | new_code: z.string(), 14 | thoughts: z.array(z.string()), 15 | time_complexity: z.string(), 16 | space_complexity: z.string() 17 | }) 18 | 19 | export type DebugInput = z.infer 20 | export type DebugOutput = z.infer -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/types/problem-info.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const ProblemInfoSchema = z.object({ 4 | title: z.string().optional(), 5 | description: z.string(), 6 | constraints: z.string().optional(), 7 | examples: z.array(z.object({ 8 | input: z.string(), 9 | output: z.string(), 10 | explanation: z.string().optional(), 11 | })).optional(), 12 | problemType: z.string().optional(), 13 | difficulty: z.string().optional(), 14 | sourceUrl: z.string().optional(), 15 | codeSnippets: z.array(z.object({ 16 | language: z.string(), 17 | code: z.string(), 18 | })).optional(), 19 | }) 20 | 21 | 22 | export const GenerateInputSchema = z.object({ 23 | language: z.string(), 24 | problemInfo: ProblemInfoSchema, 25 | }) 26 | 27 | export const ExtractInputSchema = z.object({ 28 | imageDataList: z.array(z.string()), 29 | language: z.string(), 30 | }) 31 | 32 | export type GenerateInput = z.infer 33 | export type ProblemInfo = z.infer 34 | export type ExtractInput = z.infer 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ui/dialog.tsx 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { cn } from "../../lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | const DialogTrigger = DialogPrimitive.Trigger 9 | const DialogPortal = DialogPrimitive.Portal 10 | 11 | const DialogOverlay = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 22 | 23 | const DialogContent = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | 29 | 38 | {children} 39 | 40 | 41 | )) 42 | DialogContent.displayName = DialogPrimitive.Content.displayName 43 | 44 | const DialogClose = DialogPrimitive.Close 45 | 46 | export { Dialog, DialogTrigger, DialogContent, DialogClose } 47 | -------------------------------------------------------------------------------- /backend/src/controllers/extract.controller.ts: -------------------------------------------------------------------------------- 1 | import openai from "../configs/openai" 2 | import { zodResponseFormat } from "openai/helpers/zod"; 3 | import { ExtractInput, ExtractInputSchema, ProblemInfoSchema } from "../types/problem-info"; 4 | import type { Context } from "hono"; 5 | 6 | export async function extractController(c: Context) { 7 | const body = await c.req.json() 8 | 9 | const { imageDataList, language } = ExtractInputSchema.parse(body) 10 | 11 | const extractedProblemInfo = await extractProblemInfo({ imageDataList, language }) 12 | 13 | return c.json({ 14 | problemInfo: extractedProblemInfo, 15 | success: true 16 | }) 17 | } 18 | 19 | async function extractProblemInfo(data: ExtractInput) { 20 | const { imageDataList, language } = data 21 | 22 | const prompt = ` 23 | You are a helpful assistant that extracts problem information from a given image. The language to solve the problem is ${language}. 24 | ` 25 | 26 | const response = await openai.chat.completions.create({ 27 | model: "gpt-4o", 28 | messages: [ 29 | { 30 | role: "system", 31 | content: prompt 32 | }, 33 | { 34 | role: "user", 35 | content: [ 36 | ...imageDataList.map((imageData: string) => ({ type: "image_url" as const, image_url: { url: `data:image/jpeg;base64,${imageData}` } })) 37 | ] 38 | } 39 | ], 40 | response_format: zodResponseFormat(ProblemInfoSchema, "problem_info") 41 | }) 42 | 43 | const { data: extractedProblemInfo, error } = ProblemInfoSchema.safeParse(JSON.parse(response.choices[0].message.content as any)) 44 | 45 | if (error) { 46 | console.error(error) 47 | throw new Error("Failed to parse problem info") 48 | } 49 | 50 | return extractedProblemInfo 51 | } 52 | 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/shared/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { supabase } from "../../lib/supabase" 3 | 4 | interface LanguageSelectorProps { 5 | currentLanguage: string 6 | setLanguage: (language: string) => void 7 | } 8 | 9 | export const LanguageSelector: React.FC = ({ 10 | currentLanguage, 11 | setLanguage 12 | }) => { 13 | const handleLanguageChange = async ( 14 | e: React.ChangeEvent 15 | ) => { 16 | const newLanguage = e.target.value 17 | const { 18 | data: { user } 19 | } = await supabase.auth.getUser() 20 | 21 | if (user) { 22 | const { error } = await supabase 23 | .from("subscriptions") 24 | .update({ preferred_language: newLanguage }) 25 | .eq("user_id", user.id) 26 | 27 | if (error) { 28 | console.error("Error updating language:", error) 29 | } else { 30 | window.__LANGUAGE__ = newLanguage 31 | setLanguage(newLanguage) 32 | } 33 | } 34 | } 35 | 36 | return ( 37 |
38 |
39 | Language 40 | 56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { cn } from "../../lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline" 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 rounded-md px-3 text-xs", 25 | lg: "h-10 rounded-md px-8", 26 | icon: "h-9 w-9" 27 | } 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default" 32 | } 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/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/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 | -------------------------------------------------------------------------------- /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/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/controllers/generate.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { GenerateInput, GenerateInputSchema } from "../types/problem-info"; 3 | import { anthropic } from "../configs/claude"; 4 | import { SolutionSchema } from "../types/solution"; 5 | 6 | export async function generateController(c: Context) { 7 | const body = await c.req.json() 8 | 9 | const { language, problemInfo } = GenerateInputSchema.parse(body) 10 | 11 | const answer = await generateSolution({ language, problemInfo }) 12 | return c.json({ 13 | success: true, 14 | ...answer 15 | }); 16 | } 17 | 18 | async function generateSolution(data: GenerateInput) { 19 | const { language, problemInfo } = data 20 | 21 | const prompt = ` 22 | You are a helpful assistant that can help me solve a coding problem. 23 | Here is the problem statement: 24 | ${JSON.stringify(problemInfo)} 25 | Generate a solution for the problem in ${language} language. 26 | ` 27 | 28 | const response = await anthropic.messages.create({ 29 | model: "claude-3-7-sonnet-latest", 30 | max_tokens: 4096, 31 | tools: [ 32 | { 33 | name: "solution", 34 | description: "Generate a solution for a coding problem", 35 | input_schema: { 36 | type: "object", 37 | properties: { 38 | code: { 39 | type: "string", 40 | description: "The solution code" 41 | }, 42 | thoughts: { 43 | type: "array", 44 | items: { 45 | type: "string" 46 | }, 47 | description: "Thought process behind the solution" 48 | }, 49 | time_complexity: { 50 | type: "string", 51 | description: "Time complexity of the solution" 52 | }, 53 | space_complexity: { 54 | type: "string", 55 | description: "Space complexity of the solution" 56 | } 57 | }, 58 | required: ["code", "thoughts", "time_complexity", "space_complexity"] 59 | } 60 | } 61 | ], 62 | tool_choice: { type: "tool", name: "solution" }, 63 | messages: [ 64 | { 65 | role: "user", 66 | content: [ 67 | { type: "text", text: prompt } 68 | ] 69 | } 70 | ] 71 | }); 72 | 73 | const toolResponse = response.content.find( 74 | content => content.type === "tool_use" 75 | ); 76 | 77 | if (!toolResponse) { 78 | throw new Error("No tool response found") 79 | } 80 | 81 | const answer = SolutionSchema.parse(toolResponse.input) 82 | 83 | return answer 84 | } 85 | 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/controllers/debug.controller.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { DebugInput, DebugInputSchema, DebugOutputSchema } from "../types/debug"; 3 | import { anthropic } from "../configs/claude"; 4 | 5 | export async function debugController(c: Context) { 6 | const body = await c.req.json() 7 | 8 | const { imageDataList, problemInfo, language } = DebugInputSchema.parse(body) 9 | 10 | const answer = await generateDebuggingAnswer({ imageDataList, problemInfo, language }) 11 | 12 | return c.json({ 13 | success: true, 14 | ...answer 15 | }) 16 | } 17 | 18 | 19 | async function generateDebuggingAnswer(data: DebugInput) { 20 | const { imageDataList, problemInfo, language } = data 21 | 22 | const prompt = ` 23 | You are a helpful assistant that can help me debug my code. 24 | I have written a solution in ${language} to a coding problem, but it isn't working. 25 | Here is the problem statement: 26 | ${JSON.stringify(problemInfo.problemInfo)} 27 | I have attached an image of the code that isn't working. 28 | Please help me debug the code and provide a new solution that works. 29 | ` 30 | 31 | const response = await anthropic.messages.create({ 32 | model: "claude-3-7-sonnet-latest", 33 | max_tokens: 4096, 34 | tools: [ 35 | { 36 | name: "solution", 37 | description: "Generate a solution for a coding problem", 38 | input_schema: { 39 | type: "object", 40 | properties: { 41 | new_code: { 42 | type: "string", 43 | description: "The solution code" 44 | }, 45 | thoughts: { 46 | type: "array", 47 | items: { 48 | type: "string" 49 | }, 50 | description: "Thought process behind the solution" 51 | }, 52 | time_complexity: { 53 | type: "string", 54 | description: "Time complexity of the solution" 55 | }, 56 | space_complexity: { 57 | type: "string", 58 | description: "Space complexity of the solution" 59 | } 60 | }, 61 | required: ["new_code", "thoughts", "time_complexity", "space_complexity"] 62 | } 63 | } 64 | ], 65 | tool_choice: { type: "tool", name: "solution" }, 66 | messages: [ 67 | { 68 | role: "user", 69 | content: [ 70 | { type: "text", text: prompt }, 71 | ...imageDataList.map((imageData: string) => ({ 72 | type: "image", 73 | source: { 74 | type: "base64", 75 | media_type: "image/png", 76 | data: imageData 77 | } 78 | })) as any 79 | ] 80 | } 81 | ] 82 | }); 83 | 84 | const toolResponse = response.content.find( 85 | content => content.type === "tool_use" 86 | ); 87 | 88 | if (!toolResponse) { 89 | throw new Error("No tool response found") 90 | } 91 | 92 | const answer = DebugOutputSchema.parse(toolResponse.input) 93 | 94 | return answer 95 | } 96 | -------------------------------------------------------------------------------- /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/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 VITE_BACKEND_URL: string 9 | readonly NODE_ENV: string 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv 14 | } 15 | 16 | interface ElectronAPI { 17 | openSubscriptionPortal: (authData: { 18 | id: string 19 | email: string 20 | }) => Promise<{ success: boolean; error?: string }> 21 | updateContentDimensions: (dimensions: { 22 | width: number 23 | height: number 24 | }) => Promise 25 | clearStore: () => Promise<{ success: boolean; error?: string }> 26 | getScreenshots: () => Promise<{ 27 | success: boolean 28 | previews?: Array<{ path: string; preview: string }> | null 29 | error?: string 30 | }> 31 | deleteScreenshot: ( 32 | path: string 33 | ) => Promise<{ success: boolean; error?: string }> 34 | onScreenshotTaken: ( 35 | callback: (data: { path: string; preview: string }) => void 36 | ) => () => void 37 | onResetView: (callback: () => void) => () => void 38 | onSolutionStart: (callback: () => void) => () => void 39 | onDebugStart: (callback: () => void) => () => void 40 | onDebugSuccess: (callback: (data: any) => void) => () => void 41 | onSolutionError: (callback: (error: string) => void) => () => void 42 | onProcessingNoScreenshots: (callback: () => void) => () => void 43 | onProblemExtracted: (callback: (data: any) => void) => () => void 44 | onSolutionSuccess: (callback: (data: any) => void) => () => void 45 | onUnauthorized: (callback: () => void) => () => void 46 | onDebugError: (callback: (error: string) => void) => () => void 47 | openExternal: (url: string) => void 48 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 49 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 50 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 51 | triggerReset: () => Promise<{ success: boolean; error?: string }> 52 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 53 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 54 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 55 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 56 | onSubscriptionUpdated: (callback: () => void) => () => void 57 | onSubscriptionPortalClosed: (callback: () => void) => () => void 58 | // Add update-related methods 59 | startUpdate: () => Promise<{ success: boolean; error?: string }> 60 | installUpdate: () => void 61 | onUpdateAvailable: (callback: (info: any) => void) => () => void 62 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 63 | } 64 | 65 | interface Window { 66 | electronAPI: ElectronAPI 67 | electron: { 68 | ipcRenderer: { 69 | on(channel: string, func: (...args: any[]) => void): void 70 | removeListener(channel: string, func: (...args: any[]) => void): void 71 | } 72 | } 73 | __CREDITS__: number 74 | __LANGUAGE__: string 75 | __IS_INITIALIZED__: boolean 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js" 2 | 3 | console.log("Supabase URL:", import.meta.env.VITE_SUPABASE_URL) 4 | console.log( 5 | "Supabase Anon Key:", 6 | import.meta.env.VITE_SUPABASE_ANON_KEY?.slice(0, 10) + "..." 7 | ) 8 | 9 | const supabaseUrl = import.meta.env.VITE_SUPABASE_URL 10 | const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY 11 | 12 | if (!supabaseUrl || !supabaseAnonKey) { 13 | throw new Error("Missing Supabase environment variables") 14 | } 15 | 16 | export const supabase = createClient(supabaseUrl, supabaseAnonKey, { 17 | auth: { 18 | flowType: "pkce", 19 | detectSessionInUrl: true, 20 | persistSession: true, 21 | autoRefreshToken: true, 22 | debug: true, 23 | storage: { 24 | getItem: (key) => { 25 | const item = localStorage.getItem(key) 26 | console.log("Auth storage - Getting key:", key, "Value exists:", !!item) 27 | return item 28 | }, 29 | setItem: (key, value) => { 30 | console.log("Auth storage - Setting key:", key) 31 | localStorage.setItem(key, value) 32 | }, 33 | removeItem: (key) => { 34 | console.log("Auth storage - Removing key:", key) 35 | localStorage.removeItem(key) 36 | } 37 | } 38 | }, 39 | realtime: { 40 | params: { 41 | eventsPerSecond: 10 42 | }, 43 | headers: { 44 | apikey: supabaseAnonKey 45 | } 46 | } 47 | }) 48 | 49 | let channel: ReturnType | null = null 50 | 51 | // Monitor auth state changes and manage realtime connection 52 | supabase.auth.onAuthStateChange((event, session) => { 53 | console.log("Auth state changed:", event, session?.user?.id) 54 | console.log("Full session data:", session) 55 | 56 | if (event === "SIGNED_IN" && session) { 57 | // Only establish realtime connection after successful sign in 58 | console.log("Establishing realtime connection...") 59 | 60 | // Clean up existing channel if any 61 | if (channel) { 62 | channel.unsubscribe() 63 | } 64 | 65 | channel = supabase.channel("system", { 66 | config: { 67 | presence: { 68 | key: session.user.id 69 | } 70 | } 71 | }) 72 | 73 | channel 74 | .on("system", { event: "*" }, (payload) => { 75 | console.log("System event:", payload) 76 | }) 77 | .subscribe((status) => { 78 | console.log("Realtime subscription status:", status) 79 | if (status === "SUBSCRIBED") { 80 | console.log("Successfully connected to realtime system") 81 | } 82 | if (status === "CHANNEL_ERROR") { 83 | console.error("Realtime connection error - will retry in 5s") 84 | setTimeout(() => { 85 | channel?.subscribe() 86 | }, 5000) 87 | } 88 | }) 89 | } 90 | 91 | if (event === "SIGNED_OUT") { 92 | // Clean up realtime connection on sign out 93 | if (channel) { 94 | console.log("Cleaning up realtime connection") 95 | channel.unsubscribe() 96 | channel = null 97 | } 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /electron/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from "electron-updater" 2 | import { BrowserWindow, ipcMain, app } from "electron" 3 | import log from "electron-log" 4 | 5 | export function initAutoUpdater() { 6 | console.log("Initializing auto-updater...") 7 | 8 | // Skip update checks in development 9 | if (!app.isPackaged) { 10 | console.log("Skipping auto-updater in development mode") 11 | return 12 | } 13 | 14 | if (!process.env.GH_TOKEN) { 15 | console.error("GH_TOKEN environment variable is not set") 16 | return 17 | } 18 | 19 | // Configure auto updater 20 | autoUpdater.autoDownload = true 21 | autoUpdater.autoInstallOnAppQuit = true 22 | autoUpdater.allowDowngrade = true 23 | autoUpdater.allowPrerelease = true 24 | 25 | // Enable more verbose logging 26 | autoUpdater.logger = log 27 | log.transports.file.level = "debug" 28 | console.log( 29 | "Auto-updater logger configured with level:", 30 | log.transports.file.level 31 | ) 32 | 33 | // Log all update events 34 | autoUpdater.on("checking-for-update", () => { 35 | console.log("Checking for updates...") 36 | }) 37 | 38 | autoUpdater.on("update-available", (info) => { 39 | console.log("Update available:", info) 40 | // Notify renderer process about available update 41 | BrowserWindow.getAllWindows().forEach((window) => { 42 | console.log("Sending update-available to window") 43 | window.webContents.send("update-available", info) 44 | }) 45 | }) 46 | 47 | autoUpdater.on("update-not-available", (info) => { 48 | console.log("Update not available:", info) 49 | }) 50 | 51 | autoUpdater.on("download-progress", (progressObj) => { 52 | console.log("Download progress:", progressObj) 53 | }) 54 | 55 | autoUpdater.on("update-downloaded", (info) => { 56 | console.log("Update downloaded:", info) 57 | // Notify renderer process that update is ready to install 58 | BrowserWindow.getAllWindows().forEach((window) => { 59 | console.log("Sending update-downloaded to window") 60 | window.webContents.send("update-downloaded", info) 61 | }) 62 | }) 63 | 64 | autoUpdater.on("error", (err) => { 65 | console.error("Auto updater error:", err) 66 | }) 67 | 68 | // Check for updates immediately 69 | console.log("Checking for updates...") 70 | autoUpdater 71 | .checkForUpdates() 72 | .then((result) => { 73 | console.log("Update check result:", result) 74 | }) 75 | .catch((err) => { 76 | console.error("Error checking for updates:", err) 77 | }) 78 | 79 | // Set up update checking interval (every 1 hour) 80 | setInterval(() => { 81 | console.log("Checking for updates (interval)...") 82 | autoUpdater 83 | .checkForUpdates() 84 | .then((result) => { 85 | console.log("Update check result (interval):", result) 86 | }) 87 | .catch((err) => { 88 | console.error("Error checking for updates (interval):", err) 89 | }) 90 | }, 60 * 60 * 1000) 91 | 92 | // Handle IPC messages from renderer 93 | ipcMain.handle("start-update", async () => { 94 | console.log("Start update requested") 95 | try { 96 | await autoUpdater.downloadUpdate() 97 | console.log("Update download completed") 98 | return { success: true } 99 | } catch (error) { 100 | console.error("Failed to start update:", error) 101 | return { success: false, error: error.message } 102 | } 103 | }) 104 | 105 | ipcMain.handle("install-update", () => { 106 | console.log("Install update requested") 107 | autoUpdater.quitAndInstall() 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /src/components/UpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { Dialog, DialogContent } from "./ui/dialog" 3 | import { Button } from "./ui/button" 4 | import { useToast } from "../contexts/toast" 5 | 6 | export const UpdateNotification: React.FC = () => { 7 | const [updateAvailable, setUpdateAvailable] = useState(false) 8 | const [updateDownloaded, setUpdateDownloaded] = useState(false) 9 | const [isDownloading, setIsDownloading] = useState(false) 10 | const { showToast } = useToast() 11 | 12 | useEffect(() => { 13 | console.log("UpdateNotification: Setting up event listeners") 14 | 15 | const unsubscribeAvailable = window.electronAPI.onUpdateAvailable( 16 | (info) => { 17 | console.log("UpdateNotification: Update available received", info) 18 | setUpdateAvailable(true) 19 | } 20 | ) 21 | 22 | const unsubscribeDownloaded = window.electronAPI.onUpdateDownloaded( 23 | (info) => { 24 | console.log("UpdateNotification: Update downloaded received", info) 25 | setUpdateDownloaded(true) 26 | setIsDownloading(false) 27 | } 28 | ) 29 | 30 | return () => { 31 | console.log("UpdateNotification: Cleaning up event listeners") 32 | unsubscribeAvailable() 33 | unsubscribeDownloaded() 34 | } 35 | }, []) 36 | 37 | const handleStartUpdate = async () => { 38 | console.log("UpdateNotification: Starting update download") 39 | setIsDownloading(true) 40 | const result = await window.electronAPI.startUpdate() 41 | console.log("UpdateNotification: Update download result", result) 42 | if (!result.success) { 43 | setIsDownloading(false) 44 | showToast("Error", "Failed to download update", "error") 45 | } 46 | } 47 | 48 | const handleInstallUpdate = () => { 49 | console.log("UpdateNotification: Installing update") 50 | window.electronAPI.installUpdate() 51 | } 52 | 53 | console.log("UpdateNotification: Render state", { 54 | updateAvailable, 55 | updateDownloaded, 56 | isDownloading 57 | }) 58 | if (!updateAvailable && !updateDownloaded) return null 59 | 60 | return ( 61 | 62 | e.preventDefault()} 65 | > 66 |
67 |

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

72 |

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

77 |
78 | {updateDownloaded ? ( 79 | 86 | ) : ( 87 | 95 | )} 96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/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/_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/_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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interview Coder (but now self-hostable and free) 2 | 3 | An invisible desktop application that will help you pass your technical interviews. 4 | 5 | https://www.interviewcoder.co (explains all the features) 6 | 7 | https://github.com/user-attachments/assets/0615b110-2670-4b0e-bc69-3c32a2d8a996 8 | 9 | ## How to self host for free? 10 | 11 | For image -> text translation, I went with gpt-4o. For answer generation, I've used claude 3.7 sonnet becauses it's the best. Both of these is easily changeable, and you can modify our own prompts too at backend/src/controllers 12 | 13 | ### Requirements 14 | 15 | - Bun installed (https://bun.sh/docs/installation) 16 | - OpenAI API key 17 | - Anthropic API key 18 | - Supabase account (for auth and database). We need `SUPABASE_URL` and `SUPABASE_ANON_KEY`. Also supabase CLI installed 19 | - Docker (optional, for hosting backend) 20 | 21 | ### Steps 22 | 23 | 1. Clone the repository 24 | 25 | ```bash 26 | git clone https://github.com/woaitsAryan/interview-coder-self-host 27 | cd interview-coder-self-host 28 | 29 | bun install 30 | ``` 31 | 32 | 1. Create `.env` files and populate with the appropriate credentials 33 | 34 | ```bash 35 | cp .env.sample .env 36 | cp .env.local.sample .env.local 37 | ``` 38 | 39 | 3. Login with your supabase CLI and do migrations 40 | 41 | ```bash 42 | supabase login 43 | supabase db push 44 | ``` 45 | 46 | 4. Build the app for your platform 47 | 48 | For this step, remember that whatever BACKEND_URL is in .env will be used as the backend url for the app. If you want to self-host it in a server, you'll have to change the URL and re-build this 49 | 50 | ```bash 51 | bun run build 52 | ``` 53 | 54 | 1. Run the backend 55 | 56 | Either run it directly with bun: 57 | 58 | ```bash 59 | bun run backend:dev 60 | ``` 61 | 62 | Or run it with docker: 63 | 64 | ```bash 65 | bun run compose:build && bun run compose:up 66 | ``` 67 | 68 | 6. Install the app 69 | 70 | Go to releases/ folder and then find your .exe or .dmg or whatever and install it. 71 | 72 | On install, you'll see a sign in page, click on the sign-up button, I'd recommend to just disable email otp, so you can directly sign up with email. After sign-up, it will just work 73 | 74 | That's it? yay! Do as you plesae! 75 | 76 | ### Some small caveats/issues you might experience: 77 | 78 | - If you are running the app in dev using `bun run dev` so it'll open the developer console too by default so make sure to close that if you don't need it. 79 | - I don't know how to close this app lol, I just go to process manager and kill it 80 | 81 | ## Invisibility Compatibility 82 | 83 | The application is invisible to: 84 | 85 | - Zoom versions below 6.1.6 (inclusive) 86 | - All browser-based screen recording software 87 | - All versions of Discord 88 | - Mac OS _screenshot_ functionality (Command + Shift + 3/4) 89 | 90 | Note: The application is **NOT** invisible to: 91 | 92 | - Zoom versions 6.1.6 and above 93 | - https://zoom.en.uptodown.com/mac/versions (link to downgrade Zoom if needed) 94 | - Mac OS native screen _recording_ (Command + Shift + 5) 95 | 96 | ## Features 97 | 98 | - 🎯 99% Invisibility: Undetectable window that bypasses most screen capture methods 99 | - 📸 Smart Screenshot Capture: Capture both question text and code separately for better analysis 100 | - 🤖 AI-Powered Analysis: Automatically extracts and analyzes coding problems 101 | - 💡 Solution Generation: Get detailed explanations and solutions 102 | - 🔧 Real-time Debugging: Debug your code with AI assistance 103 | - 🎨 Window Management: Freely move and position the window anywhere on screen 104 | 105 | ## Global Commands 106 | 107 | The application uses unidentifiable global keyboard shortcuts that won't be detected by browsers or other applications: 108 | 109 | - Toggle Window Visibility: [Control or Cmd + b] 110 | - Move Window: [Control or Cmd + arrows] 111 | - Take Screenshot: [Control or Cmd + H] 112 | - Process Screenshots: [Control or Cmd + Enter] 113 | - Reset View: [Control or Cmd + R] 114 | - Quit: [Control or Cmd + Q] 115 | 116 | ## Usage 117 | 118 | 1. **Initial Setup** 119 | 120 | - Launch the invisible window 121 | - Login and subscribe 122 | 123 | 2. **Capturing Problem** 124 | 125 | - Use global shortcut [Control or Cmd + H] to take screenshots 126 | - Screenshots are automatically added to the queue of up to 5. 127 | 128 | 3. **Processing** 129 | 130 | - AI analyzes the screenshots to extract: 131 | - Problem requirements 132 | - Code context 133 | - System generates optimal solution strategy 134 | 135 | 4. **Solution & Debugging** 136 | 137 | - View generated solutions 138 | - Use debugging feature to: 139 | - Test different approaches 140 | - Fix errors in your code 141 | - Get line-by-line explanations 142 | - Toggle between solutions and queue views 143 | 144 | 5. **Window Management** 145 | - Move window freely using global shortcut 146 | - Toggle visibility as needed 147 | - Window remains invisible to specified applications 148 | - Reset view using Command + R 149 | 150 | ## Prerequisites 151 | 152 | - Node.js (v16 or higher) 153 | - npm or bun package manager 154 | - Subscription on https://www.interviewcoder.co/settings (not needed for self-hosting) 155 | - Screen Recording Permission for Terminal/IDE 156 | - On macOS: 157 | 1. Go to System Preferences > Security & Privacy > Privacy > Screen Recording 158 | 2. Ensure that Interview Coder has screen recording permission enabled 159 | 3. Restart Interview Coder after enabling permissions 160 | - On Windows: 161 | - No additional permissions needed 162 | - On Linux: 163 | - May require `xhost` access depending on your distribution 164 | 165 | ## Tech Stack 166 | 167 | - Electron 168 | - React 169 | - TypeScript 170 | - Vite 171 | - Tailwind CSS 172 | - Radix UI Components 173 | - OpenAI API 174 | 175 | ## Contributing 176 | 177 | Contributions are welcome! Please feel free to submit a Pull Request. 178 | -------------------------------------------------------------------------------- /src/_pages/SubscribePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react" 2 | import { supabase } from "../lib/supabase" 3 | import { User } from "@supabase/supabase-js" 4 | 5 | interface SubscribePageProps { 6 | user: User 7 | } 8 | 9 | export default function SubscribePage({ user }: SubscribePageProps) { 10 | const [error, setError] = useState(null) 11 | const containerRef = useRef(null) 12 | 13 | useEffect(() => { 14 | const updateDimensions = () => { 15 | if (containerRef.current) { 16 | window.electronAPI.updateContentDimensions({ 17 | width: 400, // Fixed width 18 | height: 400 // Fixed height 19 | }) 20 | } 21 | } 22 | 23 | updateDimensions() 24 | }, []) 25 | 26 | const handleSignOut = async () => { 27 | try { 28 | const { error: signOutError } = await supabase.auth.signOut() 29 | if (signOutError) throw signOutError 30 | } catch (err) { 31 | console.error("Error signing out:", err) 32 | setError("Failed to sign out. Please try again.") 33 | setTimeout(() => setError(null), 3000) 34 | } 35 | } 36 | 37 | const handleSubscribe = async () => { 38 | if (!user) return 39 | 40 | try { 41 | const result = await window.electronAPI.openSubscriptionPortal({ 42 | id: user.id, 43 | email: user.email! 44 | }) 45 | 46 | if (!result.success) { 47 | throw new Error(result.error || "Failed to open subscription portal") 48 | } 49 | } catch (err) { 50 | console.error("Error opening subscription portal:", err) 51 | setError("Failed to open subscription portal. Please try again.") 52 | setTimeout(() => setError(null), 3000) 53 | } 54 | } 55 | 56 | return ( 57 |
61 |
62 |
63 |

64 | Welcome to Interview Coder 65 |

66 |

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

70 |

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

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

{error}

155 |
156 | )} 157 |
158 |
159 |
160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interview-coder-v1", 3 | "version": "1.0.18", 4 | "main": "dist-electron/main.js", 5 | "scripts": { 6 | "clean": "rimraf dist dist-electron", 7 | "dev": "cross-env NODE_ENV=development npm run clean && concurrently \"tsc -w -p tsconfig.electron.json\" \"vite\" \"wait-on http://localhost:54321 && electron .\"", 8 | "build": "cross-env NODE_ENV=production rimraf dist dist-electron && vite build && tsc -p tsconfig.electron.json && electron-builder", 9 | "build:mac": "cross-env NODE_ENV=production rimraf dist dist-electron && vite build && tsc -p tsconfig.electron.json && electron-builder --mac", 10 | "build:linux": "cross-env NODE_ENV=production rimraf dist dist-electron && vite build && tsc -p tsconfig.electron.json && electron-builder --linux", 11 | "db:push": "supabase db push", 12 | "db:migrate": "supabase db reset", 13 | "db:studio": "supabase db studio", 14 | "backend:dev": "bun run --hot backend/src/index.ts", 15 | "backend:build": "bun build backend/src/index.ts --outdir backend/dist --target bun", 16 | "backend:start": "bun run backend/dist/index.js", 17 | "compose:build": "env-cmd -f .env.local -- docker compose -f compose.yaml build", 18 | "compose:up": "env-cmd -f .env.local -- docker compose -f compose.yaml up", 19 | "compose:down": "env-cmd -f .env.local -- docker compose -f compose.yaml down" 20 | }, 21 | "build": { 22 | "appId": "com.chunginlee.interviewcoder", 23 | "productName": "Interview Coder", 24 | "files": [ 25 | "dist/**/*", 26 | "dist-electron/**/*", 27 | "package.json", 28 | "electron/**/*" 29 | ], 30 | "directories": { 31 | "output": "release", 32 | "buildResources": "assets" 33 | }, 34 | "asar": true, 35 | "compression": "maximum", 36 | "generateUpdatesFilesForAllChannels": true, 37 | "mac": { 38 | "category": "public.app-category.developer-tools", 39 | "target": [ 40 | { 41 | "target": "dmg", 42 | "arch": [ 43 | "x64", 44 | "arm64" 45 | ] 46 | }, 47 | { 48 | "target": "zip", 49 | "arch": [ 50 | "x64", 51 | "arm64" 52 | ] 53 | } 54 | ], 55 | "artifactName": "Interview-Coder-${arch}.${ext}", 56 | "icon": "assets/icons/mac/icon.icns", 57 | "hardenedRuntime": true, 58 | "gatekeeperAssess": false, 59 | "entitlements": "build/entitlements.mac.plist", 60 | "entitlementsInherit": "build/entitlements.mac.plist", 61 | "identity": "Developer ID Application", 62 | "notarize": true, 63 | "protocols": { 64 | "name": "interview-coder-protocol", 65 | "schemes": [ 66 | "interview-coder" 67 | ] 68 | } 69 | }, 70 | "win": { 71 | "target": [ 72 | "nsis" 73 | ], 74 | "icon": "assets/icons/win/icon.ico", 75 | "artifactName": "${productName}-Windows-${version}.${ext}", 76 | "protocols": { 77 | "name": "interview-coder-protocol", 78 | "schemes": [ 79 | "interview-coder" 80 | ] 81 | } 82 | }, 83 | "linux": { 84 | "target": [ 85 | "AppImage" 86 | ], 87 | "icon": "assets/icons/png/icon-256x256.png", 88 | "artifactName": "${productName}-Linux-${version}.${ext}", 89 | "protocols": { 90 | "name": "interview-coder-protocol", 91 | "schemes": [ 92 | "interview-coder" 93 | ] 94 | } 95 | }, 96 | "publish": [ 97 | { 98 | "provider": "github", 99 | "owner": "ibttf", 100 | "repo": "interview-coder", 101 | "private": false, 102 | "releaseType": "release" 103 | } 104 | ], 105 | "extraResources": [ 106 | { 107 | "from": ".env", 108 | "to": ".env", 109 | "filter": [ 110 | "**/*" 111 | ] 112 | } 113 | ], 114 | "extraMetadata": { 115 | "main": "dist-electron/main.js" 116 | } 117 | }, 118 | "keywords": [], 119 | "author": "", 120 | "license": "ISC", 121 | "description": "An invisible desktop application to help you pass your technical interviews.", 122 | "dependencies": { 123 | "@anthropic-ai/sdk": "^0.39.0", 124 | "@electron/notarize": "^2.3.0", 125 | "@emotion/react": "^11.11.0", 126 | "@emotion/styled": "^11.11.0", 127 | "@radix-ui/react-dialog": "^1.1.2", 128 | "@radix-ui/react-label": "^2.1.0", 129 | "@radix-ui/react-slot": "^1.1.0", 130 | "@radix-ui/react-toast": "^1.2.2", 131 | "@supabase/supabase-js": "^2.39.0", 132 | "@tanstack/react-query": "^5.64.0", 133 | "axios": "^1.7.7", 134 | "class-variance-authority": "^0.7.1", 135 | "clsx": "^2.1.1", 136 | "diff": "^7.0.0", 137 | "dotenv": "^16.4.7", 138 | "electron-log": "^5.2.4", 139 | "electron-store": "^10.0.0", 140 | "electron-updater": "^6.3.9", 141 | "form-data": "^4.0.1", 142 | "hono": "^4.7.2", 143 | "lucide-react": "^0.460.0", 144 | "openai": "^4.86.1", 145 | "react": "^18.2.0", 146 | "react-code-blocks": "^0.1.6", 147 | "react-dom": "^18.2.0", 148 | "react-router-dom": "^6.28.1", 149 | "react-syntax-highlighter": "^15.6.1", 150 | "screenshot-desktop": "^1.15.0", 151 | "tailwind-merge": "^2.5.5", 152 | "uuid": "^11.0.3", 153 | "zod": "^3.24.2" 154 | }, 155 | "devDependencies": { 156 | "@electron/typescript-definitions": "^8.14.0", 157 | "@types/bun": "^1.2.4", 158 | "@types/color": "^4.2.0", 159 | "@types/diff": "^6.0.0", 160 | "@types/electron-store": "^1.3.1", 161 | "@types/node": "^20.11.30", 162 | "@types/react": "^18.2.67", 163 | "@types/react-dom": "^18.2.22", 164 | "@types/react-syntax-highlighter": "^15.5.13", 165 | "@types/screenshot-desktop": "^1.12.3", 166 | "@types/uuid": "^9.0.8", 167 | "@typescript-eslint/eslint-plugin": "^7.3.1", 168 | "@typescript-eslint/parser": "^7.3.1", 169 | "@vitejs/plugin-react": "^4.2.1", 170 | "autoprefixer": "^10.4.20", 171 | "concurrently": "^8.2.2", 172 | "cross-env": "^7.0.3", 173 | "electron": "^29.1.4", 174 | "electron-builder": "^24.13.3", 175 | "electron-is-dev": "^3.0.1", 176 | "eslint": "^8.57.0", 177 | "eslint-plugin-react-hooks": "^4.6.0", 178 | "eslint-plugin-react-refresh": "^0.4.6", 179 | "postcss": "^8.4.49", 180 | "rimraf": "^6.0.1", 181 | "tailwindcss": "^3.4.15", 182 | "typescript": "^5.4.2", 183 | "vite": "^5.1.6", 184 | "vite-plugin-electron": "^0.28.4", 185 | "vite-plugin-electron-renderer": "^0.14.6", 186 | "wait-on": "^7.2.0" 187 | }, 188 | "browserslist": { 189 | "production": [ 190 | ">0.2%", 191 | "not dead", 192 | "not op_mini all" 193 | ], 194 | "development": [ 195 | "last 1 chrome version", 196 | "last 1 firefox version", 197 | "last 1 safari version" 198 | ] 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /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/ipcHandlers.ts: -------------------------------------------------------------------------------- 1 | // ipcHandlers.ts 2 | 3 | import { ipcMain, shell } from "electron" 4 | import { createClient } from "@supabase/supabase-js" 5 | import { randomBytes } from "crypto" 6 | import { IIpcHandlerDeps } from "./main" 7 | 8 | export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { 9 | console.log("Initializing IPC handlers") 10 | 11 | // Credits handlers 12 | ipcMain.handle("set-initial-credits", async (_event, credits: number) => { 13 | const mainWindow = deps.getMainWindow() 14 | if (!mainWindow) return 15 | 16 | try { 17 | // Set the credits in a way that ensures atomicity 18 | await mainWindow.webContents.executeJavaScript( 19 | `window.__CREDITS__ = ${credits}` 20 | ) 21 | mainWindow.webContents.send("credits-updated", credits) 22 | } catch (error) { 23 | console.error("Error setting initial credits:", error) 24 | throw error 25 | } 26 | }) 27 | 28 | ipcMain.handle("decrement-credits", async () => { 29 | const mainWindow = deps.getMainWindow() 30 | if (!mainWindow) return 31 | 32 | try { 33 | const currentCredits = await mainWindow.webContents.executeJavaScript( 34 | "window.__CREDITS__" 35 | ) 36 | if (currentCredits > 0) { 37 | const newCredits = currentCredits - 1 38 | await mainWindow.webContents.executeJavaScript( 39 | `window.__CREDITS__ = ${newCredits}` 40 | ) 41 | mainWindow.webContents.send("credits-updated", newCredits) 42 | } 43 | } catch (error) { 44 | console.error("Error decrementing credits:", error) 45 | } 46 | }) 47 | 48 | // Screenshot queue handlers 49 | ipcMain.handle("get-screenshot-queue", () => { 50 | return deps.getScreenshotQueue() 51 | }) 52 | 53 | ipcMain.handle("get-extra-screenshot-queue", () => { 54 | return deps.getExtraScreenshotQueue() 55 | }) 56 | 57 | ipcMain.handle("delete-screenshot", async (event, path: string) => { 58 | return deps.deleteScreenshot(path) 59 | }) 60 | 61 | ipcMain.handle("get-image-preview", async (event, path: string) => { 62 | return deps.getImagePreview(path) 63 | }) 64 | 65 | // Screenshot processing handlers 66 | ipcMain.handle("process-screenshots", async () => { 67 | await deps.processingHelper?.processScreenshots() 68 | }) 69 | 70 | // Window dimension handlers 71 | ipcMain.handle( 72 | "update-content-dimensions", 73 | async (event, { width, height }: { width: number; height: number }) => { 74 | if (width && height) { 75 | deps.setWindowDimensions(width, height) 76 | } 77 | } 78 | ) 79 | 80 | ipcMain.handle( 81 | "set-window-dimensions", 82 | (event, width: number, height: number) => { 83 | deps.setWindowDimensions(width, height) 84 | } 85 | ) 86 | 87 | // Screenshot management handlers 88 | ipcMain.handle("get-screenshots", async () => { 89 | try { 90 | let previews = [] 91 | const currentView = deps.getView() 92 | 93 | if (currentView === "queue") { 94 | const queue = deps.getScreenshotQueue() 95 | previews = await Promise.all( 96 | queue.map(async (path) => ({ 97 | path, 98 | preview: await deps.getImagePreview(path) 99 | })) 100 | ) 101 | } else { 102 | const extraQueue = deps.getExtraScreenshotQueue() 103 | previews = await Promise.all( 104 | extraQueue.map(async (path) => ({ 105 | path, 106 | preview: await deps.getImagePreview(path) 107 | })) 108 | ) 109 | } 110 | 111 | return previews 112 | } catch (error) { 113 | console.error("Error getting screenshots:", error) 114 | throw error 115 | } 116 | }) 117 | 118 | // Screenshot trigger handlers 119 | ipcMain.handle("trigger-screenshot", async () => { 120 | const mainWindow = deps.getMainWindow() 121 | if (mainWindow) { 122 | try { 123 | const screenshotPath = await deps.takeScreenshot() 124 | const preview = await deps.getImagePreview(screenshotPath) 125 | mainWindow.webContents.send("screenshot-taken", { 126 | path: screenshotPath, 127 | preview 128 | }) 129 | return { success: true } 130 | } catch (error) { 131 | console.error("Error triggering screenshot:", error) 132 | return { error: "Failed to trigger screenshot" } 133 | } 134 | } 135 | return { error: "No main window available" } 136 | }) 137 | 138 | ipcMain.handle("take-screenshot", async () => { 139 | try { 140 | const screenshotPath = await deps.takeScreenshot() 141 | const preview = await deps.getImagePreview(screenshotPath) 142 | return { path: screenshotPath, preview } 143 | } catch (error) { 144 | console.error("Error taking screenshot:", error) 145 | return { error: "Failed to take screenshot" } 146 | } 147 | }) 148 | 149 | // Auth related handlers 150 | ipcMain.handle("get-pkce-verifier", () => { 151 | return randomBytes(32).toString("base64url") 152 | }) 153 | 154 | ipcMain.handle("open-external-url", (event, url: string) => { 155 | shell.openExternal(url) 156 | }) 157 | 158 | // Subscription handlers 159 | ipcMain.handle("open-settings-portal", () => { 160 | shell.openExternal("https://www.interviewcoder.co/settings") 161 | }) 162 | ipcMain.handle("open-subscription-portal", async (_event, authData) => { 163 | try { 164 | const url = "https://www.interviewcoder.co/checkout" 165 | await shell.openExternal(url) 166 | return { success: true } 167 | } catch (error) { 168 | console.error("Error opening checkout page:", error) 169 | return { 170 | success: false, 171 | error: 172 | error instanceof Error 173 | ? error.message 174 | : "Failed to open checkout page" 175 | } 176 | } 177 | }) 178 | 179 | // Window management handlers 180 | ipcMain.handle("toggle-window", () => { 181 | try { 182 | deps.toggleMainWindow() 183 | return { success: true } 184 | } catch (error) { 185 | console.error("Error toggling window:", error) 186 | return { error: "Failed to toggle window" } 187 | } 188 | }) 189 | 190 | ipcMain.handle("reset-queues", async () => { 191 | try { 192 | deps.clearQueues() 193 | return { success: true } 194 | } catch (error) { 195 | console.error("Error resetting queues:", error) 196 | return { error: "Failed to reset queues" } 197 | } 198 | }) 199 | 200 | // Process screenshot handlers 201 | ipcMain.handle("trigger-process-screenshots", async () => { 202 | try { 203 | await deps.processingHelper?.processScreenshots() 204 | return { success: true } 205 | } catch (error) { 206 | console.error("Error processing screenshots:", error) 207 | return { error: "Failed to process screenshots" } 208 | } 209 | }) 210 | 211 | // Reset handlers 212 | ipcMain.handle("trigger-reset", () => { 213 | try { 214 | // First cancel any ongoing requests 215 | deps.processingHelper?.cancelOngoingRequests() 216 | 217 | // Clear all queues immediately 218 | deps.clearQueues() 219 | 220 | // Reset view to queue 221 | deps.setView("queue") 222 | 223 | // Get main window and send reset events 224 | const mainWindow = deps.getMainWindow() 225 | if (mainWindow && !mainWindow.isDestroyed()) { 226 | // Send reset events in sequence 227 | mainWindow.webContents.send("reset-view") 228 | mainWindow.webContents.send("reset") 229 | } 230 | 231 | return { success: true } 232 | } catch (error) { 233 | console.error("Error triggering reset:", error) 234 | return { error: "Failed to trigger reset" } 235 | } 236 | }) 237 | 238 | // Window movement handlers 239 | ipcMain.handle("trigger-move-left", () => { 240 | try { 241 | deps.moveWindowLeft() 242 | return { success: true } 243 | } catch (error) { 244 | console.error("Error moving window left:", error) 245 | return { error: "Failed to move window left" } 246 | } 247 | }) 248 | 249 | ipcMain.handle("trigger-move-right", () => { 250 | try { 251 | deps.moveWindowRight() 252 | return { success: true } 253 | } catch (error) { 254 | console.error("Error moving window right:", error) 255 | return { error: "Failed to move window right" } 256 | } 257 | }) 258 | 259 | ipcMain.handle("trigger-move-up", () => { 260 | try { 261 | deps.moveWindowUp() 262 | return { success: true } 263 | } catch (error) { 264 | console.error("Error moving window up:", error) 265 | return { error: "Failed to move window up" } 266 | } 267 | }) 268 | 269 | ipcMain.handle("trigger-move-down", () => { 270 | try { 271 | deps.moveWindowDown() 272 | return { success: true } 273 | } catch (error) { 274 | console.error("Error moving window down:", error) 275 | return { error: "Failed to move window down" } 276 | } 277 | }) 278 | } 279 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # For detailed configuration reference documentation, visit: 2 | # https://supabase.com/docs/guides/local-development/cli/config 3 | # A string used to distinguish different Supabase projects on the same host. Defaults to the 4 | # working directory name when running `supabase init`. 5 | project_id = "interview-coder" 6 | 7 | [api] 8 | enabled = true 9 | # Port to use for the API URL. 10 | port = 54321 11 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 12 | # endpoints. `public` and `graphql_public` schemas are included by default. 13 | schemas = ["public", "graphql_public"] 14 | # Extra schemas to add to the search_path of every request. 15 | extra_search_path = ["public", "extensions"] 16 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 17 | # for accidental or malicious requests. 18 | max_rows = 1000 19 | 20 | [api.tls] 21 | # Enable HTTPS endpoints locally using a self-signed certificate. 22 | enabled = false 23 | 24 | [db] 25 | # Port to use for the local database URL. 26 | port = 54322 27 | # Port used by db diff command to initialize the shadow database. 28 | shadow_port = 54320 29 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 30 | # server_version;` on the remote database to check. 31 | major_version = 15 32 | 33 | [db.pooler] 34 | enabled = false 35 | # Port to use for the local connection pooler. 36 | port = 54329 37 | # Specifies when a server connection can be reused by other clients. 38 | # Configure one of the supported pooler modes: `transaction`, `session`. 39 | pool_mode = "transaction" 40 | # How many server connections to allow per user/database pair. 41 | default_pool_size = 20 42 | # Maximum number of client connections allowed. 43 | max_client_conn = 100 44 | 45 | # [db.vault] 46 | # secret_key = "env(SECRET_VALUE)" 47 | 48 | [db.migrations] 49 | # Specifies an ordered list of schema files that describe your database. 50 | # Supports glob patterns relative to supabase directory: "./schemas/*.sql" 51 | schema_paths = [] 52 | 53 | [db.seed] 54 | # If enabled, seeds the database after migrations during a db reset. 55 | enabled = true 56 | # Specifies an ordered list of seed files to load during db reset. 57 | # Supports glob patterns relative to supabase directory: "./seeds/*.sql" 58 | sql_paths = ["./seed.sql"] 59 | 60 | [realtime] 61 | enabled = true 62 | # Bind realtime via either IPv4 or IPv6. (default: IPv4) 63 | # ip_version = "IPv6" 64 | # The maximum length in bytes of HTTP request headers. (default: 4096) 65 | # max_header_length = 4096 66 | 67 | [studio] 68 | enabled = true 69 | # Port to use for Supabase Studio. 70 | port = 54323 71 | # External URL of the API server that frontend connects to. 72 | api_url = "http://127.0.0.1" 73 | # OpenAI API Key to use for Supabase AI in the Supabase Studio. 74 | openai_api_key = "env(OPENAI_API_KEY)" 75 | 76 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 77 | # are monitored, and you can view the emails that would have been sent from the web interface. 78 | [inbucket] 79 | enabled = true 80 | # Port to use for the email testing server web interface. 81 | port = 54324 82 | # Uncomment to expose additional ports for testing user applications that send emails. 83 | # smtp_port = 54325 84 | # pop3_port = 54326 85 | # admin_email = "admin@email.com" 86 | # sender_name = "Admin" 87 | 88 | [storage] 89 | enabled = true 90 | # The maximum file size allowed (e.g. "5MB", "500KB"). 91 | file_size_limit = "50MiB" 92 | 93 | # Image transformation API is available to Supabase Pro plan. 94 | # [storage.image_transformation] 95 | # enabled = true 96 | 97 | # Uncomment to configure local storage buckets 98 | # [storage.buckets.images] 99 | # public = false 100 | # file_size_limit = "50MiB" 101 | # allowed_mime_types = ["image/png", "image/jpeg"] 102 | # objects_path = "./images" 103 | 104 | [auth] 105 | enabled = true 106 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 107 | # in emails. 108 | site_url = "http://127.0.0.1:3000" 109 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 110 | additional_redirect_urls = ["https://127.0.0.1:3000"] 111 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). 112 | jwt_expiry = 3600 113 | # If disabled, the refresh token will never expire. 114 | enable_refresh_token_rotation = true 115 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. 116 | # Requires enable_refresh_token_rotation = true. 117 | refresh_token_reuse_interval = 10 118 | # Allow/disallow new user signups to your project. 119 | enable_signup = true 120 | # Allow/disallow anonymous sign-ins to your project. 121 | enable_anonymous_sign_ins = false 122 | # Allow/disallow testing manual linking of accounts 123 | enable_manual_linking = false 124 | # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. 125 | minimum_password_length = 6 126 | # Passwords that do not meet the following requirements will be rejected as weak. Supported values 127 | # are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` 128 | password_requirements = "" 129 | 130 | # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. 131 | # [auth.captcha] 132 | # enabled = true 133 | # provider = "hcaptcha" 134 | # secret = "" 135 | 136 | [auth.email] 137 | # Allow/disallow new user signups via email to your project. 138 | enable_signup = true 139 | # If enabled, a user will be required to confirm any email change on both the old, and new email 140 | # addresses. If disabled, only the new email is required to confirm. 141 | double_confirm_changes = true 142 | # If enabled, users need to confirm their email address before signing in. 143 | enable_confirmations = false 144 | # If enabled, users will need to reauthenticate or have logged in recently to change their password. 145 | secure_password_change = false 146 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. 147 | max_frequency = "1s" 148 | # Number of characters used in the email OTP. 149 | otp_length = 6 150 | # Number of seconds before the email OTP expires (defaults to 1 hour). 151 | otp_expiry = 3600 152 | 153 | # Use a production-ready SMTP server 154 | # [auth.email.smtp] 155 | # enabled = true 156 | # host = "smtp.sendgrid.net" 157 | # port = 587 158 | # user = "apikey" 159 | # pass = "env(SENDGRID_API_KEY)" 160 | # admin_email = "admin@email.com" 161 | # sender_name = "Admin" 162 | 163 | # Uncomment to customize email template 164 | # [auth.email.template.invite] 165 | # subject = "You have been invited" 166 | # content_path = "./supabase/templates/invite.html" 167 | 168 | [auth.sms] 169 | # Allow/disallow new user signups via SMS to your project. 170 | enable_signup = false 171 | # If enabled, users need to confirm their phone number before signing in. 172 | enable_confirmations = false 173 | # Template for sending OTP to users 174 | template = "Your code is {{ .Code }}" 175 | # Controls the minimum amount of time that must pass before sending another sms otp. 176 | max_frequency = "5s" 177 | 178 | # Use pre-defined map of phone number to OTP for testing. 179 | # [auth.sms.test_otp] 180 | # 4152127777 = "123456" 181 | 182 | # Configure logged in session timeouts. 183 | # [auth.sessions] 184 | # Force log out after the specified duration. 185 | # timebox = "24h" 186 | # Force log out if the user has been inactive longer than the specified duration. 187 | # inactivity_timeout = "8h" 188 | 189 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. 190 | # [auth.hook.custom_access_token] 191 | # enabled = true 192 | # uri = "pg-functions:////" 193 | 194 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. 195 | [auth.sms.twilio] 196 | enabled = false 197 | account_sid = "" 198 | message_service_sid = "" 199 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: 200 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" 201 | 202 | # Multi-factor-authentication is available to Supabase Pro plan. 203 | [auth.mfa] 204 | # Control how many MFA factors can be enrolled at once per user. 205 | max_enrolled_factors = 10 206 | 207 | # Control MFA via App Authenticator (TOTP) 208 | [auth.mfa.totp] 209 | enroll_enabled = false 210 | verify_enabled = false 211 | 212 | # Configure MFA via Phone Messaging 213 | [auth.mfa.phone] 214 | enroll_enabled = false 215 | verify_enabled = false 216 | otp_length = 6 217 | template = "Your code is {{ .Code }}" 218 | max_frequency = "5s" 219 | 220 | # Configure MFA via WebAuthn 221 | # [auth.mfa.web_authn] 222 | # enroll_enabled = true 223 | # verify_enabled = true 224 | 225 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 226 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, 227 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 228 | [auth.external.apple] 229 | enabled = false 230 | client_id = "" 231 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: 232 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" 233 | # Overrides the default auth redirectUrl. 234 | redirect_uri = "" 235 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 236 | # or any other third-party OIDC providers. 237 | url = "" 238 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. 239 | skip_nonce_check = false 240 | 241 | # Use Firebase Auth as a third-party provider alongside Supabase Auth. 242 | [auth.third_party.firebase] 243 | enabled = false 244 | # project_id = "my-firebase-project" 245 | 246 | # Use Auth0 as a third-party provider alongside Supabase Auth. 247 | [auth.third_party.auth0] 248 | enabled = false 249 | # tenant = "my-auth0-tenant" 250 | # tenant_region = "us" 251 | 252 | # Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. 253 | [auth.third_party.aws_cognito] 254 | enabled = false 255 | # user_pool_id = "my-user-pool-id" 256 | # user_pool_region = "us-east-1" 257 | 258 | [edge_runtime] 259 | enabled = true 260 | # Configure one of the supported request policies: `oneshot`, `per_worker`. 261 | # Use `oneshot` for hot reload, or `per_worker` for load testing. 262 | policy = "oneshot" 263 | # Port to attach the Chrome inspector for debugging edge functions. 264 | inspector_port = 8083 265 | 266 | # Use these configurations to customize your Edge Function. 267 | # [functions.MY_FUNCTION_NAME] 268 | # enabled = true 269 | # verify_jwt = true 270 | # import_map = "./functions/MY_FUNCTION_NAME/deno.json" 271 | # Uncomment to specify a custom file path to the entrypoint. 272 | # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx 273 | # entrypoint = "./functions/MY_FUNCTION_NAME/index.ts" 274 | # Specifies static files to be bundled with the function. Supports glob patterns. 275 | # For example, if you want to serve static HTML pages in your function: 276 | # static_files = [ "./functions/MY_FUNCTION_NAME/*.html" ] 277 | 278 | [analytics] 279 | enabled = true 280 | port = 54327 281 | # Configure one of the supported backends: `postgres`, `bigquery`. 282 | backend = "postgres" 283 | 284 | # Experimental features may be deprecated any time 285 | [experimental] 286 | # Configures Postgres storage engine to use OrioleDB (S3) 287 | orioledb_version = "" 288 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com 289 | s3_host = "env(S3_HOST)" 290 | # Configures S3 bucket region, eg. us-east-1 291 | s3_region = "env(S3_REGION)" 292 | # Configures AWS_ACCESS_KEY_ID for S3 bucket 293 | s3_access_key = "env(S3_ACCESS_KEY)" 294 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket 295 | s3_secret_key = "env(S3_SECRET_KEY)" 296 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | console.log("Preload script starting...") 2 | import { contextBridge, ipcRenderer } from "electron" 3 | const { shell } = require("electron") 4 | 5 | // Types for the exposed Electron API 6 | interface ElectronAPI { 7 | openSubscriptionPortal: (authData: { 8 | id: string 9 | email: string 10 | }) => Promise<{ success: boolean; error?: string }> 11 | updateContentDimensions: (dimensions: { 12 | width: number 13 | height: number 14 | }) => Promise 15 | clearStore: () => Promise<{ success: boolean; error?: string }> 16 | getScreenshots: () => Promise<{ 17 | success: boolean 18 | previews?: Array<{ path: string; preview: string }> | null 19 | error?: string 20 | }> 21 | deleteScreenshot: ( 22 | path: string 23 | ) => Promise<{ success: boolean; error?: string }> 24 | onScreenshotTaken: ( 25 | callback: (data: { path: string; preview: string }) => void 26 | ) => () => void 27 | onResetView: (callback: () => void) => () => void 28 | onSolutionStart: (callback: () => void) => () => void 29 | onDebugStart: (callback: () => void) => () => void 30 | onDebugSuccess: (callback: (data: any) => void) => () => void 31 | onSolutionError: (callback: (error: string) => void) => () => void 32 | onProcessingNoScreenshots: (callback: () => void) => () => void 33 | onProblemExtracted: (callback: (data: any) => void) => () => void 34 | onSolutionSuccess: (callback: (data: any) => void) => () => void 35 | onUnauthorized: (callback: () => void) => () => void 36 | onDebugError: (callback: (error: string) => void) => () => void 37 | openExternal: (url: string) => void 38 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 39 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 40 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 41 | triggerReset: () => Promise<{ success: boolean; error?: string }> 42 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 43 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 44 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 45 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 46 | onSubscriptionUpdated: (callback: () => void) => () => void 47 | onSubscriptionPortalClosed: (callback: () => void) => () => void 48 | startUpdate: () => Promise<{ success: boolean; error?: string }> 49 | installUpdate: () => void 50 | onUpdateAvailable: (callback: (info: any) => void) => () => void 51 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 52 | decrementCredits: () => Promise 53 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 54 | onOutOfCredits: (callback: () => void) => () => void 55 | getPlatform: () => string 56 | } 57 | 58 | export const PROCESSING_EVENTS = { 59 | //global states 60 | UNAUTHORIZED: "procesing-unauthorized", 61 | NO_SCREENSHOTS: "processing-no-screenshots", 62 | OUT_OF_CREDITS: "out-of-credits", 63 | 64 | //states for generating the initial solution 65 | INITIAL_START: "initial-start", 66 | PROBLEM_EXTRACTED: "problem-extracted", 67 | SOLUTION_SUCCESS: "solution-success", 68 | INITIAL_SOLUTION_ERROR: "solution-error", 69 | RESET: "reset", 70 | 71 | //states for processing the debugging 72 | DEBUG_START: "debug-start", 73 | DEBUG_SUCCESS: "debug-success", 74 | DEBUG_ERROR: "debug-error" 75 | } as const 76 | 77 | // At the top of the file 78 | console.log("Preload script is running") 79 | 80 | const electronAPI = { 81 | openSubscriptionPortal: async (authData: { id: string; email: string }) => { 82 | return ipcRenderer.invoke("open-subscription-portal", authData) 83 | }, 84 | openSettingsPortal: () => ipcRenderer.invoke("open-settings-portal"), 85 | updateContentDimensions: (dimensions: { width: number; height: number }) => 86 | ipcRenderer.invoke("update-content-dimensions", dimensions), 87 | clearStore: () => ipcRenderer.invoke("clear-store"), 88 | getScreenshots: () => ipcRenderer.invoke("get-screenshots"), 89 | deleteScreenshot: (path: string) => 90 | ipcRenderer.invoke("delete-screenshot", path), 91 | toggleMainWindow: async () => { 92 | console.log("toggleMainWindow called from preload") 93 | try { 94 | const result = await ipcRenderer.invoke("toggle-window") 95 | console.log("toggle-window result:", result) 96 | return result 97 | } catch (error) { 98 | console.error("Error in toggleMainWindow:", error) 99 | throw error 100 | } 101 | }, 102 | // Event listeners 103 | onScreenshotTaken: ( 104 | callback: (data: { path: string; preview: string }) => void 105 | ) => { 106 | const subscription = (_: any, data: { path: string; preview: string }) => 107 | callback(data) 108 | ipcRenderer.on("screenshot-taken", subscription) 109 | return () => { 110 | ipcRenderer.removeListener("screenshot-taken", subscription) 111 | } 112 | }, 113 | onResetView: (callback: () => void) => { 114 | const subscription = () => callback() 115 | ipcRenderer.on("reset-view", subscription) 116 | return () => { 117 | ipcRenderer.removeListener("reset-view", subscription) 118 | } 119 | }, 120 | onSolutionStart: (callback: () => void) => { 121 | const subscription = () => callback() 122 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 123 | return () => { 124 | ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 125 | } 126 | }, 127 | onDebugStart: (callback: () => void) => { 128 | const subscription = () => callback() 129 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 130 | return () => { 131 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 132 | } 133 | }, 134 | onDebugSuccess: (callback: (data: any) => void) => { 135 | ipcRenderer.on("debug-success", (_event, data) => callback(data)) 136 | return () => { 137 | ipcRenderer.removeListener("debug-success", (_event, data) => 138 | callback(data) 139 | ) 140 | } 141 | }, 142 | onDebugError: (callback: (error: string) => void) => { 143 | const subscription = (_: any, error: string) => callback(error) 144 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 145 | return () => { 146 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 147 | } 148 | }, 149 | onSolutionError: (callback: (error: string) => void) => { 150 | const subscription = (_: any, error: string) => callback(error) 151 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 152 | return () => { 153 | ipcRenderer.removeListener( 154 | PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 155 | subscription 156 | ) 157 | } 158 | }, 159 | onProcessingNoScreenshots: (callback: () => void) => { 160 | const subscription = () => callback() 161 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 162 | return () => { 163 | ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 164 | } 165 | }, 166 | onOutOfCredits: (callback: () => void) => { 167 | const subscription = () => callback() 168 | ipcRenderer.on(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 169 | return () => { 170 | ipcRenderer.removeListener(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 171 | } 172 | }, 173 | onProblemExtracted: (callback: (data: any) => void) => { 174 | const subscription = (_: any, data: any) => callback(data) 175 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 176 | return () => { 177 | ipcRenderer.removeListener( 178 | PROCESSING_EVENTS.PROBLEM_EXTRACTED, 179 | subscription 180 | ) 181 | } 182 | }, 183 | onSolutionSuccess: (callback: (data: any) => void) => { 184 | const subscription = (_: any, data: any) => callback(data) 185 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 186 | return () => { 187 | ipcRenderer.removeListener( 188 | PROCESSING_EVENTS.SOLUTION_SUCCESS, 189 | subscription 190 | ) 191 | } 192 | }, 193 | onUnauthorized: (callback: () => void) => { 194 | const subscription = () => callback() 195 | ipcRenderer.on(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 196 | return () => { 197 | ipcRenderer.removeListener(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 198 | } 199 | }, 200 | openExternal: (url: string) => shell.openExternal(url), 201 | triggerScreenshot: () => ipcRenderer.invoke("trigger-screenshot"), 202 | triggerProcessScreenshots: () => 203 | ipcRenderer.invoke("trigger-process-screenshots"), 204 | triggerReset: () => ipcRenderer.invoke("trigger-reset"), 205 | triggerMoveLeft: () => ipcRenderer.invoke("trigger-move-left"), 206 | triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), 207 | triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), 208 | triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), 209 | onSubscriptionUpdated: (callback: () => void) => { 210 | const subscription = () => callback() 211 | ipcRenderer.on("subscription-updated", subscription) 212 | return () => { 213 | ipcRenderer.removeListener("subscription-updated", subscription) 214 | } 215 | }, 216 | onSubscriptionPortalClosed: (callback: () => void) => { 217 | const subscription = () => callback() 218 | ipcRenderer.on("subscription-portal-closed", subscription) 219 | return () => { 220 | ipcRenderer.removeListener("subscription-portal-closed", subscription) 221 | } 222 | }, 223 | onReset: (callback: () => void) => { 224 | const subscription = () => callback() 225 | ipcRenderer.on(PROCESSING_EVENTS.RESET, subscription) 226 | return () => { 227 | ipcRenderer.removeListener(PROCESSING_EVENTS.RESET, subscription) 228 | } 229 | }, 230 | startUpdate: () => ipcRenderer.invoke("start-update"), 231 | installUpdate: () => ipcRenderer.invoke("install-update"), 232 | onUpdateAvailable: (callback: (info: any) => void) => { 233 | const subscription = (_: any, info: any) => callback(info) 234 | ipcRenderer.on("update-available", subscription) 235 | return () => { 236 | ipcRenderer.removeListener("update-available", subscription) 237 | } 238 | }, 239 | onUpdateDownloaded: (callback: (info: any) => void) => { 240 | const subscription = (_: any, info: any) => callback(info) 241 | ipcRenderer.on("update-downloaded", subscription) 242 | return () => { 243 | ipcRenderer.removeListener("update-downloaded", subscription) 244 | } 245 | }, 246 | decrementCredits: () => ipcRenderer.invoke("decrement-credits"), 247 | onCreditsUpdated: (callback: (credits: number) => void) => { 248 | const subscription = (_event: any, credits: number) => callback(credits) 249 | ipcRenderer.on("credits-updated", subscription) 250 | return () => { 251 | ipcRenderer.removeListener("credits-updated", subscription) 252 | } 253 | }, 254 | getPlatform: () => process.platform 255 | } as ElectronAPI 256 | 257 | // Before exposing the API 258 | console.log( 259 | "About to expose electronAPI with methods:", 260 | Object.keys(electronAPI) 261 | ) 262 | 263 | // Expose the API 264 | contextBridge.exposeInMainWorld("electronAPI", electronAPI) 265 | 266 | console.log("electronAPI exposed to window") 267 | 268 | // Add this focus restoration handler 269 | ipcRenderer.on("restore-focus", () => { 270 | // Try to focus the active element if it exists 271 | const activeElement = document.activeElement as HTMLElement 272 | if (activeElement && typeof activeElement.focus === "function") { 273 | activeElement.focus() 274 | } 275 | }) 276 | 277 | // Expose protected methods that allow the renderer process to use 278 | // the ipcRenderer without exposing the entire object 279 | contextBridge.exposeInMainWorld("electron", { 280 | ipcRenderer: { 281 | on: (channel: string, func: (...args: any[]) => void) => { 282 | if (channel === "auth-callback") { 283 | ipcRenderer.on(channel, (event, ...args) => func(...args)) 284 | } 285 | }, 286 | removeListener: (channel: string, func: (...args: any[]) => void) => { 287 | if (channel === "auth-callback") { 288 | ipcRenderer.removeListener(channel, (event, ...args) => func(...args)) 289 | } 290 | } 291 | } 292 | }) 293 | -------------------------------------------------------------------------------- /src/components/Queue/QueueCommands.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | 3 | import { supabase } from "../../lib/supabase" 4 | import { useToast } from "../../contexts/toast" 5 | import { LanguageSelector } from "../shared/LanguageSelector" 6 | import { COMMAND_KEY } from "../../utils/platform" 7 | 8 | interface QueueCommandsProps { 9 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 10 | screenshotCount?: number 11 | credits: number 12 | currentLanguage: string 13 | setLanguage: (language: string) => void 14 | } 15 | 16 | const QueueCommands: React.FC = ({ 17 | onTooltipVisibilityChange, 18 | screenshotCount = 0, 19 | credits, 20 | currentLanguage, 21 | setLanguage 22 | }) => { 23 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 24 | const tooltipRef = useRef(null) 25 | const { showToast } = useToast() 26 | 27 | useEffect(() => { 28 | let tooltipHeight = 0 29 | if (tooltipRef.current && isTooltipVisible) { 30 | tooltipHeight = tooltipRef.current.offsetHeight + 10 31 | } 32 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 33 | }, [isTooltipVisible]) 34 | 35 | const handleSignOut = async () => { 36 | try { 37 | // Clear any local storage or electron-specific data first 38 | localStorage.clear() 39 | sessionStorage.clear() 40 | 41 | // Then sign out from Supabase 42 | const { error } = await supabase.auth.signOut() 43 | if (error) throw error 44 | } catch (err) { 45 | console.error("Error signing out:", err) 46 | } 47 | } 48 | 49 | const handleMouseEnter = () => { 50 | setIsTooltipVisible(true) 51 | } 52 | 53 | const handleMouseLeave = () => { 54 | setIsTooltipVisible(false) 55 | } 56 | 57 | return ( 58 |
59 |
60 |
61 | {/* Screenshot */} 62 |
{ 65 | try { 66 | const result = await window.electronAPI.triggerScreenshot() 67 | if (!result.success) { 68 | console.error("Failed to take screenshot:", result.error) 69 | showToast("Error", "Failed to take screenshot", "error") 70 | } 71 | } catch (error) { 72 | console.error("Error taking screenshot:", error) 73 | showToast("Error", "Failed to take screenshot", "error") 74 | } 75 | }} 76 | > 77 | 78 | {screenshotCount === 0 79 | ? "Take first screenshot" 80 | : screenshotCount === 1 81 | ? "Take second screenshot" 82 | : "Reset first screenshot"} 83 | 84 |
85 | 88 | 91 |
92 |
93 | 94 | {/* Solve Command */} 95 | {screenshotCount > 0 && ( 96 |
{ 101 | if (credits <= 0) { 102 | showToast( 103 | "Out of Credits", 104 | "You are out of credits. Please refill at https://www.interviewcoder.co/settings.", 105 | "error" 106 | ) 107 | return 108 | } 109 | 110 | try { 111 | const result = 112 | await window.electronAPI.triggerProcessScreenshots() 113 | if (!result.success) { 114 | console.error( 115 | "Failed to process screenshots:", 116 | result.error 117 | ) 118 | showToast("Error", "Failed to process screenshots", "error") 119 | } 120 | } catch (error) { 121 | console.error("Error processing screenshots:", error) 122 | showToast("Error", "Failed to process screenshots", "error") 123 | } 124 | }} 125 | > 126 |
127 | Solve 128 |
129 | 132 | 135 |
136 |
137 |
138 | )} 139 | 140 | {/* Separator */} 141 |
142 | 143 | {/* Settings with Tooltip */} 144 |
149 | {/* Gear icon */} 150 |
151 | 161 | 162 | 163 | 164 |
165 | 166 | {/* Tooltip Content */} 167 | {isTooltipVisible && ( 168 |
173 | {/* Add transparent bridge */} 174 |
175 |
176 |
177 |

Keyboard Shortcuts

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

219 | Show or hide this window. 220 |

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

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

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

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

320 |
321 |
322 | 323 | {/* Separator and Log Out */} 324 |
325 | 329 | 330 | {/* Credits Display */} 331 |
332 |
333 | Credits Remaining 334 | {credits} / 50 335 |
336 |
337 | Refill at{" "} 338 | 341 | window.electronAPI.openSettingsPortal() 342 | } 343 | > 344 | www.interviewcoder.co/settings 345 | 346 |
347 |
348 | 349 | 371 |
372 |
373 |
374 |
375 | )} 376 |
377 |
378 |
379 |
380 | ) 381 | } 382 | 383 | export default QueueCommands 384 | -------------------------------------------------------------------------------- /src/_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 | -------------------------------------------------------------------------------- /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 API_BASE_URL = process.env.VITE_BACKEND_URL || "http://localhost:8000" 10 | 11 | export class ProcessingHelper { 12 | private deps: IProcessingHelperDeps 13 | private screenshotHelper: ScreenshotHelper 14 | 15 | // AbortControllers for API requests 16 | private currentProcessingAbortController: AbortController | null = null 17 | private currentExtraProcessingAbortController: AbortController | null = null 18 | 19 | constructor(deps: IProcessingHelperDeps) { 20 | this.deps = deps 21 | this.screenshotHelper = deps.getScreenshotHelper() 22 | } 23 | 24 | private async waitForInitialization( 25 | mainWindow: BrowserWindow 26 | ): Promise { 27 | let attempts = 0 28 | const maxAttempts = 50 // 5 seconds total 29 | 30 | while (attempts < maxAttempts) { 31 | const isInitialized = await mainWindow.webContents.executeJavaScript( 32 | "window.__IS_INITIALIZED__" 33 | ) 34 | if (isInitialized) return 35 | await new Promise((resolve) => setTimeout(resolve, 100)) 36 | attempts++ 37 | } 38 | throw new Error("App failed to initialize after 5 seconds") 39 | } 40 | 41 | private async getCredits(): Promise { 42 | const mainWindow = this.deps.getMainWindow() 43 | if (!mainWindow) return 0 44 | 45 | try { 46 | await this.waitForInitialization(mainWindow) 47 | const credits = await mainWindow.webContents.executeJavaScript( 48 | "window.__CREDITS__" 49 | ) 50 | 51 | if ( 52 | typeof credits !== "number" || 53 | credits === undefined || 54 | credits === null 55 | ) { 56 | console.warn("Credits not properly initialized") 57 | return 0 58 | } 59 | 60 | return credits 61 | } catch (error) { 62 | console.error("Error getting credits:", error) 63 | return 0 64 | } 65 | } 66 | 67 | private async getLanguage(): Promise { 68 | const mainWindow = this.deps.getMainWindow() 69 | if (!mainWindow) return "python" 70 | 71 | try { 72 | await this.waitForInitialization(mainWindow) 73 | const language = await mainWindow.webContents.executeJavaScript( 74 | "window.__LANGUAGE__" 75 | ) 76 | 77 | if ( 78 | typeof language !== "string" || 79 | language === undefined || 80 | language === null 81 | ) { 82 | console.warn("Language not properly initialized") 83 | return "python" 84 | } 85 | 86 | return language 87 | } catch (error) { 88 | console.error("Error getting language:", error) 89 | return "python" 90 | } 91 | } 92 | 93 | public async processScreenshots(): Promise { 94 | const mainWindow = this.deps.getMainWindow() 95 | if (!mainWindow) return 96 | 97 | // Check if we have any credits left 98 | const credits = await this.getCredits() 99 | if (credits < 1) { 100 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS) 101 | return 102 | } 103 | 104 | const view = this.deps.getView() 105 | console.log("Processing screenshots in view:", view) 106 | 107 | if (view === "queue") { 108 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.INITIAL_START) 109 | const screenshotQueue = this.screenshotHelper.getScreenshotQueue() 110 | console.log("Processing main queue screenshots:", screenshotQueue) 111 | if (screenshotQueue.length === 0) { 112 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 113 | return 114 | } 115 | 116 | try { 117 | // Initialize AbortController 118 | this.currentProcessingAbortController = new AbortController() 119 | const { signal } = this.currentProcessingAbortController 120 | 121 | const screenshots = await Promise.all( 122 | screenshotQueue.map(async (path) => ({ 123 | path, 124 | preview: await this.screenshotHelper.getImagePreview(path), 125 | data: fs.readFileSync(path).toString("base64") 126 | })) 127 | ) 128 | 129 | const result = await this.processScreenshotsHelper(screenshots, signal) 130 | 131 | if (!result.success) { 132 | console.log("Processing failed:", result.error) 133 | if (result.error?.includes("API Key out of credits")) { 134 | mainWindow.webContents.send( 135 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 136 | ) 137 | } else if (result.error?.includes("OpenAI API key not found")) { 138 | mainWindow.webContents.send( 139 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 140 | "OpenAI API key not found in environment variables. Please set the OPEN_AI_API_KEY environment variable." 141 | ) 142 | } else { 143 | mainWindow.webContents.send( 144 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 145 | result.error 146 | ) 147 | } 148 | // Reset view back to queue on error 149 | console.log("Resetting view to queue due to error") 150 | this.deps.setView("queue") 151 | return 152 | } 153 | 154 | // Only set view to solutions if processing succeeded 155 | console.log("Setting view to solutions after successful processing") 156 | mainWindow.webContents.send( 157 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 158 | result.data 159 | ) 160 | this.deps.setView("solutions") 161 | } catch (error: any) { 162 | mainWindow.webContents.send( 163 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 164 | error 165 | ) 166 | console.error("Processing error:", error) 167 | if (axios.isCancel(error)) { 168 | mainWindow.webContents.send( 169 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 170 | "Processing was canceled by the user." 171 | ) 172 | } else { 173 | mainWindow.webContents.send( 174 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 175 | error.message || "Server error. Please try again." 176 | ) 177 | } 178 | // Reset view back to queue on error 179 | console.log("Resetting view to queue due to error") 180 | this.deps.setView("queue") 181 | } finally { 182 | this.currentProcessingAbortController = null 183 | } 184 | } else { 185 | // view == 'solutions' 186 | const extraScreenshotQueue = 187 | this.screenshotHelper.getExtraScreenshotQueue() 188 | console.log("Processing extra queue screenshots:", extraScreenshotQueue) 189 | if (extraScreenshotQueue.length === 0) { 190 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 191 | return 192 | } 193 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.DEBUG_START) 194 | 195 | // Initialize AbortController 196 | this.currentExtraProcessingAbortController = new AbortController() 197 | const { signal } = this.currentExtraProcessingAbortController 198 | 199 | try { 200 | const screenshots = await Promise.all( 201 | [ 202 | ...this.screenshotHelper.getScreenshotQueue(), 203 | ...extraScreenshotQueue 204 | ].map(async (path) => ({ 205 | path, 206 | preview: await this.screenshotHelper.getImagePreview(path), 207 | data: fs.readFileSync(path).toString("base64") 208 | })) 209 | ) 210 | console.log( 211 | "Combined screenshots for processing:", 212 | screenshots.map((s) => s.path) 213 | ) 214 | 215 | const result = await this.processExtraScreenshotsHelper( 216 | screenshots, 217 | signal 218 | ) 219 | 220 | if (result.success) { 221 | this.deps.setHasDebugged(true) 222 | mainWindow.webContents.send( 223 | this.deps.PROCESSING_EVENTS.DEBUG_SUCCESS, 224 | result.data 225 | ) 226 | } else { 227 | mainWindow.webContents.send( 228 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 229 | result.error 230 | ) 231 | } 232 | } catch (error: any) { 233 | if (axios.isCancel(error)) { 234 | mainWindow.webContents.send( 235 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 236 | "Extra processing was canceled by the user." 237 | ) 238 | } else { 239 | mainWindow.webContents.send( 240 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 241 | error.message 242 | ) 243 | } 244 | } finally { 245 | this.currentExtraProcessingAbortController = null 246 | } 247 | } 248 | } 249 | 250 | private async processScreenshotsHelper( 251 | screenshots: Array<{ path: string; data: string }>, 252 | signal: AbortSignal 253 | ) { 254 | const MAX_RETRIES = 0 255 | let retryCount = 0 256 | 257 | while (retryCount <= MAX_RETRIES) { 258 | try { 259 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 260 | const mainWindow = this.deps.getMainWindow() 261 | const language = await this.getLanguage() 262 | let problemInfo 263 | 264 | // First API call - extract problem info 265 | try { 266 | const extractResponse = await axios.post( 267 | `${API_BASE_URL}/api/extract`, 268 | { imageDataList, language }, 269 | { 270 | signal, 271 | timeout: 300000, 272 | validateStatus: function (status) { 273 | return status < 500 274 | }, 275 | maxRedirects: 5, 276 | headers: { 277 | "Content-Type": "application/json" 278 | } 279 | } 280 | ) 281 | 282 | problemInfo = extractResponse.data 283 | 284 | // Store problem info in AppState 285 | this.deps.setProblemInfo(problemInfo) 286 | 287 | // Send first success event 288 | if (mainWindow) { 289 | mainWindow.webContents.send( 290 | this.deps.PROCESSING_EVENTS.PROBLEM_EXTRACTED, 291 | problemInfo 292 | ) 293 | 294 | // Generate solutions after successful extraction 295 | const solutionsResult = await this.generateSolutionsHelper(signal) 296 | 297 | console.log("Solutions result:", solutionsResult.data) 298 | if (solutionsResult.success) { 299 | // Clear any existing extra screenshots before transitioning to solutions view 300 | this.screenshotHelper.clearExtraScreenshotQueue() 301 | mainWindow.webContents.send( 302 | this.deps.PROCESSING_EVENTS.SOLUTION_SUCCESS, 303 | solutionsResult.data 304 | ) 305 | return { success: true, data: solutionsResult.data } 306 | } else { 307 | throw new Error( 308 | solutionsResult.error || "Failed to generate solutions" 309 | ) 310 | } 311 | } 312 | } catch (error: any) { 313 | // If the request was cancelled, don't retry 314 | if (axios.isCancel(error)) { 315 | return { 316 | success: false, 317 | error: "Processing was canceled by the user." 318 | } 319 | } 320 | 321 | console.error("API Error Details:", { 322 | status: error.response?.status, 323 | data: error.response?.data, 324 | message: error.message, 325 | code: error.code 326 | }) 327 | 328 | // Handle API-specific errors 329 | if ( 330 | error.response?.data?.error && 331 | typeof error.response.data.error === "string" 332 | ) { 333 | if (error.response.data.error.includes("Operation timed out")) { 334 | throw new Error( 335 | "Operation timed out after 1 minute. Please try again." 336 | ) 337 | } 338 | if (error.response.data.error.includes("API Key out of credits")) { 339 | throw new Error(error.response.data.error) 340 | } 341 | throw new Error(error.response.data.error) 342 | } 343 | 344 | // If we get here, it's an unknown error 345 | throw new Error(error.message || "Server error. Please try again.") 346 | } 347 | } catch (error: any) { 348 | // Log the full error for debugging 349 | console.error("Processing error details:", { 350 | message: error.message, 351 | code: error.code, 352 | response: error.response?.data, 353 | retryCount 354 | }) 355 | 356 | // If it's a cancellation or we've exhausted retries, return the error 357 | if (axios.isCancel(error) || retryCount >= MAX_RETRIES) { 358 | return { success: false, error: error.message } 359 | } 360 | 361 | // Increment retry count and continue 362 | retryCount++ 363 | } 364 | } 365 | 366 | // If we get here, all retries failed 367 | return { 368 | success: false, 369 | error: "Failed to process after multiple attempts. Please try again." 370 | } 371 | } 372 | 373 | private async generateSolutionsHelper(signal: AbortSignal) { 374 | try { 375 | const problemInfo = this.deps.getProblemInfo() 376 | const language = await this.getLanguage() 377 | 378 | if (!problemInfo) { 379 | throw new Error("No problem info available") 380 | } 381 | 382 | const response = await axios.post( 383 | `${API_BASE_URL}/api/generate`, 384 | { ...problemInfo, language }, 385 | { 386 | signal, 387 | timeout: 300000, 388 | validateStatus: function (status) { 389 | return status < 500 390 | }, 391 | maxRedirects: 5, 392 | headers: { 393 | "Content-Type": "application/json" 394 | } 395 | } 396 | ) 397 | 398 | return { success: true, data: response.data } 399 | } catch (error: any) { 400 | const mainWindow = this.deps.getMainWindow() 401 | 402 | // Handle timeout errors (both 504 and axios timeout) 403 | if (error.code === "ECONNABORTED" || error.response?.status === 504) { 404 | // Cancel ongoing API requests 405 | this.cancelOngoingRequests() 406 | // Clear both screenshot queues 407 | this.deps.clearQueues() 408 | // Update view state to queue 409 | this.deps.setView("queue") 410 | // Notify renderer to switch view 411 | if (mainWindow && !mainWindow.isDestroyed()) { 412 | mainWindow.webContents.send("reset-view") 413 | mainWindow.webContents.send( 414 | this.deps.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 415 | "Request timed out. The server took too long to respond. Please try again." 416 | ) 417 | } 418 | return { 419 | success: false, 420 | error: "Request timed out. Please try again." 421 | } 422 | } 423 | 424 | if (error.response?.data?.error?.includes("API Key out of credits")) { 425 | if (mainWindow) { 426 | mainWindow.webContents.send( 427 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 428 | ) 429 | } 430 | return { success: false, error: error.response.data.error } 431 | } 432 | 433 | if ( 434 | error.response?.data?.error?.includes( 435 | "Please close this window and re-enter a valid Open AI API key." 436 | ) 437 | ) { 438 | if (mainWindow) { 439 | mainWindow.webContents.send( 440 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 441 | ) 442 | } 443 | return { success: false, error: error.response.data.error } 444 | } 445 | 446 | return { success: false, error: error.message } 447 | } 448 | } 449 | 450 | private async processExtraScreenshotsHelper( 451 | screenshots: Array<{ path: string; data: string }>, 452 | signal: AbortSignal 453 | ) { 454 | try { 455 | const imageDataList = screenshots.map((screenshot) => screenshot.data) 456 | const problemInfo = this.deps.getProblemInfo() 457 | const language = await this.getLanguage() 458 | 459 | if (!problemInfo) { 460 | throw new Error("No problem info available") 461 | } 462 | 463 | const response = await axios.post( 464 | `${API_BASE_URL}/api/debug`, 465 | { imageDataList, problemInfo, language }, 466 | { 467 | signal, 468 | timeout: 300000, 469 | validateStatus: function (status) { 470 | return status < 500 471 | }, 472 | maxRedirects: 5, 473 | headers: { 474 | "Content-Type": "application/json" 475 | } 476 | } 477 | ) 478 | 479 | return { success: true, data: response.data } 480 | } catch (error: any) { 481 | const mainWindow = this.deps.getMainWindow() 482 | 483 | // Handle cancellation first 484 | if (axios.isCancel(error)) { 485 | return { 486 | success: false, 487 | error: "Processing was canceled by the user." 488 | } 489 | } 490 | 491 | if (error.response?.data?.error?.includes("Operation timed out")) { 492 | // Cancel ongoing API requests 493 | this.cancelOngoingRequests() 494 | // Clear both screenshot queues 495 | this.deps.clearQueues() 496 | // Update view state to queue 497 | this.deps.setView("queue") 498 | // Notify renderer to switch view 499 | if (mainWindow && !mainWindow.isDestroyed()) { 500 | mainWindow.webContents.send("reset-view") 501 | mainWindow.webContents.send( 502 | this.deps.PROCESSING_EVENTS.DEBUG_ERROR, 503 | "Operation timed out after 1 minute. Please try again." 504 | ) 505 | } 506 | return { 507 | success: false, 508 | error: "Operation timed out after 1 minute. Please try again." 509 | } 510 | } 511 | 512 | if (error.response?.data?.error?.includes("API Key out of credits")) { 513 | if (mainWindow) { 514 | mainWindow.webContents.send( 515 | this.deps.PROCESSING_EVENTS.OUT_OF_CREDITS 516 | ) 517 | } 518 | return { success: false, error: error.response.data.error } 519 | } 520 | 521 | if ( 522 | error.response?.data?.error?.includes( 523 | "Please close this window and re-enter a valid Open AI API key." 524 | ) 525 | ) { 526 | if (mainWindow) { 527 | mainWindow.webContents.send( 528 | this.deps.PROCESSING_EVENTS.API_KEY_INVALID 529 | ) 530 | } 531 | return { success: false, error: error.response.data.error } 532 | } 533 | 534 | return { success: false, error: error.message } 535 | } 536 | } 537 | 538 | public cancelOngoingRequests(): void { 539 | let wasCancelled = false 540 | 541 | if (this.currentProcessingAbortController) { 542 | this.currentProcessingAbortController.abort() 543 | this.currentProcessingAbortController = null 544 | wasCancelled = true 545 | } 546 | 547 | if (this.currentExtraProcessingAbortController) { 548 | this.currentExtraProcessingAbortController.abort() 549 | this.currentExtraProcessingAbortController = null 550 | wasCancelled = true 551 | } 552 | 553 | // Reset hasDebugged flag 554 | this.deps.setHasDebugged(false) 555 | 556 | // Clear any pending state 557 | this.deps.setProblemInfo(null) 558 | 559 | const mainWindow = this.deps.getMainWindow() 560 | if (wasCancelled && mainWindow && !mainWindow.isDestroyed()) { 561 | // Send a clear message that processing was cancelled 562 | mainWindow.webContents.send(this.deps.PROCESSING_EVENTS.NO_SCREENSHOTS) 563 | } 564 | } 565 | } 566 | --------------------------------------------------------------------------------