├── .editorconfig ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── README.md ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico └── icon.png ├── electron-builder.yml ├── electron.vite.config.ts ├── eslint.config.mjs ├── notarize.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── resources └── icon.png ├── src ├── main │ ├── index.ts │ └── lib │ │ ├── config-manager.ts │ │ ├── ipc-handler.ts │ │ ├── keyboard-shortcut.ts │ │ ├── processing-manager.ts │ │ └── screenshot-manager.ts ├── preload │ ├── index.d.ts │ └── index.ts └── renderer │ ├── index.html │ └── src │ ├── App.tsx │ ├── assets │ ├── base.css │ ├── electron.svg │ ├── main.css │ └── wavy-lines.svg │ ├── components │ ├── debug │ │ ├── code-section.tsx │ │ └── index.tsx │ ├── language-selector.tsx │ ├── main-app.tsx │ ├── queue │ │ ├── queue-commands.tsx │ │ ├── screenshot-item.tsx │ │ └── screenshot-queue.tsx │ ├── screenshots-view.tsx │ ├── settings-dialog.tsx │ ├── solutions │ │ ├── complexity-section.tsx │ │ ├── content-section.tsx │ │ ├── index.tsx │ │ ├── solution-commands.tsx │ │ └── solution-section.tsx │ ├── ui │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── toast.tsx │ └── welcome-screen.tsx │ ├── env.d.ts │ ├── lib │ ├── languages.ts │ ├── types.ts │ └── utils.ts │ ├── main.tsx │ └── providers │ ├── query-provider.tsx │ ├── toast-context.tsx │ └── toast-provider.tsx ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Silent Coder 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | # Keep the overall job timeout 14 | timeout-minutes: 60 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [macos-latest, windows-latest] 20 | 21 | steps: 22 | - name: Check out Git repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 # Or your preferred Node version 29 | 30 | - name: Install pnpm 31 | uses: pnpm/action-setup@v2 32 | with: 33 | version: 10.5.2 # Or your preferred pnpm version 34 | 35 | - name: Install dependencies 36 | # This installs @electron/notarize from package.json 37 | run: pnpm install --no-frozen-lockfile 38 | 39 | # Windows-specific publish step 40 | - name: Build and Publish (Windows) 41 | if: matrix.os == 'windows-latest' 42 | uses: nick-fields/retry@v2 43 | with: 44 | timeout_minutes: 45 45 | max_attempts: 3 46 | command: pnpm run build:win -- --publish always 47 | env: 48 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | DEBUG: electron-builder # Optional: for detailed logs 50 | 51 | # macOS-specific publish step (Build and Sign) 52 | - name: Build and Publish (macOS) 53 | if: matrix.os == 'macos-latest' 54 | uses: nick-fields/retry@v2 55 | with: 56 | timeout_minutes: 45 # Keep extended timeout for signing 57 | max_attempts: 3 58 | # Assumes 'pnpm run build:mac' uses your electron-builder.yml 59 | command: pnpm run build:mac -- --publish always 60 | env: 61 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | APPLE_ID: ${{ secrets.APPLE_ID }} 63 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} 64 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 65 | CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} 66 | CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} 67 | CSC_IDENTITY_AUTO_DISCOVERY: true # Use auto-discovery 68 | CSC_TIMEOUT: 1800000 # Keep increased timeout (30 minutes) 69 | # Add more specific debug logs for signing 70 | DEBUG: 'electron-builder*,electron-osx-sign*' 71 | 72 | # Separate Notarization Step for macOS 73 | - name: Notarize macOS App 74 | if: matrix.os == 'macos-latest' 75 | # Add retry here as well, notarization can sometimes fail temporarily 76 | uses: nick-fields/retry@v2 77 | with: 78 | timeout_minutes: 30 # Generous timeout for notarization 79 | max_attempts: 3 80 | # Execute the dedicated notarization script 81 | command: node notarize.js 82 | env: 83 | # Pass necessary values to the script via environment variables 84 | APP_BUNDLE_ID: com.kuluruvineeth.silentcoder 85 | APP_PATH: dist/mac-arm64/silent-coder.app 86 | APPLE_ID: ${{ secrets.APPLE_ID }} 87 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} # Ensure this uses the correct secret name 88 | APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} 89 | # shell: bash # Not needed when using node -e 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | .eslintcache 6 | *.log* 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Main Process", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", 10 | "windows": { 11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" 12 | }, 13 | "runtimeArgs": ["--sourcemap"], 14 | "env": { 15 | "REMOTE_DEBUGGING_PORT": "9222" 16 | } 17 | }, 18 | { 19 | "name": "Debug Renderer Process", 20 | "port": 9222, 21 | "request": "attach", 22 | "type": "chrome", 23 | "webRoot": "${workspaceFolder}/src/renderer", 24 | "timeout": 60000, 25 | "presentation": { 26 | "hidden": true 27 | } 28 | } 29 | ], 30 | "compounds": [ 31 | { 32 | "name": "Debug All", 33 | "configurations": ["Debug Main Process", "Debug Renderer Process"], 34 | "presentation": { 35 | "order": 1 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # silent-coder 2 | 3 | An Electron application with React and TypeScript 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 8 | 9 | ## Project Setup 10 | 11 | ### Install 12 | 13 | ```bash 14 | $ pnpm install 15 | ``` 16 | 17 | ### Development 18 | 19 | ```bash 20 | $ pnpm dev 21 | ``` 22 | 23 | ### Build 24 | 25 | ```bash 26 | # For windows 27 | $ pnpm build:win 28 | 29 | # For macOS 30 | $ pnpm build:mac 31 | 32 | # For Linux 33 | $ pnpm build:linux 34 | ``` 35 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-dyld-environment-variables 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.cs.allow-jit 10 | 11 | com.apple.security.cs.allow-unsigned-executable-memory 12 | 13 | com.apple.security.cs.debugger 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.network.server 18 | 19 | com.apple.security.files.user-selected.read-only 20 | 21 | com.apple.security.inherit 22 | 23 | com.apple.security.automation.apple-events 24 | 25 | 26 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuluruvineeth/silent-coder/0e6633325902c82dfef97b799ed1532812c725f7/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuluruvineeth/silent-coder/0e6633325902c82dfef97b799ed1532812c725f7/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuluruvineeth/silent-coder/0e6633325902c82dfef97b799ed1532812c725f7/build/icon.png -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.kuluruvineeth.silentcoder 2 | productName: silent-coder 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | win: 15 | executableName: silent-coder 16 | nsis: 17 | artifactName: ${name}-${version}-setup.${ext} 18 | shortcutName: ${productName} 19 | uninstallDisplayName: ${productName} 20 | createDesktopShortcut: always 21 | mac: 22 | entitlementsInherit: build/entitlements.mac.plist 23 | entitlements: build/entitlements.mac.plist 24 | hardenedRuntime: true 25 | category: public.app-category.productivity 26 | darkModeSupport: true 27 | target: 28 | - dmg 29 | - zip 30 | dmg: 31 | artifactName: ${name}-${version}.${ext} 32 | sign: true 33 | mas: 34 | entitlements: build/entitlements.mas.plist 35 | entitlementsInherit: build/entitlements.mas.inherit.plist 36 | provisioningProfile: build/embedded.provisionprofile 37 | hardenedRuntime: false 38 | linux: 39 | target: 40 | - AppImage 41 | - snap 42 | - deb 43 | maintainer: electronjs.org 44 | category: Utility 45 | appImage: 46 | artifactName: ${name}-${version}.${ext} 47 | npmRebuild: false 48 | publish: 49 | provider: github 50 | owner: kuluruvineeth 51 | repo: silent-coder 52 | releaseType: release 53 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import react from '@vitejs/plugin-react' 6 | 7 | export default defineConfig({ 8 | main: { 9 | plugins: [externalizeDepsPlugin()] 10 | }, 11 | preload: { 12 | plugins: [externalizeDepsPlugin()] 13 | }, 14 | renderer: { 15 | resolve: { 16 | alias: { 17 | '@renderer': resolve('src/renderer/src') 18 | } 19 | }, 20 | plugins: [react()] 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from '@electron-toolkit/eslint-config-ts' 2 | import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier' 3 | import eslintPluginReact from 'eslint-plugin-react' 4 | import eslintPluginReactHooks from 'eslint-plugin-react-hooks' 5 | import eslintPluginReactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default tseslint.config( 8 | { ignores: ['**/node_modules', '**/dist', '**/out'] }, 9 | tseslint.configs.recommended, 10 | eslintPluginReact.configs.flat.recommended, 11 | eslintPluginReact.configs.flat['jsx-runtime'], 12 | { 13 | settings: { 14 | react: { 15 | version: 'detect' 16 | } 17 | } 18 | }, 19 | { 20 | files: ['**/*.{ts,tsx}'], 21 | plugins: { 22 | 'react-hooks': eslintPluginReactHooks, 23 | 'react-refresh': eslintPluginReactRefresh 24 | }, 25 | rules: { 26 | ...eslintPluginReactHooks.configs.recommended.rules, 27 | ...eslintPluginReactRefresh.configs.vite.rules, 28 | 'react/prop-types': 'off', 29 | '@typescript-eslint/no-unused-vars': 'off', 30 | '@typescript-eslint/explicit-function-return-type': 'off' 31 | } 32 | }, 33 | eslintConfigPrettier 34 | ) 35 | -------------------------------------------------------------------------------- /notarize.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 2 | const fs = require('fs') 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 4 | const { notarize } = require('@electron/notarize') 5 | 6 | // Define the main async function 7 | async function notarizeApp() { 8 | console.log('--- Starting notarize.js Script ---') 9 | 10 | // Get configuration from environment variables 11 | const appBundleId = process.env.APP_BUNDLE_ID 12 | const appPath = process.env.APP_PATH 13 | const appleId = process.env.APPLE_ID 14 | const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD 15 | const teamId = process.env.APPLE_TEAM_ID 16 | 17 | // Only run on macOS 18 | if (process.platform !== 'darwin') { 19 | console.log('Not on macOS, skipping notarization.') 20 | return 21 | } 22 | 23 | console.log(`App Path: ${appPath}`) 24 | console.log(`App Bundle ID: ${appBundleId}`) 25 | console.log(`Apple ID: ${appleId ? '******' : 'Not Set'}`) 26 | console.log(`Team ID: ${teamId ? teamId : 'Not Set'}`) 27 | 28 | if (!appBundleId) throw new Error('APP_BUNDLE_ID environment variable not set') 29 | if (!appPath) throw new Error('APP_PATH environment variable not set') 30 | if (!appleId) throw new Error('APPLE_ID environment variable not set') 31 | if (!appleIdPassword) throw new Error('APPLE_APP_SPECIFIC_PASSWORD environment variable not set') 32 | if (!teamId) throw new Error('APPLE_TEAM_ID environment variable not set') 33 | 34 | if (!fs.existsSync(appPath)) { 35 | console.error(`Error: App path does not exist: ${appPath}`) 36 | throw new Error(`App path not found: ${appPath}`) 37 | } 38 | 39 | const notarizeOptions = { 40 | appBundleId: appBundleId, 41 | appPath: appPath, 42 | appleId: appleId, 43 | appleIdPassword: appleIdPassword, 44 | teamId: teamId 45 | } 46 | 47 | console.log('Attempting to call notarize function...') 48 | try { 49 | await notarize(notarizeOptions) 50 | console.log(`--- Notarization Successful for ${appPath} ---`) 51 | } catch (error) { 52 | console.error('--- Notarization Failed ---') 53 | console.error(error) 54 | // Check if the error is due to the app already being notarized 55 | if (error.message && error.message.includes('Package already notarized')) { 56 | console.log('Warning: App seems to be already notarized. Continuing...') 57 | } else { 58 | // Re-throw the error to ensure the script exits with a non-zero code on failure 59 | throw error 60 | } 61 | } 62 | } 63 | 64 | // Export the function for potential use as a hook (though we run it directly) 65 | exports.default = notarizeApp 66 | 67 | // Execute the function if the script is run directly (e.g., by `node notarize.js`) 68 | if (require.main === module) { 69 | notarizeApp().catch((err) => { 70 | console.error('--- Error executing notarizeApp script directly ---') 71 | console.error(err) 72 | process.exit(1) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silent-coder", 3 | "version": "1.0.7", 4 | "description": "An Electron application with React and TypeScript", 5 | "main": "./out/main/index.js", 6 | "author": "example.com", 7 | "homepage": "https://electron-vite.org", 8 | "scripts": { 9 | "format": "prettier --write .", 10 | "lint": "eslint --cache .", 11 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 12 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 13 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 14 | "start": "electron-vite preview", 15 | "dev": "electron-vite dev", 16 | "build": "npm run typecheck && electron-vite build", 17 | "postinstall": "electron-builder install-app-deps", 18 | "build:unpack": "npm run build && electron-builder --dir", 19 | "build:win": "npm run build && electron-builder --win", 20 | "build:mac": "electron-vite build && electron-builder --mac", 21 | "build:linux": "electron-vite build && electron-builder --linux" 22 | }, 23 | "dependencies": { 24 | "@electron-toolkit/preload": "^3.0.1", 25 | "@electron-toolkit/utils": "^4.0.0", 26 | "@google/generative-ai": "^0.24.0", 27 | "@radix-ui/react-dialog": "^1.1.7", 28 | "@radix-ui/react-toast": "^1.2.7", 29 | "@tailwindcss/postcss": "^4.1.4", 30 | "@tanstack/react-query": "^5.74.4", 31 | "class-variance-authority": "^0.7.1", 32 | "clsx": "^2.1.1", 33 | "lucide-react": "^0.488.0", 34 | "openai": "^4.94.0", 35 | "postcss": "^8.5.3", 36 | "react-syntax-highlighter": "^15.6.1", 37 | "screenshot-desktop": "^1.15.1", 38 | "tailwind-merge": "^3.2.0", 39 | "tailwindcss": "^4.1.4", 40 | "uuid": "^11.1.0" 41 | }, 42 | "devDependencies": { 43 | "@electron-toolkit/eslint-config-prettier": "^3.0.0", 44 | "@electron-toolkit/eslint-config-ts": "^3.0.0", 45 | "@electron-toolkit/tsconfig": "^1.0.1", 46 | "@electron/notarize": "^2.3.0", 47 | "@types/node": "^22.14.1", 48 | "@types/react": "^19.1.1", 49 | "@types/react-dom": "^19.1.2", 50 | "@vitejs/plugin-react": "^4.3.4", 51 | "electron": "35.0.0", 52 | "electron-builder": "^25.1.8", 53 | "electron-vite": "^3.1.0", 54 | "eslint": "^9.24.0", 55 | "eslint-plugin-react": "^7.37.5", 56 | "eslint-plugin-react-hooks": "^5.2.0", 57 | "eslint-plugin-react-refresh": "^0.4.19", 58 | "prettier": "^3.5.3", 59 | "react": "^19.1.0", 60 | "react-dom": "^19.1.0", 61 | "typescript": "^5.8.3", 62 | "vite": "^6.2.6" 63 | }, 64 | "pnpm": { 65 | "onlyBuiltDependencies": [ 66 | "electron", 67 | "esbuild" 68 | ] 69 | }, 70 | "packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b" 71 | } 72 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuluruvineeth/silent-coder/0e6633325902c82dfef97b799ed1532812c725f7/resources/icon.png -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow, screen } from 'electron' 2 | import path, { join } from 'path' 3 | import { is } from '@electron-toolkit/utils' 4 | import icon from '../../resources/icon.png?asset' 5 | import fs from 'fs' 6 | import { initializeIpcHandler } from './lib/ipc-handler' 7 | import { KeyboardShortcutHelper } from './lib/keyboard-shortcut' 8 | import { ScreenshotManager } from './lib/screenshot-manager' 9 | import { ProcessingManager } from './lib/processing-manager' 10 | import { configManager } from './lib/config-manager' 11 | export const state = { 12 | mainWindow: null as BrowserWindow | null, 13 | isWindowVisible: false, 14 | windowPosition: null as { x: number; y: number } | null, 15 | windowSize: null as { width: number; height: number } | null, 16 | screenWidth: 0, 17 | screenHeight: 0, 18 | step: 0, 19 | currentX: 0, 20 | currentY: 0, 21 | 22 | keyboardShortcutHelper: null as KeyboardShortcutHelper | null, 23 | screenshotManager: null as ScreenshotManager | null, 24 | processingManager: null as ProcessingManager | null, 25 | 26 | view: 'queue' as 'queue' | 'solutions' | 'debug', 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | problemInfo: null as any, 29 | hasDebugged: false, 30 | 31 | PROCESSING_EVENTS: { 32 | NO_SCREENSHOTS: 'processing-no-screenshots', 33 | API_KEY_INVALID: 'api-key-invalid', 34 | INITIAL_START: 'initial-start', 35 | PROBLEM_EXTRACTED: 'problem-extracted', 36 | SOLUTION_SUCCESS: 'solution-success', 37 | INITIAL_SOLUTION_ERROR: 'solution-error', 38 | DEBUG_START: 'debug-start', 39 | DEBUG_SUCCESS: 'debug-success', 40 | DEBUG_ERROR: 'debug-error' 41 | } 42 | } 43 | 44 | async function createWindow(): Promise { 45 | if (state.mainWindow) { 46 | if (state.mainWindow.isMinimized()) state.mainWindow.restore() 47 | state.mainWindow.focus() 48 | return 49 | } 50 | 51 | const primaryDisplay = screen.getPrimaryDisplay() 52 | const workArea = primaryDisplay.workAreaSize 53 | state.screenWidth = workArea.width 54 | state.screenHeight = workArea.height 55 | 56 | state.step = 60 57 | state.currentY = 50 58 | 59 | // Create the browser window. 60 | const windowSettings: Electron.BrowserWindowConstructorOptions = { 61 | width: 800, 62 | height: 600, 63 | minWidth: 750, 64 | minHeight: 550, 65 | x: state.currentX, 66 | y: state.currentY, 67 | alwaysOnTop: true, 68 | frame: false, 69 | transparent: true, 70 | fullscreenable: false, 71 | hasShadow: false, 72 | opacity: 1.0, 73 | backgroundColor: '#00000000', 74 | focusable: true, 75 | skipTaskbar: true, 76 | type: 'panel', 77 | paintWhenInitiallyHidden: true, 78 | titleBarStyle: 'hidden', 79 | enableLargerThanScreen: true, 80 | movable: true, 81 | show: false, 82 | autoHideMenuBar: true, 83 | ...(process.platform === 'linux' ? { icon } : {}), 84 | webPreferences: { 85 | preload: join(__dirname, '../preload/index.js'), 86 | sandbox: false, 87 | nodeIntegration: false, 88 | contextIsolation: true, 89 | scrollBounce: true 90 | } 91 | } 92 | 93 | state.mainWindow = new BrowserWindow(windowSettings) 94 | 95 | state.mainWindow.on('ready-to-show', () => { 96 | state.mainWindow?.show() 97 | }) 98 | 99 | state.mainWindow.webContents.setWindowOpenHandler((details) => { 100 | shell.openExternal(details.url) 101 | return { action: 'deny' } 102 | }) 103 | 104 | // HMR for renderer base on electron-vite cli. 105 | // Load the remote URL for development or the local html file for production. 106 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 107 | state.mainWindow?.loadURL(process.env['ELECTRON_RENDERER_URL']) 108 | } else { 109 | state.mainWindow?.loadFile(join(__dirname, '../renderer/index.html')) 110 | } 111 | 112 | state.mainWindow.webContents.setZoomFactor(1) 113 | //TODO: Comment this out when not in development 114 | // state.mainWindow.webContents.openDevTools() 115 | state.mainWindow.webContents.setWindowOpenHandler((details) => { 116 | shell.openExternal(details.url) 117 | return { action: 'deny' } 118 | }) 119 | 120 | state.mainWindow.setContentProtection(true) 121 | 122 | state.mainWindow.setVisibleOnAllWorkspaces(true, { 123 | visibleOnFullScreen: true 124 | }) 125 | state.mainWindow.setAlwaysOnTop(true, 'screen-saver', 1) 126 | 127 | if (process.platform === 'darwin') { 128 | state.mainWindow.setHiddenInMissionControl(true) 129 | state.mainWindow.setWindowButtonVisibility(false) 130 | state.mainWindow.setBackgroundColor('#00000000') 131 | 132 | state.mainWindow.setSkipTaskbar(true) 133 | state.mainWindow.setHasShadow(false) 134 | } 135 | 136 | state.mainWindow.on('close', () => { 137 | state.mainWindow = null 138 | state.isWindowVisible = false 139 | }) 140 | 141 | state.mainWindow.webContents.setBackgroundThrottling(false) 142 | state.mainWindow.webContents.setFrameRate(60) 143 | 144 | state.mainWindow.on('move', handleWindowMove) 145 | state.mainWindow.on('resize', handleWindowResize) 146 | state.mainWindow.on('closed', handleWindowClosed) 147 | 148 | const bounds = state.mainWindow.getBounds() 149 | state.windowPosition = { x: bounds.x, y: bounds.y } 150 | state.windowSize = { width: bounds.width, height: bounds.height } 151 | state.currentX = bounds.x 152 | state.currentY = bounds.y 153 | state.isWindowVisible = true 154 | 155 | const savedOpacity = configManager.getOpacity() 156 | console.log('savedOpacity', savedOpacity) 157 | 158 | state.mainWindow.showInactive() 159 | 160 | if (savedOpacity <= 0.1) { 161 | console.log('Initial opacity too low, setting to 0 and hiding window') 162 | state.mainWindow.setOpacity(0) 163 | state.isWindowVisible = false 164 | } else { 165 | console.log('Setting opacity to', savedOpacity) 166 | state.mainWindow.setOpacity(savedOpacity) 167 | state.isWindowVisible = true 168 | } 169 | } 170 | 171 | function getMainWindow(): BrowserWindow | null { 172 | return state.mainWindow 173 | } 174 | 175 | async function takeScreenshot(): Promise { 176 | if (!state.mainWindow) throw new Error('Main window not found') 177 | 178 | return ( 179 | state.screenshotManager?.takeScreenshot( 180 | () => hideMainWindow(), 181 | () => showMainWindow() 182 | ) || '' 183 | ) 184 | } 185 | 186 | async function getImagePreview(filePath: string): Promise { 187 | return state.screenshotManager?.getImagePreview(filePath) || '' 188 | } 189 | 190 | function setView(view: 'queue' | 'solutions' | 'debug'): void { 191 | state.view = view 192 | state.screenshotManager?.setView(view) 193 | } 194 | 195 | function getView(): 'queue' | 'solutions' | 'debug' { 196 | return state.view 197 | } 198 | 199 | function clearQueues(): void { 200 | state.screenshotManager?.clearQueues() 201 | state.problemInfo = null 202 | setView('queue') 203 | } 204 | 205 | function getScreenshotQueue(): string[] { 206 | return state.screenshotManager?.getScreenshotQueue() || [] 207 | } 208 | 209 | function getExtraScreenshotQueue(): string[] { 210 | return state.screenshotManager?.getExtraScreenshotQueue() || [] 211 | } 212 | 213 | async function deleteScreenshot(path: string): Promise<{ success: boolean; error?: string }> { 214 | return ( 215 | state.screenshotManager?.deleteScreenshot(path) || { 216 | success: false, 217 | error: 'Failed to delete screenshot' 218 | } 219 | ) 220 | } 221 | 222 | function handleWindowMove(): void { 223 | if (!state.mainWindow) return 224 | 225 | const bounds = state.mainWindow.getBounds() 226 | state.windowPosition = { x: bounds.x, y: bounds.y } 227 | state.currentX = bounds.x 228 | state.currentY = bounds.y 229 | } 230 | 231 | function handleWindowResize(): void { 232 | if (!state.mainWindow) return 233 | 234 | const bounds = state.mainWindow.getBounds() 235 | state.windowSize = { width: bounds.width, height: bounds.height } 236 | } 237 | 238 | function handleWindowClosed(): void { 239 | state.mainWindow = null 240 | state.isWindowVisible = false 241 | state.windowPosition = null 242 | state.windowSize = null 243 | } 244 | 245 | function moveWindowHorizontal(updateFn: (x: number) => number): void { 246 | if (!state.mainWindow) return 247 | state.currentX = updateFn(state.currentX) 248 | state.mainWindow.setPosition(Math.round(state.currentX), Math.round(state.currentY)) 249 | } 250 | 251 | function moveWindowVertical(updateFn: (y: number) => number): void { 252 | if (!state.mainWindow) return 253 | 254 | const newY = updateFn(state.currentY) 255 | 256 | const maxUpLimit = (-(state.windowSize?.height || 0) * 2) / 3 257 | const maxDownLimit = state.screenHeight + ((state.windowSize?.height || 0) * 2) / 3 258 | 259 | console.log({ 260 | newY, 261 | maxUpLimit, 262 | maxDownLimit, 263 | screenHeight: state.screenHeight, 264 | windowHeight: state.windowSize?.height, 265 | currentY: state.currentY 266 | }) 267 | 268 | if (newY >= maxUpLimit && newY <= maxDownLimit) { 269 | state.currentY = newY 270 | state.mainWindow.setPosition(Math.round(state.currentX), Math.round(state.currentY)) 271 | } 272 | } 273 | 274 | function hideMainWindow(): void { 275 | if (!state.mainWindow?.isDestroyed()) { 276 | const bounds = state.mainWindow?.getBounds() 277 | if (!bounds) return 278 | state.windowPosition = { x: bounds.x, y: bounds.y } 279 | state.windowSize = { width: bounds.width, height: bounds.height } 280 | state.mainWindow?.setIgnoreMouseEvents(true, { forward: true }) 281 | state.mainWindow?.setOpacity(0) 282 | state.isWindowVisible = false 283 | console.log('Hiding main window') 284 | } 285 | } 286 | 287 | function showMainWindow(): void { 288 | if (!state.mainWindow?.isDestroyed()) { 289 | if (state.windowPosition && state.windowSize) { 290 | state?.mainWindow?.setBounds({ 291 | ...state.windowPosition, 292 | ...state.windowSize 293 | }) 294 | } 295 | state.mainWindow?.setIgnoreMouseEvents(false) 296 | state.mainWindow?.setAlwaysOnTop(true, 'screen-saver', 1) 297 | state.mainWindow?.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) 298 | state.mainWindow?.setContentProtection(true) 299 | state.mainWindow?.setOpacity(0) 300 | state.mainWindow?.showInactive() 301 | state.mainWindow?.setOpacity(1) 302 | state.isWindowVisible = true 303 | console.log('Showing main window') 304 | } 305 | } 306 | 307 | function toggleMainWindow(): void { 308 | console.log('Toggling main window') 309 | if (state.isWindowVisible) { 310 | hideMainWindow() 311 | } else { 312 | showMainWindow() 313 | } 314 | } 315 | 316 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 317 | function getProblemInfo(): any { 318 | return state.problemInfo 319 | } 320 | 321 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 322 | function setProblemInfo(problemInfo: any): void { 323 | state.problemInfo = problemInfo 324 | } 325 | 326 | function getHasDebugged(): boolean { 327 | return state.hasDebugged 328 | } 329 | 330 | function setHasDebugged(hasDebugged: boolean): void { 331 | state.hasDebugged = hasDebugged 332 | } 333 | 334 | function getScreenshotManager(): ScreenshotManager | null { 335 | return state.screenshotManager 336 | } 337 | 338 | function initializeHelpers() { 339 | state.screenshotManager = new ScreenshotManager(state.view) 340 | state.processingManager = new ProcessingManager({ 341 | getView, 342 | setView, 343 | getProblemInfo, 344 | setProblemInfo, 345 | getScreenshotQueue, 346 | getExtraScreenshotQueue, 347 | clearQueues, 348 | takeScreenshot, 349 | getImagePreview, 350 | deleteScreenshot, 351 | setHasDebugged, 352 | getHasDebugged, 353 | getMainWindow, 354 | getScreenshotManager, 355 | PROCESSING_EVENTS: state.PROCESSING_EVENTS 356 | }) 357 | state.keyboardShortcutHelper = new KeyboardShortcutHelper({ 358 | moveWindowLeft: () => 359 | moveWindowHorizontal((x) => Math.max(-(state.windowSize?.width || 0) / 2, x - state.step)), 360 | moveWindowRight: () => 361 | moveWindowHorizontal((x) => 362 | Math.min(state.screenWidth - (state.windowSize?.width || 0) / 2, x + state.step) 363 | ), 364 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 365 | moveWindowDown: () => moveWindowVertical((y) => y + state.step), 366 | toggleMainWindow: toggleMainWindow, 367 | isVisible: () => state.isWindowVisible, 368 | getMainWindow: getMainWindow, 369 | takeScreenshot: takeScreenshot, 370 | getImagePreview: getImagePreview, 371 | clearQueues: clearQueues, 372 | setView: setView, 373 | processingManager: state.processingManager 374 | }) 375 | } 376 | 377 | function setWindowDimensions(width: number, height: number): void { 378 | if (!state.mainWindow?.isDestroyed()) { 379 | const [currentX, currentY] = state.mainWindow?.getPosition() || [0, 0] 380 | const primaryDisplay = screen.getPrimaryDisplay() 381 | const workArea = primaryDisplay.workAreaSize 382 | const maxWidth = Math.floor(workArea.width * 0.5) 383 | 384 | state.mainWindow?.setBounds({ 385 | x: Math.min(currentX, workArea.width - maxWidth), 386 | y: currentY, 387 | width: Math.min(width + 32, maxWidth), 388 | height: Math.ceil(height) 389 | }) 390 | } 391 | } 392 | 393 | async function initializeApp() { 394 | try { 395 | const appDataPath = path.join(app.getPath('appData'), 'silent-coder') 396 | const sessionPath = path.join(appDataPath, 'session') 397 | const tempPath = path.join(appDataPath, 'temp') 398 | const cachePath = path.join(appDataPath, 'cache') 399 | console.log('App data path:', appDataPath) 400 | 401 | for (const dir of [appDataPath, sessionPath, tempPath, cachePath]) { 402 | if (!fs.existsSync(dir)) { 403 | fs.mkdirSync(dir, { recursive: true }) 404 | } 405 | } 406 | 407 | app.setPath('userData', appDataPath) 408 | app.setPath('sessionData', sessionPath) 409 | app.setPath('temp', tempPath) 410 | app.setPath('cache', cachePath) 411 | 412 | initializeHelpers() 413 | initializeIpcHandler({ 414 | getView, 415 | getMainWindow, 416 | takeScreenshot, 417 | clearQueues, 418 | setView, 419 | moveWindowLeft: () => 420 | moveWindowHorizontal((x) => Math.max(-(state.windowSize?.width || 0) / 2, x - state.step)), 421 | moveWindowRight: () => 422 | moveWindowHorizontal((x) => 423 | Math.min(state.screenWidth - (state.windowSize?.width || 0) / 2, x + state.step) 424 | ), 425 | moveWindowUp: () => moveWindowVertical((y) => y - state.step), 426 | moveWindowDown: () => moveWindowVertical((y) => y + state.step), 427 | toggleMainWindow: toggleMainWindow, 428 | isVisible: () => state.isWindowVisible, 429 | getScreenshotQueue: getScreenshotQueue, 430 | getExtraScreenshotQueue: getExtraScreenshotQueue, 431 | deleteScreenshot: deleteScreenshot, 432 | getImagePreview: getImagePreview, 433 | PROCESSING_EVENTS: state.PROCESSING_EVENTS, 434 | processingManager: state.processingManager, 435 | setWindowDimensions: setWindowDimensions 436 | }) 437 | 438 | await createWindow() 439 | 440 | state.keyboardShortcutHelper?.registerGlobalShortcuts() 441 | } catch (error) { 442 | console.error('Failed to initialize app:', error) 443 | app.quit() 444 | } 445 | } 446 | 447 | // This method will be called when Electron has finished 448 | // initialization and is ready to create browser windows. 449 | // Some APIs can only be used after this event occurs. 450 | app.whenReady().then(initializeApp) 451 | 452 | // Quit when all windows are closed, except on macOS. There, it's common 453 | // for applications and their menu bar to stay active until the user quits 454 | // explicitly with Cmd + Q. 455 | app.on('window-all-closed', () => { 456 | if (process.platform !== 'darwin') { 457 | app.quit() 458 | } 459 | }) 460 | 461 | // In this file you can include the rest of your app's specific main process 462 | // code. You can also put them in separate files and require them here. 463 | -------------------------------------------------------------------------------- /src/main/lib/config-manager.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import { EventEmitter } from 'events' 3 | import path from 'path' 4 | import fs from 'fs' 5 | import OpenAI from 'openai' 6 | 7 | interface Config { 8 | apiKey: string 9 | apiProvider: 'openai' | 'gemini' 10 | extractionModel: string 11 | solutionModel: string 12 | debuggingModel: string 13 | language: string 14 | opacity: number 15 | } 16 | 17 | export class ConfigManager extends EventEmitter { 18 | private configPath: string 19 | private defaultConfig: Config = { 20 | apiKey: '', 21 | apiProvider: 'openai', 22 | extractionModel: 'gpt-4o-mini', 23 | solutionModel: 'gpt-4o-mini', 24 | debuggingModel: 'gpt-4o-mini', 25 | language: 'python', 26 | opacity: 1.0 27 | } 28 | 29 | constructor() { 30 | super() 31 | try { 32 | this.configPath = path.join(app.getPath('userData'), 'config.json') 33 | console.log('Config path:', this.configPath) 34 | } catch (error) { 35 | console.error('Error getting config path:', error) 36 | this.configPath = path.join(process.cwd(), 'config.json') 37 | } 38 | 39 | this.ensureConfigFileExists() 40 | } 41 | 42 | private ensureConfigFileExists(): void { 43 | try { 44 | if (!fs.existsSync(this.configPath)) { 45 | this.saveConfig(this.defaultConfig) 46 | console.log('Created default config file:', this.configPath) 47 | } 48 | } catch (error) { 49 | console.error('Failed to ensure config file exists:', error) 50 | } 51 | } 52 | 53 | public saveConfig(config: Config): void { 54 | try { 55 | const configDir = path.dirname(this.configPath) 56 | if (!fs.existsSync(configDir)) { 57 | fs.mkdirSync(configDir, { recursive: true }) 58 | } 59 | fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)) 60 | } catch (error) { 61 | console.error('Failed to save config:', error) 62 | } 63 | } 64 | 65 | private sanitizeModelSelection(model: string, provider: 'openai' | 'gemini') { 66 | if (provider === 'openai') { 67 | const allowedModels = ['gpt-4o-mini', 'gpt-4o'] 68 | if (!allowedModels.includes(model)) { 69 | console.log(`Invalid model: ${model} for provider: ${provider}. Defaulting to gpt-4o`) 70 | return 'gpt-4o' 71 | } 72 | return model 73 | } else if (provider === 'gemini') { 74 | const allowedModels = ['gemini-2.0-flash', 'gemini-1.5-pro'] 75 | if (!allowedModels.includes(model)) { 76 | console.log( 77 | `Invalid model: ${model} for provider: ${provider}. Defaulting to gemini-1.5-flash` 78 | ) 79 | return 'gemini-2.0-flash' 80 | } 81 | return model 82 | } 83 | return model 84 | } 85 | 86 | public loadConfig(): Config { 87 | try { 88 | if (fs.existsSync(this.configPath)) { 89 | const configData = fs.readFileSync(this.configPath, 'utf-8') 90 | const config = JSON.parse(configData) 91 | 92 | if (config.apiProvider !== 'openai' && config.apiProvider !== 'gemini') { 93 | console.log('Invalid API provider. Defaulting to openai') 94 | config.apiProvider = 'openai' 95 | } 96 | 97 | if (config.extractionModel) { 98 | config.extractionModel = this.sanitizeModelSelection( 99 | config.extractionModel, 100 | config.apiProvider 101 | ) 102 | } 103 | 104 | if (config.solutionModel) { 105 | config.solutionModel = this.sanitizeModelSelection( 106 | config.solutionModel, 107 | config.apiProvider 108 | ) 109 | } 110 | 111 | if (config.debuggingModel) { 112 | config.debuggingModel = this.sanitizeModelSelection( 113 | config.debuggingModel, 114 | config.apiProvider 115 | ) 116 | } 117 | 118 | return { 119 | ...this.defaultConfig, 120 | ...config 121 | } 122 | } 123 | 124 | this.saveConfig(this.defaultConfig) 125 | return this.defaultConfig 126 | } catch (error) { 127 | console.error('Failed to load config:', error) 128 | return this.defaultConfig 129 | } 130 | } 131 | 132 | public updateConfig(updates: Partial): Config { 133 | try { 134 | const currentConfig = this.loadConfig() 135 | let provider = updates.apiProvider || currentConfig.apiProvider 136 | 137 | if (updates.apiKey && !updates.apiProvider) { 138 | if (updates.apiKey.trim().startsWith('sk-')) { 139 | provider = 'openai' 140 | console.log('Detected OpenAI API key. Setting provider to openai') 141 | } else { 142 | provider = 'gemini' 143 | console.log('Detected Gemini API key. Setting provider to gemini') 144 | } 145 | } 146 | 147 | updates.apiProvider = provider 148 | 149 | if (updates.apiProvider && updates.apiProvider !== currentConfig.apiProvider) { 150 | if (updates.apiProvider === 'openai') { 151 | updates.extractionModel = 'gpt-4o' 152 | updates.solutionModel = 'gpt-4o' 153 | updates.debuggingModel = 'gpt-4o' 154 | } else { 155 | updates.extractionModel = 'gemini-2.0-flash' 156 | updates.solutionModel = 'gemini-2.0-flash' 157 | updates.debuggingModel = 'gemini-2.0-flash' 158 | } 159 | } 160 | 161 | if (updates.extractionModel) { 162 | updates.extractionModel = this.sanitizeModelSelection( 163 | updates.extractionModel, 164 | updates.apiProvider 165 | ) 166 | } 167 | if (updates.solutionModel) { 168 | updates.solutionModel = this.sanitizeModelSelection( 169 | updates.solutionModel, 170 | updates.apiProvider 171 | ) 172 | } 173 | if (updates.debuggingModel) { 174 | updates.debuggingModel = this.sanitizeModelSelection( 175 | updates.debuggingModel, 176 | updates.apiProvider 177 | ) 178 | } 179 | 180 | const newConfig = { 181 | ...currentConfig, 182 | ...updates 183 | } 184 | 185 | this.saveConfig(newConfig) 186 | 187 | if ( 188 | updates.apiKey !== undefined || 189 | updates.apiProvider !== undefined || 190 | updates.extractionModel !== undefined || 191 | updates.solutionModel !== undefined || 192 | updates.debuggingModel !== undefined || 193 | updates.language !== undefined 194 | ) { 195 | this.emit('config-updated', newConfig) 196 | } 197 | 198 | return newConfig 199 | } catch (error) { 200 | console.error('Failed to update config:', error) 201 | return this.defaultConfig 202 | } 203 | } 204 | 205 | public hasApiKey(): boolean { 206 | const config = this.loadConfig() 207 | return !!config.apiKey && config.apiKey.trim().length > 0 208 | } 209 | 210 | public isValidApiKeyFormat(apiKey: string, provider?: 'openai' | 'gemini'): boolean { 211 | if (!provider) { 212 | if (apiKey.trim().startsWith('sk-')) { 213 | provider = 'openai' 214 | } else { 215 | provider = 'gemini' 216 | } 217 | } 218 | 219 | if (provider === 'openai') { 220 | return /^sk-\w{48}$/.test(apiKey) 221 | } else if (provider === 'gemini') { 222 | return /^AIzaSyB.*$/.test(apiKey) 223 | } 224 | return false 225 | } 226 | 227 | public async testApiKey( 228 | apiKey: string, 229 | provider?: 'openai' | 'gemini' 230 | ): Promise<{ 231 | valid: boolean 232 | error?: string 233 | }> { 234 | if (!provider) { 235 | if (apiKey.trim().startsWith('sk-')) { 236 | provider = 'openai' 237 | } else { 238 | provider = 'gemini' 239 | } 240 | } 241 | 242 | if (provider === 'openai') { 243 | return this.testOpenAiKey(apiKey) 244 | } else if (provider === 'gemini') { 245 | return this.testGeminiKey() 246 | } 247 | 248 | return { valid: false, error: 'Invalid provider' } 249 | } 250 | 251 | private async testOpenAiKey(apiKey: string): Promise<{ 252 | valid: boolean 253 | error?: string 254 | }> { 255 | try { 256 | const openai = new OpenAI({ 257 | apiKey 258 | }) 259 | 260 | await openai.models.list() 261 | return { valid: true } 262 | } catch (error) { 263 | console.error('OpenAI API key test failed:', error) 264 | return { valid: false, error: 'Invalid API key' } 265 | } 266 | } 267 | 268 | private async testGeminiKey(): Promise<{ 269 | valid: boolean 270 | error?: string 271 | }> { 272 | try { 273 | return { valid: true } 274 | } catch (error) { 275 | console.error('Gemini API key test failed:', error) 276 | return { valid: false, error: 'Invalid API key' } 277 | } 278 | } 279 | 280 | public getOpacity(): number { 281 | const config = this.loadConfig() 282 | return config.opacity !== undefined ? config.opacity : 1.0 283 | } 284 | 285 | public setOpacity(opacity: number): void { 286 | const validOpacity = Math.min(1.0, Math.max(0.1, opacity)) 287 | this.updateConfig({ opacity: validOpacity }) 288 | } 289 | 290 | public getLanguage(): string { 291 | const config = this.loadConfig() 292 | return config.language !== undefined ? config.language : 'python' 293 | } 294 | 295 | public setLanguage(language: string): void { 296 | this.updateConfig({ language }) 297 | } 298 | } 299 | 300 | export const configManager = new ConfigManager() 301 | -------------------------------------------------------------------------------- /src/main/lib/ipc-handler.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain, shell } from 'electron' 2 | import { configManager } from './config-manager' 3 | import { state } from '../index' 4 | import { ProcessingManager } from './processing-manager' 5 | export interface IIPCHandler { 6 | getMainWindow: () => BrowserWindow | null 7 | takeScreenshot: () => Promise 8 | getImagePreview: (filePath: string) => Promise 9 | clearQueues: () => void 10 | setView: (view: 'queue' | 'solutions' | 'debug') => void 11 | getView: () => 'queue' | 'solutions' | 'debug' 12 | getScreenshotQueue: () => string[] 13 | getExtraScreenshotQueue: () => string[] 14 | moveWindowLeft: () => void 15 | moveWindowRight: () => void 16 | moveWindowUp: () => void 17 | moveWindowDown: () => void 18 | toggleMainWindow: () => void 19 | isVisible: () => boolean 20 | deleteScreenshot: (path: string) => Promise<{ success: boolean; error?: string }> 21 | PROCESSING_EVENTS: typeof state.PROCESSING_EVENTS 22 | processingManager: ProcessingManager | null 23 | setWindowDimensions: (width: number, height: number) => void 24 | } 25 | 26 | export function initializeIpcHandler(deps: IIPCHandler): void { 27 | ipcMain.handle('get-config', () => { 28 | return configManager.loadConfig() 29 | }) 30 | 31 | ipcMain.handle('update-config', (_event, updates) => { 32 | return configManager.updateConfig(updates) 33 | }) 34 | 35 | ipcMain.handle('check-api-key', () => { 36 | return configManager.hasApiKey() 37 | }) 38 | 39 | ipcMain.handle('validate-api-key', async (_event, apiKey) => { 40 | if (!configManager.isValidApiKeyFormat(apiKey)) { 41 | return { 42 | valid: false, 43 | error: 'Invalid API key format' 44 | } 45 | } 46 | 47 | const result = await configManager.testApiKey(apiKey) 48 | return result 49 | }) 50 | 51 | ipcMain.handle('get-screenshots', async () => { 52 | try { 53 | let previews: { path: string; preview: string }[] = [] 54 | const currentView = deps.getView() 55 | console.log('currentView', currentView) 56 | 57 | if (currentView === 'queue') { 58 | const queue = deps.getScreenshotQueue() 59 | previews = await Promise.all( 60 | queue.map(async (path) => { 61 | const preview = await deps.getImagePreview(path) 62 | return { path, preview } 63 | }) 64 | ) 65 | } else { 66 | const queue = deps.getExtraScreenshotQueue() 67 | previews = await Promise.all( 68 | queue.map(async (path) => { 69 | const preview = await deps.getImagePreview(path) 70 | return { path, preview } 71 | }) 72 | ) 73 | } 74 | 75 | return previews 76 | } catch (error) { 77 | console.error('Error getting screenshots:', error) 78 | throw error 79 | } 80 | }) 81 | ipcMain.handle('delete-screenshot', async (_, path: string) => { 82 | return deps.deleteScreenshot(path) 83 | }) 84 | ipcMain.handle('trigger-screenshot', async () => { 85 | const mainWindow = deps.getMainWindow() 86 | if (mainWindow) { 87 | try { 88 | const screenshotPath = await deps.takeScreenshot() 89 | const preview = await deps.getImagePreview(screenshotPath) 90 | mainWindow.webContents.send('screenshot-taken', { path: screenshotPath, preview }) 91 | return { success: true } 92 | } catch (error) { 93 | console.error('Error triggering screenshot:', error) 94 | return { success: false, error: 'Failed to take screenshot' } 95 | } 96 | } 97 | return { success: false, error: 'Main window not found' } 98 | }) 99 | ipcMain.handle('toggle-main-window', async () => { 100 | return deps.toggleMainWindow() 101 | }) 102 | ipcMain.handle('delete-last-screenshot', async () => { 103 | try { 104 | const queue = 105 | deps.getView() === 'queue' ? deps.getScreenshotQueue() : deps.getExtraScreenshotQueue() 106 | console.log('queue', queue) 107 | 108 | if (queue.length === 0) { 109 | return { success: false, error: 'No screenshots to delete' } 110 | } 111 | 112 | const lastScreenshot = queue[queue.length - 1] 113 | const result = await deps.deleteScreenshot(lastScreenshot) 114 | 115 | const mainWindow = deps.getMainWindow() 116 | if (mainWindow && !mainWindow.isDestroyed()) { 117 | mainWindow.webContents.send('screenshot-deleted') 118 | } 119 | 120 | return result 121 | } catch (error) { 122 | console.error('Error deleting last screenshot:', error) 123 | return { success: false, error: 'Failed to delete screenshot' } 124 | } 125 | }) 126 | ipcMain.handle('open-settings-portal', async () => { 127 | const mainWindow = deps.getMainWindow() 128 | if (mainWindow) { 129 | mainWindow.webContents.send('show-settings-dialog') 130 | return { success: true } 131 | } 132 | return { success: false, error: 'Main window not found' } 133 | }) 134 | ipcMain.handle('trigger-process-screenshots', async () => { 135 | try { 136 | if (!configManager.hasApiKey()) { 137 | const mainWindow = deps.getMainWindow() 138 | if (mainWindow) { 139 | mainWindow.webContents.send(deps.PROCESSING_EVENTS.API_KEY_INVALID) 140 | } 141 | return { success: false, error: 'No API key found' } 142 | } 143 | await deps.processingManager?.processScreenshots() 144 | return { success: true } 145 | } catch (error) { 146 | console.error('Error triggering process screenshots:', error) 147 | return { success: false, error: 'Failed to process screenshots' } 148 | } 149 | }) 150 | ipcMain.handle('trigger-reset', async () => { 151 | try { 152 | deps.processingManager?.cancelOngoingRequest() 153 | 154 | deps.clearQueues() 155 | deps.setView('queue') 156 | 157 | const mainWindow = deps.getMainWindow() 158 | if (mainWindow && !mainWindow.isDestroyed()) { 159 | mainWindow.webContents.send('reset-view') 160 | } 161 | return { success: true } 162 | } catch (error) { 163 | console.error('Error triggering reset:', error) 164 | return { success: false, error: 'Failed to reset' } 165 | } 166 | }) 167 | ipcMain.handle('set-window-dimensions', (_, width: number, height: number) => { 168 | return deps.setWindowDimensions(width, height) 169 | }) 170 | ipcMain.handle( 171 | 'update-content-dimensions', 172 | async (_, { width, height }: { width: number; height: number }) => { 173 | console.log('update-content-dimensions', width, height) 174 | if (width && height) { 175 | deps.setWindowDimensions(width, height) 176 | } 177 | } 178 | ) 179 | ipcMain.handle('openLink', (_, url: string) => { 180 | try { 181 | console.log('openLink', url) 182 | shell.openExternal(url) 183 | return { success: true } 184 | } catch (error) { 185 | console.error('Error opening link:', error) 186 | return { success: false, error: 'Failed to open link' } 187 | } 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /src/main/lib/keyboard-shortcut.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, globalShortcut, app } from 'electron' 2 | import { ProcessingManager } from './processing-manager' 3 | import { configManager } from './config-manager' 4 | export interface IKeyboardShortcutHelper { 5 | moveWindowLeft: () => void 6 | moveWindowRight: () => void 7 | moveWindowUp: () => void 8 | moveWindowDown: () => void 9 | toggleMainWindow: () => void 10 | isVisible: () => boolean 11 | getMainWindow: () => BrowserWindow | null 12 | takeScreenshot: () => Promise 13 | getImagePreview: (filePath: string) => Promise 14 | clearQueues: () => void 15 | setView: (view: 'queue' | 'solutions' | 'debug') => void 16 | processingManager: ProcessingManager | null 17 | } 18 | 19 | export class KeyboardShortcutHelper { 20 | private deps: IKeyboardShortcutHelper 21 | 22 | constructor(deps: IKeyboardShortcutHelper) { 23 | this.deps = deps 24 | } 25 | 26 | private adjustOpacity(delta: number): void { 27 | const mainWindow = this.deps.getMainWindow() 28 | if (!mainWindow) return 29 | 30 | const currentOpacity = mainWindow.getOpacity() 31 | const newOpacity = Math.max(0.1, Math.min(1, currentOpacity + delta)) 32 | console.log('adjusting opacity', currentOpacity, newOpacity) 33 | mainWindow.setOpacity(newOpacity) 34 | 35 | try { 36 | const config = configManager.loadConfig() 37 | config.opacity = newOpacity 38 | configManager.saveConfig(config) 39 | } catch (error) { 40 | console.error('Failed to save config:', error) 41 | } 42 | } 43 | 44 | public registerGlobalShortcuts(): void { 45 | globalShortcut.register('CommandOrControl+Left', () => { 46 | console.log('moveWindowLeft') 47 | this.deps.moveWindowLeft() 48 | }) 49 | globalShortcut.register('CommandOrControl+Right', () => { 50 | console.log('moveWindowRight') 51 | this.deps.moveWindowRight() 52 | }) 53 | globalShortcut.register('CommandOrControl+Up', () => { 54 | console.log('moveWindowUp') 55 | this.deps.moveWindowUp() 56 | }) 57 | globalShortcut.register('CommandOrControl+Down', () => { 58 | console.log('moveWindowDown') 59 | this.deps.moveWindowDown() 60 | }) 61 | globalShortcut.register('CommandOrControl+B', () => { 62 | console.log('toggleMainWindow') 63 | this.deps.toggleMainWindow() 64 | }) 65 | globalShortcut.register('CommandOrControl+H', async () => { 66 | const mainWindow = this.deps.getMainWindow() 67 | if (mainWindow) { 68 | console.log('taking screenshot') 69 | try { 70 | const screenshotPath = await this.deps.takeScreenshot() 71 | const preview = await this.deps.getImagePreview(screenshotPath) 72 | console.log('screenshot taken', screenshotPath, preview) 73 | mainWindow.webContents.send('screenshot-taken', { 74 | path: screenshotPath, 75 | preview 76 | }) 77 | } catch (error) { 78 | console.error('Failed to take screenshot:', error) 79 | } 80 | } 81 | }) 82 | globalShortcut.register('CommandOrControl+L', async () => { 83 | console.log('deleteLastScreenshot') 84 | const mainWindow = this.deps.getMainWindow() 85 | if (mainWindow) { 86 | mainWindow.webContents.send('screenshot-deleted') 87 | } 88 | }) 89 | globalShortcut.register('CommandOrControl+Enter', async () => { 90 | await this.deps.processingManager?.processScreenshots() 91 | }) 92 | globalShortcut.register('CommandOrControl+[', () => { 93 | console.log('decreaseOpacity') 94 | this.adjustOpacity(-0.1) 95 | }) 96 | globalShortcut.register('CommandOrControl+]', () => { 97 | console.log('increaseOpacity') 98 | this.adjustOpacity(0.1) 99 | }) 100 | globalShortcut.register('CommandOrControl+-', () => { 101 | console.log('zoom out') 102 | const mainWindow = this.deps.getMainWindow() 103 | if (mainWindow) { 104 | const currentZoom = mainWindow.webContents.getZoomLevel() 105 | mainWindow.webContents.setZoomLevel(currentZoom - 0.5) 106 | } 107 | }) 108 | globalShortcut.register('CommandOrControl+=', () => { 109 | console.log('zoom in') 110 | const mainWindow = this.deps.getMainWindow() 111 | if (mainWindow) { 112 | const currentZoom = mainWindow.webContents.getZoomLevel() 113 | mainWindow.webContents.setZoomLevel(currentZoom + 0.5) 114 | } 115 | }) 116 | globalShortcut.register('CommandOrControl+0', () => { 117 | console.log('resetZoom') 118 | const mainWindow = this.deps.getMainWindow() 119 | if (mainWindow) { 120 | mainWindow.webContents.setZoomLevel(1) 121 | } 122 | }) 123 | globalShortcut.register('CommandOrControl+Q', () => { 124 | console.log('quit') 125 | app.quit() 126 | }) 127 | globalShortcut.register('CommandOrControl+R', () => { 128 | console.log('Cancel ongoing request') 129 | this.deps.processingManager?.cancelOngoingRequest() 130 | 131 | this.deps.clearQueues() 132 | this.deps.setView('queue') 133 | 134 | const mainWindow = this.deps.getMainWindow() 135 | if (mainWindow && !mainWindow.isDestroyed()) { 136 | mainWindow.webContents.send('reset-view') 137 | } 138 | }) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/lib/screenshot-manager.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import path from 'path' 3 | import fs from 'fs' 4 | import screenshot from 'screenshot-desktop' 5 | import { v4 as uuidv4 } from 'uuid' 6 | import { execFile } from 'child_process' 7 | import { promisify } from 'util' 8 | 9 | const execFileAsync = promisify(execFile) 10 | 11 | export class ScreenshotManager { 12 | private screenshotQueue: string[] = [] 13 | private extraScreenshotQueue: string[] = [] 14 | private readonly MAX_SCREENSHOTS = 5 15 | 16 | private readonly screenshotDir: string 17 | private readonly extraScreenshotDir: string 18 | private readonly tempDir: string 19 | 20 | private view: 'queue' | 'solutions' | 'debug' = 'queue' 21 | 22 | constructor(view: 'queue' | 'solutions' | 'debug' = 'queue') { 23 | this.view = view 24 | 25 | this.screenshotDir = path.join(app.getPath('userData'), 'screenshots') 26 | this.extraScreenshotDir = path.join(app.getPath('userData'), 'extra_screenshots') 27 | this.tempDir = path.join(app.getPath('temp'), 'silent-coder-screenshots') 28 | 29 | this.ensureDirectoriesExist() 30 | this.cleanScreenshotDirectories() 31 | } 32 | 33 | private ensureDirectoriesExist(): void { 34 | const directories = [this.screenshotDir, this.extraScreenshotDir, this.tempDir] 35 | for (const dir of directories) { 36 | if (!fs.existsSync(dir)) { 37 | try { 38 | fs.mkdirSync(dir, { recursive: true }) 39 | console.log(`Created directory: ${dir}`) 40 | } catch (error) { 41 | console.error(`Failed to create directory ${dir}:`, error) 42 | } 43 | } 44 | } 45 | } 46 | 47 | private cleanScreenshotDirectories(): void { 48 | try { 49 | if (fs.existsSync(this.screenshotDir)) { 50 | const files = fs 51 | .readdirSync(this.screenshotDir) 52 | .filter((file) => file.endsWith('.png')) 53 | .map((file) => path.join(this.screenshotDir, file)) 54 | 55 | for (const file of files) { 56 | try { 57 | fs.unlinkSync(file) 58 | console.log(`Deleted screenshot file: ${file}`) 59 | } catch (error) { 60 | console.error(`Failed to delete file ${file}:`, error) 61 | } 62 | } 63 | } 64 | 65 | if (fs.existsSync(this.extraScreenshotDir)) { 66 | const files = fs 67 | .readdirSync(this.extraScreenshotDir) 68 | .filter((file) => file.endsWith('.png')) 69 | .map((file) => path.join(this.extraScreenshotDir, file)) 70 | 71 | for (const file of files) { 72 | try { 73 | fs.unlinkSync(file) 74 | console.log(`Deleted extra screenshot file: ${file}`) 75 | } catch (error) { 76 | console.error(`Failed to delete file ${file}:`, error) 77 | } 78 | } 79 | } 80 | 81 | console.log('Screenshot directories cleaned successfully') 82 | } catch (error) { 83 | console.error('Failed to clean screenshot directories:', error) 84 | } 85 | } 86 | 87 | public getView(): 'queue' | 'solutions' | 'debug' { 88 | return this.view 89 | } 90 | 91 | public setView(view: 'queue' | 'solutions' | 'debug'): void { 92 | this.view = view 93 | } 94 | 95 | public getScreenshotQueue(): string[] { 96 | return this.screenshotQueue 97 | } 98 | 99 | public getExtraScreenshotQueue(): string[] { 100 | return this.extraScreenshotQueue 101 | } 102 | 103 | public clearQueues(): void { 104 | this.screenshotQueue.forEach((file) => { 105 | try { 106 | fs.unlinkSync(file) 107 | console.log(`Deleted screenshot file: ${file}`) 108 | } catch (error) { 109 | console.error(`Failed to delete file ${file}:`, error) 110 | } 111 | }) 112 | this.screenshotQueue = [] 113 | 114 | this.extraScreenshotQueue.forEach((file) => { 115 | try { 116 | fs.unlinkSync(file) 117 | console.log(`Deleted extra screenshot file: ${file}`) 118 | } catch (error) { 119 | console.error(`Failed to delete file ${file}:`, error) 120 | } 121 | }) 122 | this.extraScreenshotQueue = [] 123 | 124 | console.log('Screenshot queues cleared successfully') 125 | } 126 | 127 | private async captureScreenshot(): Promise { 128 | try { 129 | console.log('Starting screenshot capture...') 130 | 131 | if (process.platform === 'win32') { 132 | return await this.captureWindowsScreenshot() 133 | } 134 | 135 | console.log('Taking screenshot on non-Windows platform...') 136 | const buffer = await screenshot({ 137 | format: 'png' 138 | }) 139 | 140 | console.log('Screenshot captured successfully') 141 | return buffer 142 | } catch (error) { 143 | console.error('Failed to capture screenshot:', error) 144 | throw error 145 | } 146 | } 147 | 148 | private async captureWindowsScreenshot(): Promise { 149 | try { 150 | console.log('Starting Windows screenshot capture...') 151 | 152 | const tempFilePath = path.join(this.tempDir, `temp-${uuidv4()}.png`) 153 | await screenshot({ 154 | path: tempFilePath 155 | }) 156 | 157 | if (fs.existsSync(tempFilePath)) { 158 | const buffer = await fs.promises.readFile(tempFilePath) 159 | 160 | try { 161 | await fs.promises.unlink(tempFilePath) 162 | } catch (error) { 163 | console.error('Failed to delete temp file:', error) 164 | } 165 | 166 | console.log('Screenshot captured successfully') 167 | return buffer 168 | } else { 169 | console.error('Failed to capture screenshot: Temp file not found') 170 | throw new Error('Failed to capture screenshot') 171 | } 172 | } catch (error) { 173 | console.error('Failed to capture Windows screenshot:', error) 174 | 175 | try { 176 | console.log('Trying powrshell method') 177 | const tempFilePath = path.join(this.tempDir, `temp-${uuidv4()}.png`) 178 | 179 | const psScript = ` 180 | Add-Type -AssemblyName System.Windows.Forms,System.Drawing 181 | $screens = [System.Windows.Forms.Screen]::AllScreens 182 | $top = ($screens | ForEach-Object {$_.Bounds.Top} | Measure-Object -Minimum).Minimum 183 | $left = ($screens | ForEach-Object {$_.Bounds.Left} | Measure-Object -Minimum).Minimum 184 | $width = ($screens | ForEach-Object {$_.Bounds.Right} | Measure-Object -Maximum).Maximum 185 | $height = ($screens | ForEach-Object {$_.Bounds.Bottom} | Measure-Object -Maximum).Maximum 186 | $bounds = [System.Drawing.Rectangle]::FromLTRB($left, $top, $width, $height) 187 | $bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height 188 | $graphics = [System.Drawing.Graphics]::FromImage($bmp) 189 | $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size) 190 | $bmp.Save('${tempFilePath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png) 191 | $graphics.Dispose() 192 | $bmp.Dispose() 193 | ` 194 | 195 | await execFileAsync('powershell', [ 196 | '-NoProfile', 197 | '-ExecutionPolicy', 198 | 'Bypass', 199 | '-Command', 200 | psScript 201 | ]) 202 | 203 | if (fs.existsSync(tempFilePath)) { 204 | const buffer = await fs.promises.readFile(tempFilePath) 205 | 206 | try { 207 | await fs.promises.unlink(tempFilePath) 208 | } catch (error) { 209 | console.error('Failed to delete temp file:', error) 210 | } 211 | 212 | console.log('Screenshot captured successfully') 213 | return buffer 214 | } else { 215 | console.error('Failed to capture screenshot: Temp file not found') 216 | throw new Error('Failed to capture screenshot') 217 | } 218 | } catch (error) { 219 | console.error('Failed to capture screenshot using PowerShell:', error) 220 | throw error 221 | } 222 | } 223 | } 224 | 225 | public async takeScreenshot( 226 | hideMainWindow: () => void, 227 | showMainWindow: () => void 228 | ): Promise { 229 | console.log('Taking screenshot in view', this.view) 230 | hideMainWindow() 231 | 232 | const hideDelay = process.platform === 'win32' ? 500 : 300 233 | await new Promise((resolve) => setTimeout(resolve, hideDelay)) 234 | 235 | let screenshotPath = '' 236 | try { 237 | const screenshotBuffer = await this.captureScreenshot() 238 | 239 | if (!screenshotBuffer || screenshotBuffer.length === 0) { 240 | throw new Error('Failed to capture screenshot') 241 | } 242 | 243 | if (this.view === 'queue') { 244 | screenshotPath = path.join(this.screenshotDir, `screenshot-${uuidv4()}.png`) 245 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 246 | console.log(`Screenshot saved to ${screenshotPath}`) 247 | this.screenshotQueue.push(screenshotPath) 248 | if (this.screenshotQueue.length > this.MAX_SCREENSHOTS) { 249 | const removedPath = this.screenshotQueue.shift() 250 | if (removedPath) { 251 | try { 252 | await fs.promises.unlink(removedPath) 253 | console.log(`Deleted old screenshot file: ${removedPath}`) 254 | } catch (error) { 255 | console.error(`Failed to delete file ${removedPath}:`, error) 256 | } 257 | } 258 | } 259 | } else { 260 | // solutions view 261 | screenshotPath = path.join(this.extraScreenshotDir, `screenshot-${uuidv4()}.png`) 262 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 263 | console.log(`Screenshot saved to ${screenshotPath}`) 264 | this.extraScreenshotQueue.push(screenshotPath) 265 | if (this.extraScreenshotQueue.length > this.MAX_SCREENSHOTS) { 266 | const removedPath = this.extraScreenshotQueue.shift() 267 | if (removedPath) { 268 | try { 269 | await fs.promises.unlink(removedPath) 270 | console.log(`Deleted old screenshot file: ${removedPath}`) 271 | } catch (error) { 272 | console.error(`Failed to delete file ${removedPath}:`, error) 273 | } 274 | } 275 | } 276 | } 277 | } catch (error) { 278 | console.error('Failed to take screenshot:', error) 279 | throw error 280 | } finally { 281 | await new Promise((resolve) => setTimeout(resolve, 200)) 282 | showMainWindow() 283 | } 284 | return screenshotPath 285 | } 286 | 287 | public async getImagePreview(filePath: string): Promise { 288 | try { 289 | if (!fs.existsSync(filePath)) { 290 | console.error(`File does not exist: ${filePath}`) 291 | return '' 292 | } 293 | 294 | const data = await fs.promises.readFile(filePath) 295 | const base64 = data.toString('base64') 296 | return `data:image/png;base64,${base64}` 297 | } catch (error) { 298 | console.error('Failed to get image preview:', error) 299 | return '' 300 | } 301 | } 302 | 303 | public async deleteScreenshot(path: string): Promise<{ success: boolean; error?: string }> { 304 | try { 305 | if (fs.existsSync(path)) { 306 | await fs.promises.unlink(path) 307 | } 308 | if (this.view === 'queue') { 309 | this.screenshotQueue = this.screenshotQueue.filter((p) => p !== path) 310 | } else if (this.view === 'solutions') { 311 | this.extraScreenshotQueue = this.extraScreenshotQueue.filter((p) => p !== path) 312 | } 313 | return { success: true } 314 | } catch (error) { 315 | console.error('Failed to delete screenshot:', error) 316 | return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } 317 | } 318 | } 319 | 320 | public clearExtraScreenshotQueue(): void { 321 | this.extraScreenshotQueue.forEach((path) => { 322 | if (fs.existsSync(path)) { 323 | fs.unlink(path, (err) => { 324 | if (err) { 325 | console.error(`Failed to delete extra screenshot file: ${path}`, err) 326 | } 327 | }) 328 | } 329 | }) 330 | 331 | this.extraScreenshotQueue = [] 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface ElectronAPI { 2 | getConfig: () => Promise<{ 3 | apiKey?: string 4 | apiProvider?: 'openai' | 'gemini' 5 | extractionModel?: string 6 | solutionModel?: string 7 | debuggingModel?: string 8 | language?: string 9 | }> 10 | updateConfig: (config: { 11 | apiKey?: string 12 | apiProvider?: 'openai' | 'gemini' 13 | extractionModel?: string 14 | solutionModel?: string 15 | debuggingModel?: string 16 | language?: string 17 | }) => Promise 18 | checkApiKey: () => Promise 19 | validateApiKey: (apiKey: string) => Promise<{ 20 | valid: boolean 21 | error?: string 22 | }> 23 | onApiKeyInvalid: (callback: () => void) => () => void 24 | getScreenshots: () => Promise<{ path: string; preview: string }[]> 25 | deleteScreenshot: (path: string) => Promise<{ success: boolean; error?: string }> 26 | onScreenshotTaken: (callback: (data: { path: string; preview: string }) => void) => () => void 27 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 28 | getPlatform: () => string 29 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 30 | deleteLastScreenshot: () => Promise<{ success: boolean; error?: string }> 31 | openSettingsPortal: () => Promise<{ success: boolean; error?: string }> 32 | onDeleteLastScreenshot: (callback: () => void) => () => void 33 | onShowSettings: (callback: () => void) => () => void 34 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 35 | onSolutionStart: (callback: () => void) => () => void 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | onSolutionSuccess: (callback: (data: any) => void) => () => void 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | onProblemExtracted: (callback: (data: any) => void) => () => void 40 | onResetView: (callback: () => void) => () => void 41 | triggerReset: () => Promise<{ success: boolean; error?: string }> 42 | onDebugStart: (callback: () => void) => () => void 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | onDebugSuccess: (callback: (data: any) => void) => () => void 45 | onDebugError: (callback: (error: string) => void) => () => void 46 | onProcessingNoScreenshots: (callback: () => void) => () => void 47 | onSolutionError: (callback: (error: string) => void) => () => void 48 | updateContentDimensions: (dimensions: { width: number; height: number }) => void 49 | openLink: (url: string) => Promise<{ success: boolean; error?: string }> 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | removeListener: (eventName: string, callback: (...args: any[]) => void) => void 52 | } 53 | 54 | declare global { 55 | interface Window { 56 | electronAPI: ElectronAPI 57 | __LANGUAGE__: string 58 | __IS_INITIALIZED__: boolean 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | export const PROCESSING_EVENTS = { 4 | API_KEY_INVALID: 'api-key-invalid', 5 | NO_SCREENSHOTS: 'no-screenshots', 6 | INITIAL_SOLUTION_ERROR: 'initial-solution-error', 7 | SOLUTION_SUCCESS: 'solution-success', 8 | PROBLEM_EXTRACTED: 'problem-extracted', 9 | INITIAL_START: 'initial-start', 10 | RESET: 'reset', 11 | 12 | DEBUG_START: 'debug-start', 13 | DEBUG_SUCCESS: 'debug-success', 14 | DEBUG_ERROR: 'debug-error' 15 | } 16 | 17 | const electronAPI = { 18 | getConfig: () => ipcRenderer.invoke('get-config'), 19 | updateConfig: (config: { 20 | apiKey?: string 21 | apiProvider?: 'openai' | 'gemini' 22 | extractionModel?: string 23 | solutionModel?: string 24 | debuggingModel?: string 25 | language?: string 26 | }) => ipcRenderer.invoke('update-config', config), 27 | checkApiKey: () => ipcRenderer.invoke('check-api-key'), 28 | validateApiKey: (apiKey: string) => ipcRenderer.invoke('validate-api-key', apiKey), 29 | getScreenshots: () => ipcRenderer.invoke('get-screenshots'), 30 | deleteScreenshot: (path: string) => ipcRenderer.invoke('delete-screenshot', path), 31 | toggleMainWindow: async () => { 32 | console.log('toggleMainWindow called from preload') 33 | try { 34 | const result = await ipcRenderer.invoke('toggle-main-window') 35 | return result 36 | } catch (error) { 37 | console.error('Error toggling main window:', error) 38 | throw error 39 | } 40 | }, 41 | onScreenshotTaken: (callback: (data: { path: string; preview: string }) => void) => { 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | const subscription = (_: any, data: { path: string; preview: string }) => callback(data) 44 | ipcRenderer.on('screenshot-taken', subscription) 45 | return () => ipcRenderer.removeListener('screenshot-taken', subscription) 46 | }, 47 | getPlatform: () => process.platform, 48 | triggerScreenshot: () => ipcRenderer.invoke('trigger-screenshot'), 49 | onDeleteLastScreenshot: (callback: () => void) => { 50 | const subscription = () => callback() 51 | ipcRenderer.on('screenshot-deleted', subscription) 52 | return () => ipcRenderer.removeListener('screenshot-deleted', subscription) 53 | }, 54 | deleteLastScreenshot: () => ipcRenderer.invoke('delete-last-screenshot'), 55 | openSettingsPortal: () => ipcRenderer.invoke('open-settings-portal'), 56 | onShowSettings: (callback: () => void) => { 57 | const subscription = () => callback() 58 | ipcRenderer.on('show-settings-dialog', subscription) 59 | return () => ipcRenderer.removeListener('show-settings-dialog', subscription) 60 | }, 61 | triggerProcessScreenshots: () => ipcRenderer.invoke('trigger-process-screenshots'), 62 | onSolutionStart: (callback: () => void) => { 63 | const subscription = () => callback() 64 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 65 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 66 | }, 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | onSolutionSuccess: (callback: (data: any) => void) => { 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | const subscription = (_: any, data: any) => callback(data) 71 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 72 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 73 | }, 74 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 75 | onProblemExtracted: (callback: (data: any) => void) => { 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | const subscription = (_: any, data: any) => callback(data) 78 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 79 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 80 | }, 81 | onResetView: (callback: () => void) => { 82 | const subscription = () => callback() 83 | ipcRenderer.on('reset-view', subscription) 84 | return () => ipcRenderer.removeListener('reset-view', subscription) 85 | }, 86 | triggerReset: () => ipcRenderer.invoke('trigger-reset'), 87 | onDebugStart: (callback: () => void) => { 88 | const subscription = () => callback() 89 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 90 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 91 | }, 92 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 93 | onDebugSuccess: (callback: (data: any) => void) => { 94 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 95 | const subscription = (_: any, data: any) => callback(data) 96 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_SUCCESS, subscription) 97 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_SUCCESS, subscription) 98 | }, 99 | onDebugError: (callback: (error: string) => void) => { 100 | const subscription = (_, error: string) => callback(error) 101 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 102 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 103 | }, 104 | onProcessingNoScreenshots: (callback: () => void) => { 105 | const subscription = () => callback() 106 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 107 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 108 | }, 109 | onSolutionError: (callback: (error: string) => void) => { 110 | const subscription = (_, error: string) => callback(error) 111 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 112 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 113 | }, 114 | updateContentDimensions: (dimensions: { width: number; height: number }) => 115 | ipcRenderer.invoke('update-content-dimensions', dimensions), 116 | openLink: (url: string) => ipcRenderer.invoke('openLink', url), 117 | onApiKeyInvalid: (callback: () => void) => { 118 | const subscription = () => callback() 119 | ipcRenderer.on(PROCESSING_EVENTS.API_KEY_INVALID, subscription) 120 | return () => ipcRenderer.removeListener(PROCESSING_EVENTS.API_KEY_INVALID, subscription) 121 | }, 122 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 123 | removeListener: (eventName: string, callback: (...args: any[]) => void) => { 124 | ipcRenderer.removeListener(eventName, callback) 125 | } 126 | } 127 | 128 | contextBridge.exposeInMainWorld('electronAPI', electronAPI) 129 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron 6 | 7 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryProvider } from './providers/query-provider' 2 | import { WelcomeScreen } from './components/welcome-screen' 3 | import { useCallback, useEffect, useState } from 'react' 4 | import { SettingsDialog } from './components/settings-dialog' 5 | import MainApp from './components/main-app' 6 | import { ToastContext } from '@renderer/providers/toast-context' 7 | import { 8 | Toast, 9 | ToastProvider, 10 | ToastViewport, 11 | ToastDescription, 12 | ToastTitle 13 | } from './components/ui/toast' 14 | 15 | interface AppConfig { 16 | apiKey?: string 17 | apiProvider?: 'openai' | 'gemini' 18 | extractionModel?: string 19 | solutionModel?: string 20 | debuggingModel?: string 21 | language?: string 22 | } 23 | 24 | function App(): React.JSX.Element { 25 | const [isSettingsOpen, setIsSettingsOpen] = useState(false) 26 | const [isInitialized, setIsInitialized] = useState(false) 27 | const [currentLanguage, setCurrentLanguage] = useState('python') 28 | const [hasApiKey, setHasApiKey] = useState(false) 29 | const [_, setApiKeyDialogOpen] = useState(false) 30 | const [toastState, setToastState] = useState({ 31 | open: false, 32 | title: '', 33 | description: '', 34 | variant: 'neutral' as 'neutral' | 'success' | 'error' 35 | }) 36 | 37 | const handleOpenSettings = useCallback(() => { 38 | console.log('open settings') 39 | setIsSettingsOpen(true) 40 | console.log('isSettingsOpen', isSettingsOpen) 41 | }, []) 42 | 43 | const handleCloseSettings = useCallback((open: boolean) => { 44 | setIsSettingsOpen(open) 45 | }, []) 46 | 47 | const markInitialized = useCallback(() => { 48 | setIsInitialized(true) 49 | window.__IS_INITIALIZED__ = true 50 | }, []) 51 | 52 | const showToast = useCallback( 53 | (title: string, description: string, variant: 'neutral' | 'success' | 'error') => { 54 | setToastState({ 55 | open: true, 56 | title, 57 | description, 58 | variant 59 | }) 60 | }, 61 | [] 62 | ) 63 | 64 | useEffect(() => { 65 | const checkApiKey = async () => { 66 | try { 67 | const hasKey = await window.electronAPI.checkApiKey() 68 | setHasApiKey(hasKey) 69 | 70 | if (!hasKey) { 71 | setTimeout(() => { 72 | setIsSettingsOpen(true) 73 | }, 1000) 74 | } 75 | } catch (error) { 76 | console.error('Error checking API key:', error) 77 | showToast('Error checking API key', 'Please check your API key', 'error') 78 | } 79 | } 80 | 81 | if (isInitialized) { 82 | checkApiKey() 83 | } 84 | }, [isInitialized]) 85 | 86 | useEffect(() => { 87 | const initializeApp = async () => { 88 | try { 89 | const config = (await window.electronAPI.getConfig()) as AppConfig 90 | 91 | if (config?.language) { 92 | setCurrentLanguage(config.language) 93 | } 94 | 95 | markInitialized() 96 | } catch (error) { 97 | console.error('Error initializing app:', error) 98 | markInitialized() 99 | } 100 | } 101 | initializeApp() 102 | 103 | const onApiKeyInvalid = () => { 104 | showToast('API key invalid', 'Please check your API key', 'error') 105 | setApiKeyDialogOpen(true) 106 | } 107 | 108 | window.electronAPI.onApiKeyInvalid(onApiKeyInvalid) 109 | 110 | return () => { 111 | window.electronAPI.removeListener('API_KEY_INVALID', onApiKeyInvalid) 112 | window.__IS_INITIALIZED__ = false 113 | setIsInitialized(false) 114 | } 115 | }, [markInitialized, showToast]) 116 | 117 | const handleLanguageChange = useCallback((language: string) => { 118 | setCurrentLanguage(language) 119 | window.__LANGUAGE__ = language 120 | }, []) 121 | 122 | useEffect(() => { 123 | const unsubscribeSettings = window.electronAPI.onShowSettings(() => { 124 | setIsSettingsOpen(true) 125 | }) 126 | 127 | return () => { 128 | unsubscribeSettings() 129 | } 130 | }, []) 131 | 132 | return ( 133 | 134 | 135 | 136 |
137 | {isInitialized ? ( 138 | hasApiKey ? ( 139 | 140 | ) : ( 141 | 142 | ) 143 | ) : ( 144 |
145 |
146 |
147 |

Initializing...

148 |
149 |
150 | )} 151 |
152 | 153 | setToastState((prev) => ({ ...prev, open }))} 156 | variant={toastState.variant} 157 | duration={1500} 158 | > 159 | {toastState.title} 160 | {toastState.description} 161 | 162 | 163 |
164 |
165 |
166 | ) 167 | } 168 | 169 | export default App 170 | -------------------------------------------------------------------------------- /src/renderer/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ev-c-white: #ffffff; 3 | --ev-c-white-soft: #f8f8f8; 4 | --ev-c-white-mute: #f2f2f2; 5 | 6 | --ev-c-black: #1b1b1f; 7 | --ev-c-black-soft: #222222; 8 | --ev-c-black-mute: #282828; 9 | 10 | --ev-c-gray-1: #515c67; 11 | --ev-c-gray-2: #414853; 12 | --ev-c-gray-3: #32363f; 13 | 14 | --ev-c-text-1: rgba(255, 255, 245, 0.86); 15 | --ev-c-text-2: rgba(235, 235, 245, 0.6); 16 | --ev-c-text-3: rgba(235, 235, 245, 0.38); 17 | 18 | --ev-button-alt-border: transparent; 19 | --ev-button-alt-text: var(--ev-c-text-1); 20 | --ev-button-alt-bg: var(--ev-c-gray-3); 21 | --ev-button-alt-hover-border: transparent; 22 | --ev-button-alt-hover-text: var(--ev-c-text-1); 23 | --ev-button-alt-hover-bg: var(--ev-c-gray-2); 24 | } 25 | 26 | :root { 27 | --color-background: var(--ev-c-black); 28 | --color-background-soft: var(--ev-c-black-soft); 29 | --color-background-mute: var(--ev-c-black-mute); 30 | 31 | --color-text: var(--ev-c-text-1); 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: border-box; 38 | margin: 0; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | body { 47 | min-height: 100vh; 48 | color: var(--color-text); 49 | background: var(--color-background); 50 | line-height: 1.6; 51 | font-family: 52 | Inter, 53 | -apple-system, 54 | BlinkMacSystemFont, 55 | 'Segoe UI', 56 | Roboto, 57 | Oxygen, 58 | Ubuntu, 59 | Cantarell, 60 | 'Fira Sans', 61 | 'Droid Sans', 62 | 'Helvetica Neue', 63 | sans-serif; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/assets/electron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | ::-webkit-scrollbar { 4 | width: 8px; 5 | height: 8px; 6 | } 7 | 8 | ::-webkit-scrollbar-track { 9 | background: rgba(255, 255, 255, 0.05); 10 | border-radius: 4px; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | background: rgba(255, 255, 255, 0.2); 15 | border-radius: 4px; 16 | } 17 | 18 | ::-webkit-scrollbar-thumb:hover { 19 | background: rgba(255, 255, 255, 0.3); 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/assets/wavy-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/src/components/debug/code-section.tsx: -------------------------------------------------------------------------------- 1 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 2 | import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism' 3 | 4 | export const CodeSection = ({ 5 | title, 6 | code, 7 | isLoading, 8 | currentLanguage 9 | }: { 10 | title: string 11 | code: React.ReactNode 12 | isLoading: boolean 13 | currentLanguage: string 14 | }) => ( 15 |
16 |

