├── src ├── types │ ├── global.d.ts │ ├── index.tsx │ ├── solutions.ts │ └── electron.d.ts ├── components │ ├── ui │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ └── toast.tsx │ ├── Queue │ │ ├── ScreenshotQueue.tsx │ │ ├── ScreenshotItem.tsx │ │ └── QueueCommands.tsx │ ├── Chat │ │ └── ChatModal.tsx │ ├── Solutions │ │ └── SolutionCommands.tsx │ └── Audio │ │ └── AudioTranscriber.tsx ├── vite-env.d.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── App.tsx └── _pages │ └── Queue.tsx ├── 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 ├── bun.lockb ├── assets ├── .DS_Store └── icons │ ├── mac │ └── icon.icns │ ├── win │ └── icon.ico │ └── png │ └── icon-256x256.png ├── postcss.config.js ├── tsconfig.node.json ├── .gitattributes ├── electron ├── tsconfig.json ├── ipcHandlers.ts ├── shortcuts.ts ├── ScreenshotHelper.ts ├── ProcessingHelper.ts ├── LLMHelper.ts ├── main.ts ├── preload.ts └── WindowHelper.ts ├── index.html ├── .gitignore copy ├── tsconfig.json ├── worker-script └── node │ └── index.js ├── tailwind.config.js ├── README.md ├── dist-electron ├── LLMHelper.js.map ├── ipcHandlers.js.map ├── shortcuts.js.map ├── ipcHandlers.js ├── ProcessingHelper.js.map ├── ScreenshotHelper.js.map ├── main.js.map ├── shortcuts.js ├── preload.js.map ├── ProcessingHelper.js ├── ScreenshotHelper.js ├── preload.js ├── LLMHelper.js ├── main.js ├── WindowHelper.js.map └── WindowHelper.js ├── .gitignore └── package.json /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /renderer/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/assets/.DS_Store -------------------------------------------------------------------------------- /renderer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /renderer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/renderer/public/favicon.ico -------------------------------------------------------------------------------- /renderer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/renderer/public/logo192.png -------------------------------------------------------------------------------- /renderer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/renderer/public/logo512.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /assets/icons/png/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prat011/free-interview-coder/HEAD/assets/icons/png/icon-256x256.png -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // src/lib/utils.ts 2 | 3 | export function cn(...classes: (string | undefined)[]) { 4 | return classes.filter(Boolean).join(" ") 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 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 | }, 16 | "include": ["*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /renderer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Interview Coder", 3 | "name": "Interview Coder", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /renderer/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 | -------------------------------------------------------------------------------- /.gitignore copy: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | /.pnp 4 | .pnp.js 5 | 6 | # Testing 7 | /coverage 8 | 9 | # Production 10 | /build 11 | /dist 12 | /dist-electron 13 | /release 14 | 15 | # Misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Logs 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | logs 28 | *.log 29 | 30 | # Editor directories and files 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | *.swn 36 | *.bak 37 | 38 | # OS generated files 39 | .DS_Store 40 | .DS_Store? 41 | ._* 42 | .Spotlight-V100 43 | .Trashes 44 | ehthumbs.db 45 | Thumbs.db -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], // Browser APIs 5 | "module": "ESNext", // For Vite/Modern browsers 6 | "moduleResolution": "bundler", // For Vite 7 | "jsx": "react-jsx", // React specific 8 | "noEmit": true, // Vite handles the building 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "strict": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src"], // Only React source files 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | interface ElectronAPI { 2 | updateContentDimensions: (dimensions: { 3 | width: number 4 | height: number 5 | }) => Promise 6 | getScreenshots: () => Promise> 7 | deleteScreenshot: (path: string) => Promise<{ success: boolean; error?: string }> 8 | onScreenshotTaken: (callback: (data: { path: string; preview: string }) => void) => () => void 9 | onSolutionsReady: (callback: (solutions: string) => void) => () => void 10 | onResetView: (callback: () => void) => () => void 11 | onSolutionStart: (callback: () => void) => () => void 12 | onDebugStart: (callback: () => void) => () => void 13 | onDebugSuccess: (callback: (data: any) => void) => () => void 14 | onSolutionError: (callback: (error: string) => void) => () => void 15 | onProcessingNoScreenshots: (callback: () => void) => () => void 16 | onProblemExtracted: (callback: (data: any) => void) => () => void 17 | onSolutionSuccess: (callback: (data: any) => void) => () => void 18 | onUnauthorized: (callback: () => void) => () => void 19 | onDebugError: (callback: (error: string) => void) => () => void 20 | takeScreenshot: () => Promise 21 | moveWindowLeft: () => Promise 22 | moveWindowRight: () => Promise 23 | quitApp: () => Promise 24 | getApiKey: () => Promise 25 | } 26 | 27 | interface Window { 28 | electronAPI: ElectronAPI 29 | } -------------------------------------------------------------------------------- /worker-script/node/index.js: -------------------------------------------------------------------------------- 1 | const { parentPort } = require('worker_threads'); 2 | 3 | // Handle messages from the main thread 4 | parentPort.on('message', async (message) => { 5 | try { 6 | // Process the message based on its type 7 | switch (message.type) { 8 | case 'process': 9 | // Add your processing logic here 10 | const result = await processTask(message.data); 11 | parentPort.postMessage({ type: 'result', data: result }); 12 | break; 13 | 14 | default: 15 | parentPort.postMessage({ 16 | type: 'error', 17 | error: `Unknown message type: ${message.type}` 18 | }); 19 | } 20 | } catch (error) { 21 | parentPort.postMessage({ 22 | type: 'error', 23 | error: error.message 24 | }); 25 | } 26 | }); 27 | 28 | async function processTask(data) { 29 | // Add your task processing logic here 30 | return { 31 | status: 'success', 32 | result: `Processed: ${JSON.stringify(data)}` 33 | }; 34 | } 35 | 36 | // Error handling for the worker 37 | process.on('uncaughtException', (error) => { 38 | parentPort.postMessage({ 39 | type: 'error', 40 | error: `Uncaught Exception: ${error.message}` 41 | }); 42 | }); 43 | 44 | process.on('unhandledRejection', (reason) => { 45 | parentPort.postMessage({ 46 | type: 'error', 47 | error: `Unhandled Rejection: ${reason}` 48 | }); 49 | }); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | export const content = ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"] 2 | 3 | module.exports = { 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | fontFamily: { 8 | sans: ["Inter", "system-ui", "sans-serif"] 9 | }, 10 | animation: { 11 | in: "in 0.2s ease-out", 12 | out: "out 0.2s ease-in", 13 | pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", 14 | shimmer: "shimmer 2s linear infinite", 15 | "text-gradient-wave": "textGradientWave 2s infinite ease-in-out" 16 | }, 17 | keyframes: { 18 | textGradientWave: { 19 | "0%": { backgroundPosition: "0% 50%" }, 20 | "100%": { backgroundPosition: "200% 50%" } 21 | }, 22 | shimmer: { 23 | "0%": { 24 | backgroundPosition: "200% 0" 25 | }, 26 | "100%": { 27 | backgroundPosition: "-200% 0" 28 | } 29 | }, 30 | in: { 31 | "0%": { transform: "translateY(100%)", opacity: 0 }, 32 | "100%": { transform: "translateY(0)", opacity: 1 } 33 | }, 34 | out: { 35 | "0%": { transform: "translateY(0)", opacity: 1 }, 36 | "100%": { transform: "translateY(100%)", opacity: 0 } 37 | }, 38 | pulse: { 39 | "0%, 100%": { 40 | opacity: 1 41 | }, 42 | "50%": { 43 | opacity: 0.5 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | plugins: [] 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 |
32 |
33 | {isLoading && ( 34 |
35 |
36 |
37 | )} 38 | Screenshot 47 |
48 | {!isLoading && ( 49 | 59 | )} 60 |
61 | 62 | ) 63 | } 64 | 65 | export default ScreenshotItem 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Free Interview Coder 2 | 3 | A desktop application to help with coding interviews. 4 | 5 | ## 🚀 Quick Start Guide 6 | 7 | ### Prerequisites 8 | - Make sure you have Node.js installed on your computer 9 | - Git installed on your computer 10 | - A Gemini API key (get it from [Google AI Studio](https://makersuite.google.com/app/apikey)) 11 | 12 | ### Installation Steps 13 | 14 | 1. Clone the repository: 15 | ```bash 16 | git clone [repository-url] 17 | cd interview-coder 18 | ``` 19 | 20 | 2. Install dependencies: 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | 3. Set up environment variables: 26 | - Create a file named `.env` in the root folder 27 | - Add your Gemini API key: 28 | ``` 29 | GEMINI_API_KEY=your_api_key_here 30 | ``` 31 | - Save the file 32 | 33 | ### Running the App 34 | 35 | #### Method 1: Development Mode (Recommended for first run) 36 | 1. Open a terminal and run: 37 | ```bash 38 | npm run dev -- --port 5180 39 | ``` 40 | 41 | 2. Open another terminal in the same folder and run: 42 | ```bash 43 | NODE_ENV=development npm run electron:dev 44 | ``` 45 | 46 | #### Method 2: Production Mode 47 | ```bash 48 | npm run app:build 49 | ``` 50 | The built app will be in the `release` folder. 51 | 52 | ### ⚠️ Important Notes 53 | 54 | 1. **Closing the App**: 55 | - Press `Cmd + Q` (Mac) or `Ctrl + Q` (Windows/Linux) to quit 56 | - Or use Activity Monitor/Task Manager to close `Interview Coder` 57 | - The X button currently doesn't work (known issue) 58 | 59 | 2. **If the app doesn't start**: 60 | - Make sure no other app is using port 5180 61 | - Try killing existing processes: 62 | ```bash 63 | # Find processes using port 5180 64 | lsof -i :5180 65 | # Kill them (replace [PID] with the process ID) 66 | kill [PID] 67 | ``` 68 | 69 | 3. **Keyboard Shortcuts**: 70 | - `Cmd/Ctrl + B`: Toggle window visibility 71 | - `Cmd/Ctrl + H`: Take screenshot 72 | - `Cmd/Ctrl + Arrow Keys`: Move window 73 | 74 | ### Troubleshooting 75 | 76 | If you see errors: 77 | 1. Delete the `node_modules` folder 78 | 2. Delete `package-lock.json` 79 | 3. Run `npm install` again 80 | 4. Try running the app again using Method 1 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dist-electron/LLMHelper.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"LLMHelper.js","sourceRoot":"","sources":["../electron/LLMHelper.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist-electron/ipcHandlers.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ipcHandlers.js","sourceRoot":"","sources":["../electron/ipcHandlers.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /electron/ipcHandlers.ts: -------------------------------------------------------------------------------- 1 | // ipcHandlers.ts 2 | 3 | import { ipcMain, app } from "electron" 4 | import { AppState } from "./main" 5 | 6 | export function initializeIpcHandlers(appState: AppState): void { 7 | ipcMain.handle( 8 | "update-content-dimensions", 9 | async (event, { width, height }: { width: number; height: number }) => { 10 | if (width && height) { 11 | appState.setWindowDimensions(width, height) 12 | } 13 | } 14 | ) 15 | 16 | ipcMain.handle("delete-screenshot", async (event, path: string) => { 17 | return appState.deleteScreenshot(path) 18 | }) 19 | 20 | ipcMain.handle("take-screenshot", async () => { 21 | try { 22 | const screenshotPath = await appState.takeScreenshot() 23 | const preview = await appState.getImagePreview(screenshotPath) 24 | return { path: screenshotPath, preview } 25 | } catch (error) { 26 | console.error("Error taking screenshot:", error) 27 | throw error 28 | } 29 | }) 30 | 31 | ipcMain.handle("get-screenshots", async () => { 32 | console.log({ view: appState.getView() }) 33 | try { 34 | let previews = [] 35 | if (appState.getView() === "queue") { 36 | previews = await Promise.all( 37 | appState.getScreenshotQueue().map(async (path) => ({ 38 | path, 39 | preview: await appState.getImagePreview(path) 40 | })) 41 | ) 42 | } else { 43 | previews = await Promise.all( 44 | appState.getExtraScreenshotQueue().map(async (path) => ({ 45 | path, 46 | preview: await appState.getImagePreview(path) 47 | })) 48 | ) 49 | } 50 | previews.forEach((preview: any) => console.log(preview.path)) 51 | return previews 52 | } catch (error) { 53 | console.error("Error getting screenshots:", error) 54 | throw error 55 | } 56 | }) 57 | 58 | ipcMain.handle("toggle-window", async () => { 59 | appState.toggleMainWindow() 60 | }) 61 | 62 | ipcMain.handle("reset-queues", async () => { 63 | try { 64 | appState.clearQueues() 65 | console.log("Screenshot queues have been cleared.") 66 | return { success: true } 67 | } catch (error: any) { 68 | console.error("Error resetting queues:", error) 69 | return { success: false, error: error.message } 70 | } 71 | }) 72 | 73 | ipcMain.handle("quit-app", () => { 74 | app.quit() 75 | }) 76 | 77 | ipcMain.handle("get-gemini-api-key", async () => { 78 | return process.env.GEMINI_API_KEY || "" 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /renderer/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist-electron/shortcuts.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"shortcuts.js","sourceRoot":"","sources":["../electron/shortcuts.ts"],"names":[],"mappings":";;;AAAA,uCAA8C;AAG9C,MAAa,eAAe;IAClB,QAAQ,CAAU;IAE1B,YAAY,QAAkB;QAC5B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;IAEM,uBAAuB;QAC5B,yBAAc,CAAC,QAAQ,CAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;YACvD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;YAChD,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;gBACnC,IAAI,CAAC;oBACH,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAA;oBAC3D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,CAAA;oBACnE,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,kBAAkB,EAAE;wBAC9C,IAAI,EAAE,cAAc;wBACpB,OAAO;qBACR,CAAC,CAAA;gBACJ,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;gBACrD,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,yBAAc,CAAC,QAAQ,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,CAAA;QAC3D,CAAC,CAAC,CAAA;QAEF,yBAAc,CAAC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;YACjD,OAAO,CAAC,GAAG,CACT,iEAAiE,CAClE,CAAA;YAED,8BAA8B;YAC9B,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,CAAA;YAEtD,+BAA+B;YAC/B,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;YAE3B,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;YAE9B,mCAAmC;YACnC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YAE9B,oDAAoD;YACpD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;YAChD,IAAI,UAAU,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC;gBAC5C,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAC3C,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,sCAAsC;QACtC,yBAAc,CAAC,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;YACpD,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAA;YAC/D,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAA;QAChC,CAAC,CAAC,CAAA;QAEF,yBAAc,CAAC,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;YACrD,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAA;YACjE,IAAI,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAA;QACjC,CAAC,CAAC,CAAA;QACF,yBAAc,CAAC,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;YACpD,OAAO,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAA;YAC/D,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAA;QAChC,CAAC,CAAC,CAAA;QACF,yBAAc,CAAC,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;YAClD,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAA;YAC3D,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAA;QAC9B,CAAC,CAAC,CAAA;QAEF,yBAAc,CAAC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;YACjD,IAAI,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAA;YAChC,2DAA2D;YAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;YAChD,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;gBAC7C,yCAAyC;gBACzC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;oBAClC,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;oBACzC,wCAAwC;oBACxC,UAAU,CAAC,GAAG,EAAE;wBACd,IAAI,UAAU,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC;4BAC5C,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;wBAC7C,CAAC;oBACH,CAAC,EAAE,GAAG,CAAC,CAAA;gBACT,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,qCAAqC;QACrC,cAAG,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;YACvB,yBAAc,CAAC,aAAa,EAAE,CAAA;QAChC,CAAC,CAAC,CAAA;IACJ,CAAC;CACF;AA9FD,0CA8FC"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /dist-electron/ipcHandlers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // ipcHandlers.ts 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.initializeIpcHandlers = initializeIpcHandlers; 5 | const electron_1 = require("electron"); 6 | function initializeIpcHandlers(appState) { 7 | electron_1.ipcMain.handle("update-content-dimensions", async (event, { width, height }) => { 8 | if (width && height) { 9 | appState.setWindowDimensions(width, height); 10 | } 11 | }); 12 | electron_1.ipcMain.handle("delete-screenshot", async (event, path) => { 13 | return appState.deleteScreenshot(path); 14 | }); 15 | electron_1.ipcMain.handle("take-screenshot", async () => { 16 | try { 17 | const screenshotPath = await appState.takeScreenshot(); 18 | const preview = await appState.getImagePreview(screenshotPath); 19 | return { path: screenshotPath, preview }; 20 | } 21 | catch (error) { 22 | console.error("Error taking screenshot:", error); 23 | throw error; 24 | } 25 | }); 26 | electron_1.ipcMain.handle("get-screenshots", async () => { 27 | console.log({ view: appState.getView() }); 28 | try { 29 | let previews = []; 30 | if (appState.getView() === "queue") { 31 | previews = await Promise.all(appState.getScreenshotQueue().map(async (path) => ({ 32 | path, 33 | preview: await appState.getImagePreview(path) 34 | }))); 35 | } 36 | else { 37 | previews = await Promise.all(appState.getExtraScreenshotQueue().map(async (path) => ({ 38 | path, 39 | preview: await appState.getImagePreview(path) 40 | }))); 41 | } 42 | previews.forEach((preview) => console.log(preview.path)); 43 | return previews; 44 | } 45 | catch (error) { 46 | console.error("Error getting screenshots:", error); 47 | throw error; 48 | } 49 | }); 50 | electron_1.ipcMain.handle("toggle-window", async () => { 51 | appState.toggleMainWindow(); 52 | }); 53 | electron_1.ipcMain.handle("reset-queues", async () => { 54 | try { 55 | appState.clearQueues(); 56 | console.log("Screenshot queues have been cleared."); 57 | return { success: true }; 58 | } 59 | catch (error) { 60 | console.error("Error resetting queues:", error); 61 | return { success: false, error: error.message }; 62 | } 63 | }); 64 | electron_1.ipcMain.handle("quit-app", () => { 65 | electron_1.app.quit(); 66 | }); 67 | electron_1.ipcMain.handle("get-gemini-api-key", async () => { 68 | return process.env.GEMINI_API_KEY || ""; 69 | }); 70 | } 71 | //# sourceMappingURL=ipcHandlers.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interview-coder", 3 | "version": "1.0.0", 4 | "main": "./dist-electron/main.js", 5 | "scripts": { 6 | "clean": "rimraf dist dist-electron", 7 | "dev": "vite", 8 | "build": "npm run clean && tsc && vite build", 9 | "preview": "vite preview", 10 | "electron:dev": "tsc -p electron/tsconfig.json && electron .", 11 | "app:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && cross-env electron .\"", 12 | "app:build": "npm run build && electron-builder", 13 | "watch": "tsc -p electron/tsconfig.json --watch" 14 | }, 15 | "build": { 16 | "appId": "com.electron.interview-coder", 17 | "productName": "Interview Coder", 18 | "files": [ 19 | "dist/**/*", 20 | "dist-electron/**/*", 21 | "package.json" 22 | ], 23 | "directories": { 24 | "output": "release" 25 | }, 26 | "mac": { 27 | "category": "public.app-category.utilities", 28 | "target": [ 29 | "dmg", 30 | "zip" 31 | ], 32 | "icon": "assets/icons/mac/icon.icns" 33 | }, 34 | "win": { 35 | "target": [ 36 | "nsis", 37 | "portable" 38 | ], 39 | "icon": "assets/icons/win/icon.ico" 40 | }, 41 | "linux": { 42 | "target": [ 43 | "AppImage", 44 | "deb" 45 | ], 46 | "icon": "assets/icons/png/icon-256x256.png" 47 | }, 48 | "publish": [ 49 | { 50 | "provider": "github", 51 | "owner": "ibttf", 52 | "repo": "interview-coder-frontend" 53 | } 54 | ] 55 | }, 56 | "keywords": [], 57 | "author": "", 58 | "license": "ISC", 59 | "description": "", 60 | "devDependencies": { 61 | "@types/color": "^4.2.0", 62 | "@types/diff": "^6.0.0", 63 | "@types/electron": "^1.4.38", 64 | "@types/node": "^22.9.0", 65 | "@types/react": "^18.3.12", 66 | "@types/react-dom": "^18.3.1", 67 | "@types/react-syntax-highlighter": "^15.5.13", 68 | "@types/screenshot-desktop": "^1.12.3", 69 | "@types/uuid": "^9.0.8", 70 | "@typescript-eslint/eslint-plugin": "^8.14.0", 71 | "@typescript-eslint/parser": "^8.14.0", 72 | "@vitejs/plugin-react": "^4.3.3", 73 | "autoprefixer": "^10.4.20", 74 | "concurrently": "^9.1.0", 75 | "cross-env": "^7.0.3", 76 | "electron": "^33.2.0", 77 | "electron-builder": "^25.1.8", 78 | "electron-is-dev": "^3.0.1", 79 | "postcss": "^8.4.49", 80 | "rimraf": "^6.0.1", 81 | "tailwindcss": "^3.4.15", 82 | "typescript": "^5.6.3", 83 | "vite": "^5.4.11", 84 | "vite-plugin-electron": "^0.28.8", 85 | "vite-plugin-electron-renderer": "^0.14.6", 86 | "wait-on": "^8.0.1" 87 | }, 88 | "dependencies": { 89 | "@google/genai": "^1.28.0", 90 | "@google/generative-ai": "^0.2.1", 91 | "@radix-ui/react-dialog": "^1.1.2", 92 | "@radix-ui/react-toast": "^1.2.2", 93 | "axios": "^1.7.7", 94 | "class-variance-authority": "^0.7.0", 95 | "clsx": "^2.1.1", 96 | "diff": "^7.0.0", 97 | "dotenv": "^17.2.3", 98 | "form-data": "^4.0.1", 99 | "lucide-react": "^0.460.0", 100 | "react": "^18.3.1", 101 | "react-code-blocks": "^0.1.6", 102 | "react-dom": "^18.3.1", 103 | "react-icons": "^5.3.0", 104 | "react-query": "^3.39.3", 105 | "react-syntax-highlighter": "^15.6.1", 106 | "screenshot-desktop": "^1.15.0", 107 | "sharp": "^0.33.5", 108 | "tailwind-merge": "^2.5.4", 109 | "tesseract.js": "^5.0.5", 110 | "uuid": "^11.0.3" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /electron/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut, app } from "electron" 2 | import { AppState } from "./main" // Adjust the import path if necessary 3 | 4 | export class ShortcutsHelper { 5 | private appState: AppState 6 | 7 | constructor(appState: AppState) { 8 | this.appState = appState 9 | } 10 | 11 | public registerGlobalShortcuts(): void { 12 | globalShortcut.register("CommandOrControl+H", async () => { 13 | const mainWindow = this.appState.getMainWindow() 14 | if (mainWindow) { 15 | console.log("Taking screenshot...") 16 | try { 17 | const screenshotPath = await this.appState.takeScreenshot() 18 | const preview = await this.appState.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.appState.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.appState.processingHelper.cancelOngoingRequests() 40 | 41 | // Clear both screenshot queues 42 | this.appState.clearQueues() 43 | 44 | console.log("Cleared queues.") 45 | 46 | // Update the view state to 'queue' 47 | this.appState.setView("queue") 48 | 49 | // Notify renderer process to switch view to 'queue' 50 | const mainWindow = this.appState.getMainWindow() 51 | if (mainWindow && !mainWindow.isDestroyed()) { 52 | mainWindow.webContents.send("reset-view") 53 | } 54 | }) 55 | 56 | // New shortcuts for moving the window 57 | globalShortcut.register("CommandOrControl+Left", () => { 58 | console.log("Command/Ctrl + Left pressed. Moving window left.") 59 | this.appState.moveWindowLeft() 60 | }) 61 | 62 | globalShortcut.register("CommandOrControl+Right", () => { 63 | console.log("Command/Ctrl + Right pressed. Moving window right.") 64 | this.appState.moveWindowRight() 65 | }) 66 | globalShortcut.register("CommandOrControl+Down", () => { 67 | console.log("Command/Ctrl + down pressed. Moving window down.") 68 | this.appState.moveWindowDown() 69 | }) 70 | globalShortcut.register("CommandOrControl+Up", () => { 71 | console.log("Command/Ctrl + Up pressed. Moving window Up.") 72 | this.appState.moveWindowUp() 73 | }) 74 | 75 | globalShortcut.register("CommandOrControl+B", () => { 76 | this.appState.toggleMainWindow() 77 | // If window exists and we're showing it, bring it to front 78 | const mainWindow = this.appState.getMainWindow() 79 | if (mainWindow && !this.appState.isVisible()) { 80 | // Force the window to the front on macOS 81 | if (process.platform === "darwin") { 82 | mainWindow.setAlwaysOnTop(true, "normal") 83 | // Reset alwaysOnTop after a brief delay 84 | setTimeout(() => { 85 | if (mainWindow && !mainWindow.isDestroyed()) { 86 | mainWindow.setAlwaysOnTop(true, "floating") 87 | } 88 | }, 100) 89 | } 90 | } 91 | }) 92 | 93 | // Unregister shortcuts when quitting 94 | app.on("will-quit", () => { 95 | globalShortcut.unregisterAll() 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /dist-electron/ProcessingHelper.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ProcessingHelper.js","sourceRoot":"","sources":["../electron/ProcessingHelper.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist-electron/ScreenshotHelper.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ScreenshotHelper.js","sourceRoot":"","sources":["../electron/ScreenshotHelper.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /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 { 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 | } 35 | 36 | const toastVariants: Record = { 37 | neutral: "bg-yellow-500 text-white", 38 | success: "bg-green-500 text-white", 39 | error: "bg-red-500 text-white" 40 | } 41 | 42 | const Toast = React.forwardRef< 43 | React.ElementRef, 44 | ToastProps 45 | >(({ className, variant = "neutral", ...props }, ref) => ( 46 | 55 | )) 56 | Toast.displayName = ToastPrimitive.Root.displayName 57 | 58 | const ToastAction = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 67 | )) 68 | ToastAction.displayName = ToastPrimitive.Action.displayName 69 | 70 | const ToastClose = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, ...props }, ref) => ( 74 | 82 | 83 | 84 | )) 85 | ToastClose.displayName = ToastPrimitive.Close.displayName 86 | 87 | const ToastTitle = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => ( 91 | 96 | )) 97 | ToastTitle.displayName = ToastPrimitive.Title.displayName 98 | 99 | const ToastDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | ToastDescription.displayName = ToastPrimitive.Description.displayName 110 | 111 | export type { ToastProps, ToastVariant } 112 | export { 113 | ToastProvider, 114 | ToastViewport, 115 | Toast, 116 | ToastAction, 117 | ToastClose, 118 | ToastTitle, 119 | ToastDescription 120 | } 121 | -------------------------------------------------------------------------------- /dist-electron/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sourceRoot":"","sources":["../electron/main.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist-electron/shortcuts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ShortcutsHelper = void 0; 4 | const electron_1 = require("electron"); 5 | class ShortcutsHelper { 6 | appState; 7 | constructor(appState) { 8 | this.appState = appState; 9 | } 10 | registerGlobalShortcuts() { 11 | electron_1.globalShortcut.register("CommandOrControl+H", async () => { 12 | const mainWindow = this.appState.getMainWindow(); 13 | if (mainWindow) { 14 | console.log("Taking screenshot..."); 15 | try { 16 | const screenshotPath = await this.appState.takeScreenshot(); 17 | const preview = await this.appState.getImagePreview(screenshotPath); 18 | mainWindow.webContents.send("screenshot-taken", { 19 | path: screenshotPath, 20 | preview 21 | }); 22 | } 23 | catch (error) { 24 | console.error("Error capturing screenshot:", error); 25 | } 26 | } 27 | }); 28 | electron_1.globalShortcut.register("CommandOrControl+Enter", async () => { 29 | await this.appState.processingHelper.processScreenshots(); 30 | }); 31 | electron_1.globalShortcut.register("CommandOrControl+R", () => { 32 | console.log("Command + R pressed. Canceling requests and resetting queues..."); 33 | // Cancel ongoing API requests 34 | this.appState.processingHelper.cancelOngoingRequests(); 35 | // Clear both screenshot queues 36 | this.appState.clearQueues(); 37 | console.log("Cleared queues."); 38 | // Update the view state to 'queue' 39 | this.appState.setView("queue"); 40 | // Notify renderer process to switch view to 'queue' 41 | const mainWindow = this.appState.getMainWindow(); 42 | if (mainWindow && !mainWindow.isDestroyed()) { 43 | mainWindow.webContents.send("reset-view"); 44 | } 45 | }); 46 | // New shortcuts for moving the window 47 | electron_1.globalShortcut.register("CommandOrControl+Left", () => { 48 | console.log("Command/Ctrl + Left pressed. Moving window left."); 49 | this.appState.moveWindowLeft(); 50 | }); 51 | electron_1.globalShortcut.register("CommandOrControl+Right", () => { 52 | console.log("Command/Ctrl + Right pressed. Moving window right."); 53 | this.appState.moveWindowRight(); 54 | }); 55 | electron_1.globalShortcut.register("CommandOrControl+Down", () => { 56 | console.log("Command/Ctrl + down pressed. Moving window down."); 57 | this.appState.moveWindowDown(); 58 | }); 59 | electron_1.globalShortcut.register("CommandOrControl+Up", () => { 60 | console.log("Command/Ctrl + Up pressed. Moving window Up."); 61 | this.appState.moveWindowUp(); 62 | }); 63 | electron_1.globalShortcut.register("CommandOrControl+B", () => { 64 | this.appState.toggleMainWindow(); 65 | // If window exists and we're showing it, bring it to front 66 | const mainWindow = this.appState.getMainWindow(); 67 | if (mainWindow && !this.appState.isVisible()) { 68 | // Force the window to the front on macOS 69 | if (process.platform === "darwin") { 70 | mainWindow.setAlwaysOnTop(true, "normal"); 71 | // Reset alwaysOnTop after a brief delay 72 | setTimeout(() => { 73 | if (mainWindow && !mainWindow.isDestroyed()) { 74 | mainWindow.setAlwaysOnTop(true, "floating"); 75 | } 76 | }, 100); 77 | } 78 | } 79 | }); 80 | // Unregister shortcuts when quitting 81 | electron_1.app.on("will-quit", () => { 82 | electron_1.globalShortcut.unregisterAll(); 83 | }); 84 | } 85 | } 86 | exports.ShortcutsHelper = ShortcutsHelper; 87 | //# sourceMappingURL=shortcuts.js.map -------------------------------------------------------------------------------- /dist-electron/preload.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"preload.js","sourceRoot":"","sources":["../electron/preload.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ToastProvider } from "./components/ui/toast" 2 | import Queue from "./_pages/Queue" 3 | import { ToastViewport } from "@radix-ui/react-toast" 4 | import { useEffect, useRef, useState } from "react" 5 | import Solutions from "./_pages/Solutions" 6 | import { QueryClient, QueryClientProvider } from "react-query" 7 | 8 | const queryClient = new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | staleTime: Infinity, 12 | cacheTime: Infinity 13 | } 14 | } 15 | }) 16 | 17 | const App: React.FC = () => { 18 | const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") 19 | const [isChatOpen, setIsChatOpen] = useState(false) 20 | const containerRef = useRef(null) 21 | 22 | // Effect for height monitoring 23 | useEffect(() => { 24 | const cleanup = window.electronAPI.onResetView(() => { 25 | console.log("Received 'reset-view' message from main process.") 26 | queryClient.invalidateQueries(["screenshots"]) 27 | queryClient.invalidateQueries(["problem_statement"]) 28 | queryClient.invalidateQueries(["solution"]) 29 | queryClient.invalidateQueries(["new_solution"]) 30 | setView("queue") 31 | }) 32 | 33 | return () => { 34 | cleanup() 35 | } 36 | }, []) 37 | 38 | useEffect(() => { 39 | if (!containerRef.current) return 40 | 41 | const updateHeight = () => { 42 | if (!containerRef.current) return 43 | const height = containerRef.current.scrollHeight 44 | const width = containerRef.current.scrollWidth 45 | window.electronAPI?.updateContentDimensions({ width, height }) 46 | } 47 | 48 | const resizeObserver = new ResizeObserver(() => { 49 | updateHeight() 50 | }) 51 | 52 | // Initial height update 53 | updateHeight() 54 | 55 | // Observe for changes 56 | resizeObserver.observe(containerRef.current) 57 | 58 | // Also update height when view changes 59 | const mutationObserver = new MutationObserver(() => { 60 | updateHeight() 61 | }) 62 | 63 | mutationObserver.observe(containerRef.current, { 64 | childList: true, 65 | subtree: true, 66 | attributes: true, 67 | characterData: true 68 | }) 69 | 70 | return () => { 71 | resizeObserver.disconnect() 72 | mutationObserver.disconnect() 73 | } 74 | }, [view]) // Re-run when view changes 75 | 76 | useEffect(() => { 77 | const cleanupFunctions = [ 78 | window.electronAPI.onSolutionStart(() => { 79 | setView("solutions") 80 | console.log("starting processing") 81 | }), 82 | 83 | window.electronAPI.onUnauthorized(() => { 84 | queryClient.removeQueries(["screenshots"]) 85 | queryClient.removeQueries(["solution"]) 86 | queryClient.removeQueries(["problem_statement"]) 87 | setView("queue") 88 | console.log("Unauthorized") 89 | }), 90 | // Update this reset handler 91 | window.electronAPI.onResetView(() => { 92 | console.log("Received 'reset-view' message from main process") 93 | 94 | queryClient.removeQueries(["screenshots"]) 95 | queryClient.removeQueries(["solution"]) 96 | queryClient.removeQueries(["problem_statement"]) 97 | setView("queue") 98 | console.log("View reset to 'queue' via Command+R shortcut") 99 | }), 100 | window.electronAPI.onProblemExtracted((data: any) => { 101 | if (view === "queue") { 102 | console.log("Problem extracted successfully") 103 | queryClient.invalidateQueries(["problem_statement"]) 104 | queryClient.setQueryData(["problem_statement"], data) 105 | } 106 | }) 107 | ] 108 | return () => cleanupFunctions.forEach((cleanup) => cleanup()) 109 | }, []) 110 | 111 | // Handle Command+Enter to open chat 112 | useEffect(() => { 113 | const handleKeyDown = (e: KeyboardEvent) => { 114 | if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { 115 | e.preventDefault() 116 | setIsChatOpen(true) 117 | } 118 | } 119 | 120 | window.addEventListener("keydown", handleKeyDown) 121 | return () => window.removeEventListener("keydown", handleKeyDown) 122 | }, []) 123 | 124 | return ( 125 |
126 | 127 | 128 | {view === "queue" ? ( 129 | 130 | ) : view === "solutions" ? ( 131 | 132 | ) : ( 133 | <> 134 | )} 135 | 136 | 137 | 138 |
139 | ) 140 | } 141 | 142 | export default App 143 | -------------------------------------------------------------------------------- /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 screenshot from "screenshot-desktop" 8 | 9 | export class ScreenshotHelper { 10 | private screenshotQueue: string[] = [] 11 | private extraScreenshotQueue: string[] = [] 12 | private readonly MAX_SCREENSHOTS = 5 13 | 14 | private readonly screenshotDir: string 15 | private readonly extraScreenshotDir: string 16 | 17 | private view: "queue" | "solutions" = "queue" 18 | 19 | constructor(view: "queue" | "solutions" = "queue") { 20 | this.view = view 21 | 22 | // Initialize directories 23 | this.screenshotDir = path.join(app.getPath("userData"), "screenshots") 24 | this.extraScreenshotDir = path.join( 25 | app.getPath("userData"), 26 | "extra_screenshots" 27 | ) 28 | 29 | // Create directories if they don't exist 30 | if (!fs.existsSync(this.screenshotDir)) { 31 | fs.mkdirSync(this.screenshotDir) 32 | } 33 | if (!fs.existsSync(this.extraScreenshotDir)) { 34 | fs.mkdirSync(this.extraScreenshotDir) 35 | } 36 | } 37 | 38 | public getView(): "queue" | "solutions" { 39 | return this.view 40 | } 41 | 42 | public setView(view: "queue" | "solutions"): void { 43 | this.view = view 44 | } 45 | 46 | public getScreenshotQueue(): string[] { 47 | return this.screenshotQueue 48 | } 49 | 50 | public getExtraScreenshotQueue(): string[] { 51 | return this.extraScreenshotQueue 52 | } 53 | 54 | public clearQueues(): void { 55 | // Clear screenshotQueue 56 | this.screenshotQueue.forEach((screenshotPath) => { 57 | fs.unlink(screenshotPath, (err) => { 58 | if (err) 59 | console.error(`Error deleting screenshot at ${screenshotPath}:`, err) 60 | }) 61 | }) 62 | this.screenshotQueue = [] 63 | 64 | // Clear extraScreenshotQueue 65 | this.extraScreenshotQueue.forEach((screenshotPath) => { 66 | fs.unlink(screenshotPath, (err) => { 67 | if (err) 68 | console.error( 69 | `Error deleting extra screenshot at ${screenshotPath}:`, 70 | err 71 | ) 72 | }) 73 | }) 74 | this.extraScreenshotQueue = [] 75 | } 76 | 77 | public async takeScreenshot( 78 | hideMainWindow: () => void, 79 | showMainWindow: () => void 80 | ): Promise { 81 | hideMainWindow() 82 | let screenshotPath = "" 83 | 84 | if (this.view === "queue") { 85 | screenshotPath = path.join(this.screenshotDir, `${uuidv4()}.png`) 86 | await screenshot({ filename: screenshotPath }) 87 | 88 | this.screenshotQueue.push(screenshotPath) 89 | if (this.screenshotQueue.length > this.MAX_SCREENSHOTS) { 90 | const removedPath = this.screenshotQueue.shift() 91 | if (removedPath) { 92 | try { 93 | await fs.promises.unlink(removedPath) 94 | } catch (error) { 95 | console.error("Error removing old screenshot:", error) 96 | } 97 | } 98 | } 99 | } else { 100 | screenshotPath = path.join(this.extraScreenshotDir, `${uuidv4()}.png`) 101 | await screenshot({ filename: screenshotPath }) 102 | 103 | this.extraScreenshotQueue.push(screenshotPath) 104 | if (this.extraScreenshotQueue.length > this.MAX_SCREENSHOTS) { 105 | const removedPath = this.extraScreenshotQueue.shift() 106 | if (removedPath) { 107 | try { 108 | await fs.promises.unlink(removedPath) 109 | } catch (error) { 110 | console.error("Error removing old screenshot:", error) 111 | } 112 | } 113 | } 114 | } 115 | 116 | showMainWindow() 117 | return screenshotPath 118 | } 119 | 120 | public async getImagePreview(filepath: string): Promise { 121 | try { 122 | const data = await fs.promises.readFile(filepath) 123 | return `data:image/png;base64,${data.toString("base64")}` 124 | } catch (error) { 125 | console.error("Error reading image:", error) 126 | throw error 127 | } 128 | } 129 | 130 | public async deleteScreenshot( 131 | path: string 132 | ): Promise<{ success: boolean; error?: string }> { 133 | try { 134 | await fs.promises.unlink(path) 135 | if (this.view === "queue") { 136 | this.screenshotQueue = this.screenshotQueue.filter( 137 | (filePath) => filePath !== path 138 | ) 139 | } else { 140 | this.extraScreenshotQueue = this.extraScreenshotQueue.filter( 141 | (filePath) => filePath !== path 142 | ) 143 | } 144 | return { success: true } 145 | } catch (error) { 146 | console.error("Error deleting file:", error) 147 | return { success: false, error: error.message } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /electron/ProcessingHelper.ts: -------------------------------------------------------------------------------- 1 | // ProcessingHelper.ts 2 | 3 | import { AppState } from "./main" 4 | import { LLMHelper } from "./LLMHelper" 5 | import dotenv from "dotenv" 6 | 7 | dotenv.config() 8 | 9 | const isDev = process.env.NODE_ENV === "development" 10 | const isDevTest = process.env.IS_DEV_TEST === "true" 11 | const MOCK_API_WAIT_TIME = Number(process.env.MOCK_API_WAIT_TIME) || 500 12 | 13 | export class ProcessingHelper { 14 | private appState: AppState 15 | private llmHelper: LLMHelper 16 | private currentProcessingAbortController: AbortController | null = null 17 | private currentExtraProcessingAbortController: AbortController | null = null 18 | 19 | constructor(appState: AppState) { 20 | this.appState = appState 21 | const apiKey = process.env.GEMINI_API_KEY 22 | if (!apiKey) { 23 | throw new Error("GEMINI_API_KEY not found in environment variables") 24 | } 25 | this.llmHelper = new LLMHelper(apiKey) 26 | } 27 | 28 | public async processScreenshots(): Promise { 29 | const mainWindow = this.appState.getMainWindow() 30 | if (!mainWindow) return 31 | 32 | const view = this.appState.getView() 33 | 34 | if (view === "queue") { 35 | const screenshotQueue = this.appState.getScreenshotHelper().getScreenshotQueue() 36 | if (screenshotQueue.length === 0) { 37 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.NO_SCREENSHOTS) 38 | return 39 | } 40 | 41 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.INITIAL_START) 42 | this.appState.setView("solutions") 43 | 44 | this.currentProcessingAbortController = new AbortController() 45 | 46 | try { 47 | // Extract problem information using LLM with vision 48 | const problemInfo = await this.llmHelper.extractProblemFromImages(screenshotQueue) 49 | 50 | // Store problem info 51 | this.appState.setProblemInfo(problemInfo) 52 | 53 | // Send problem extracted event 54 | mainWindow.webContents.send( 55 | this.appState.PROCESSING_EVENTS.PROBLEM_EXTRACTED, 56 | problemInfo 57 | ) 58 | 59 | // Generate solution 60 | const solution = await this.llmHelper.generateSolution(problemInfo) 61 | 62 | mainWindow.webContents.send( 63 | this.appState.PROCESSING_EVENTS.SOLUTION_SUCCESS, 64 | solution 65 | ) 66 | 67 | } catch (error: any) { 68 | console.error("Processing error:", error) 69 | mainWindow.webContents.send( 70 | this.appState.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 71 | error.message 72 | ) 73 | } finally { 74 | this.currentProcessingAbortController = null 75 | } 76 | } else { 77 | // Debug mode 78 | const extraScreenshotQueue = this.appState.getScreenshotHelper().getExtraScreenshotQueue() 79 | if (extraScreenshotQueue.length === 0) { 80 | console.log("No extra screenshots to process") 81 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.NO_SCREENSHOTS) 82 | return 83 | } 84 | 85 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.DEBUG_START) 86 | this.currentExtraProcessingAbortController = new AbortController() 87 | 88 | try { 89 | // Get problem info and current solution 90 | const problemInfo = this.appState.getProblemInfo() 91 | if (!problemInfo) { 92 | throw new Error("No problem info available") 93 | } 94 | 95 | // Get current solution from state 96 | const currentSolution = await this.llmHelper.generateSolution(problemInfo) 97 | const currentCode = currentSolution.solution.code 98 | 99 | // Debug the solution using vision model 100 | const debugResult = await this.llmHelper.debugSolutionWithImages( 101 | problemInfo, 102 | currentCode, 103 | extraScreenshotQueue 104 | ) 105 | 106 | this.appState.setHasDebugged(true) 107 | mainWindow.webContents.send( 108 | this.appState.PROCESSING_EVENTS.DEBUG_SUCCESS, 109 | debugResult 110 | ) 111 | 112 | } catch (error: any) { 113 | console.error("Debug processing error:", error) 114 | mainWindow.webContents.send( 115 | this.appState.PROCESSING_EVENTS.DEBUG_ERROR, 116 | error.message 117 | ) 118 | } finally { 119 | this.currentExtraProcessingAbortController = null 120 | } 121 | } 122 | } 123 | 124 | public cancelOngoingRequests(): void { 125 | if (this.currentProcessingAbortController) { 126 | this.currentProcessingAbortController.abort() 127 | this.currentProcessingAbortController = null 128 | } 129 | 130 | if (this.currentExtraProcessingAbortController) { 131 | this.currentExtraProcessingAbortController.abort() 132 | this.currentExtraProcessingAbortController = null 133 | } 134 | 135 | this.appState.setHasDebugged(false) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /dist-electron/ProcessingHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // ProcessingHelper.ts 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | exports.ProcessingHelper = void 0; 8 | const LLMHelper_1 = require("./LLMHelper"); 9 | const dotenv_1 = __importDefault(require("dotenv")); 10 | dotenv_1.default.config(); 11 | const isDev = process.env.NODE_ENV === "development"; 12 | const isDevTest = process.env.IS_DEV_TEST === "true"; 13 | const MOCK_API_WAIT_TIME = Number(process.env.MOCK_API_WAIT_TIME) || 500; 14 | class ProcessingHelper { 15 | appState; 16 | llmHelper; 17 | currentProcessingAbortController = null; 18 | currentExtraProcessingAbortController = null; 19 | constructor(appState) { 20 | this.appState = appState; 21 | const apiKey = process.env.GEMINI_API_KEY; 22 | if (!apiKey) { 23 | throw new Error("GEMINI_API_KEY not found in environment variables"); 24 | } 25 | this.llmHelper = new LLMHelper_1.LLMHelper(apiKey); 26 | } 27 | async processScreenshots() { 28 | const mainWindow = this.appState.getMainWindow(); 29 | if (!mainWindow) 30 | return; 31 | const view = this.appState.getView(); 32 | if (view === "queue") { 33 | const screenshotQueue = this.appState.getScreenshotHelper().getScreenshotQueue(); 34 | if (screenshotQueue.length === 0) { 35 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.NO_SCREENSHOTS); 36 | return; 37 | } 38 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.INITIAL_START); 39 | this.appState.setView("solutions"); 40 | this.currentProcessingAbortController = new AbortController(); 41 | try { 42 | // Extract problem information using LLM with vision 43 | const problemInfo = await this.llmHelper.extractProblemFromImages(screenshotQueue); 44 | // Store problem info 45 | this.appState.setProblemInfo(problemInfo); 46 | // Send problem extracted event 47 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.PROBLEM_EXTRACTED, problemInfo); 48 | // Generate solution 49 | const solution = await this.llmHelper.generateSolution(problemInfo); 50 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.SOLUTION_SUCCESS, solution); 51 | } 52 | catch (error) { 53 | console.error("Processing error:", error); 54 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, error.message); 55 | } 56 | finally { 57 | this.currentProcessingAbortController = null; 58 | } 59 | } 60 | else { 61 | // Debug mode 62 | const extraScreenshotQueue = this.appState.getScreenshotHelper().getExtraScreenshotQueue(); 63 | if (extraScreenshotQueue.length === 0) { 64 | console.log("No extra screenshots to process"); 65 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.NO_SCREENSHOTS); 66 | return; 67 | } 68 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.DEBUG_START); 69 | this.currentExtraProcessingAbortController = new AbortController(); 70 | try { 71 | // Get problem info and current solution 72 | const problemInfo = this.appState.getProblemInfo(); 73 | if (!problemInfo) { 74 | throw new Error("No problem info available"); 75 | } 76 | // Get current solution from state 77 | const currentSolution = await this.llmHelper.generateSolution(problemInfo); 78 | const currentCode = currentSolution.solution.code; 79 | // Debug the solution using vision model 80 | const debugResult = await this.llmHelper.debugSolutionWithImages(problemInfo, currentCode, extraScreenshotQueue); 81 | this.appState.setHasDebugged(true); 82 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.DEBUG_SUCCESS, debugResult); 83 | } 84 | catch (error) { 85 | console.error("Debug processing error:", error); 86 | mainWindow.webContents.send(this.appState.PROCESSING_EVENTS.DEBUG_ERROR, error.message); 87 | } 88 | finally { 89 | this.currentExtraProcessingAbortController = null; 90 | } 91 | } 92 | } 93 | cancelOngoingRequests() { 94 | if (this.currentProcessingAbortController) { 95 | this.currentProcessingAbortController.abort(); 96 | this.currentProcessingAbortController = null; 97 | } 98 | if (this.currentExtraProcessingAbortController) { 99 | this.currentExtraProcessingAbortController.abort(); 100 | this.currentExtraProcessingAbortController = null; 101 | } 102 | this.appState.setHasDebugged(false); 103 | } 104 | } 105 | exports.ProcessingHelper = ProcessingHelper; 106 | //# sourceMappingURL=ProcessingHelper.js.map -------------------------------------------------------------------------------- /electron/LLMHelper.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai" 2 | import fs from "fs" 3 | 4 | export class LLMHelper { 5 | private model: GenerativeModel 6 | 7 | constructor(apiKey: string) { 8 | const genAI = new GoogleGenerativeAI(apiKey) 9 | this.model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }) 10 | } 11 | 12 | private async fileToGenerativePart(imagePath: string) { 13 | const imageData = await fs.promises.readFile(imagePath) 14 | return { 15 | inlineData: { 16 | data: imageData.toString("base64"), 17 | mimeType: "image/png" 18 | } 19 | } 20 | } 21 | 22 | private cleanJsonResponse(text: string): string { 23 | // Remove markdown code block syntax if present 24 | text = text.replace(/^```(?:json)?\n/, '').replace(/\n```$/, ''); 25 | // Remove any leading/trailing whitespace 26 | text = text.trim(); 27 | return text; 28 | } 29 | 30 | public async extractProblemFromImages(imagePaths: string[]) { 31 | try { 32 | const imageParts = await Promise.all(imagePaths.map(path => this.fileToGenerativePart(path))) 33 | 34 | const prompt = `You are a coding problem analyzer. Please analyze these images of a coding problem and extract the following information in JSON format: 35 | { 36 | "problem_statement": "The complete problem statement", 37 | "input_format": { 38 | "description": "Description of input format", 39 | "parameters": [{"name": "param name", "type": "param type", "description": "param description"}] 40 | }, 41 | "output_format": { 42 | "description": "Description of what should be output", 43 | "type": "The expected type of the output" 44 | }, 45 | "constraints": [ 46 | {"description": "Each constraint in plain text"} 47 | ], 48 | "test_cases": [ 49 | { 50 | "input": "Example input", 51 | "output": "Expected output", 52 | "explanation": "Explanation if provided" 53 | } 54 | ] 55 | } 56 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.` 57 | 58 | const result = await this.model.generateContent([prompt, ...imageParts]) 59 | const response = await result.response 60 | const text = this.cleanJsonResponse(response.text()) 61 | return JSON.parse(text) 62 | } catch (error) { 63 | console.error("Error extracting problem from images:", error) 64 | throw error 65 | } 66 | } 67 | 68 | public async generateSolution(problemInfo: any) { 69 | const prompt = `Given this coding problem: 70 | ${JSON.stringify(problemInfo, null, 2)} 71 | 72 | Please provide a solution in the following JSON format: 73 | { 74 | "solution": { 75 | "explanation": "Detailed explanation of the approach", 76 | "complexity": { 77 | "time": "Time complexity", 78 | "space": "Space complexity" 79 | }, 80 | "code": "The complete solution code", 81 | "test_results": [ 82 | { 83 | "input": "test case input", 84 | "expected": "expected output", 85 | "actual": "actual output", 86 | "passed": true/false 87 | } 88 | ] 89 | } 90 | } 91 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.` 92 | 93 | const result = await this.model.generateContent(prompt) 94 | const response = await result.response 95 | const text = this.cleanJsonResponse(response.text()) 96 | return JSON.parse(text) 97 | } 98 | 99 | public async debugSolutionWithImages(problemInfo: any, currentCode: string, debugImagePaths: string[]) { 100 | try { 101 | const imageParts = await Promise.all(debugImagePaths.map(path => this.fileToGenerativePart(path))) 102 | 103 | const prompt = `You are a coding problem debugger. Given: 104 | 1. The original problem: ${JSON.stringify(problemInfo, null, 2)} 105 | 2. The current solution: ${currentCode} 106 | 3. The debug information in the provided images 107 | 108 | Please analyze the debug information and provide feedback in this JSON format: 109 | { 110 | "analysis": { 111 | "issues_found": [ 112 | { 113 | "description": "Description of the issue", 114 | "location": "Where in the code", 115 | "severity": "high/medium/low" 116 | } 117 | ], 118 | "suggested_fixes": [ 119 | { 120 | "description": "Description of the fix", 121 | "code_change": "The specific code change needed" 122 | } 123 | ] 124 | }, 125 | "improved_solution": { 126 | "code": "The complete improved solution", 127 | "explanation": "Explanation of the changes made" 128 | } 129 | } 130 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.` 131 | 132 | const result = await this.model.generateContent([prompt, ...imageParts]) 133 | const response = await result.response 134 | const text = this.cleanJsonResponse(response.text()) 135 | return JSON.parse(text) 136 | } catch (error) { 137 | console.error("Error debugging solution with images:", error) 138 | throw error 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /dist-electron/ScreenshotHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // ScreenshotHelper.ts 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | exports.ScreenshotHelper = void 0; 8 | const node_path_1 = __importDefault(require("node:path")); 9 | const node_fs_1 = __importDefault(require("node:fs")); 10 | const electron_1 = require("electron"); 11 | const uuid_1 = require("uuid"); 12 | const screenshot_desktop_1 = __importDefault(require("screenshot-desktop")); 13 | class ScreenshotHelper { 14 | screenshotQueue = []; 15 | extraScreenshotQueue = []; 16 | MAX_SCREENSHOTS = 5; 17 | screenshotDir; 18 | extraScreenshotDir; 19 | view = "queue"; 20 | constructor(view = "queue") { 21 | this.view = view; 22 | // Initialize directories 23 | this.screenshotDir = node_path_1.default.join(electron_1.app.getPath("userData"), "screenshots"); 24 | this.extraScreenshotDir = node_path_1.default.join(electron_1.app.getPath("userData"), "extra_screenshots"); 25 | // Create directories if they don't exist 26 | if (!node_fs_1.default.existsSync(this.screenshotDir)) { 27 | node_fs_1.default.mkdirSync(this.screenshotDir); 28 | } 29 | if (!node_fs_1.default.existsSync(this.extraScreenshotDir)) { 30 | node_fs_1.default.mkdirSync(this.extraScreenshotDir); 31 | } 32 | } 33 | getView() { 34 | return this.view; 35 | } 36 | setView(view) { 37 | this.view = view; 38 | } 39 | getScreenshotQueue() { 40 | return this.screenshotQueue; 41 | } 42 | getExtraScreenshotQueue() { 43 | return this.extraScreenshotQueue; 44 | } 45 | clearQueues() { 46 | // Clear screenshotQueue 47 | this.screenshotQueue.forEach((screenshotPath) => { 48 | node_fs_1.default.unlink(screenshotPath, (err) => { 49 | if (err) 50 | console.error(`Error deleting screenshot at ${screenshotPath}:`, err); 51 | }); 52 | }); 53 | this.screenshotQueue = []; 54 | // Clear extraScreenshotQueue 55 | this.extraScreenshotQueue.forEach((screenshotPath) => { 56 | node_fs_1.default.unlink(screenshotPath, (err) => { 57 | if (err) 58 | console.error(`Error deleting extra screenshot at ${screenshotPath}:`, err); 59 | }); 60 | }); 61 | this.extraScreenshotQueue = []; 62 | } 63 | async takeScreenshot(hideMainWindow, showMainWindow) { 64 | hideMainWindow(); 65 | let screenshotPath = ""; 66 | if (this.view === "queue") { 67 | screenshotPath = node_path_1.default.join(this.screenshotDir, `${(0, uuid_1.v4)()}.png`); 68 | await (0, screenshot_desktop_1.default)({ filename: screenshotPath }); 69 | this.screenshotQueue.push(screenshotPath); 70 | if (this.screenshotQueue.length > this.MAX_SCREENSHOTS) { 71 | const removedPath = this.screenshotQueue.shift(); 72 | if (removedPath) { 73 | try { 74 | await node_fs_1.default.promises.unlink(removedPath); 75 | } 76 | catch (error) { 77 | console.error("Error removing old screenshot:", error); 78 | } 79 | } 80 | } 81 | } 82 | else { 83 | screenshotPath = node_path_1.default.join(this.extraScreenshotDir, `${(0, uuid_1.v4)()}.png`); 84 | await (0, screenshot_desktop_1.default)({ filename: screenshotPath }); 85 | this.extraScreenshotQueue.push(screenshotPath); 86 | if (this.extraScreenshotQueue.length > this.MAX_SCREENSHOTS) { 87 | const removedPath = this.extraScreenshotQueue.shift(); 88 | if (removedPath) { 89 | try { 90 | await node_fs_1.default.promises.unlink(removedPath); 91 | } 92 | catch (error) { 93 | console.error("Error removing old screenshot:", error); 94 | } 95 | } 96 | } 97 | } 98 | showMainWindow(); 99 | return screenshotPath; 100 | } 101 | async getImagePreview(filepath) { 102 | try { 103 | const data = await node_fs_1.default.promises.readFile(filepath); 104 | return `data:image/png;base64,${data.toString("base64")}`; 105 | } 106 | catch (error) { 107 | console.error("Error reading image:", error); 108 | throw error; 109 | } 110 | } 111 | async deleteScreenshot(path) { 112 | try { 113 | await node_fs_1.default.promises.unlink(path); 114 | if (this.view === "queue") { 115 | this.screenshotQueue = this.screenshotQueue.filter((filePath) => filePath !== path); 116 | } 117 | else { 118 | this.extraScreenshotQueue = this.extraScreenshotQueue.filter((filePath) => filePath !== path); 119 | } 120 | return { success: true }; 121 | } 122 | catch (error) { 123 | console.error("Error deleting file:", error); 124 | return { success: false, error: error.message }; 125 | } 126 | } 127 | } 128 | exports.ScreenshotHelper = ScreenshotHelper; 129 | //# sourceMappingURL=ScreenshotHelper.js.map -------------------------------------------------------------------------------- /dist-electron/preload.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.PROCESSING_EVENTS = void 0; 4 | const electron_1 = require("electron"); 5 | exports.PROCESSING_EVENTS = { 6 | //global states 7 | UNAUTHORIZED: "procesing-unauthorized", 8 | NO_SCREENSHOTS: "processing-no-screenshots", 9 | //states for generating the initial solution 10 | INITIAL_START: "initial-start", 11 | PROBLEM_EXTRACTED: "problem-extracted", 12 | SOLUTION_SUCCESS: "solution-success", 13 | INITIAL_SOLUTION_ERROR: "solution-error", 14 | //states for processing the debugging 15 | DEBUG_START: "debug-start", 16 | DEBUG_SUCCESS: "debug-success", 17 | DEBUG_ERROR: "debug-error" 18 | }; 19 | window.GEMINI_API_KEY = process.env.GEMINI_API_KEY; 20 | // Expose the Electron API to the renderer process 21 | electron_1.contextBridge.exposeInMainWorld("electronAPI", { 22 | updateContentDimensions: (dimensions) => electron_1.ipcRenderer.invoke("update-content-dimensions", dimensions), 23 | takeScreenshot: () => electron_1.ipcRenderer.invoke("take-screenshot"), 24 | getScreenshots: () => electron_1.ipcRenderer.invoke("get-screenshots"), 25 | deleteScreenshot: (path) => electron_1.ipcRenderer.invoke("delete-screenshot", path), 26 | // Event listeners 27 | onScreenshotTaken: (callback) => { 28 | const subscription = (_, data) => callback(data); 29 | electron_1.ipcRenderer.on("screenshot-taken", subscription); 30 | return () => { 31 | electron_1.ipcRenderer.removeListener("screenshot-taken", subscription); 32 | }; 33 | }, 34 | onSolutionsReady: (callback) => { 35 | const subscription = (_, solutions) => callback(solutions); 36 | electron_1.ipcRenderer.on("solutions-ready", subscription); 37 | return () => { 38 | electron_1.ipcRenderer.removeListener("solutions-ready", subscription); 39 | }; 40 | }, 41 | onResetView: (callback) => { 42 | const subscription = () => callback(); 43 | electron_1.ipcRenderer.on("reset-view", subscription); 44 | return () => { 45 | electron_1.ipcRenderer.removeListener("reset-view", subscription); 46 | }; 47 | }, 48 | onSolutionStart: (callback) => { 49 | const subscription = () => callback(); 50 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.INITIAL_START, subscription); 51 | return () => { 52 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.INITIAL_START, subscription); 53 | }; 54 | }, 55 | onDebugStart: (callback) => { 56 | const subscription = () => callback(); 57 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.DEBUG_START, subscription); 58 | return () => { 59 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.DEBUG_START, subscription); 60 | }; 61 | }, 62 | onDebugSuccess: (callback) => { 63 | electron_1.ipcRenderer.on("debug-success", (_event, data) => callback(data)); 64 | return () => { 65 | electron_1.ipcRenderer.removeListener("debug-success", (_event, data) => callback(data)); 66 | }; 67 | }, 68 | onDebugError: (callback) => { 69 | const subscription = (_, error) => callback(error); 70 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.DEBUG_ERROR, subscription); 71 | return () => { 72 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.DEBUG_ERROR, subscription); 73 | }; 74 | }, 75 | onSolutionError: (callback) => { 76 | const subscription = (_, error) => callback(error); 77 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription); 78 | return () => { 79 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription); 80 | }; 81 | }, 82 | onProcessingNoScreenshots: (callback) => { 83 | const subscription = () => callback(); 84 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.NO_SCREENSHOTS, subscription); 85 | return () => { 86 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.NO_SCREENSHOTS, subscription); 87 | }; 88 | }, 89 | onProblemExtracted: (callback) => { 90 | const subscription = (_, data) => callback(data); 91 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription); 92 | return () => { 93 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription); 94 | }; 95 | }, 96 | onSolutionSuccess: (callback) => { 97 | const subscription = (_, data) => callback(data); 98 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription); 99 | return () => { 100 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription); 101 | }; 102 | }, 103 | onUnauthorized: (callback) => { 104 | const subscription = () => callback(); 105 | electron_1.ipcRenderer.on(exports.PROCESSING_EVENTS.UNAUTHORIZED, subscription); 106 | return () => { 107 | electron_1.ipcRenderer.removeListener(exports.PROCESSING_EVENTS.UNAUTHORIZED, subscription); 108 | }; 109 | }, 110 | moveWindowLeft: () => electron_1.ipcRenderer.invoke("move-window-left"), 111 | moveWindowRight: () => electron_1.ipcRenderer.invoke("move-window-right"), 112 | quitApp: () => electron_1.ipcRenderer.invoke("quit-app"), 113 | getApiKey: () => electron_1.ipcRenderer.invoke("get-gemini-api-key") 114 | }); 115 | //# sourceMappingURL=preload.js.map -------------------------------------------------------------------------------- /dist-electron/LLMHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.LLMHelper = void 0; 7 | const generative_ai_1 = require("@google/generative-ai"); 8 | const fs_1 = __importDefault(require("fs")); 9 | class LLMHelper { 10 | model; 11 | constructor(apiKey) { 12 | const genAI = new generative_ai_1.GoogleGenerativeAI(apiKey); 13 | this.model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }); 14 | } 15 | async fileToGenerativePart(imagePath) { 16 | const imageData = await fs_1.default.promises.readFile(imagePath); 17 | return { 18 | inlineData: { 19 | data: imageData.toString("base64"), 20 | mimeType: "image/png" 21 | } 22 | }; 23 | } 24 | cleanJsonResponse(text) { 25 | // Remove markdown code block syntax if present 26 | text = text.replace(/^```(?:json)?\n/, '').replace(/\n```$/, ''); 27 | // Remove any leading/trailing whitespace 28 | text = text.trim(); 29 | return text; 30 | } 31 | async extractProblemFromImages(imagePaths) { 32 | try { 33 | const imageParts = await Promise.all(imagePaths.map(path => this.fileToGenerativePart(path))); 34 | const prompt = `You are a coding problem analyzer. Please analyze these images of a coding problem and extract the following information in JSON format: 35 | { 36 | "problem_statement": "The complete problem statement", 37 | "input_format": { 38 | "description": "Description of input format", 39 | "parameters": [{"name": "param name", "type": "param type", "description": "param description"}] 40 | }, 41 | "output_format": { 42 | "description": "Description of what should be output", 43 | "type": "The expected type of the output" 44 | }, 45 | "constraints": [ 46 | {"description": "Each constraint in plain text"} 47 | ], 48 | "test_cases": [ 49 | { 50 | "input": "Example input", 51 | "output": "Expected output", 52 | "explanation": "Explanation if provided" 53 | } 54 | ] 55 | } 56 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.`; 57 | const result = await this.model.generateContent([prompt, ...imageParts]); 58 | const response = await result.response; 59 | const text = this.cleanJsonResponse(response.text()); 60 | return JSON.parse(text); 61 | } 62 | catch (error) { 63 | console.error("Error extracting problem from images:", error); 64 | throw error; 65 | } 66 | } 67 | async generateSolution(problemInfo) { 68 | const prompt = `Given this coding problem: 69 | ${JSON.stringify(problemInfo, null, 2)} 70 | 71 | Please provide a solution in the following JSON format: 72 | { 73 | "solution": { 74 | "explanation": "Detailed explanation of the approach", 75 | "complexity": { 76 | "time": "Time complexity", 77 | "space": "Space complexity" 78 | }, 79 | "code": "The complete solution code", 80 | "test_results": [ 81 | { 82 | "input": "test case input", 83 | "expected": "expected output", 84 | "actual": "actual output", 85 | "passed": true/false 86 | } 87 | ] 88 | } 89 | } 90 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.`; 91 | const result = await this.model.generateContent(prompt); 92 | const response = await result.response; 93 | const text = this.cleanJsonResponse(response.text()); 94 | return JSON.parse(text); 95 | } 96 | async debugSolutionWithImages(problemInfo, currentCode, debugImagePaths) { 97 | try { 98 | const imageParts = await Promise.all(debugImagePaths.map(path => this.fileToGenerativePart(path))); 99 | const prompt = `You are a coding problem debugger. Given: 100 | 1. The original problem: ${JSON.stringify(problemInfo, null, 2)} 101 | 2. The current solution: ${currentCode} 102 | 3. The debug information in the provided images 103 | 104 | Please analyze the debug information and provide feedback in this JSON format: 105 | { 106 | "analysis": { 107 | "issues_found": [ 108 | { 109 | "description": "Description of the issue", 110 | "location": "Where in the code", 111 | "severity": "high/medium/low" 112 | } 113 | ], 114 | "suggested_fixes": [ 115 | { 116 | "description": "Description of the fix", 117 | "code_change": "The specific code change needed" 118 | } 119 | ] 120 | }, 121 | "improved_solution": { 122 | "code": "The complete improved solution", 123 | "explanation": "Explanation of the changes made" 124 | } 125 | } 126 | Important: Return ONLY the JSON object, without any markdown formatting or code blocks.`; 127 | const result = await this.model.generateContent([prompt, ...imageParts]); 128 | const response = await result.response; 129 | const text = this.cleanJsonResponse(response.text()); 130 | return JSON.parse(text); 131 | } 132 | catch (error) { 133 | console.error("Error debugging solution with images:", error); 134 | throw error; 135 | } 136 | } 137 | } 138 | exports.LLMHelper = LLMHelper; 139 | //# sourceMappingURL=LLMHelper.js.map -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from "electron" 2 | import { initializeIpcHandlers } from "./ipcHandlers" 3 | import { WindowHelper } from "./WindowHelper" 4 | import { ScreenshotHelper } from "./ScreenshotHelper" 5 | import { ShortcutsHelper } from "./shortcuts" 6 | import { ProcessingHelper } from "./ProcessingHelper" 7 | import * as dotenv from "dotenv" 8 | 9 | // Load environment variables from .env file 10 | dotenv.config() 11 | 12 | export class AppState { 13 | private static instance: AppState | null = null 14 | 15 | private windowHelper: WindowHelper 16 | private screenshotHelper: ScreenshotHelper 17 | public shortcutsHelper: ShortcutsHelper 18 | public processingHelper: ProcessingHelper 19 | 20 | // View management 21 | private view: "queue" | "solutions" = "queue" 22 | 23 | private problemInfo: { 24 | problem_statement: string 25 | input_format: Record 26 | output_format: Record 27 | constraints: Array> 28 | test_cases: Array> 29 | } | null = null // Allow null 30 | 31 | private hasDebugged: boolean = false 32 | 33 | // Processing events 34 | public readonly PROCESSING_EVENTS = { 35 | //global states 36 | UNAUTHORIZED: "procesing-unauthorized", 37 | NO_SCREENSHOTS: "processing-no-screenshots", 38 | 39 | //states for generating the initial solution 40 | INITIAL_START: "initial-start", 41 | PROBLEM_EXTRACTED: "problem-extracted", 42 | SOLUTION_SUCCESS: "solution-success", 43 | INITIAL_SOLUTION_ERROR: "solution-error", 44 | 45 | //states for processing the debugging 46 | DEBUG_START: "debug-start", 47 | DEBUG_SUCCESS: "debug-success", 48 | DEBUG_ERROR: "debug-error" 49 | } as const 50 | 51 | constructor() { 52 | // Initialize WindowHelper with this 53 | this.windowHelper = new WindowHelper(this) 54 | 55 | // Initialize ScreenshotHelper 56 | this.screenshotHelper = new ScreenshotHelper(this.view) 57 | 58 | // Initialize ProcessingHelper 59 | this.processingHelper = new ProcessingHelper(this) 60 | 61 | // Initialize ShortcutsHelper 62 | this.shortcutsHelper = new ShortcutsHelper(this) 63 | } 64 | 65 | public static getInstance(): AppState { 66 | if (!AppState.instance) { 67 | AppState.instance = new AppState() 68 | } 69 | return AppState.instance 70 | } 71 | 72 | // Getters and Setters 73 | public getMainWindow(): BrowserWindow | null { 74 | return this.windowHelper.getMainWindow() 75 | } 76 | 77 | public getView(): "queue" | "solutions" { 78 | return this.view 79 | } 80 | 81 | public setView(view: "queue" | "solutions"): void { 82 | this.view = view 83 | this.screenshotHelper.setView(view) 84 | } 85 | 86 | public isVisible(): boolean { 87 | return this.windowHelper.isVisible() 88 | } 89 | 90 | public getScreenshotHelper(): ScreenshotHelper { 91 | return this.screenshotHelper 92 | } 93 | 94 | public getProblemInfo(): any { 95 | return this.problemInfo 96 | } 97 | 98 | public setProblemInfo(problemInfo: any): void { 99 | this.problemInfo = problemInfo 100 | } 101 | 102 | public getScreenshotQueue(): string[] { 103 | return this.screenshotHelper.getScreenshotQueue() 104 | } 105 | 106 | public getExtraScreenshotQueue(): string[] { 107 | return this.screenshotHelper.getExtraScreenshotQueue() 108 | } 109 | 110 | // Window management methods 111 | public createWindow(): void { 112 | this.windowHelper.createWindow() 113 | } 114 | 115 | public hideMainWindow(): void { 116 | this.windowHelper.hideMainWindow() 117 | } 118 | 119 | public showMainWindow(): void { 120 | this.windowHelper.showMainWindow() 121 | } 122 | 123 | public toggleMainWindow(): void { 124 | console.log( 125 | "Screenshots: ", 126 | this.screenshotHelper.getScreenshotQueue().length, 127 | "Extra screenshots: ", 128 | this.screenshotHelper.getExtraScreenshotQueue().length 129 | ) 130 | this.windowHelper.toggleMainWindow() 131 | } 132 | 133 | public setWindowDimensions(width: number, height: number): void { 134 | this.windowHelper.setWindowDimensions(width, height) 135 | } 136 | 137 | public clearQueues(): void { 138 | this.screenshotHelper.clearQueues() 139 | 140 | // Clear problem info 141 | this.problemInfo = null 142 | 143 | // Reset view to initial state 144 | this.setView("queue") 145 | } 146 | 147 | // Screenshot management methods 148 | public async takeScreenshot(): Promise { 149 | if (!this.getMainWindow()) throw new Error("No main window available") 150 | 151 | const screenshotPath = await this.screenshotHelper.takeScreenshot( 152 | () => this.hideMainWindow(), 153 | () => this.showMainWindow() 154 | ) 155 | 156 | return screenshotPath 157 | } 158 | 159 | public async getImagePreview(filepath: string): Promise { 160 | return this.screenshotHelper.getImagePreview(filepath) 161 | } 162 | 163 | public async deleteScreenshot( 164 | path: string 165 | ): Promise<{ success: boolean; error?: string }> { 166 | return this.screenshotHelper.deleteScreenshot(path) 167 | } 168 | 169 | // New methods to move the window 170 | public moveWindowLeft(): void { 171 | this.windowHelper.moveWindowLeft() 172 | } 173 | 174 | public moveWindowRight(): void { 175 | this.windowHelper.moveWindowRight() 176 | } 177 | public moveWindowDown(): void { 178 | this.windowHelper.moveWindowDown() 179 | } 180 | public moveWindowUp(): void { 181 | this.windowHelper.moveWindowUp() 182 | } 183 | 184 | public setHasDebugged(value: boolean): void { 185 | this.hasDebugged = value 186 | } 187 | 188 | public getHasDebugged(): boolean { 189 | return this.hasDebugged 190 | } 191 | } 192 | 193 | // Application initialization 194 | async function initializeApp() { 195 | const appState = AppState.getInstance() 196 | 197 | // Initialize IPC handlers before window creation 198 | initializeIpcHandlers(appState) 199 | 200 | app.whenReady().then(() => { 201 | console.log("App is ready") 202 | appState.createWindow() 203 | // Register global shortcuts using ShortcutsHelper 204 | appState.shortcutsHelper.registerGlobalShortcuts() 205 | }) 206 | 207 | app.on("activate", () => { 208 | console.log("App activated") 209 | if (appState.getMainWindow() === null) { 210 | appState.createWindow() 211 | } 212 | }) 213 | 214 | // Quit when all windows are closed, except on macOS 215 | app.on("window-all-closed", () => { 216 | if (process.platform !== "darwin") { 217 | app.quit() 218 | } 219 | }) 220 | 221 | app.dock?.hide() // Hide dock icon (optional) 222 | app.commandLine.appendSwitch("disable-background-timer-throttling") 223 | } 224 | 225 | // Start the application 226 | initializeApp().catch(console.error) 227 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron" 2 | 3 | // Types for the exposed Electron API 4 | interface ElectronAPI { 5 | updateContentDimensions: (dimensions: { 6 | width: number 7 | height: number 8 | }) => Promise 9 | getScreenshots: () => Promise<{ 10 | success: boolean 11 | previews?: Array<{ path: string; preview: string }> | null 12 | error?: string 13 | }> 14 | deleteScreenshot: ( 15 | path: string 16 | ) => Promise<{ success: boolean; error?: string }> 17 | onScreenshotTaken: ( 18 | callback: (data: { path: string; preview: string }) => void 19 | ) => () => void 20 | onSolutionsReady: (callback: (solutions: string) => void) => () => void 21 | onResetView: (callback: () => void) => () => void 22 | onSolutionStart: (callback: () => void) => () => void 23 | onDebugStart: (callback: () => void) => () => void 24 | onDebugSuccess: (callback: (data: any) => void) => () => void 25 | onSolutionError: (callback: (error: string) => void) => () => void 26 | onProcessingNoScreenshots: (callback: () => void) => () => void 27 | onProblemExtracted: (callback: (data: any) => void) => () => void 28 | onSolutionSuccess: (callback: (data: any) => void) => () => void 29 | 30 | onUnauthorized: (callback: () => void) => () => void 31 | onDebugError: (callback: (error: string) => void) => () => void 32 | takeScreenshot: () => Promise 33 | moveWindowLeft: () => Promise 34 | moveWindowRight: () => Promise 35 | quitApp: () => Promise 36 | getApiKey: () => Promise 37 | } 38 | 39 | export const PROCESSING_EVENTS = { 40 | //global states 41 | UNAUTHORIZED: "procesing-unauthorized", 42 | NO_SCREENSHOTS: "processing-no-screenshots", 43 | 44 | //states for generating the initial solution 45 | INITIAL_START: "initial-start", 46 | PROBLEM_EXTRACTED: "problem-extracted", 47 | SOLUTION_SUCCESS: "solution-success", 48 | INITIAL_SOLUTION_ERROR: "solution-error", 49 | 50 | //states for processing the debugging 51 | DEBUG_START: "debug-start", 52 | DEBUG_SUCCESS: "debug-success", 53 | DEBUG_ERROR: "debug-error" 54 | } as const 55 | 56 | // Synchronously expose the GEMINI_API_KEY to the window object 57 | // This must be done before exposeInMainWorld 58 | ;(window as any).GEMINI_API_KEY = process.env.GEMINI_API_KEY 59 | 60 | // Expose the Electron API to the renderer process 61 | contextBridge.exposeInMainWorld("electronAPI", { 62 | updateContentDimensions: (dimensions: { width: number; height: number }) => 63 | ipcRenderer.invoke("update-content-dimensions", dimensions), 64 | takeScreenshot: () => ipcRenderer.invoke("take-screenshot"), 65 | getScreenshots: () => ipcRenderer.invoke("get-screenshots"), 66 | deleteScreenshot: (path: string) => 67 | ipcRenderer.invoke("delete-screenshot", path), 68 | 69 | // Event listeners 70 | onScreenshotTaken: ( 71 | callback: (data: { path: string; preview: string }) => void 72 | ) => { 73 | const subscription = (_: any, data: { path: string; preview: string }) => 74 | callback(data) 75 | ipcRenderer.on("screenshot-taken", subscription) 76 | return () => { 77 | ipcRenderer.removeListener("screenshot-taken", subscription) 78 | } 79 | }, 80 | onSolutionsReady: (callback: (solutions: string) => void) => { 81 | const subscription = (_: any, solutions: string) => callback(solutions) 82 | ipcRenderer.on("solutions-ready", subscription) 83 | return () => { 84 | ipcRenderer.removeListener("solutions-ready", subscription) 85 | } 86 | }, 87 | onResetView: (callback: () => void) => { 88 | const subscription = () => callback() 89 | ipcRenderer.on("reset-view", subscription) 90 | return () => { 91 | ipcRenderer.removeListener("reset-view", subscription) 92 | } 93 | }, 94 | onSolutionStart: (callback: () => void) => { 95 | const subscription = () => callback() 96 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 97 | return () => { 98 | ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 99 | } 100 | }, 101 | onDebugStart: (callback: () => void) => { 102 | const subscription = () => callback() 103 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 104 | return () => { 105 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 106 | } 107 | }, 108 | 109 | onDebugSuccess: (callback: (data: any) => void) => { 110 | ipcRenderer.on("debug-success", (_event, data) => callback(data)) 111 | return () => { 112 | ipcRenderer.removeListener("debug-success", (_event, data) => 113 | callback(data) 114 | ) 115 | } 116 | }, 117 | onDebugError: (callback: (error: string) => void) => { 118 | const subscription = (_: any, error: string) => callback(error) 119 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 120 | return () => { 121 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 122 | } 123 | }, 124 | onSolutionError: (callback: (error: string) => void) => { 125 | const subscription = (_: any, error: string) => callback(error) 126 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 127 | return () => { 128 | ipcRenderer.removeListener( 129 | PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 130 | subscription 131 | ) 132 | } 133 | }, 134 | onProcessingNoScreenshots: (callback: () => void) => { 135 | const subscription = () => callback() 136 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 137 | return () => { 138 | ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 139 | } 140 | }, 141 | 142 | onProblemExtracted: (callback: (data: any) => void) => { 143 | const subscription = (_: any, data: any) => callback(data) 144 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 145 | return () => { 146 | ipcRenderer.removeListener( 147 | PROCESSING_EVENTS.PROBLEM_EXTRACTED, 148 | subscription 149 | ) 150 | } 151 | }, 152 | onSolutionSuccess: (callback: (data: any) => void) => { 153 | const subscription = (_: any, data: any) => callback(data) 154 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 155 | return () => { 156 | ipcRenderer.removeListener( 157 | PROCESSING_EVENTS.SOLUTION_SUCCESS, 158 | subscription 159 | ) 160 | } 161 | }, 162 | onUnauthorized: (callback: () => void) => { 163 | const subscription = () => callback() 164 | ipcRenderer.on(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 165 | return () => { 166 | ipcRenderer.removeListener(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 167 | } 168 | }, 169 | moveWindowLeft: () => ipcRenderer.invoke("move-window-left"), 170 | moveWindowRight: () => ipcRenderer.invoke("move-window-right"), 171 | quitApp: () => ipcRenderer.invoke("quit-app"), 172 | getApiKey: () => ipcRenderer.invoke("get-gemini-api-key") 173 | } as ElectronAPI) 174 | -------------------------------------------------------------------------------- /src/components/Chat/ChatModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react" 2 | import { Send } from "lucide-react" 3 | 4 | interface Message { 5 | id: string 6 | text: string 7 | sender: "user" | "assistant" 8 | timestamp: Date 9 | } 10 | 11 | interface ChatModalProps { 12 | isOpen: boolean 13 | onClose: () => void 14 | } 15 | 16 | const ChatModal: React.FC = ({ isOpen, onClose: _onClose }) => { 17 | const [messages, setMessages] = useState([ 18 | { 19 | id: "1", 20 | text: "Hi! I'm your coding assistant. Ask me anything about algorithms, data structures, or coding problems!", 21 | sender: "assistant", 22 | timestamp: new Date() 23 | } 24 | ]) 25 | const [inputValue, setInputValue] = useState("") 26 | const [isLoading, setIsLoading] = useState(false) 27 | const messagesEndRef = useRef(null) 28 | 29 | const scrollToBottom = () => { 30 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) 31 | } 32 | 33 | useEffect(() => { 34 | scrollToBottom() 35 | }, [messages]) 36 | 37 | if (!isOpen) return null 38 | 39 | const handleSendMessage = async () => { 40 | if (!inputValue.trim()) return 41 | 42 | const userText = inputValue 43 | 44 | // Add user message 45 | const userMessage: Message = { 46 | id: Date.now().toString(), 47 | text: userText, 48 | sender: "user", 49 | timestamp: new Date() 50 | } 51 | 52 | setMessages((prev) => [...prev, userMessage]) 53 | setInputValue("") 54 | setIsLoading(true) 55 | 56 | try { 57 | // Get API key from window (set by preload script synchronously) 58 | let apiKey = (window as any).GEMINI_API_KEY 59 | 60 | // Fallback: If still not available, request it asynchronously 61 | if (!apiKey && (window as any).electronAPI?.getApiKey) { 62 | apiKey = await (window as any).electronAPI.getApiKey() 63 | } 64 | 65 | if (!apiKey) { 66 | throw new Error("API key not available. Make sure GEMINI_API_KEY is set.") 67 | } 68 | 69 | // Call Gemini API (non-streaming for simplicity) 70 | const response = await fetch( 71 | `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, 72 | { 73 | method: "POST", 74 | headers: { 75 | "Content-Type": "application/json" 76 | }, 77 | body: JSON.stringify({ 78 | contents: [ 79 | { 80 | parts: [ 81 | { 82 | text: `You are a helpful coding assistant. Answer concisely with code examples when relevant.\n\nQuestion: ${userText}` 83 | } 84 | ] 85 | } 86 | ], 87 | generationConfig: { 88 | temperature: 0.7, 89 | topK: 40, 90 | topP: 0.95, 91 | maxOutputTokens: 2048 92 | } 93 | }) 94 | } 95 | ) 96 | 97 | if (!response.ok) { 98 | throw new Error(`API error: ${response.status}`) 99 | } 100 | 101 | const data = await response.json() 102 | const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "No response received" 103 | 104 | // Create assistant message with response 105 | const assistantMessage: Message = { 106 | id: (Date.now() + 1).toString(), 107 | text: text, 108 | sender: "assistant", 109 | timestamp: new Date() 110 | } 111 | setMessages((prev) => [...prev, assistantMessage]) 112 | } catch (error) { 113 | console.error("Chat error:", error) 114 | const errorMessage: Message = { 115 | id: (Date.now() + 1).toString(), 116 | text: `Error: ${error instanceof Error ? error.message : "Failed to get response"}`, 117 | sender: "assistant", 118 | timestamp: new Date() 119 | } 120 | setMessages((prev) => [...prev, errorMessage]) 121 | } finally { 122 | setIsLoading(false) 123 | } 124 | } 125 | 126 | const handleKeyDown = (e: React.KeyboardEvent) => { 127 | if (e.key === "Enter" && !e.shiftKey) { 128 | e.preventDefault() 129 | handleSendMessage() 130 | } 131 | } 132 | 133 | return ( 134 |
135 |
136 |
137 | 138 | {/* Messages Container */} 139 |
140 | {messages.map((message) => ( 141 |
145 |
152 | {message.text || (message.sender === "assistant" && isLoading ? ( 153 |
154 | 155 | 156 | 157 |
158 | ) : message.text)} 159 |
160 |
161 | ))} 162 |
163 |
164 | 165 | {/* Input Container */} 166 |
167 | setInputValue(e.target.value)} 171 | onKeyDown={handleKeyDown} 172 | placeholder="Ask me anything..." 173 | className="flex-1 px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-sm text-white/90 placeholder-white/50 focus:outline-none focus:border-white/40 focus:bg-white/15 transition-colors" 174 | disabled={isLoading} 175 | autoFocus 176 | /> 177 | 184 |
185 |
186 |
187 |
188 | ) 189 | } 190 | 191 | export default ChatModal 192 | -------------------------------------------------------------------------------- /dist-electron/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | Object.defineProperty(exports, "__esModule", { value: true }); 26 | exports.AppState = void 0; 27 | const electron_1 = require("electron"); 28 | const ipcHandlers_1 = require("./ipcHandlers"); 29 | const WindowHelper_1 = require("./WindowHelper"); 30 | const ScreenshotHelper_1 = require("./ScreenshotHelper"); 31 | const shortcuts_1 = require("./shortcuts"); 32 | const ProcessingHelper_1 = require("./ProcessingHelper"); 33 | const dotenv = __importStar(require("dotenv")); 34 | // Load environment variables from .env file 35 | dotenv.config(); 36 | class AppState { 37 | static instance = null; 38 | windowHelper; 39 | screenshotHelper; 40 | shortcutsHelper; 41 | processingHelper; 42 | // View management 43 | view = "queue"; 44 | problemInfo = null; // Allow null 45 | hasDebugged = false; 46 | // Processing events 47 | PROCESSING_EVENTS = { 48 | //global states 49 | UNAUTHORIZED: "procesing-unauthorized", 50 | NO_SCREENSHOTS: "processing-no-screenshots", 51 | //states for generating the initial solution 52 | INITIAL_START: "initial-start", 53 | PROBLEM_EXTRACTED: "problem-extracted", 54 | SOLUTION_SUCCESS: "solution-success", 55 | INITIAL_SOLUTION_ERROR: "solution-error", 56 | //states for processing the debugging 57 | DEBUG_START: "debug-start", 58 | DEBUG_SUCCESS: "debug-success", 59 | DEBUG_ERROR: "debug-error" 60 | }; 61 | constructor() { 62 | // Initialize WindowHelper with this 63 | this.windowHelper = new WindowHelper_1.WindowHelper(this); 64 | // Initialize ScreenshotHelper 65 | this.screenshotHelper = new ScreenshotHelper_1.ScreenshotHelper(this.view); 66 | // Initialize ProcessingHelper 67 | this.processingHelper = new ProcessingHelper_1.ProcessingHelper(this); 68 | // Initialize ShortcutsHelper 69 | this.shortcutsHelper = new shortcuts_1.ShortcutsHelper(this); 70 | } 71 | static getInstance() { 72 | if (!AppState.instance) { 73 | AppState.instance = new AppState(); 74 | } 75 | return AppState.instance; 76 | } 77 | // Getters and Setters 78 | getMainWindow() { 79 | return this.windowHelper.getMainWindow(); 80 | } 81 | getView() { 82 | return this.view; 83 | } 84 | setView(view) { 85 | this.view = view; 86 | this.screenshotHelper.setView(view); 87 | } 88 | isVisible() { 89 | return this.windowHelper.isVisible(); 90 | } 91 | getScreenshotHelper() { 92 | return this.screenshotHelper; 93 | } 94 | getProblemInfo() { 95 | return this.problemInfo; 96 | } 97 | setProblemInfo(problemInfo) { 98 | this.problemInfo = problemInfo; 99 | } 100 | getScreenshotQueue() { 101 | return this.screenshotHelper.getScreenshotQueue(); 102 | } 103 | getExtraScreenshotQueue() { 104 | return this.screenshotHelper.getExtraScreenshotQueue(); 105 | } 106 | // Window management methods 107 | createWindow() { 108 | this.windowHelper.createWindow(); 109 | } 110 | hideMainWindow() { 111 | this.windowHelper.hideMainWindow(); 112 | } 113 | showMainWindow() { 114 | this.windowHelper.showMainWindow(); 115 | } 116 | toggleMainWindow() { 117 | console.log("Screenshots: ", this.screenshotHelper.getScreenshotQueue().length, "Extra screenshots: ", this.screenshotHelper.getExtraScreenshotQueue().length); 118 | this.windowHelper.toggleMainWindow(); 119 | } 120 | setWindowDimensions(width, height) { 121 | this.windowHelper.setWindowDimensions(width, height); 122 | } 123 | clearQueues() { 124 | this.screenshotHelper.clearQueues(); 125 | // Clear problem info 126 | this.problemInfo = null; 127 | // Reset view to initial state 128 | this.setView("queue"); 129 | } 130 | // Screenshot management methods 131 | async takeScreenshot() { 132 | if (!this.getMainWindow()) 133 | throw new Error("No main window available"); 134 | const screenshotPath = await this.screenshotHelper.takeScreenshot(() => this.hideMainWindow(), () => this.showMainWindow()); 135 | return screenshotPath; 136 | } 137 | async getImagePreview(filepath) { 138 | return this.screenshotHelper.getImagePreview(filepath); 139 | } 140 | async deleteScreenshot(path) { 141 | return this.screenshotHelper.deleteScreenshot(path); 142 | } 143 | // New methods to move the window 144 | moveWindowLeft() { 145 | this.windowHelper.moveWindowLeft(); 146 | } 147 | moveWindowRight() { 148 | this.windowHelper.moveWindowRight(); 149 | } 150 | moveWindowDown() { 151 | this.windowHelper.moveWindowDown(); 152 | } 153 | moveWindowUp() { 154 | this.windowHelper.moveWindowUp(); 155 | } 156 | setHasDebugged(value) { 157 | this.hasDebugged = value; 158 | } 159 | getHasDebugged() { 160 | return this.hasDebugged; 161 | } 162 | } 163 | exports.AppState = AppState; 164 | // Application initialization 165 | async function initializeApp() { 166 | const appState = AppState.getInstance(); 167 | // Initialize IPC handlers before window creation 168 | (0, ipcHandlers_1.initializeIpcHandlers)(appState); 169 | electron_1.app.whenReady().then(() => { 170 | console.log("App is ready"); 171 | appState.createWindow(); 172 | // Register global shortcuts using ShortcutsHelper 173 | appState.shortcutsHelper.registerGlobalShortcuts(); 174 | }); 175 | electron_1.app.on("activate", () => { 176 | console.log("App activated"); 177 | if (appState.getMainWindow() === null) { 178 | appState.createWindow(); 179 | } 180 | }); 181 | // Quit when all windows are closed, except on macOS 182 | electron_1.app.on("window-all-closed", () => { 183 | if (process.platform !== "darwin") { 184 | electron_1.app.quit(); 185 | } 186 | }); 187 | electron_1.app.dock?.hide(); // Hide dock icon (optional) 188 | electron_1.app.commandLine.appendSwitch("disable-background-timer-throttling"); 189 | } 190 | // Start the application 191 | initializeApp().catch(console.error); 192 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /src/_pages/Queue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { useQuery } from "react-query" 3 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 4 | import { 5 | Toast, 6 | ToastTitle, 7 | ToastDescription, 8 | ToastVariant, 9 | ToastMessage 10 | } from "../components/ui/toast" 11 | import QueueCommands from "../components/Queue/QueueCommands" 12 | import ChatModal from "../components/Chat/ChatModal" 13 | import AudioTranscriber from "../components/Audio/AudioTranscriber" 14 | 15 | interface QueueProps { 16 | setView: React.Dispatch> 17 | setIsChatOpen: React.Dispatch> 18 | isChatOpen: boolean 19 | } 20 | 21 | const Queue: React.FC = ({ setView, setIsChatOpen, isChatOpen }) => { 22 | const [toastOpen, setToastOpen] = useState(false) 23 | const [toastMessage, setToastMessage] = useState({ 24 | title: "", 25 | description: "", 26 | variant: "neutral" 27 | }) 28 | 29 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 30 | const [tooltipHeight, setTooltipHeight] = useState(0) 31 | const [position, setPosition] = useState({ x: 0, y: 0 }) 32 | const [isDragging, setIsDragging] = useState(false) 33 | const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) 34 | const contentRef = useRef(null) 35 | const dragHandleRef = useRef(null) 36 | 37 | const { data: screenshots = [], refetch } = useQuery({ 38 | queryKey: ["screenshots"], 39 | queryFn: async () => { 40 | try { 41 | const existing = await window.electronAPI.getScreenshots() 42 | return existing 43 | } catch (error) { 44 | console.error("Error loading screenshots:", error) 45 | showToast("Error", "Failed to load existing screenshots", "error") 46 | return [] 47 | } 48 | }, 49 | staleTime: Infinity, 50 | cacheTime: Infinity, 51 | refetchOnWindowFocus: true, 52 | refetchOnMount: true 53 | }) 54 | 55 | const showToast = ( 56 | title: string, 57 | description: string, 58 | variant: ToastVariant 59 | ) => { 60 | setToastMessage({ title, description, variant }) 61 | setToastOpen(true) 62 | } 63 | 64 | const handleDeleteScreenshot = async (index: number) => { 65 | const screenshotToDelete = screenshots[index] 66 | 67 | try { 68 | const response = await window.electronAPI.deleteScreenshot( 69 | screenshotToDelete.path 70 | ) 71 | 72 | if (response.success) { 73 | refetch() 74 | } else { 75 | console.error("Failed to delete screenshot:", response.error) 76 | showToast("Error", "Failed to delete the screenshot file", "error") 77 | } 78 | } catch (error) { 79 | console.error("Error deleting screenshot:", error) 80 | } 81 | } 82 | 83 | useEffect(() => { 84 | const updateDimensions = () => { 85 | if (contentRef.current) { 86 | let contentHeight = contentRef.current.scrollHeight 87 | const contentWidth = contentRef.current.scrollWidth 88 | if (isTooltipVisible) { 89 | contentHeight += tooltipHeight 90 | } 91 | window.electronAPI.updateContentDimensions({ 92 | width: contentWidth, 93 | height: contentHeight 94 | }) 95 | } 96 | } 97 | 98 | const resizeObserver = new ResizeObserver(updateDimensions) 99 | if (contentRef.current) { 100 | resizeObserver.observe(contentRef.current) 101 | } 102 | updateDimensions() 103 | 104 | const cleanupFunctions = [ 105 | window.electronAPI.onScreenshotTaken(() => refetch()), 106 | window.electronAPI.onResetView(() => refetch()), 107 | window.electronAPI.onSolutionError((error: string) => { 108 | showToast( 109 | "Processing Failed", 110 | "There was an error processing your screenshots.", 111 | "error" 112 | ) 113 | setView("queue") 114 | console.error("Processing error:", error) 115 | }), 116 | window.electronAPI.onProcessingNoScreenshots(() => { 117 | showToast( 118 | "No Screenshots", 119 | "There are no screenshots to process.", 120 | "neutral" 121 | ) 122 | }) 123 | ] 124 | 125 | return () => { 126 | resizeObserver.disconnect() 127 | cleanupFunctions.forEach((cleanup) => cleanup()) 128 | } 129 | }, [isTooltipVisible, tooltipHeight]) 130 | 131 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 132 | setIsTooltipVisible(visible) 133 | setTooltipHeight(height) 134 | } 135 | 136 | const handleMouseDown = (e: React.MouseEvent) => { 137 | if (!contentRef.current) return 138 | setIsDragging(true) 139 | const rect = contentRef.current.getBoundingClientRect() 140 | setDragOffset({ 141 | x: e.clientX - rect.left, 142 | y: e.clientY - rect.top 143 | }) 144 | } 145 | 146 | const handleMouseMove = (e: MouseEvent) => { 147 | if (!isDragging) return 148 | setPosition({ 149 | x: e.clientX - dragOffset.x, 150 | y: e.clientY - dragOffset.y 151 | }) 152 | } 153 | 154 | const handleMouseUp = () => { 155 | setIsDragging(false) 156 | } 157 | 158 | useEffect(() => { 159 | if (isDragging) { 160 | window.addEventListener('mousemove', handleMouseMove) 161 | window.addEventListener('mouseup', handleMouseUp) 162 | return () => { 163 | window.removeEventListener('mousemove', handleMouseMove) 164 | window.removeEventListener('mouseup', handleMouseUp) 165 | } 166 | } 167 | }, [isDragging, dragOffset]) 168 | 169 | return ( 170 |
181 |
182 | {/* Drag Handle */} 183 |
188 |
189 |
190 | 196 | {toastMessage.title} 197 | {toastMessage.description} 198 | 199 | 200 |
201 | 206 | 212 | 213 | {/* Audio Transcriber */} 214 | {}} /> 215 |
216 |
217 | 218 | {isChatOpen && setIsChatOpen(false)} />} 219 | 220 | {/* Close chat when clicking outside - escape key */} 221 | {isChatOpen && ( 222 |
setIsChatOpen(false)} 225 | onKeyDown={(e) => { 226 | if (e.key === "Escape") setIsChatOpen(false) 227 | }} 228 | /> 229 | )} 230 |
231 | ) 232 | } 233 | 234 | export default Queue 235 | -------------------------------------------------------------------------------- /dist-electron/WindowHelper.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"WindowHelper.js","sourceRoot":"","sources":["../electron/WindowHelper.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /src/components/Queue/QueueCommands.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { IoLogOutOutline } from "react-icons/io5" 3 | import { MessageCircle } from "lucide-react" 4 | 5 | interface QueueCommandsProps { 6 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 7 | screenshots: Array<{ path: string; preview: string }> 8 | setIsChatOpen: React.Dispatch> 9 | isChatOpen: boolean 10 | } 11 | 12 | const QueueCommands: React.FC = ({ 13 | onTooltipVisibilityChange, 14 | screenshots, 15 | setIsChatOpen, 16 | isChatOpen 17 | }) => { 18 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 19 | const tooltipRef = useRef(null) 20 | 21 | useEffect(() => { 22 | let tooltipHeight = 0 23 | if (tooltipRef.current && isTooltipVisible) { 24 | tooltipHeight = tooltipRef.current.offsetHeight + 10 25 | } 26 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 27 | }, [isTooltipVisible]) 28 | 29 | const handleMouseEnter = () => { 30 | setIsTooltipVisible(true) 31 | } 32 | 33 | const handleMouseLeave = () => { 34 | setIsTooltipVisible(false) 35 | } 36 | 37 | return ( 38 |
39 |
40 | {/* Show/Hide */} 41 |
42 | Show/Hide 43 |
44 | 47 | 50 |
51 |
52 | 53 | {/* Screenshot */} 54 |
55 | 56 | {screenshots.length === 0 ? "Take first screenshot" : "Screenshot"} 57 | 58 |
59 | 62 | 65 |
66 |
67 | 68 | {/* Solve Command */} 69 | {screenshots.length > 0 && ( 70 |
71 | Solve 72 |
73 | 76 | 79 |
80 |
81 | )} 82 | 83 | {/* Question mark with tooltip */} 84 |
89 |
90 | ? 91 |
92 | 93 | {/* Tooltip Content */} 94 | {isTooltipVisible && ( 95 |
99 |
100 |
101 |

Keyboard Shortcuts

102 |
103 | {/* Toggle Command */} 104 |
105 |
106 | Toggle Window 107 |
108 | 109 | ⌘ 110 | 111 | 112 | B 113 | 114 |
115 |
116 |

117 | Show or hide this window. 118 |

119 |
120 | {/* Screenshot Command */} 121 |
122 |
123 | Take Screenshot 124 |
125 | 126 | ⌘ 127 | 128 | 129 | H 130 | 131 |
132 |
133 |

134 | Take a screenshot of the problem description. The tool 135 | will extract and analyze the problem. The 5 latest 136 | screenshots are saved. 137 |

138 |
139 | 140 | {/* Solve Command */} 141 |
142 |
143 | Solve Problem 144 |
145 | 146 | ⌘ 147 | 148 | 149 | ↵ 150 | 151 |
152 |
153 |

154 | Generate a solution based on the current problem. 155 |

156 |
157 |
158 |
159 |
160 |
161 | )} 162 |
163 | 164 | {/* Separator */} 165 |
166 | 167 | {/* Chat Button */} 168 | 180 | 181 | {/* Sign Out Button - Moved to end */} 182 | 189 |
190 |
191 | ) 192 | } 193 | 194 | export default QueueCommands 195 | -------------------------------------------------------------------------------- /electron/WindowHelper.ts: -------------------------------------------------------------------------------- 1 | // electron/WindowHelper.ts 2 | 3 | import { BrowserWindow, screen } from "electron" 4 | import { AppState } from "main" 5 | import path from "node:path" 6 | 7 | const isDev = process.env.NODE_ENV === "development" 8 | 9 | const startUrl = isDev 10 | ? "http://localhost:5180" 11 | : `file://${path.join(__dirname, "../dist/index.html")}` 12 | 13 | export class WindowHelper { 14 | private mainWindow: BrowserWindow | null = null 15 | private isWindowVisible: boolean = false 16 | private windowPosition: { x: number; y: number } | null = null 17 | private windowSize: { width: number; height: number } | null = null 18 | private appState: AppState 19 | 20 | // Initialize with explicit number type and 0 value 21 | private screenWidth: number = 0 22 | private screenHeight: number = 0 23 | private step: number = 0 24 | private currentX: number = 0 25 | private currentY: number = 0 26 | 27 | constructor(appState: AppState) { 28 | this.appState = appState 29 | } 30 | 31 | public setWindowDimensions(width: number, height: number): void { 32 | if (!this.mainWindow || this.mainWindow.isDestroyed()) return 33 | 34 | // Get current window position 35 | const [currentX, currentY] = this.mainWindow.getPosition() 36 | 37 | // Get screen dimensions 38 | const primaryDisplay = screen.getPrimaryDisplay() 39 | const workArea = primaryDisplay.workAreaSize 40 | 41 | // Use 75% width if debugging has occurred, otherwise use 60% 42 | const maxAllowedWidth = Math.floor( 43 | workArea.width * (this.appState.getHasDebugged() ? 0.75 : 0.5) 44 | ) 45 | 46 | // Ensure width doesn't exceed max allowed width and height is reasonable 47 | const newWidth = Math.min(width + 32, maxAllowedWidth) 48 | const newHeight = Math.max(Math.ceil(height), 1200) // Ensure minimum height of 1200px 49 | 50 | // Center the window horizontally if it would go off screen 51 | const maxX = workArea.width - newWidth 52 | const newX = Math.min(Math.max(currentX, 0), maxX) 53 | 54 | // Update window bounds 55 | this.mainWindow.setBounds({ 56 | x: newX, 57 | y: currentY, 58 | width: newWidth, 59 | height: newHeight 60 | }) 61 | 62 | // Update internal state 63 | this.windowPosition = { x: newX, y: currentY } 64 | this.windowSize = { width: newWidth, height: newHeight } 65 | this.currentX = newX 66 | } 67 | 68 | public createWindow(): void { 69 | if (this.mainWindow !== null) return 70 | 71 | const primaryDisplay = screen.getPrimaryDisplay() 72 | const workArea = primaryDisplay.workAreaSize 73 | this.screenWidth = workArea.width 74 | this.screenHeight = workArea.height 75 | 76 | this.step = Math.floor(this.screenWidth / 10) // 10 steps 77 | this.currentX = 0 // Start at the left 78 | 79 | const windowSettings: Electron.BrowserWindowConstructorOptions = { 80 | height: 1200, 81 | minWidth: undefined, 82 | maxWidth: undefined, 83 | x: this.currentX, 84 | y: 0, 85 | webPreferences: { 86 | nodeIntegration: true, 87 | contextIsolation: true, 88 | preload: path.join(__dirname, "preload.js") 89 | }, 90 | show: true, 91 | alwaysOnTop: true, 92 | frame: false, 93 | transparent: true, 94 | fullscreenable: false, 95 | hasShadow: false, 96 | backgroundColor: "#00000000", 97 | focusable: true 98 | } 99 | 100 | this.mainWindow = new BrowserWindow(windowSettings) 101 | // this.mainWindow.webContents.openDevTools() 102 | this.mainWindow.setContentProtection(true) 103 | 104 | if (process.platform === "darwin") { 105 | this.mainWindow.setVisibleOnAllWorkspaces(true, { 106 | visibleOnFullScreen: true 107 | }) 108 | this.mainWindow.setHiddenInMissionControl(true) 109 | this.mainWindow.setAlwaysOnTop(true, "floating") 110 | } 111 | if (process.platform === "linux") { 112 | // Linux-specific optimizations for stealth overlays 113 | if (this.mainWindow.setHasShadow) { 114 | this.mainWindow.setHasShadow(false) 115 | } 116 | this.mainWindow.setFocusable(false) 117 | } 118 | this.mainWindow.setSkipTaskbar(true) 119 | this.mainWindow.setAlwaysOnTop(true) 120 | 121 | this.mainWindow.loadURL(startUrl).catch((err) => { 122 | console.error("Failed to load URL:", err) 123 | }) 124 | 125 | const bounds = this.mainWindow.getBounds() 126 | this.windowPosition = { x: bounds.x, y: bounds.y } 127 | this.windowSize = { width: bounds.width, height: bounds.height } 128 | this.currentX = bounds.x 129 | this.currentY = bounds.y 130 | 131 | this.setupWindowListeners() 132 | this.isWindowVisible = true 133 | } 134 | 135 | private setupWindowListeners(): void { 136 | if (!this.mainWindow) return 137 | 138 | this.mainWindow.on("move", () => { 139 | if (this.mainWindow) { 140 | const bounds = this.mainWindow.getBounds() 141 | this.windowPosition = { x: bounds.x, y: bounds.y } 142 | this.currentX = bounds.x 143 | this.currentY = bounds.y 144 | } 145 | }) 146 | 147 | this.mainWindow.on("resize", () => { 148 | if (this.mainWindow) { 149 | const bounds = this.mainWindow.getBounds() 150 | this.windowSize = { width: bounds.width, height: bounds.height } 151 | } 152 | }) 153 | 154 | this.mainWindow.on("closed", () => { 155 | this.mainWindow = null 156 | this.isWindowVisible = false 157 | this.windowPosition = null 158 | this.windowSize = null 159 | }) 160 | } 161 | 162 | public getMainWindow(): BrowserWindow | null { 163 | return this.mainWindow 164 | } 165 | 166 | public isVisible(): boolean { 167 | return this.isWindowVisible 168 | } 169 | 170 | public hideMainWindow(): void { 171 | if (!this.mainWindow || this.mainWindow.isDestroyed()) { 172 | console.warn("Main window does not exist or is destroyed.") 173 | return 174 | } 175 | 176 | const bounds = this.mainWindow.getBounds() 177 | this.windowPosition = { x: bounds.x, y: bounds.y } 178 | this.windowSize = { width: bounds.width, height: bounds.height } 179 | this.mainWindow.hide() 180 | this.isWindowVisible = false 181 | } 182 | 183 | public showMainWindow(): void { 184 | if (!this.mainWindow || this.mainWindow.isDestroyed()) { 185 | console.warn("Main window does not exist or is destroyed.") 186 | return 187 | } 188 | 189 | if (this.windowPosition && this.windowSize) { 190 | this.mainWindow.setBounds({ 191 | x: this.windowPosition.x, 192 | y: this.windowPosition.y, 193 | width: this.windowSize.width, 194 | height: this.windowSize.height 195 | }) 196 | } 197 | 198 | this.mainWindow.showInactive() 199 | 200 | this.isWindowVisible = true 201 | } 202 | 203 | public toggleMainWindow(): void { 204 | if (this.isWindowVisible) { 205 | this.hideMainWindow() 206 | } else { 207 | this.showMainWindow() 208 | } 209 | } 210 | 211 | // New methods for window movement 212 | public moveWindowRight(): void { 213 | if (!this.mainWindow) return 214 | 215 | const windowWidth = this.windowSize?.width || 0 216 | const halfWidth = windowWidth / 2 217 | 218 | // Ensure currentX and currentY are numbers 219 | this.currentX = Number(this.currentX) || 0 220 | this.currentY = Number(this.currentY) || 0 221 | 222 | this.currentX = Math.min( 223 | this.screenWidth - halfWidth, 224 | this.currentX + this.step 225 | ) 226 | this.mainWindow.setPosition( 227 | Math.round(this.currentX), 228 | Math.round(this.currentY) 229 | ) 230 | } 231 | 232 | public moveWindowLeft(): void { 233 | if (!this.mainWindow) return 234 | 235 | const windowWidth = this.windowSize?.width || 0 236 | const halfWidth = windowWidth / 2 237 | 238 | // Ensure currentX and currentY are numbers 239 | this.currentX = Number(this.currentX) || 0 240 | this.currentY = Number(this.currentY) || 0 241 | 242 | this.currentX = Math.max(-halfWidth, this.currentX - this.step) 243 | this.mainWindow.setPosition( 244 | Math.round(this.currentX), 245 | Math.round(this.currentY) 246 | ) 247 | } 248 | 249 | public moveWindowDown(): void { 250 | if (!this.mainWindow) return 251 | 252 | const windowHeight = this.windowSize?.height || 0 253 | const halfHeight = windowHeight / 2 254 | 255 | // Ensure currentX and currentY are numbers 256 | this.currentX = Number(this.currentX) || 0 257 | this.currentY = Number(this.currentY) || 0 258 | 259 | this.currentY = Math.min( 260 | this.screenHeight - halfHeight, 261 | this.currentY + this.step 262 | ) 263 | this.mainWindow.setPosition( 264 | Math.round(this.currentX), 265 | Math.round(this.currentY) 266 | ) 267 | } 268 | 269 | public moveWindowUp(): void { 270 | if (!this.mainWindow) return 271 | 272 | const windowHeight = this.windowSize?.height || 0 273 | const halfHeight = windowHeight / 2 274 | 275 | // Ensure currentX and currentY are numbers 276 | this.currentX = Number(this.currentX) || 0 277 | this.currentY = Number(this.currentY) || 0 278 | 279 | this.currentY = Math.max(-halfHeight, this.currentY - this.step) 280 | this.mainWindow.setPosition( 281 | Math.round(this.currentX), 282 | Math.round(this.currentY) 283 | ) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /dist-electron/WindowHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // electron/WindowHelper.ts 3 | var __importDefault = (this && this.__importDefault) || function (mod) { 4 | return (mod && mod.__esModule) ? mod : { "default": mod }; 5 | }; 6 | Object.defineProperty(exports, "__esModule", { value: true }); 7 | exports.WindowHelper = void 0; 8 | const electron_1 = require("electron"); 9 | const node_path_1 = __importDefault(require("node:path")); 10 | const isDev = process.env.NODE_ENV === "development"; 11 | const startUrl = isDev 12 | ? "http://localhost:5180" 13 | : `file://${node_path_1.default.join(__dirname, "../dist/index.html")}`; 14 | class WindowHelper { 15 | mainWindow = null; 16 | isWindowVisible = false; 17 | windowPosition = null; 18 | windowSize = null; 19 | appState; 20 | // Initialize with explicit number type and 0 value 21 | screenWidth = 0; 22 | screenHeight = 0; 23 | step = 0; 24 | currentX = 0; 25 | currentY = 0; 26 | constructor(appState) { 27 | this.appState = appState; 28 | } 29 | setWindowDimensions(width, height) { 30 | if (!this.mainWindow || this.mainWindow.isDestroyed()) 31 | return; 32 | // Get current window position 33 | const [currentX, currentY] = this.mainWindow.getPosition(); 34 | // Get screen dimensions 35 | const primaryDisplay = electron_1.screen.getPrimaryDisplay(); 36 | const workArea = primaryDisplay.workAreaSize; 37 | // Use 75% width if debugging has occurred, otherwise use 60% 38 | const maxAllowedWidth = Math.floor(workArea.width * (this.appState.getHasDebugged() ? 0.75 : 0.5)); 39 | // Ensure width doesn't exceed max allowed width and height is reasonable 40 | const newWidth = Math.min(width + 32, maxAllowedWidth); 41 | const newHeight = Math.max(Math.ceil(height), 1200); // Ensure minimum height of 1200px 42 | // Center the window horizontally if it would go off screen 43 | const maxX = workArea.width - newWidth; 44 | const newX = Math.min(Math.max(currentX, 0), maxX); 45 | // Update window bounds 46 | this.mainWindow.setBounds({ 47 | x: newX, 48 | y: currentY, 49 | width: newWidth, 50 | height: newHeight 51 | }); 52 | // Update internal state 53 | this.windowPosition = { x: newX, y: currentY }; 54 | this.windowSize = { width: newWidth, height: newHeight }; 55 | this.currentX = newX; 56 | } 57 | createWindow() { 58 | if (this.mainWindow !== null) 59 | return; 60 | const primaryDisplay = electron_1.screen.getPrimaryDisplay(); 61 | const workArea = primaryDisplay.workAreaSize; 62 | this.screenWidth = workArea.width; 63 | this.screenHeight = workArea.height; 64 | this.step = Math.floor(this.screenWidth / 10); // 10 steps 65 | this.currentX = 0; // Start at the left 66 | const windowSettings = { 67 | height: 1200, 68 | minWidth: undefined, 69 | maxWidth: undefined, 70 | x: this.currentX, 71 | y: 0, 72 | webPreferences: { 73 | nodeIntegration: true, 74 | contextIsolation: true, 75 | preload: node_path_1.default.join(__dirname, "preload.js") 76 | }, 77 | show: true, 78 | alwaysOnTop: true, 79 | frame: false, 80 | transparent: true, 81 | fullscreenable: false, 82 | hasShadow: false, 83 | backgroundColor: "#00000000", 84 | focusable: true 85 | }; 86 | this.mainWindow = new electron_1.BrowserWindow(windowSettings); 87 | // this.mainWindow.webContents.openDevTools() 88 | this.mainWindow.setContentProtection(true); 89 | if (process.platform === "darwin") { 90 | this.mainWindow.setVisibleOnAllWorkspaces(true, { 91 | visibleOnFullScreen: true 92 | }); 93 | this.mainWindow.setHiddenInMissionControl(true); 94 | this.mainWindow.setAlwaysOnTop(true, "floating"); 95 | } 96 | if (process.platform === "linux") { 97 | // Linux-specific optimizations for stealth overlays 98 | if (this.mainWindow.setHasShadow) { 99 | this.mainWindow.setHasShadow(false); 100 | } 101 | this.mainWindow.setFocusable(false); 102 | } 103 | this.mainWindow.setSkipTaskbar(true); 104 | this.mainWindow.setAlwaysOnTop(true); 105 | this.mainWindow.loadURL(startUrl).catch((err) => { 106 | console.error("Failed to load URL:", err); 107 | }); 108 | const bounds = this.mainWindow.getBounds(); 109 | this.windowPosition = { x: bounds.x, y: bounds.y }; 110 | this.windowSize = { width: bounds.width, height: bounds.height }; 111 | this.currentX = bounds.x; 112 | this.currentY = bounds.y; 113 | this.setupWindowListeners(); 114 | this.isWindowVisible = true; 115 | } 116 | setupWindowListeners() { 117 | if (!this.mainWindow) 118 | return; 119 | this.mainWindow.on("move", () => { 120 | if (this.mainWindow) { 121 | const bounds = this.mainWindow.getBounds(); 122 | this.windowPosition = { x: bounds.x, y: bounds.y }; 123 | this.currentX = bounds.x; 124 | this.currentY = bounds.y; 125 | } 126 | }); 127 | this.mainWindow.on("resize", () => { 128 | if (this.mainWindow) { 129 | const bounds = this.mainWindow.getBounds(); 130 | this.windowSize = { width: bounds.width, height: bounds.height }; 131 | } 132 | }); 133 | this.mainWindow.on("closed", () => { 134 | this.mainWindow = null; 135 | this.isWindowVisible = false; 136 | this.windowPosition = null; 137 | this.windowSize = null; 138 | }); 139 | } 140 | getMainWindow() { 141 | return this.mainWindow; 142 | } 143 | isVisible() { 144 | return this.isWindowVisible; 145 | } 146 | hideMainWindow() { 147 | if (!this.mainWindow || this.mainWindow.isDestroyed()) { 148 | console.warn("Main window does not exist or is destroyed."); 149 | return; 150 | } 151 | const bounds = this.mainWindow.getBounds(); 152 | this.windowPosition = { x: bounds.x, y: bounds.y }; 153 | this.windowSize = { width: bounds.width, height: bounds.height }; 154 | this.mainWindow.hide(); 155 | this.isWindowVisible = false; 156 | } 157 | showMainWindow() { 158 | if (!this.mainWindow || this.mainWindow.isDestroyed()) { 159 | console.warn("Main window does not exist or is destroyed."); 160 | return; 161 | } 162 | if (this.windowPosition && this.windowSize) { 163 | this.mainWindow.setBounds({ 164 | x: this.windowPosition.x, 165 | y: this.windowPosition.y, 166 | width: this.windowSize.width, 167 | height: this.windowSize.height 168 | }); 169 | } 170 | this.mainWindow.showInactive(); 171 | this.isWindowVisible = true; 172 | } 173 | toggleMainWindow() { 174 | if (this.isWindowVisible) { 175 | this.hideMainWindow(); 176 | } 177 | else { 178 | this.showMainWindow(); 179 | } 180 | } 181 | // New methods for window movement 182 | moveWindowRight() { 183 | if (!this.mainWindow) 184 | return; 185 | const windowWidth = this.windowSize?.width || 0; 186 | const halfWidth = windowWidth / 2; 187 | // Ensure currentX and currentY are numbers 188 | this.currentX = Number(this.currentX) || 0; 189 | this.currentY = Number(this.currentY) || 0; 190 | this.currentX = Math.min(this.screenWidth - halfWidth, this.currentX + this.step); 191 | this.mainWindow.setPosition(Math.round(this.currentX), Math.round(this.currentY)); 192 | } 193 | moveWindowLeft() { 194 | if (!this.mainWindow) 195 | return; 196 | const windowWidth = this.windowSize?.width || 0; 197 | const halfWidth = windowWidth / 2; 198 | // Ensure currentX and currentY are numbers 199 | this.currentX = Number(this.currentX) || 0; 200 | this.currentY = Number(this.currentY) || 0; 201 | this.currentX = Math.max(-halfWidth, this.currentX - this.step); 202 | this.mainWindow.setPosition(Math.round(this.currentX), Math.round(this.currentY)); 203 | } 204 | moveWindowDown() { 205 | if (!this.mainWindow) 206 | return; 207 | const windowHeight = this.windowSize?.height || 0; 208 | const halfHeight = windowHeight / 2; 209 | // Ensure currentX and currentY are numbers 210 | this.currentX = Number(this.currentX) || 0; 211 | this.currentY = Number(this.currentY) || 0; 212 | this.currentY = Math.min(this.screenHeight - halfHeight, this.currentY + this.step); 213 | this.mainWindow.setPosition(Math.round(this.currentX), Math.round(this.currentY)); 214 | } 215 | moveWindowUp() { 216 | if (!this.mainWindow) 217 | return; 218 | const windowHeight = this.windowSize?.height || 0; 219 | const halfHeight = windowHeight / 2; 220 | // Ensure currentX and currentY are numbers 221 | this.currentX = Number(this.currentX) || 0; 222 | this.currentY = Number(this.currentY) || 0; 223 | this.currentY = Math.max(-halfHeight, this.currentY - this.step); 224 | this.mainWindow.setPosition(Math.round(this.currentX), Math.round(this.currentY)); 225 | } 226 | } 227 | exports.WindowHelper = WindowHelper; 228 | //# sourceMappingURL=WindowHelper.js.map -------------------------------------------------------------------------------- /src/components/Solutions/SolutionCommands.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { IoLogOutOutline } from "react-icons/io5" 3 | 4 | interface SolutionCommandsProps { 5 | extraScreenshots: any[] 6 | onTooltipVisibilityChange?: (visible: boolean, height: number) => void 7 | } 8 | 9 | const SolutionCommands: React.FC = ({ 10 | extraScreenshots, 11 | onTooltipVisibilityChange 12 | }) => { 13 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 14 | const tooltipRef = useRef(null) 15 | 16 | useEffect(() => { 17 | if (onTooltipVisibilityChange) { 18 | let tooltipHeight = 0 19 | if (tooltipRef.current && isTooltipVisible) { 20 | tooltipHeight = tooltipRef.current.offsetHeight + 10 // Adjust if necessary 21 | } 22 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 23 | } 24 | }, [isTooltipVisible, onTooltipVisibilityChange]) 25 | 26 | const handleMouseEnter = () => { 27 | setIsTooltipVisible(true) 28 | } 29 | 30 | const handleMouseLeave = () => { 31 | setIsTooltipVisible(false) 32 | } 33 | 34 | return ( 35 |
36 |
37 |
38 | {/* Show/Hide */} 39 |
40 | Show/Hide 41 |
42 | 45 | 48 |
49 |
50 | 51 | {/* Screenshot */} 52 |
53 | 54 | {extraScreenshots.length === 0 55 | ? "Screenshot your code" 56 | : "Screenshot"} 57 | 58 |
59 | 62 | 65 |
66 |
67 | {extraScreenshots.length > 0 && ( 68 |
69 | Debug 70 |
71 | 74 | 77 |
78 |
79 | )} 80 | 81 | {/* Start Over */} 82 |
83 | Start over 84 |
85 | 88 | 91 |
92 |
93 | 94 | {/* Question Mark with Tooltip */} 95 |
100 | {/* Question mark circle */} 101 |
102 | ? 103 |
104 | 105 | {/* Tooltip Content */} 106 | {isTooltipVisible && ( 107 |
112 |
113 | {/* Tooltip content */} 114 |
115 |

116 | Keyboard Shortcuts 117 |

118 |
119 | {/* Toggle Command */} 120 |
121 |
122 | 123 | Toggle Window 124 | 125 |
126 | 127 | ⌘ 128 | 129 | 130 | B 131 | 132 |
133 |
134 |

135 | Show or hide this window. 136 |

137 |
138 | {/* Screenshot Command */} 139 |
140 |
141 | 142 | Take Screenshot 143 | 144 |
145 | 146 | ⌘ 147 | 148 | 149 | H 150 | 151 |
152 |
153 |

154 | Capture additional parts of the question or your 155 | solution for debugging help. Up to 5 extra screenshots 156 | are saved. 157 |

158 |
159 | {/* Debug Command */} 160 |
161 |
162 | Debug 163 |
164 | 165 | ⌘ 166 | 167 | 168 | ↵ 169 | 170 |
171 |
172 |

173 | Generate new solutions based on all previous and newly 174 | added screenshots. 175 |

176 |
177 | {/* Start Over Command */} 178 |
179 |
180 | Start Over 181 |
182 | 183 | ⌘ 184 | 185 | 186 | R 187 | 188 |
189 |
190 |

191 | Start fresh with a new question. 192 |

193 |
194 |
195 |
196 |
197 |
198 | )} 199 |
200 | 201 | {/* Sign Out Button */} 202 | 209 |
210 |
211 |
212 | ) 213 | } 214 | 215 | export default SolutionCommands 216 | -------------------------------------------------------------------------------- /src/components/Audio/AudioTranscriber.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react" 2 | import { Mic, MicOff, Volume2 } from "lucide-react" 3 | import { GoogleGenAI, Modality } from '@google/genai' 4 | 5 | interface AudioTranscriberProps { 6 | onTranscript: (text: string) => void 7 | } 8 | 9 | const AudioTranscriber: React.FC = ({ onTranscript }) => { 10 | const [isRecording, setIsRecording] = useState(false) 11 | const [transcript, setTranscript] = useState("") 12 | const [error, setError] = useState("") 13 | const [isConnecting, setIsConnecting] = useState(false) 14 | const sessionRef = useRef(null) 15 | const streamRef = useRef(null) 16 | const audioContextRef = useRef(null) 17 | const processorRef = useRef(null) 18 | 19 | const startRecording = async () => { 20 | try { 21 | setError("") 22 | setTranscript("") 23 | setIsConnecting(true) 24 | 25 | // Get API key 26 | let apiKey = (window as any).GEMINI_API_KEY 27 | if (!apiKey && (window as any).electronAPI?.getApiKey) { 28 | apiKey = await (window as any).electronAPI.getApiKey() 29 | } 30 | 31 | if (!apiKey) { 32 | throw new Error("API key not available") 33 | } 34 | 35 | // Initialize Google GenAI 36 | const ai = new GoogleGenAI({ apiKey }) 37 | 38 | const config = { 39 | responseModalities: [Modality.TEXT], 40 | inputAudioTranscription: {} 41 | } 42 | 43 | // Connect to Gemini Live 44 | const session = await ai.live.connect({ 45 | model: 'gemini-2.0-flash-exp', 46 | callbacks: { 47 | onopen: () => { 48 | setIsConnecting(false) 49 | setIsRecording(true) 50 | }, 51 | onmessage: (message: any) => { 52 | // Get input transcription (what user said) 53 | if (message.serverContent?.inputTranscription?.text) { 54 | const text = message.serverContent.inputTranscription.text 55 | setTranscript((prev) => { 56 | const newTranscript = prev + text + " " 57 | onTranscript(newTranscript) 58 | return newTranscript 59 | }) 60 | } 61 | }, 62 | onerror: (e: any) => { 63 | setError(`Error: ${e.message}`) 64 | stopRecording() 65 | }, 66 | onclose: (e: any) => { 67 | if (e.code !== 1000) { 68 | setError(`Connection closed: ${e.reason || "Unknown error"}`) 69 | } 70 | setIsRecording(false) 71 | setIsConnecting(false) 72 | }, 73 | }, 74 | config: config, 75 | }) 76 | 77 | sessionRef.current = session 78 | 79 | // Create audio context first 80 | const audioContext = new AudioContext({ sampleRate: 16000 }) 81 | audioContextRef.current = audioContext 82 | 83 | // Create a destination to mix audio streams 84 | const destination = audioContext.createMediaStreamDestination() 85 | 86 | try { 87 | // Request microphone access 88 | const micStream = await navigator.mediaDevices.getUserMedia({ 89 | audio: { 90 | echoCancellation: true, 91 | noiseSuppression: true, 92 | autoGainControl: true, 93 | sampleRate: 16000 94 | } 95 | }) 96 | 97 | const micSource = audioContext.createMediaStreamSource(micStream) 98 | micSource.connect(destination) 99 | 100 | // Try to get system audio (desktop/tab audio) 101 | try { 102 | const systemStream = await navigator.mediaDevices.getDisplayMedia({ 103 | video: false, 104 | audio: { 105 | echoCancellation: false, 106 | noiseSuppression: false, 107 | autoGainControl: false, 108 | sampleRate: 16000 109 | } 110 | }) 111 | 112 | const systemSource = audioContext.createMediaStreamSource(systemStream) 113 | systemSource.connect(destination) 114 | } catch (e) { 115 | console.log("System audio not available, using mic only") 116 | } 117 | } catch (e) { 118 | throw new Error("Failed to access audio devices") 119 | } 120 | 121 | // Use the mixed stream 122 | streamRef.current = destination.stream 123 | 124 | const source = audioContext.createMediaStreamSource(destination.stream) 125 | const processor = audioContext.createScriptProcessor(4096, 1, 1) 126 | processorRef.current = processor 127 | 128 | source.connect(processor) 129 | processor.connect(audioContext.destination) 130 | 131 | processor.onaudioprocess = (e) => { 132 | if (session && sessionRef.current) { 133 | const inputData = e.inputBuffer.getChannelData(0) 134 | 135 | // Convert float32 to int16 136 | const int16Data = new Int16Array(inputData.length) 137 | for (let i = 0; i < inputData.length; i++) { 138 | const s = Math.max(-1, Math.min(1, inputData[i])) 139 | int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF 140 | } 141 | 142 | // Convert to base64 143 | const base64Audio = btoa( 144 | String.fromCharCode(...new Uint8Array(int16Data.buffer)) 145 | ) 146 | 147 | // Send audio to Gemini Live 148 | session.sendRealtimeInput({ 149 | audio: { 150 | data: base64Audio, 151 | mimeType: "audio/pcm;rate=16000" 152 | } 153 | }) 154 | } 155 | } 156 | 157 | } catch (error: any) { 158 | setError(`Failed to start recording: ${error.message}`) 159 | setIsRecording(false) 160 | setIsConnecting(false) 161 | } 162 | } 163 | 164 | const stopRecording = () => { 165 | // Disconnect audio processor 166 | if (processorRef.current) { 167 | processorRef.current.disconnect() 168 | processorRef.current = null 169 | } 170 | 171 | // Close audio context 172 | if (audioContextRef.current) { 173 | audioContextRef.current.close() 174 | audioContextRef.current = null 175 | } 176 | 177 | // Stop all tracks 178 | if (streamRef.current) { 179 | streamRef.current.getTracks().forEach(track => track.stop()) 180 | streamRef.current = null 181 | } 182 | 183 | // Close session 184 | if (sessionRef.current) { 185 | sessionRef.current.close() 186 | sessionRef.current = null 187 | } 188 | 189 | setIsRecording(false) 190 | setIsConnecting(false) 191 | } 192 | 193 | const toggleRecording = () => { 194 | if (isRecording || isConnecting) { 195 | stopRecording() 196 | } else { 197 | startRecording() 198 | } 199 | } 200 | 201 | const clearTranscript = () => { 202 | setTranscript("") 203 | onTranscript("") 204 | } 205 | 206 | const saveTranscript = () => { 207 | if (!transcript) return 208 | 209 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-') 210 | const filename = `transcript_${timestamp}.txt` 211 | const blob = new Blob([transcript], { type: 'text/plain' }) 212 | const url = URL.createObjectURL(blob) 213 | const a = document.createElement('a') 214 | a.href = url 215 | a.download = filename 216 | a.click() 217 | URL.revokeObjectURL(url) 218 | } 219 | 220 | return ( 221 |
222 | {/* Recording Button */} 223 | 253 | 254 | {/* Error Display */} 255 | {error && ( 256 |
257 |

{error}

258 |
259 | )} 260 | 261 | {/* Transcript Display */} 262 | {transcript && ( 263 |
264 |
265 |
266 | {/* Header */} 267 |
268 | Transcript 269 |
270 | 277 | 283 |
284 |
285 | 286 | {/* Transcript Content */} 287 |
288 |
289 | {transcript} 290 |
291 |
292 |
293 |
294 |
295 | )} 296 |
297 | ) 298 | } 299 | 300 | export default AudioTranscriber 301 | --------------------------------------------------------------------------------