{title}

17 | {isLoading ? ( 18 |
19 |
20 |

21 | Loading solution... 22 |

23 |
24 |
25 | ) : ( 26 |
27 | 41 | {code as string} 42 | 43 |
44 | )} 45 |
46 | ) 47 | -------------------------------------------------------------------------------- /src/renderer/src/components/debug/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react' 2 | import { useToast } from '@renderer/providers/toast-context' 3 | import { useQuery, useQueryClient } from '@tanstack/react-query' 4 | import ScreenshotQueue from '../queue/screenshot-queue' 5 | import SolutionCommands from '../solutions/solution-commands' 6 | import { ContentSection } from '../solutions/content-section' 7 | import { CodeSection } from './code-section' 8 | import { ComplexitySection } from '../solutions/complexity-section' 9 | interface DebugProps { 10 | isProcessing: boolean 11 | setIsProcessing: (isProcessing: boolean) => void 12 | currentLanguage: string 13 | setLanguage: (language: string) => void 14 | } 15 | 16 | async function fetchScreenshots() { 17 | try { 18 | const existing = await window.electronAPI.getScreenshots() 19 | console.log('Raw screenshots', existing) 20 | return (Array.isArray(existing) ? existing : []).map((screenshot) => ({ 21 | id: screenshot.path, 22 | path: screenshot.path, 23 | preview: screenshot.preview, 24 | timestamp: Date.now() 25 | })) 26 | } catch (error) { 27 | console.error('Error fetching screenshots', error) 28 | throw error 29 | } 30 | } 31 | 32 | const Debug: React.FC = ({ 33 | isProcessing, 34 | setIsProcessing, 35 | currentLanguage, 36 | setLanguage 37 | }) => { 38 | const [tooltipVisible, setTooltipVisible] = useState(false) 39 | const [tooltipHeight, setTooltipHeight] = useState(0) 40 | const { showToast } = useToast() 41 | 42 | const { data: screenshots = [], refetch } = useQuery({ 43 | queryKey: ['screenshots'], 44 | queryFn: fetchScreenshots, 45 | staleTime: Infinity, 46 | gcTime: Infinity, 47 | refetchOnWindowFocus: false 48 | }) 49 | 50 | const [newCode, setNewCode] = useState(null) 51 | const [thoughtsData, setThoughtsData] = useState(null) 52 | const [timeComplexityData, setTimeComplexityData] = useState(null) 53 | const [spaceComplexityData, setSpaceComplexityData] = useState(null) 54 | 55 | const [debugAnalysis, setDebugAnalysis] = useState(null) 56 | 57 | const queryClient = useQueryClient() 58 | const contentRef = useRef(null) 59 | 60 | useEffect(() => { 61 | const newSolution = queryClient.getQueryData(['new_solution']) as { 62 | code: string 63 | debug_analysis: string 64 | thoughts: string[] 65 | time_complexity: string 66 | space_complexity: string 67 | } | null 68 | 69 | if (newSolution) { 70 | console.log('Found cached solution', newSolution) 71 | 72 | if (newSolution.debug_analysis) { 73 | setDebugAnalysis(newSolution.debug_analysis) 74 | setNewCode(newSolution.code || '// Debug mode - see analysis below') 75 | 76 | if (newSolution.debug_analysis.includes('\n\n')) { 77 | const sections = newSolution.debug_analysis.split('\n\n').filter(Boolean) 78 | setThoughtsData(sections.slice(0, 3)) 79 | } else { 80 | setThoughtsData(['Debug analysis based on your screenshots']) 81 | } 82 | } else { 83 | setNewCode(newSolution.code || '// Debug mode - see analysis below') 84 | setThoughtsData(newSolution.thoughts || ['Debug analysis based on your screenshots']) 85 | } 86 | setTimeComplexityData(newSolution.time_complexity) 87 | setSpaceComplexityData(newSolution.space_complexity) 88 | setIsProcessing(false) 89 | } 90 | 91 | const cleanupFunctions = [ 92 | window.electronAPI.onScreenshotTaken(() => refetch()), 93 | window.electronAPI.onResetView(() => refetch()), 94 | window.electronAPI.onDebugSuccess((data) => { 95 | console.log('Debug success', data) 96 | queryClient.setQueryData(['new_solution'], data) 97 | 98 | if (data.debug_analysis) { 99 | setDebugAnalysis(data.debug_analysis) 100 | setNewCode(data.code || '// Debug mode - see analysis below') 101 | 102 | if (data.debug_analysis.includes('\n\n')) { 103 | const sections = data.debug_analysis.split('\n\n').filter(Boolean) 104 | setThoughtsData(sections.slice(0, 3)) 105 | } else if (data.debug_analysis.includes('\n')) { 106 | const lines = data.debug_analysis.split('\n') 107 | const bulletPoints = lines.filter( 108 | (line) => 109 | line.trim().match(/^[\d*\-•]+\s/) || 110 | // eslint-disable-next-line no-useless-escape 111 | line.trim().match(/^[A-Z][\d\.\)\:]/) || 112 | (line.includes(':') && line.length < 100) 113 | ) 114 | 115 | if (bulletPoints.length > 0) { 116 | setThoughtsData(bulletPoints.slice(0, 5)) 117 | } else { 118 | setThoughtsData(['Debug analysis based on your screenshots']) 119 | } 120 | } else { 121 | setThoughtsData(['Debug analysis based on your screenshots']) 122 | } 123 | } else { 124 | setNewCode(data.code || '// Debug mode - see analysis below') 125 | setThoughtsData(data.thoughts || ['Debug analysis based on your screenshots']) 126 | setDebugAnalysis(null) 127 | } 128 | 129 | setTimeComplexityData(data.time_complexity) 130 | setSpaceComplexityData(data.space_complexity) 131 | setIsProcessing(false) 132 | }), 133 | window.electronAPI.onDebugStart(() => { 134 | setIsProcessing(true) 135 | }), 136 | window.electronAPI.onDebugError((error) => { 137 | showToast( 138 | 'Processing Failed', 139 | 'There was an error processing your code. Please try again.', 140 | 'error' 141 | ) 142 | setIsProcessing(false) 143 | console.error('Debug error', error) 144 | }) 145 | ] 146 | 147 | const updateDimensions = () => { 148 | if (contentRef.current) { 149 | let contentHeight = contentRef.current.scrollHeight 150 | const contentWidth = contentRef.current.scrollWidth 151 | if (tooltipVisible) { 152 | contentHeight += tooltipHeight 153 | } 154 | window.electronAPI.updateContentDimensions({ 155 | width: contentWidth, 156 | height: contentHeight 157 | }) 158 | } 159 | } 160 | 161 | const resizeObserver = new ResizeObserver(updateDimensions) 162 | if (contentRef.current) { 163 | resizeObserver.observe(contentRef.current) 164 | } 165 | 166 | updateDimensions() 167 | 168 | return () => { 169 | cleanupFunctions.forEach((fn) => fn()) 170 | resizeObserver.disconnect() 171 | } 172 | }, [queryClient, setIsProcessing]) 173 | 174 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 175 | setTooltipVisible(visible) 176 | setTooltipHeight(height) 177 | } 178 | 179 | const handleDeleteExtraScreenshot = async (index: number) => { 180 | const screenshotToDelete = screenshots[index] 181 | 182 | try { 183 | const response = await window.electronAPI.deleteScreenshot(screenshotToDelete.path) 184 | if (response.success) { 185 | refetch() 186 | } else { 187 | console.error('Failed to delete screenshot', response.error) 188 | showToast('Delete Failed', 'Failed to delete screenshot. Please try again.', 'error') 189 | } 190 | } catch (error) { 191 | console.error('Error deleting screenshot', error) 192 | showToast('Delete Failed', 'Failed to delete screenshot. Please try again.', 'error') 193 | } 194 | } 195 | 196 | return ( 197 |
198 |
199 |
200 |
201 |
202 | 207 |
208 |
209 |
210 | 211 | 219 | 220 |
221 |
222 |
223 | 228 |
229 | {thoughtsData.map((thought, index) => ( 230 |
231 |
232 |
{thought}
233 |
234 | ))} 235 |
236 |
237 | ) 238 | } 239 | isLoading={isProcessing} 240 | /> 241 | 242 | 248 | 249 |
250 |

251 | Analysis & Improvements 252 |

253 | {!debugAnalysis ? ( 254 |
255 |
256 |

257 | Loading debug analysis... 258 |

259 |
260 |
261 | ) : ( 262 |
263 | {(() => { 264 | const sections: { title: string; content: string[] }[] = [] 265 | const mainSections = debugAnalysis.split( 266 | /(?=^#{1,3}\s|^\*\*\*|^\s*[A-Z][\w\s]+\s*$)/m 267 | ) 268 | mainSections.filter(Boolean).forEach((sectionText) => { 269 | const lines = sectionText.split('\n') 270 | let title = '' 271 | let startLineIndex = 0 272 | 273 | if ( 274 | lines[0] && 275 | (lines[0].startsWith('#') || 276 | lines[0].startsWith('**') || 277 | lines[0].match(/^[A-Z][\w\s]+$/) || 278 | lines[0].includes('Issues') || 279 | lines[0].includes('Improvements') || 280 | lines[0].includes('Optimizations')) 281 | ) { 282 | title = lines[0].replace(/^#+\s*|\*\*/g, '') 283 | startLineIndex = 1 284 | } 285 | 286 | sections.push({ 287 | title, 288 | content: lines.slice(startLineIndex).filter(Boolean) 289 | }) 290 | }) 291 | 292 | return sections.map((section, sectionIndex) => ( 293 |
294 | {section.title && ( 295 |
296 | {section.title} 297 |
298 | )} 299 |
300 | {section.content.map((line, lineIndex) => { 301 | // Handle code blocks - detect full code blocks 302 | if (line.trim().startsWith('```')) { 303 | // If we find the start of a code block, collect all lines until the end 304 | if (line.trim() === '```' || line.trim().startsWith('```')) { 305 | // Find end of this code block 306 | const codeBlockEndIndex = section.content.findIndex( 307 | (l, i) => i > lineIndex && l.trim() === '```' 308 | ) 309 | 310 | if (codeBlockEndIndex > lineIndex) { 311 | // Get the code content 312 | const codeContent = section.content 313 | .slice(lineIndex + 1, codeBlockEndIndex) 314 | .join('\n') 315 | 316 | // Skip ahead in our loop 317 | lineIndex = codeBlockEndIndex 318 | 319 | return ( 320 |
324 | {codeContent} 325 |
326 | ) 327 | } 328 | } 329 | } 330 | 331 | // Handle bullet points 332 | // eslint-disable-next-line no-useless-escape 333 | if (line.trim().match(/^[\-*•]\s/) || line.trim().match(/^\d+\.\s/)) { 334 | return ( 335 |
336 |
337 |
338 | {line.replace(/^[-*•]\s|^\d+\.\s/, '')} 339 |
340 |
341 | ) 342 | } 343 | 344 | // Handle inline code 345 | if (line.includes('`')) { 346 | const parts = line.split(/(`[^`]+`)/g) 347 | return ( 348 |
349 | {parts.map((part, partIndex) => { 350 | if (part.startsWith('`') && part.endsWith('`')) { 351 | return ( 352 | 356 | {part.slice(1, -1)} 357 | 358 | ) 359 | } 360 | return {part} 361 | })} 362 |
363 | ) 364 | } 365 | 366 | // Handle sub-headers 367 | if ( 368 | line.trim().match(/^#+\s/) || 369 | (line.trim().match(/^[A-Z][\w\s]+:/) && line.length < 60) 370 | ) { 371 | return ( 372 |
376 | {line.replace(/^#+\s+/, '')} 377 |
378 | ) 379 | } 380 | 381 | // Regular text 382 | return ( 383 |
384 | {line} 385 |
386 | ) 387 | })} 388 |
389 |
390 | )) 391 | })()} 392 |
393 | )} 394 |
395 | 396 | 401 |
402 |
403 |
404 |
405 |
406 | ) 407 | } 408 | 409 | export default Debug 410 | -------------------------------------------------------------------------------- /src/renderer/src/components/language-selector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { languageOptions } from '@renderer/lib/languages' 3 | 4 | interface LanguageSelectorProps { 5 | currentLanguage: string 6 | setLanguage: (language: string) => void 7 | } 8 | 9 | export const LanguageSelector: React.FC = ({ 10 | currentLanguage, 11 | setLanguage 12 | }) => { 13 | const handleRadioChange = async (e: React.ChangeEvent) => { 14 | const newLanguage = e.target.value 15 | 16 | try { 17 | await window.electronAPI.updateConfig({ language: newLanguage }) 18 | window.__LANGUAGE__ = newLanguage 19 | setLanguage(newLanguage) 20 | console.log('Language updated to:', newLanguage) 21 | } catch (error) { 22 | console.error('Error updating language:', error) 23 | } 24 | } 25 | 26 | return ( 27 |
28 | Language 29 |
30 | {languageOptions.map((option) => ( 31 |
32 | 41 | 47 |
48 | ))} 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/src/components/main-app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import ScreenshotsView from './screenshots-view' 3 | import { useQueryClient } from '@tanstack/react-query' 4 | import Solutions from './solutions' 5 | import { useToast } from '@renderer/providers/toast-context' 6 | interface MainAppProps { 7 | currentLanguage: string 8 | setLanguage: (language: string) => void 9 | } 10 | 11 | // eslint-disable-next-line react-refresh/only-export-components, react/prop-types 12 | const MainApp: React.FC = ({ currentLanguage, setLanguage }) => { 13 | const [view, setView] = useState<'queue' | 'solutions' | 'debug'>('queue') 14 | const containerRef = useRef(null) 15 | const queryClient = useQueryClient() 16 | const { showToast } = useToast() 17 | useEffect(() => { 18 | const cleanup = window.electronAPI.onResetView(() => { 19 | queryClient.invalidateQueries({ 20 | queryKey: ['screenshots'] 21 | }) 22 | queryClient.invalidateQueries({ 23 | queryKey: ['problem_statement'] 24 | }) 25 | queryClient.invalidateQueries({ 26 | queryKey: ['solution'] 27 | }) 28 | queryClient.invalidateQueries({ 29 | queryKey: ['new_solution'] 30 | }) 31 | setView('queue') 32 | }) 33 | 34 | return () => cleanup() 35 | }, []) 36 | 37 | useEffect(() => { 38 | const cleanupFunctions = [ 39 | window.electronAPI.onSolutionStart(() => { 40 | setView('solutions') 41 | }), 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | window.electronAPI.onProblemExtracted((data: any) => { 44 | console.log('problem extracted', data) 45 | if (view === 'queue') { 46 | queryClient.invalidateQueries({ 47 | queryKey: ['problem_statement'] 48 | }) 49 | queryClient.setQueryData(['problem_statement'], data) 50 | } 51 | }), 52 | window.electronAPI.onResetView(() => { 53 | queryClient.invalidateQueries({ 54 | queryKey: ['screenshots'] 55 | }) 56 | queryClient.invalidateQueries({ 57 | queryKey: ['problem_statement'] 58 | }) 59 | queryClient.invalidateQueries({ 60 | queryKey: ['solution'] 61 | }) 62 | setView('queue') 63 | }), 64 | window.electronAPI.onResetView(() => { 65 | queryClient.setQueryData(['problem_statement'], null) 66 | }), 67 | window.electronAPI.onSolutionError((error: string) => { 68 | showToast('Error', error, 'error') 69 | }) 70 | ] 71 | 72 | return () => { 73 | cleanupFunctions.forEach((cleanup) => { 74 | cleanup() 75 | }) 76 | } 77 | }, [view]) 78 | 79 | useEffect(() => { 80 | if (!containerRef.current) return 81 | 82 | const updateDimensions = () => { 83 | if (!containerRef.current) return 84 | const height = containerRef.current.scrollHeight || 600 85 | const width = containerRef.current.scrollWidth || 800 86 | window.electronAPI?.updateContentDimensions({ 87 | width, 88 | height 89 | }) 90 | } 91 | 92 | updateDimensions() 93 | 94 | const fallbackTimer = setTimeout(() => { 95 | window.electronAPI?.updateContentDimensions({ 96 | width: 800, 97 | height: 600 98 | }) 99 | }, 500) 100 | 101 | const resizeObserver = new ResizeObserver(updateDimensions) 102 | resizeObserver.observe(containerRef.current) 103 | 104 | const mutationObserver = new MutationObserver(updateDimensions) 105 | mutationObserver.observe(containerRef.current, { 106 | childList: true, 107 | subtree: true, 108 | attributes: true, 109 | characterData: true 110 | }) 111 | 112 | const delayedUpdate = setTimeout(updateDimensions, 1000) 113 | 114 | return () => { 115 | clearTimeout(fallbackTimer) 116 | clearTimeout(delayedUpdate) 117 | resizeObserver.disconnect() 118 | mutationObserver.disconnect() 119 | } 120 | }, [view]) 121 | 122 | return ( 123 |
124 | {view === 'queue' ? ( 125 | 130 | ) : view === 'solutions' ? ( 131 | 132 | ) : null} 133 |
134 | ) 135 | } 136 | 137 | export default MainApp 138 | -------------------------------------------------------------------------------- /src/renderer/src/components/queue/queue-commands.tsx: -------------------------------------------------------------------------------- 1 | import { COMMAND_KEY } from '@renderer/lib/utils' 2 | import { useEffect, useRef, useState } from 'react' 3 | import { LanguageSelector } from '../language-selector' 4 | import { useToast } from '@renderer/providers/toast-context' 5 | 6 | interface QueueCommandsProps { 7 | screenshotCount?: number 8 | currentLanguage: string 9 | setLanguage: (language: string) => void 10 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 11 | } 12 | 13 | export const QueueCommands: React.FC = ({ 14 | screenshotCount, 15 | currentLanguage, 16 | setLanguage, 17 | onTooltipVisibilityChange 18 | }) => { 19 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 20 | const tooltipRef = useRef(null) 21 | const { showToast } = useToast() 22 | 23 | const handleMouseEnter = () => { 24 | setIsTooltipVisible(true) 25 | } 26 | 27 | const handleMouseLeave = () => { 28 | setIsTooltipVisible(false) 29 | } 30 | 31 | useEffect(() => { 32 | let tooltipHeight = 0 33 | if (tooltipRef.current && isTooltipVisible) { 34 | tooltipHeight = tooltipRef.current.offsetHeight + 10 35 | } 36 | 37 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 38 | }, [isTooltipVisible]) 39 | 40 | return ( 41 |
42 |
43 |
44 |
{ 47 | try { 48 | const result = await window.electronAPI.triggerScreenshot() 49 | if (!result.success) { 50 | console.log('Failed to trigger screenshot:', result.error) 51 | showToast('Error', 'Failed to trigger screenshot', 'error') 52 | } 53 | } catch (error) { 54 | console.error('Error triggering screenshot:', error) 55 | showToast('Error', 'Failed to trigger screenshot', 'error') 56 | } 57 | }} 58 | > 59 | 60 | {screenshotCount === 0 61 | ? 'Take first screenshot' 62 | : screenshotCount === 1 63 | ? 'Take second screenshot' 64 | : screenshotCount === 2 65 | ? 'Take third screenshot' 66 | : screenshotCount === 3 67 | ? 'Take fourth screenshot' 68 | : screenshotCount === 4 69 | ? 'Take fifth screenshot' 70 | : 'Next will repeat first screenshot'} 71 | 72 |
73 | 76 | 79 |
80 |
81 | 82 | {screenshotCount! > 0 && ( 83 |
{ 86 | try { 87 | const result = await window.electronAPI.triggerProcessScreenshots() 88 | if (!result.success) { 89 | console.log('Failed to process screenshot:', result.error) 90 | showToast('Error', 'Failed to process screenshot', 'error') 91 | } 92 | } catch (error) { 93 | console.error('Error processing screenshot:', error) 94 | showToast('Error', 'Failed to process screenshot', 'error') 95 | } 96 | }} 97 | > 98 |
99 | Solve 100 |
101 | 104 | 107 |
108 |
109 |
110 | )} 111 | 112 |
113 | 114 |
119 |
120 | 130 | 131 | 132 | 133 |
134 | 135 | {isTooltipVisible && ( 136 |
143 |
144 |
145 |
146 |

Keyboard Shortcuts

147 |
148 |
{ 151 | try { 152 | const result = await window.electronAPI.toggleMainWindow() 153 | if (!result.success) { 154 | console.log('Failed to toggle window:', result.error) 155 | showToast('Error', 'Failed to toggle window', 'error') 156 | } 157 | } catch (error) { 158 | console.error('Error toggling window:', error) 159 | showToast('Error', 'Failed to toggle window', 'error') 160 | } 161 | }} 162 | > 163 |
164 | Toggle Window 165 |
166 | 167 | {COMMAND_KEY} 168 | 169 | 170 | B 171 | 172 |
173 |
174 |

175 | Show or hide this window 176 |

177 |
178 |
{ 181 | try { 182 | const result = await window.electronAPI.triggerScreenshot() 183 | if (!result.success) { 184 | console.log('Failed to trigger screenshot:', result.error) 185 | showToast('Error', 'Failed to trigger screenshot', 'error') 186 | } 187 | } catch (error) { 188 | console.error('Error triggering screenshot:', error) 189 | showToast('Error', 'Failed to trigger screenshot', 'error') 190 | } 191 | }} 192 | > 193 |
194 | Take Screenshot 195 |
196 | 197 | {COMMAND_KEY} 198 | 199 | 200 | H 201 | 202 |
203 |
204 |

205 | Take a screenshot of the problem statement 206 |

207 |
208 |
0 ? '' : 'opacity-50 cursor-not-allowed' 211 | }`} 212 | onClick={async () => { 213 | if (screenshotCount === 0) return 214 | try { 215 | const result = await window.electronAPI.triggerProcessScreenshots() 216 | if (!result.success) { 217 | console.log('Failed to process screenshot:', result.error) 218 | showToast('Error', 'Failed to process screenshot', 'error') 219 | } 220 | } catch (error) { 221 | console.error('Error processing screenshot:', error) 222 | showToast('Error', 'Failed to process screenshot', 'error') 223 | } 224 | }} 225 | > 226 |
227 | Solve Problem 228 |
229 | 230 | {COMMAND_KEY} 231 | 232 | 233 | ↵ 234 | 235 |
236 |
237 |

238 | {screenshotCount! > 0 239 | ? 'Generate a solution based on the current problem' 240 | : 'Take a screenshot first to generate a solution.'} 241 |

242 |
243 |
0 ? '' : 'opacity-50 cursor-not-allowed' 246 | }`} 247 | onClick={async () => { 248 | if (screenshotCount === 0) return 249 | try { 250 | const result = await window.electronAPI.deleteLastScreenshot() 251 | if (!result.success) { 252 | console.log('Failed to delete last screenshot:', result.error) 253 | showToast('Error', 'Failed to delete last screenshot', 'error') 254 | } 255 | } catch (error) { 256 | console.error('Error deleting last screenshot:', error) 257 | showToast('Error', 'Failed to delete last screenshot', 'error') 258 | } 259 | }} 260 | > 261 |
262 | Delete Last Screenshot 263 |
264 | 265 | {COMMAND_KEY} 266 | 267 | 268 | L 269 | 270 |
271 |
272 |

273 | {screenshotCount! > 0 274 | ? 'Delete the last screenshot' 275 | : 'No screenshots to delete'} 276 |

277 |
278 |
279 | 280 |
281 | 285 | 286 |
287 |
288 | AI API Settings 289 | 295 |
296 |
297 |
298 |
299 |
300 |
301 | )} 302 |
303 |
304 |
305 |
306 | ) 307 | } 308 | -------------------------------------------------------------------------------- /src/renderer/src/components/queue/screenshot-item.tsx: -------------------------------------------------------------------------------- 1 | import { X } from 'lucide-react' 2 | 3 | interface Screenshot { 4 | path: string 5 | preview: string 6 | } 7 | 8 | interface ScreenshotItemProps { 9 | screenshot: Screenshot 10 | onDelete: (index: number) => void 11 | index: number 12 | isLoading: boolean 13 | } 14 | 15 | const ScreenshotItem: React.FC = ({ 16 | screenshot, 17 | onDelete, 18 | index, 19 | isLoading 20 | }) => { 21 | const handleDelete = async () => { 22 | await onDelete(index) 23 | } 24 | 25 | return ( 26 | <> 27 |
30 |
31 | {isLoading && ( 32 |
33 |
34 |
35 | )} 36 | Screenshot preview 45 | {!isLoading && ( 46 | 56 | )} 57 |
58 |
59 | 60 | ) 61 | } 62 | 63 | export default ScreenshotItem 64 | -------------------------------------------------------------------------------- /src/renderer/src/components/queue/screenshot-queue.tsx: -------------------------------------------------------------------------------- 1 | import ScreenshotItem from './screenshot-item' 2 | 3 | interface Screenshot { 4 | path: string 5 | preview: string 6 | } 7 | 8 | interface ScreenshotQueueProps { 9 | screenshots: Screenshot[] 10 | isLoading: boolean 11 | onDeleteScreenshot: (index: number) => void 12 | } 13 | 14 | const ScreenshotQueue: React.FC = ({ 15 | screenshots, 16 | isLoading, 17 | onDeleteScreenshot 18 | }) => { 19 | if (screenshots.length === 0) { 20 | return <> 21 | } 22 | 23 | const displayScreenshots = screenshots.slice(0, 5) 24 | 25 | return ( 26 |
27 | {displayScreenshots.map((screenshot, index) => ( 28 | 35 | ))} 36 |
37 | ) 38 | } 39 | 40 | export default ScreenshotQueue 41 | -------------------------------------------------------------------------------- /src/renderer/src/components/screenshots-view.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import ScreenshotQueue from './queue/screenshot-queue' 3 | import { useQuery } from '@tanstack/react-query' 4 | import { useToast } from '../providers/toast-context' 5 | import { QueueCommands } from './queue/queue-commands' 6 | export interface Screenshot { 7 | path: string 8 | preview: string 9 | } 10 | 11 | async function fetchScreenshots(): Promise { 12 | try { 13 | const existing = await window.electronAPI.getScreenshots() 14 | return existing 15 | } catch (error) { 16 | console.error('Error fetching screenshots:', error) 17 | throw error 18 | } 19 | } 20 | 21 | interface ScreenshotsViewProps { 22 | setView: (view: 'queue' | 'solutions' | 'debug') => void 23 | currentLanguage: string 24 | setLanguage: (language: string) => void 25 | } 26 | 27 | const ScreenshotsView: React.FC = ({ 28 | setView, 29 | currentLanguage, 30 | setLanguage 31 | }) => { 32 | const contentRef = useRef(null) 33 | const { showToast } = useToast() 34 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 35 | const [tooltipHeight, setTooltipHeight] = useState(0) 36 | 37 | const { 38 | data: screenshots = [], 39 | isLoading, 40 | refetch 41 | } = useQuery({ 42 | queryKey: ['screenshots'], 43 | queryFn: fetchScreenshots, 44 | staleTime: Infinity, 45 | gcTime: Infinity, 46 | refetchOnWindowFocus: false 47 | }) 48 | 49 | console.log('screenshots', screenshots) 50 | 51 | useEffect(() => { 52 | const updateDimensions = () => { 53 | if (contentRef.current) { 54 | let contentHeight = contentRef.current.scrollHeight 55 | const contentWidth = contentRef.current.scrollWidth 56 | if (isTooltipVisible) { 57 | contentHeight += tooltipHeight 58 | } 59 | window.electronAPI.updateContentDimensions({ 60 | width: contentWidth, 61 | height: contentHeight 62 | }) 63 | } 64 | } 65 | 66 | const resizeObserver = new ResizeObserver(updateDimensions) 67 | if (contentRef.current) { 68 | resizeObserver.observe(contentRef.current) 69 | } 70 | 71 | updateDimensions() 72 | 73 | const cleanupFunctions = [ 74 | window.electronAPI.onScreenshotTaken(() => refetch()), 75 | window.electronAPI.onResetView(() => refetch()), 76 | window.electronAPI.onDeleteLastScreenshot(async () => { 77 | if (screenshots.length > 0) { 78 | await handleDeleteScreenshot(screenshots.length - 1) 79 | } else { 80 | showToast('Error', 'No screenshots to delete', 'error') 81 | } 82 | }), 83 | window.electronAPI.onSolutionError((error: string) => { 84 | showToast('Error', error, 'error') 85 | setView('queue') 86 | console.log('error', error) 87 | }), 88 | window.electronAPI.onProcessingNoScreenshots(() => { 89 | showToast('Error', 'No screenshots to process', 'error') 90 | }) 91 | ] 92 | return () => { 93 | cleanupFunctions.forEach((cleanup) => cleanup()) 94 | resizeObserver.disconnect() 95 | } 96 | }, [screenshots, isTooltipVisible, tooltipHeight]) 97 | 98 | const handleDeleteScreenshot = async (index: number) => { 99 | const screenshotToDelete = screenshots[index] 100 | try { 101 | const response = await window.electronAPI.deleteScreenshot(screenshotToDelete.path) 102 | if (response.success) { 103 | refetch() 104 | } else { 105 | console.error('Error deleting screenshot:', response.error) 106 | showToast('Error', response.error || 'Failed to delete screenshot', 'error') 107 | } 108 | } catch (error) { 109 | console.error('Error deleting screenshot:', error) 110 | } 111 | } 112 | 113 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 114 | setIsTooltipVisible(visible) 115 | setTooltipHeight(height) 116 | } 117 | 118 | return ( 119 |
120 |
121 |
122 | 127 | 133 |
134 |
135 |
136 | ) 137 | } 138 | 139 | export default ScreenshotsView 140 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { 3 | DialogContent, 4 | DialogHeader, 5 | DialogTitle, 6 | Dialog, 7 | DialogDescription, 8 | DialogFooter 9 | } from './ui/dialog' 10 | import { Input } from './ui/input' 11 | import { Button } from './ui/button' 12 | import { useToast } from '@renderer/providers/toast-context' 13 | type APIProvider = 'openai' | 'gemini' 14 | 15 | type AIModel = { 16 | id: string 17 | name: string 18 | description: string 19 | } 20 | 21 | type ModelCategory = { 22 | key: 'extractionModel' | 'solutionModel' | 'debuggingModel' 23 | title: string 24 | description: string 25 | openaiModels: AIModel[] 26 | geminiModels: AIModel[] 27 | } 28 | 29 | const modelCategories: ModelCategory[] = [ 30 | { 31 | key: 'extractionModel', 32 | title: 'Problem Extraction', 33 | description: 'Model used to analyze the screenshots and extract the problem statement', 34 | openaiModels: [ 35 | { 36 | id: 'gpt-4o', 37 | name: 'GPT-4o', 38 | description: 'Best overall performance for problem extraction' 39 | }, 40 | { 41 | id: 'gpt-4o-mini', 42 | name: 'GPT-4o Mini', 43 | description: 'Faster, more cost-effective model for problem extraction' 44 | } 45 | ], 46 | geminiModels: [ 47 | { 48 | id: 'gemini-1.5-pro', 49 | name: 'Gemini 1.5 Pro', 50 | description: 'Best overall performance for problem extraction' 51 | }, 52 | { 53 | id: 'gemini-2.0-flash', 54 | name: 'Gemini 2.0 Flash', 55 | description: 'Faster, more cost-effective model for problem extraction' 56 | } 57 | ] 58 | }, 59 | { 60 | key: 'solutionModel', 61 | title: 'Solution Generation', 62 | description: 'Model used to generate the solution to the problem', 63 | openaiModels: [ 64 | { 65 | id: 'gpt-4o', 66 | name: 'GPT-4o', 67 | description: 'Best overall performance for solution generation' 68 | }, 69 | { 70 | id: 'gpt-4o-mini', 71 | name: 'GPT-4o Mini', 72 | description: 'Faster, more cost-effective model for solution generation' 73 | } 74 | ], 75 | geminiModels: [ 76 | { 77 | id: 'gemini-1.5-pro', 78 | name: 'Gemini 1.5 Pro', 79 | description: 'Best overall performance for solution generation' 80 | }, 81 | { 82 | id: 'gemini-2.0-flash', 83 | name: 'Gemini 2.0 Flash', 84 | description: 'Faster, more cost-effective model for solution generation' 85 | } 86 | ] 87 | }, 88 | { 89 | key: 'debuggingModel', 90 | title: 'Debugging', 91 | description: 'Model used to debug the solution', 92 | openaiModels: [ 93 | { 94 | id: 'gpt-4o', 95 | name: 'GPT-4o', 96 | description: 'Best overall performance for debugging' 97 | }, 98 | { 99 | id: 'gpt-4o-mini', 100 | name: 'GPT-4o Mini', 101 | description: 'Faster, more cost-effective model for debugging' 102 | } 103 | ], 104 | geminiModels: [ 105 | { 106 | id: 'gemini-1.5-pro', 107 | name: 'Gemini 1.5 Pro', 108 | description: 'Best overall performance for debugging' 109 | }, 110 | { 111 | id: 'gemini-2.0-flash', 112 | name: 'Gemini 2.0 Flash', 113 | description: 'Faster, more cost-effective model for debugging' 114 | } 115 | ] 116 | } 117 | ] 118 | 119 | interface SettingsDialogProps { 120 | open: boolean 121 | onOpenChange: (open: boolean) => void 122 | } 123 | 124 | export function SettingsDialog({ open: openProp, onOpenChange }: SettingsDialogProps) { 125 | const [open, setOpen] = useState(openProp || false) 126 | console.log('open', open) 127 | const [apiKey, setApiKey] = useState('') 128 | const [apiProvider, setApiProvider] = useState('openai') 129 | const [extractionModel, setExtractionModel] = useState('gpt-4o') 130 | const [solutionModel, setSolutionModel] = useState('gpt-4o') 131 | const [debuggingModel, setDebuggingModel] = useState('gpt-4o') 132 | const [isLoading, setIsLoading] = useState(false) 133 | 134 | const { showToast } = useToast() 135 | 136 | const handleOpenChange = (newOpen: boolean) => { 137 | setOpen(newOpen) 138 | if (onOpenChange && newOpen !== openProp) { 139 | onOpenChange(newOpen) 140 | } 141 | } 142 | 143 | useEffect(() => { 144 | if (openProp !== undefined) { 145 | setOpen(openProp) 146 | } 147 | }, [openProp]) 148 | 149 | const handleProviderChange = (provider: APIProvider) => { 150 | setApiProvider(provider) 151 | 152 | if (provider === 'openai') { 153 | setExtractionModel('gpt-4o') 154 | setSolutionModel('gpt-4o') 155 | setDebuggingModel('gpt-4o') 156 | } else { 157 | setExtractionModel('gemini-1.5-pro') 158 | setSolutionModel('gemini-1.5-pro') 159 | setDebuggingModel('gemini-1.5-pro') 160 | } 161 | } 162 | 163 | const maskApiKey = (key: string) => { 164 | if (!key) return '' 165 | return `${key.substring(0, 4)}....${key.substring(key.length - 4)}` 166 | } 167 | 168 | const handleSave = async () => { 169 | setIsLoading(true) 170 | try { 171 | const result = await window.electronAPI.updateConfig({ 172 | apiKey, 173 | apiProvider, 174 | extractionModel, 175 | solutionModel, 176 | debuggingModel 177 | }) 178 | 179 | if (result) { 180 | showToast('Success', 'Settings saved successfully', 'success') 181 | handleOpenChange(false) 182 | 183 | setTimeout(() => { 184 | window.location.reload() 185 | }, 1500) 186 | } 187 | } catch (error) { 188 | console.error('Failed to save settings:', error) 189 | showToast('Error', 'Failed to save settings', 'error') 190 | } finally { 191 | setIsLoading(false) 192 | } 193 | } 194 | 195 | const openExternalLink = (url: string) => { 196 | window.electronAPI.openLink(url) 197 | } 198 | 199 | useEffect(() => { 200 | if (open) { 201 | setIsLoading(true) 202 | interface Config { 203 | apiKey?: string 204 | apiProvider?: 'openai' | 'gemini' 205 | extractionModel?: string 206 | solutionModel?: string 207 | debuggingModel?: string 208 | } 209 | 210 | window.electronAPI 211 | .getConfig() 212 | .then((config: Config) => { 213 | setApiKey(config.apiKey || '') 214 | setApiProvider(config.apiProvider || 'openai') 215 | setExtractionModel(config.extractionModel || 'gpt-4o') 216 | setSolutionModel(config.solutionModel || 'gpt-4o') 217 | setDebuggingModel(config.debuggingModel || 'gpt-4o') 218 | }) 219 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 220 | .catch((error: any) => { 221 | console.error('Failed to fetch config:', error) 222 | showToast('Error', 'Failed to load settings', 'error') 223 | }) 224 | .finally(() => { 225 | setIsLoading(false) 226 | }) 227 | } 228 | }, [open, showToast]) 229 | 230 | return ( 231 | 232 | 255 | 256 | API Settings 257 | 258 | Configure your API key and model preferences. You'll need your own API key to use 259 | this application. 260 | 261 | 262 |
263 |
264 | 265 |
266 |
handleProviderChange('openai')} 273 | > 274 |
275 |
280 |
281 |

OpenAI

282 |

GPT-4o models

283 |
284 |
285 |
286 |
handleProviderChange('gemini')} 293 | > 294 |
295 |
300 |
301 |

Gemini

302 |

Gemini 1.5 models

303 |
304 |
305 |
306 |
307 |
308 | 309 |
310 | 313 | setApiKey(e.target.value)} 319 | className="bg-black/50 border-white/10 text-white" 320 | /> 321 | {apiKey &&

Current {maskApiKey(apiKey)}

} 322 |

323 | Your API key is stored locally in your browser. It is not sent to any servers. 324 | {apiProvider === 'gemini' ? 'OpenAI' : 'Google'} 325 |

326 |
327 |

Don't have an API key?

328 | {apiProvider === 'openai' ? ( 329 | <> 330 |

331 | 1. Create an account at{' '} 332 | 338 |

339 |

340 | 2. Go to{' '} 341 | 347 |

348 |

3. Create an API key and paste it here

349 | 350 | ) : ( 351 | <> 352 |

353 | 1. Create an account at{' '} 354 | 360 |

361 |

362 | 2. Go to{' '} 363 | 369 |

370 |

3. Create an API key and paste it here

371 | 372 | )} 373 |
374 |
375 |
376 | 377 |
378 |
379 |
Toggle Visibility
380 |
Ctrl+B / Cmd+B
381 | 382 |
Take Screenshot
383 |
Ctrl+H / Cmd+H
384 | 385 |
Process Screenshot
386 |
Ctrl+Enter / Cmd+Enter
387 | 388 |
Delete Last Screenshot
389 |
Ctrl+L / Cmd+L
390 | 391 |
Reset View
392 |
Ctrl+R / Cmd+R
393 | 394 |
Quit Application
395 |
Ctrl+Q / Cmd+Q
396 | 397 |
Move Window
398 |
Ctrl+Arrow Keys
399 | 400 |
Decrease Opacity
401 |
Ctrl+[ / Cmd+[
402 | 403 |
Increase Opacity
404 |
Ctrl+] / Cmd+]
405 | 406 |
Zoom Out
407 |
Ctrl+- / Cmd+-
408 | 409 |
Zoom In
410 |
Ctrl+= / Cmd+=
411 | 412 |
Reset Zoom
413 |
Ctrl+0 / Cmd+0
414 |
415 |
416 |
417 | 418 |
419 | 420 |

421 | Select which models to use for each stage of the process 422 |

423 | {modelCategories.map((category) => { 424 | const models = 425 | apiProvider === 'openai' ? category.openaiModels : category.geminiModels 426 | 427 | return ( 428 |
429 | 432 |

{category.description}

433 |
434 | {models.map((m) => { 435 | const currentValue = 436 | category.key === 'extractionModel' 437 | ? extractionModel 438 | : category.key === 'solutionModel' 439 | ? solutionModel 440 | : debuggingModel 441 | 442 | const setValue = 443 | category.key === 'extractionModel' 444 | ? setExtractionModel 445 | : category.key === 'solutionModel' 446 | ? setSolutionModel 447 | : setDebuggingModel 448 | 449 | return ( 450 |
setValue(m.id)} 458 | > 459 |
460 |
465 |
466 |

{m.name}

467 |

{m.description}

468 |
469 |
470 |
471 | ) 472 | })} 473 |
474 |
475 | ) 476 | })} 477 |
478 |
479 | 480 | 487 | 494 | 495 | 496 |
497 | ) 498 | } 499 | -------------------------------------------------------------------------------- /src/renderer/src/components/solutions/complexity-section.tsx: -------------------------------------------------------------------------------- 1 | export const ComplexitySection = ({ 2 | timeComplexity, 3 | spaceComplexity, 4 | isLoading 5 | }: { 6 | timeComplexity: string | null 7 | spaceComplexity: string | null 8 | isLoading: boolean 9 | }) => { 10 | const formatComplexity = (complexity: string | null) => { 11 | if (!complexity || complexity.trim() === '') { 12 | return 'Complexity not available' 13 | } 14 | 15 | const bigORegex = /O\([^)]+\)/i 16 | if (bigORegex.test(complexity)) { 17 | return complexity 18 | } 19 | 20 | return `O(${complexity})` 21 | } 22 | 23 | const formattedTimeComplexity = formatComplexity(timeComplexity) 24 | const formattedSpaceComplexity = formatComplexity(spaceComplexity) 25 | 26 | return ( 27 |
28 |

Complexity

29 | {isLoading ? ( 30 |

31 | Calculating complexity... 32 |

33 | ) : ( 34 |
35 |
36 |
37 |
38 |
39 | Time: {formattedTimeComplexity} 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Space: {formattedSpaceComplexity} 48 |
49 |
50 |
51 |
52 | )} 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/src/components/solutions/content-section.tsx: -------------------------------------------------------------------------------- 1 | export const ContentSection = ({ 2 | title, 3 | content, 4 | isLoading 5 | }: { 6 | title: string 7 | content: React.ReactNode 8 | isLoading: boolean 9 | }) => { 10 | return ( 11 |
12 |

{title}

13 | {isLoading ? ( 14 |
15 |

16 | Extracting problem statement... 17 |

18 |
19 | ) : ( 20 |
{content}
21 | )} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/src/components/solutions/index.tsx: -------------------------------------------------------------------------------- 1 | import { ProblemStatementData } from '@renderer/lib/types' 2 | import { useQueryClient } from '@tanstack/react-query' 3 | import { useEffect, useRef, useState } from 'react' 4 | import { ContentSection } from './content-section' 5 | import { COMMAND_KEY } from '@renderer/lib/utils' 6 | import { SolutionSection } from './solution-section' 7 | import { ComplexitySection } from './complexity-section' 8 | import ScreenshotQueue from '../queue/screenshot-queue' 9 | import { useToast } from '@renderer/providers/toast-context' 10 | import SolutionCommands from './solution-commands' 11 | import Debug from '../debug' 12 | export interface SolutionsProps { 13 | setView: (view: 'queue' | 'solutions' | 'debug') => void 14 | currentLanguage: string 15 | setLanguage: (language: string) => void 16 | } 17 | 18 | const Solutions: React.FC = ({ setView, currentLanguage, setLanguage }) => { 19 | const queryClient = useQueryClient() 20 | const contentRef = useRef(null) 21 | const { showToast } = useToast() 22 | const [debugProcessing, setDebugProcessing] = useState(false) 23 | const [problemStatementData, setProblemStatementData] = useState( 24 | null 25 | ) 26 | 27 | const [solutionData, setSolutionData] = useState(null) 28 | const [thoughtsData, setThoughtsData] = useState(null) 29 | const [timeComplexityData, setTimeComplexityData] = useState(null) 30 | const [spaceComplexityData, setSpaceComplexityData] = useState(null) 31 | 32 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 33 | const [tooltipHeight, setTooltipHeight] = useState(0) 34 | const [isResetting, setIsResetting] = useState(false) 35 | 36 | interface Screenshot { 37 | id: string 38 | path: string 39 | preview: string 40 | timestamp: number 41 | } 42 | 43 | const [extraScreenshots, setExtraScreenshots] = useState([]) 44 | 45 | useEffect(() => { 46 | setProblemStatementData(queryClient.getQueryData(['problem_statement']) || null) 47 | setSolutionData(queryClient.getQueryData(['solution']) || null) 48 | const unsubscribe = queryClient.getQueryCache().subscribe((event) => { 49 | if (event?.query.queryKey[0] === 'problem_statement') { 50 | setProblemStatementData(queryClient.getQueryData(['problem_statement']) || null) 51 | } 52 | if (event?.query.queryKey[0] === 'solution') { 53 | const solution = queryClient.getQueryData(['solution']) as { 54 | code: string 55 | thoughts: string[] 56 | time_complexity: string 57 | space_complexity: string 58 | } | null 59 | 60 | setSolutionData(solution?.code || null) 61 | setThoughtsData(solution?.thoughts || null) 62 | setTimeComplexityData(solution?.time_complexity || null) 63 | setSpaceComplexityData(solution?.space_complexity || null) 64 | } 65 | }) 66 | 67 | return () => unsubscribe() 68 | }, [queryClient]) 69 | 70 | useEffect(() => { 71 | const updateDimensions = () => { 72 | if (contentRef.current) { 73 | let contentHeight = contentRef.current.scrollHeight 74 | const contentWidth = contentRef.current.scrollWidth 75 | if (isTooltipVisible) { 76 | contentHeight += tooltipHeight 77 | } 78 | window.electronAPI.updateContentDimensions({ 79 | width: contentWidth, 80 | height: contentHeight 81 | }) 82 | } 83 | } 84 | 85 | const resizeObserver = new ResizeObserver(updateDimensions) 86 | if (contentRef.current) { 87 | resizeObserver.observe(contentRef.current) 88 | } 89 | 90 | updateDimensions() 91 | 92 | const cleanupFunctions = [ 93 | window.electronAPI.onProblemExtracted((data) => { 94 | queryClient.setQueryData(['problem_statement'], data) 95 | }), 96 | window.electronAPI.onResetView(() => { 97 | setIsResetting(true) 98 | 99 | queryClient.removeQueries({ 100 | queryKey: ['solution'] 101 | }) 102 | queryClient.removeQueries({ 103 | queryKey: ['new_solution'] 104 | }) 105 | 106 | setExtraScreenshots([]) 107 | 108 | setTimeout(() => { 109 | setIsResetting(false) 110 | }, 0) 111 | }), 112 | window.electronAPI.onSolutionStart(() => { 113 | setSolutionData(null) 114 | setThoughtsData(null) 115 | setTimeComplexityData(null) 116 | setSpaceComplexityData(null) 117 | }), 118 | window.electronAPI.onSolutionSuccess((data) => { 119 | if (!data) { 120 | console.warn('Received empty or invalid solution data') 121 | return 122 | } 123 | console.log('Solution data received:', data) 124 | const solutionData = { 125 | code: data.code, 126 | thoughts: data.thoughts, 127 | time_complexity: data.time_complexity, 128 | space_complexity: data.space_complexity 129 | } 130 | queryClient.setQueryData(['solution'], solutionData) 131 | setSolutionData(solutionData.code || null) 132 | setThoughtsData(solutionData.thoughts || null) 133 | setTimeComplexityData(solutionData.time_complexity || null) 134 | setSpaceComplexityData(solutionData.space_complexity || null) 135 | 136 | const fetchScreenshots = async () => { 137 | try { 138 | const existing = await window.electronAPI.getScreenshots() 139 | const screenshots = (Array.isArray(existing) ? existing : []).map((screenshot) => ({ 140 | id: screenshot.path, 141 | path: screenshot.path, 142 | preview: screenshot.preview, 143 | timestamp: Date.now() 144 | })) 145 | 146 | setExtraScreenshots(screenshots) 147 | } catch (error) { 148 | console.error('Error fetching screenshots:', error) 149 | } 150 | } 151 | 152 | fetchScreenshots() 153 | }), 154 | window.electronAPI.onDebugStart(() => { 155 | setDebugProcessing(true) 156 | }), 157 | window.electronAPI.onDebugSuccess((data) => { 158 | queryClient.setQueryData(['new_solution'], data) 159 | setDebugProcessing(false) 160 | }), 161 | window.electronAPI.onDebugError(() => { 162 | showToast('Processing Failed', 'There was an error debugging your solution', 'error') 163 | setDebugProcessing(false) 164 | }), 165 | window.electronAPI.onProcessingNoScreenshots(() => { 166 | showToast('No Screenshots', 'There are no extra screenshots to debug', 'neutral') 167 | }), 168 | window.electronAPI.onScreenshotTaken(async () => { 169 | try { 170 | const existing = await window.electronAPI.getScreenshots() 171 | const screenshots = (Array.isArray(existing) ? existing : []).map((screenshot) => ({ 172 | id: screenshot.path, 173 | path: screenshot.path, 174 | preview: screenshot.preview, 175 | timestamp: Date.now() 176 | })) 177 | 178 | setExtraScreenshots(screenshots) 179 | } catch (error) { 180 | console.error('Error fetching screenshots:', error) 181 | } 182 | }), 183 | window.electronAPI.onSolutionError((error: string) => { 184 | showToast('Error', error, 'error') 185 | 186 | const solution = queryClient.getQueryData(['solution']) as { 187 | code: string 188 | thoughts: string[] 189 | time_complexity: string 190 | space_complexity: string 191 | } | null 192 | 193 | if (!solution) { 194 | setView('queue') 195 | } 196 | 197 | setSolutionData(solution?.code || null) 198 | setThoughtsData(solution?.thoughts || null) 199 | setTimeComplexityData(solution?.time_complexity || null) 200 | setSpaceComplexityData(solution?.space_complexity || null) 201 | console.log('processing error', error) 202 | }) 203 | ] 204 | 205 | return () => { 206 | cleanupFunctions.forEach((fn) => fn()) 207 | resizeObserver.disconnect() 208 | } 209 | }, [isTooltipVisible, tooltipHeight]) 210 | 211 | const handleDeleteExtraScreenshot = async (index: number) => { 212 | const screenshotToDelete = extraScreenshots[index] 213 | 214 | try { 215 | const response = await window.electronAPI.deleteScreenshot(screenshotToDelete.path) 216 | 217 | if (response.success) { 218 | const existing = await window.electronAPI.getScreenshots() 219 | const screenshots = (Array.isArray(existing) ? existing : []).map((screenshot) => ({ 220 | id: screenshot.path, 221 | path: screenshot.path, 222 | preview: screenshot.preview, 223 | timestamp: Date.now() 224 | })) 225 | 226 | setExtraScreenshots(screenshots) 227 | } else { 228 | console.error('Failed to delete screenshot:', response.error) 229 | showToast('Error', 'Failed to delete screenshot', 'error') 230 | } 231 | } catch (error) { 232 | console.error('Error deleting screenshot:', error) 233 | showToast('Error', 'Failed to delete screenshot', 'error') 234 | } 235 | } 236 | 237 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 238 | setIsTooltipVisible(visible) 239 | setTooltipHeight(height) 240 | } 241 | 242 | return ( 243 | <> 244 | {!isResetting && queryClient.getQueryData(['new_solution']) ? ( 245 | 251 | ) : ( 252 |
253 |
254 | {solutionData && ( 255 |
256 |
257 |
258 | 263 |
264 |
265 |
266 | )} 267 | 268 | 275 | 276 |
277 |
278 |
279 | {!solutionData && ( 280 | <> 281 | 286 | {problemStatementData && ( 287 |
288 |

289 | Generating solution... 290 |

291 |
292 | )} 293 | 294 | )} 295 | 296 | {solutionData && ( 297 | <> 298 | 303 |
304 | {thoughtsData.map((thought, index) => ( 305 |
306 |
307 |
{thought}
308 |
309 | ))} 310 |
311 |
312 | ) 313 | } 314 | isLoading={!thoughtsData} 315 | /> 316 | 317 | 323 | 324 | 329 | 330 | )} 331 |
332 |
333 |
334 |
335 |
336 | )} 337 | 338 | ) 339 | } 340 | 341 | export default Solutions 342 | -------------------------------------------------------------------------------- /src/renderer/src/components/solutions/solution-commands.tsx: -------------------------------------------------------------------------------- 1 | import { Screenshot } from '@renderer/lib/types' 2 | import { useEffect, useRef, useState } from 'react' 3 | import { useToast } from '@renderer/providers/toast-context' 4 | import { COMMAND_KEY } from '@renderer/lib/utils' 5 | import { LanguageSelector } from '../language-selector' 6 | 7 | export interface SolutionCommandsProps { 8 | onTooltipVisibilityChange: (visible: boolean, height: number) => void 9 | isProcessing: boolean 10 | screenshots?: Screenshot[] 11 | extraScreenshots?: Screenshot[] 12 | currentLanguage: string 13 | setLanguage: (language: string) => void 14 | } 15 | 16 | const SolutionCommands: React.FC = ({ 17 | onTooltipVisibilityChange, 18 | isProcessing, 19 | extraScreenshots = [], 20 | currentLanguage, 21 | setLanguage 22 | }) => { 23 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 24 | const tooltipRef = useRef(null) 25 | const { showToast } = useToast() 26 | 27 | useEffect(() => { 28 | if (onTooltipVisibilityChange) { 29 | let tooltipHeight = 0 30 | if (tooltipRef.current && isTooltipVisible) { 31 | tooltipHeight = tooltipRef.current.offsetHeight + 10 32 | } 33 | onTooltipVisibilityChange(isTooltipVisible, tooltipHeight) 34 | } 35 | }, [isTooltipVisible, onTooltipVisibilityChange]) 36 | 37 | const handleMouseEnter = () => { 38 | setIsTooltipVisible(true) 39 | } 40 | 41 | const handleMouseLeave = () => { 42 | setIsTooltipVisible(false) 43 | } 44 | 45 | return ( 46 |
47 |
48 |
49 |
{ 52 | try { 53 | const result = await window.electronAPI.toggleMainWindow() 54 | if (!result.success) { 55 | console.error('Failed to toggle main window', result.error) 56 | showToast('Error', 'Failed to toggle main window', 'error') 57 | } 58 | } catch (error) { 59 | console.error('Failed to toggle main window', error) 60 | showToast('Error', 'Failed to toggle main window', 'error') 61 | } 62 | }} 63 | > 64 | Show/Hide 65 |
66 | 69 | 72 |
73 |
74 | 75 | {!isProcessing && ( 76 | <> 77 |
{ 80 | try { 81 | const result = await window.electronAPI.triggerScreenshot() 82 | if (!result.success) { 83 | console.error('Failed to trigger screenshot', result.error) 84 | showToast('Error', 'Failed to trigger screenshot', 'error') 85 | } 86 | } catch (error) { 87 | console.error('Failed to trigger screenshot', error) 88 | showToast('Error', 'Failed to trigger screenshot', 'error') 89 | } 90 | }} 91 | > 92 | 93 | {extraScreenshots?.length === 0 ? 'Screenshot your code' : 'Screenshot'} 94 | 95 |
96 | 99 | 102 |
103 |
104 | 105 | {extraScreenshots?.length > 0 && ( 106 |
{ 109 | try { 110 | const result = await window.electronAPI.triggerProcessScreenshots() 111 | if (!result.success) { 112 | console.error('Failed to process screenshots', result.error) 113 | showToast('Error', 'Failed to process screenshots', 'error') 114 | } 115 | } catch (error) { 116 | console.error('Failed to process screenshots', error) 117 | showToast('Error', 'Failed to process screenshots', 'error') 118 | } 119 | }} 120 | > 121 | Debug 122 |
123 | 126 | 129 |
130 |
131 | )} 132 | 133 | )} 134 | 135 |
{ 138 | try { 139 | const result = await window.electronAPI.triggerReset() 140 | if (!result.success) { 141 | console.error('Failed to reset', result.error) 142 | showToast('Error', 'Failed to reset', 'error') 143 | } 144 | } catch (error) { 145 | console.error('Failed to reset', error) 146 | showToast('Error', 'Failed to reset', 'error') 147 | } 148 | }} 149 | > 150 | Start Over 151 |
152 | 155 | 158 |
159 |
160 | 161 |
162 | 163 |
168 |
169 | 179 | 180 | 181 | 182 |
183 | 184 | {isTooltipVisible && ( 185 |
192 |
193 |
194 |
195 |

Keyboard Shortcuts

196 |
197 |
{ 200 | try { 201 | const result = await window.electronAPI.toggleMainWindow() 202 | if (!result.success) { 203 | console.log('Failed to toggle window:', result.error) 204 | showToast('Error', 'Failed to toggle window', 'error') 205 | } 206 | } catch (error) { 207 | console.error('Error toggling window:', error) 208 | showToast('Error', 'Failed to toggle window', 'error') 209 | } 210 | }} 211 | > 212 |
213 | Toggle Window 214 |
215 | 216 | {COMMAND_KEY} 217 | 218 | 219 | B 220 | 221 |
222 |
223 |

224 | Show or hide this window 225 |

226 |
227 | {!isProcessing && ( 228 | <> 229 |
{ 232 | try { 233 | const result = await window.electronAPI.triggerScreenshot() 234 | if (!result.success) { 235 | console.log('Failed to trigger screenshot:', result.error) 236 | showToast('Error', 'Failed to trigger screenshot', 'error') 237 | } 238 | } catch (error) { 239 | console.error('Error triggering screenshot:', error) 240 | showToast('Error', 'Failed to trigger screenshot', 'error') 241 | } 242 | }} 243 | > 244 |
245 | Take Screenshot 246 |
247 | 248 | {COMMAND_KEY} 249 | 250 | 251 | H 252 | 253 |
254 |
255 |

256 | Take a screenshot of the problem statement 257 |

258 |
259 | 260 | )} 261 | 262 | {extraScreenshots?.length > 0 && ( 263 |
{ 266 | try { 267 | const result = await window.electronAPI.triggerProcessScreenshots() 268 | if (!result.success) { 269 | console.error('Failed to process screenshots', result.error) 270 | showToast('Error', 'Failed to process screenshots', 'error') 271 | } 272 | } catch (error) { 273 | console.error('Error processing screenshots:', error) 274 | showToast('Error', 'Failed to process screenshots', 'error') 275 | } 276 | }} 277 | > 278 |
279 | Solve Problem 280 |
281 | 282 | {COMMAND_KEY} 283 | 284 | 285 | ↵ 286 | 287 |
288 |
289 |

290 | Generate new solutions based on all previous and newly added 291 | screenshots. 292 |

293 |
294 | )} 295 |
296 | 297 |
298 | 302 | 303 |
304 |
305 | AI API Settings 306 | 312 |
313 |
314 |
315 |
316 |
317 |
318 | )} 319 |
320 |
321 |
322 |
323 | ) 324 | } 325 | 326 | export default SolutionCommands 327 | -------------------------------------------------------------------------------- /src/renderer/src/components/solutions/solution-section.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' 3 | import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism' 4 | 5 | export const SolutionSection = ({ 6 | title, 7 | content, 8 | isLoading, 9 | currentLanguage 10 | }: { 11 | title: string 12 | content: React.ReactNode 13 | isLoading: boolean 14 | currentLanguage: string 15 | }) => { 16 | const [copied, setCopied] = useState(false) 17 | 18 | const copyToClipboard = () => { 19 | if (typeof content === 'string') { 20 | navigator.clipboard.writeText(content).then(() => { 21 | setCopied(true) 22 | setTimeout(() => { 23 | setCopied(false) 24 | }, 2000) 25 | }) 26 | } 27 | } 28 | return ( 29 |
30 |

{title}

31 | {isLoading ? ( 32 |
33 |
34 |

35 | Loading solution... 36 |

37 |
38 |
39 | ) : ( 40 |
41 | 47 | 61 | {content as string} 62 | 63 |
64 | )} 65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Slot } from '@radix-ui/react-slot' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '../../lib/utils' 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 13 | destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 14 | outline: 15 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 16 | secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 17 | ghost: 'hover:bg-accent hover:text-accent-foreground', 18 | link: 'text-primary underline-offset-4 hover:underline' 19 | }, 20 | size: { 21 | default: 'h-9 px-4 py-2', 22 | sm: 'h-8 rounded-md px-3 text-xs', 23 | lg: 'h-10 rounded-md px-8', 24 | icon: 'h-9 w-9' 25 | } 26 | }, 27 | defaultVariants: { 28 | variant: 'default', 29 | size: 'default' 30 | } 31 | } 32 | ) 33 | 34 | export interface ButtonProps 35 | extends React.ButtonHTMLAttributes, 36 | VariantProps { 37 | asChild?: boolean 38 | } 39 | 40 | const Button = React.forwardRef( 41 | ({ className, variant, size, asChild = false, ...props }, ref) => { 42 | const Comp = asChild ? Slot : 'button' 43 | return ( 44 | 45 | ) 46 | } 47 | ) 48 | Button.displayName = 'Button' 49 | 50 | // eslint-disable-next-line react-refresh/only-export-components 51 | export { Button, buttonVariants } 52 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as DialogPrimitive from '@radix-ui/react-dialog' 3 | import { X } from 'lucide-react' 4 | 5 | import { cn } from '../../lib/utils' 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 55 |
56 | ) 57 | DialogHeader.displayName = 'DialogHeader' 58 | 59 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 60 |
64 | ) 65 | DialogFooter.displayName = 'DialogFooter' 66 | 67 | const DialogTitle = React.forwardRef< 68 | React.ElementRef, 69 | React.ComponentPropsWithoutRef 70 | >(({ className, ...props }, ref) => ( 71 | 76 | )) 77 | DialogTitle.displayName = DialogPrimitive.Title.displayName 78 | 79 | const DialogDescription = React.forwardRef< 80 | React.ElementRef, 81 | React.ComponentPropsWithoutRef 82 | >(({ className, ...props }, ref) => ( 83 | 88 | )) 89 | DialogDescription.displayName = DialogPrimitive.Description.displayName 90 | 91 | export { 92 | Dialog, 93 | DialogPortal, 94 | DialogOverlay, 95 | DialogTrigger, 96 | DialogClose, 97 | DialogContent, 98 | DialogHeader, 99 | DialogFooter, 100 | DialogTitle, 101 | DialogDescription 102 | } 103 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '../../lib/utils' 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = 'Input' 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ToastPrimitives from '@radix-ui/react-toast' 3 | import { cn } from '../../lib/utils' 4 | import { AlertCircle, CheckCircle2, Info, X } from 'lucide-react' 5 | 6 | const ToastProvider = ToastPrimitives.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 = ToastPrimitives.Viewport.displayName 28 | 29 | type ToastVariant = 'neutral' | 'success' | 'error' 30 | 31 | interface ToastProps extends React.ComponentPropsWithoutRef { 32 | variant?: ToastVariant 33 | swipeDirection?: 'right' | 'left' | 'up' | 'down' 34 | } 35 | 36 | const toastVariants: Record = { 37 | neutral: { 38 | icon: , 39 | bgColor: 'bg-white border-gray-200 text-gray-900' 40 | }, 41 | success: { 42 | icon: , 43 | bgColor: 'bg-green-50 border-green-200 text-green-900' 44 | }, 45 | error: { 46 | icon: , 47 | bgColor: 'bg-red-50 border-red-200 text-red-900' 48 | } 49 | } 50 | 51 | const Toast = React.forwardRef< 52 | React.ElementRef, 53 | React.ComponentPropsWithoutRef & { 54 | variant?: 'success' | 'error' | 'neutral' 55 | } 56 | >(({ className, variant = 'neutral', ...props }, ref) => { 57 | const variantStyles = { 58 | neutral: 'bg-white border-gray-200 text-gray-900', 59 | success: 'bg-green-50 border-green-200 text-green-900', 60 | error: 'bg-red-50 border-red-200 text-red-900' 61 | } 62 | 63 | return ( 64 | 73 | {toastVariants[variant].icon} 74 |
{props.children}
75 | 76 | 77 | 78 |
79 | ) 80 | }) 81 | Toast.displayName = ToastPrimitives.Root.displayName 82 | 83 | const ToastAction = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 92 | )) 93 | ToastAction.displayName = ToastPrimitives.Action.displayName 94 | 95 | const ToastTitle = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 100 | )) 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName 102 | 103 | const ToastDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName 114 | 115 | export type { ToastProps, ToastVariant } 116 | export { ToastProvider, ToastViewport, Toast, ToastAction, ToastTitle, ToastDescription } 117 | -------------------------------------------------------------------------------- /src/renderer/src/components/welcome-screen.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from './ui/button' 2 | 3 | interface WelcomeScreenProps { 4 | onOpenSettings: () => void 5 | } 6 | 7 | export const WelcomeScreen = ({ onOpenSettings }: WelcomeScreenProps): React.JSX.Element => { 8 | return ( 9 |
10 |
11 |

12 | Silent Coder 13 |

14 | 15 |
16 |

Welcome to Silent Coder

17 |

18 | This application helps you by providing AI powered solutions to coding problems. 19 |

20 |
21 |

Global Shortcuts

22 |
    23 |
  • 24 | Toggle Visibility 25 | Ctrl+B / Cmd+B 26 |
  • 27 |
  • 28 | Take Screenshot 29 | Ctrl+H / Cmd+H 30 |
  • 31 |
  • 32 | Delete Last Screenshot 33 | Ctrl+L / Cmd+L 34 |
  • 35 |
  • 36 | Process Screenshot 37 | Ctrl+Enter / Cmd+Enter 38 |
  • 39 |
  • 40 | Reset View 41 | Ctrl+R / Cmd+R 42 |
  • 43 |
  • 44 | Quit App 45 | Ctrl+Q / Cmd+Q 46 |
  • 47 |
48 |
49 |
50 | 51 |
52 |

Getting Started

53 |

54 | Before using the application, you need to set up your API key. 55 |

56 | 62 |
63 | 64 |
65 | Start by taking screenshots of your coding problem (Ctrl+H / Cmd+H) 66 |
67 |
68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/renderer/src/lib/languages.ts: -------------------------------------------------------------------------------- 1 | // Define and export the language options 2 | export const languageOptions = [ 3 | { value: 'python', label: 'Python' }, 4 | { value: 'javascript', label: 'JavaScript' }, 5 | { value: 'typescript', label: 'TypeScript' }, 6 | { value: 'java', label: 'Java' }, 7 | { value: 'csharp', label: 'C#' }, 8 | { value: 'go', label: 'Go' }, 9 | { value: 'ruby', label: 'Ruby' }, 10 | { value: 'php', label: 'PHP' }, 11 | { value: 'kotlin', label: 'Kotlin' }, 12 | { value: 'swift', label: 'Swift' }, 13 | { value: 'rust', label: 'Rust' }, 14 | { value: 'scala', label: 'Scala' }, 15 | { value: 'cpp', label: 'C++' }, 16 | { value: 'sql', label: 'SQL' }, 17 | { value: 'r', label: 'R' } 18 | ] 19 | -------------------------------------------------------------------------------- /src/renderer/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProblemStatementData { 2 | problem_statement: string 3 | input_format: { 4 | description: string 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | parameters: any 7 | } 8 | output_format: { 9 | description: string 10 | type: string 11 | subtype: string 12 | } 13 | complexity: { 14 | time: string 15 | space: string 16 | } 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | test_cases: any[] 19 | validation_type: string 20 | difficulty: string 21 | } 22 | 23 | export interface Solution { 24 | initial_thoughts: string[] 25 | thought_steps: string[] 26 | description: string 27 | code: string 28 | } 29 | 30 | export interface SolutionsResponse { 31 | [key: string]: Solution 32 | } 33 | 34 | export interface Screenshot { 35 | path: string 36 | preview: string 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | const getPlatform = () => { 9 | try { 10 | return window.electronAPI.getPlatform() || 'darwin' 11 | } catch { 12 | return 'darwin' 13 | } 14 | } 15 | 16 | export const COMMAND_KEY = getPlatform() === 'darwin' ? '⌘' : 'ctrl' 17 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { StrictMode } from 'react' 4 | import { createRoot } from 'react-dom/client' 5 | import App from './App' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/renderer/src/providers/query-provider.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | 3 | const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | staleTime: 0, 7 | gcTime: Infinity, 8 | retry: 1, 9 | refetchOnWindowFocus: false 10 | }, 11 | mutations: { 12 | retry: 1 13 | } 14 | } 15 | }) 16 | 17 | export const QueryProvider = ({ children }: { children: React.ReactNode }): React.JSX.Element => { 18 | return {children} 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/providers/toast-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | type ToastVariant = 'neutral' | 'success' | 'error' 4 | 5 | interface ToastContextType { 6 | showToast: (title: string, description: string, variant: ToastVariant) => void 7 | } 8 | 9 | export const ToastContext = createContext(undefined) 10 | 11 | export function useToast(): ToastContextType { 12 | const context = useContext(ToastContext) 13 | 14 | if (!context) { 15 | throw new Error('useToast must be used within a ToastProvider') 16 | } 17 | 18 | return context 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/providers/toast-provider.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { ToastContext } from './toast-context' 3 | 4 | import { 5 | Toast, 6 | ToastTitle, 7 | ToastDescription, 8 | ToastProvider as ToastPrimitivesProvider, 9 | ToastViewport 10 | } from '../components/ui/toast' 11 | 12 | type ToastVariant = 'neutral' | 'success' | 'error' 13 | 14 | interface ToastState { 15 | open: boolean 16 | title: string 17 | description: string 18 | variant: ToastVariant 19 | } 20 | 21 | interface ToastProviderProps { 22 | children: React.ReactNode 23 | } 24 | 25 | export function ToastProvider({ children }: ToastProviderProps): React.JSX.Element { 26 | const [toastState, setToastState] = useState({ 27 | open: false, 28 | title: '', 29 | description: '', 30 | variant: 'neutral' 31 | }) 32 | 33 | const showToast = (title: string, description: string, variant: ToastVariant) => { 34 | setToastState({ open: true, title, description, variant }) 35 | } 36 | 37 | return ( 38 | 39 | 40 | {children} 41 | 42 | setToastState((prev) => ({ ...prev, open }))} 45 | variant={toastState.variant} 46 | duration={1500} 47 | > 48 | {toastState.title} 49 | {toastState.description} 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/**/*.tsx", 7 | "src/preload/*.d.ts" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "jsx": "react-jsx", 12 | "baseUrl": ".", 13 | "paths": { 14 | "@renderer/*": [ 15 | "src/renderer/src/*" 16 | ] 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------