├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE-SHORT ├── README.md ├── assets └── icons │ ├── mac │ └── icon.icns │ └── win │ └── icon.ico ├── build └── entitlements.mac.plist ├── bun.lockb ├── cleanup-for-github.bat ├── cleanup-for-github.sh ├── config.js ├── electron ├── ConfigHelper.ts ├── ProcessingHelper.ts ├── ScreenshotHelper.ts ├── autoUpdater.ts ├── ipcHandlers.ts ├── main.ts ├── preload.ts ├── shortcuts.ts ├── store.ts └── tsconfig.json ├── env.d.ts ├── eslint.config.mjs ├── index.html ├── invisible_launcher.vbs ├── package-lock.json ├── package.json ├── postcss.config.js ├── renderer ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts └── tsconfig.json ├── src ├── App.tsx ├── _pages │ ├── Debug.tsx │ ├── Queue.tsx │ ├── Solutions.tsx │ ├── SubscribePage.tsx │ └── SubscribedApp.tsx ├── components │ ├── Header │ │ └── Header.tsx │ ├── Queue │ │ ├── QueueCommands.tsx │ │ ├── ScreenshotItem.tsx │ │ └── ScreenshotQueue.tsx │ ├── Settings │ │ └── SettingsDialog.tsx │ ├── Solutions │ │ └── SolutionCommands.tsx │ ├── UpdateNotification.tsx │ ├── WelcomeScreen.tsx │ ├── shared │ │ └── LanguageSelector.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── toast.tsx ├── contexts │ └── toast.tsx ├── env.d.ts ├── index.css ├── lib │ ├── supabase.ts │ └── utils.ts ├── main.tsx ├── types │ ├── electron.d.ts │ ├── global.d.ts │ ├── index.tsx │ ├── screenshots.ts │ └── solutions.ts ├── utils │ └── platform.ts └── vite-env.d.ts ├── stealth-run.bat ├── stealth-run.sh ├── tailwind.config.js ├── tsconfig.electron.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | release/Interview[[:space:]]Coder-1.0.0-arm64-mac.zip filter=lfs diff=lfs merge=lfs -text 2 | release/Interview[[:space:]]Coder-1.0.0-arm64.dmg filter=lfs diff=lfs merge=lfs -text 3 | release/mac-arm64/Interview[[:space:]]Coder.app/Contents/Frameworks/Electron[[:space:]]Framework.framework/Versions/A/Electron[[:space:]]Framework filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | build-and-test: 12 | name: Build and Test on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: 'npm' 27 | 28 | - name: Install dependencies 29 | run: npm i 30 | 31 | - name: Clearing previous builds 32 | run: npm run clean 33 | 34 | - name: Run Linter 35 | run: npm run lint 36 | continue-on-error: true 37 | 38 | - name: Type Check 39 | run: npx tsc --noEmit 40 | continue-on-error: true 41 | 42 | - name: Build 43 | run: npm run build 44 | 45 | - name: Run Tests 46 | run: npm test 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | pull_request_target: 20 | branches: [ "main" ] 21 | schedule: 22 | - cron: '18 13 * * 2' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze (${{ matrix.language }}) 27 | # Runner size impacts CodeQL analysis time. To learn more, please see: 28 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 29 | # - https://gh.io/supported-runners-and-hardware-resources 30 | # - https://gh.io/using-larger-runners (GitHub.com only) 31 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 32 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 33 | permissions: 34 | # required for all workflows 35 | security-events: write 36 | 37 | # required to fetch internal or private CodeQL packs 38 | packages: read 39 | 40 | # only required for workflows in private repositories 41 | actions: read 42 | contents: read 43 | 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | include: 48 | - language: javascript-typescript 49 | build-mode: none 50 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 51 | # Use `c-cpp` to analyze code written in C, C++ or both 52 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 53 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 54 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 55 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 56 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 57 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v4 61 | 62 | # Add any setup steps before running the `github/codeql-action/init` action. 63 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 64 | # or others). This is typically only required for manual builds. 65 | # - name: Setup runtime (example) 66 | # uses: actions/setup-example@v1 67 | 68 | # Initializes the CodeQL tools for scanning. 69 | - name: Initialize CodeQL 70 | uses: github/codeql-action/init@v3 71 | with: 72 | languages: ${{ matrix.language }} 73 | build-mode: ${{ matrix.build-mode }} 74 | # If you wish to specify custom queries, you can do so here or in a config file. 75 | # By default, queries listed here will override any specified in a config file. 76 | # Prefix the list here with "+" to use these queries and those in the config file. 77 | 78 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 79 | # queries: security-extended,security-and-quality 80 | 81 | # If the analyze step fails for one of the languages you are analyzing with 82 | # "We were unable to automatically build your code", modify the matrix above 83 | # to set the build mode to "manual" for that language. Then modify this step 84 | # to build your code. 85 | # ℹ️ Command-line programs to run using the OS shell. 86 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 87 | - if: matrix.build-mode == 'manual' 88 | shell: bash 89 | run: | 90 | echo 'If you are using a "manual" build mode for one or more of the' \ 91 | 'languages you are analyzing, replace this with the commands to build' \ 92 | 'your code, for example:' 93 | echo ' make bootstrap' 94 | echo ' make release' 95 | exit 1 96 | 97 | - name: Perform CodeQL Analysis 98 | uses: github/codeql-action/analyze@v3 99 | with: 100 | category: "/language:${{matrix.language}}" 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist-electron 3 | release 4 | dist 5 | .env 6 | .env.* 7 | **/.DS_Store 8 | **/.vscode 9 | **/.idea 10 | scripts/ 11 | **/scripts/ 12 | scripts/manual-notarize.js -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Interview Coder - Unlocked Edition - Changes 2 | 3 | ## Major Architectural Changes 4 | 5 | ### Removal of Supabase Authentication System 6 | 1. **Complete Removal of Supabase Dependencies**: 7 | - Removed all Supabase code, imports, and API calls 8 | - Eliminated the authentication system completely 9 | - Removed all subscription and payment-related code 10 | 11 | 2. **Replaced with Local Configuration**: 12 | - Added `ConfigHelper.ts` for local storage of settings 13 | - Implemented direct OpenAI API integration 14 | - Created a simplified settings system with model selection 15 | 16 | 3. **User Interface Simplification**: 17 | - Removed login/signup screens 18 | - Added a welcome screen for new users 19 | - Added comprehensive settings dialog for API key and model management 20 | 21 | ## Fixes and Improvements 22 | 23 | ### UI Improvements 24 | 1. **Fixed Language Dropdown Functionality**: 25 | - Enabled the language dropdown in the settings panel 26 | - Added proper language change handling 27 | - Made language selection persist across sessions 28 | 29 | 2. **Implemented Working Logout Button**: 30 | - Added proper API key clearing functionality to the logout button 31 | - Added success feedback via toast message 32 | - Implemented app reload after logout to reset state 33 | 34 | 3. **Fixed Window Size Issues**: 35 | - Added explicit window dimensions in main.ts (width: 800px, height: 600px) 36 | - Added minimum window size constraints to prevent UI issues 37 | - Improved dimension handling with fallback sizes 38 | 39 | 4. **Improved Settings Dialog Positioning**: 40 | - Made settings dialog responsive with min/max constraints 41 | - Added z-index to ensure dialog appears above other content 42 | - Improved positioning to center properly regardless of window size 43 | 44 | 5. **Enhanced Dropdown Initialization**: 45 | - Improved dropdown initialization timing 46 | - Reduced initialization delay for better responsiveness 47 | 48 | ### Maintaining Original UI Design 49 | - Preserved the original UI design and interaction patterns 50 | - Fixed functionality within the existing UI rather than adding new elements 51 | - Kept the settings accessible through the gear icon menu 52 | 53 | These changes fix the issues while preserving the original app's look and feel, just removing the payment restrictions and making everything work properly. 54 | 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Interview Coder - Unlocked Edition 2 | 3 | Thank you for your interest in contributing to Interview Coder - Unlocked Edition! This free, open-source tool exists to empower the coding community with accessible interview preparation resources, and your efforts can make it even better. We're thrilled to have you join us in this collaborative journey! 4 | 5 | ## Our Community Values 6 | 7 | We're building a supportive and inclusive environment based on the following principles: 8 | 9 | - **Collaborative Development**: We grow stronger by working together—let's avoid duplicating efforts and unite our skills. 10 | - **Open Access**: This tool is for everyone, and we aim to keep it free and accessible to all who need it. 11 | - **Continuous Improvement**: Every contribution, big or small, helps refine features like AI-powered analysis, invisibility, and debugging assistance. 12 | 13 | ## How to Get Started 14 | 15 | ### 1. Fork the Repository 16 | 17 | - Visit the repository at [github.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource](https://github.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource). 18 | - Click the "Fork" button to create your own copy. 19 | - Clone your fork locally: 20 | ```bash 21 | git clone https://github.com/YOUR-USERNAME/interview-coder-withoupaywall-opensource.git 22 | ``` 23 | - Set up the upstream remote to sync with the original: 24 | ```bash 25 | git remote add upstream https://github.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource.git 26 | ``` 27 | 28 | ### 2. Create a Branch 29 | 30 | - Create a descriptive branch for your work: 31 | ```bash 32 | git checkout -b feature/your-feature-name 33 | ``` 34 | - Examples: `feature/add-python-support`, `bugfix/fix-screenshot-capture`. 35 | - Keep branches focused on a single feature or fix to streamline reviews. 36 | 37 | ### 3. Make and Test Your Changes 38 | 39 | - Implement your improvements or fixes in the codebase (e.g., `electron/ProcessingHelper.ts`, `src/components/Settings/SettingsDialog.tsx`). 40 | - Test thoroughly, especially for features like screenshot capture or AI integration, to ensure compatibility with macOS, Windows, and Linux. 41 | - Follow the existing code style (TypeScript, React, Tailwind CSS) and run: 42 | 43 | ### 4. Commit and Push 44 | 45 | - Commit your changes with clear messages: 46 | ```bash 47 | git commit -m "feat: add Python language support with detailed testing" 48 | ``` 49 | - Push to your fork: 50 | ```bash 51 | git push origin feature/your-feature-name 52 | ``` 53 | 54 | ### 5. Submit a Pull Request (PR) 55 | 56 | - Go to the original repository and click "New Pull Request". 57 | - Select your branch and create the PR. 58 | - Provide a detailed description: 59 | - What problem does it solve? 60 | - How was it tested? 61 | - Reference related issues (e.g., `Fixes #123`). 62 | - Assign reviewers (e.g., `@anshumansingh01`, `@bhaumikmaan`) if applicable. 63 | 64 | ## Contribution Guidelines 65 | 66 | To ensure a smooth and high-quality collaboration, please follow these guidelines: 67 | 68 | ### Code Quality 69 | 70 | - Write clear, well-documented code. Add comments for complex logic (e.g., AI model integration in `ProcessingHelper.ts`). 71 | - Test your changes locally using the provided `stealth-run.sh` or `stealth-run.bat` scripts. 72 | - Avoid breaking existing features (e.g., invisibility, screenshot capture). 73 | 74 | ### PR Workflow 75 | 76 | - All changes must be submitted via a PR to the main branch, as required by our branch protection rules. 77 | - PRs require 2 approving reviews, including an independent approval of the most recent push (someone other than the pusher). 78 | - Resolve all code-related conversations before merging—unresolved feedback will block the PR. 79 | - Expect stale approvals to be dismissed if new commits are pushed, ensuring reviews reflect the latest code. 80 | 81 | ## Licensing and Attribution 82 | 83 | This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). As a contributor: 84 | 85 | - **Contribute Back**: Submit your improvements as PRs to the main repository to benefit the community. 86 | - **Share Openly**: If you modify and deploy the software (e.g., on a server), make your source code available to users under AGPL-3.0. 87 | - **Maintain Attribution**: Preserve original copyright notices and license text in your modifications. 88 | 89 | ## Community Etiquette 90 | 91 | - Be respectful and constructive in comments and reviews. 92 | - Actively review others' PRs to share the workload—your input is valuable! 93 | - Report bugs or suggest features by opening an issue with: 94 | - A clear title. 95 | - Steps to reproduce. 96 | - Expected vs. actual behavior. 97 | 98 | ## Development Tips 99 | 100 | - **Environment Setup**: Ensure Node.js (v16+) and npm/bun are installed. Grant screen recording permissions (see README.md for details). 101 | - **API Usage**: Be mindful of OpenAI API costs when testing AI features. 102 | - **Troubleshooting**: Run `npm run clean` before builds if issues arise, and use Ctrl+B/Cmd+B to toggle visibility. 103 | 104 | ## Maintainer Notes 105 | 106 | As the primary maintainer, I (Ornithopter-pilot) oversee merges and ensure stability. Your PRs will be reviewed, but I rely on your help to maintain quality. Please: 107 | 108 | - Ping me (@Ornithopter-pilot) if a PR is urgent. 109 | - Be patient—high PR volume may delay responses. 110 | 111 | ## Thank You! 112 | 113 | Your contributions make Interview Coder - Unlocked Edition a powerful, community-owned tool. Whether it's adding language support, enhancing UI, or fixing bugs, every effort counts. Let's build something amazing together! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to -------------------------------------------------------------------------------- /LICENSE-SHORT: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2025 Interview Coder - Unlocked Edition 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | 19 | The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.html 20 | 21 | ADDITIONAL TERMS: 22 | - Any modifications made to this software should be contributed back to the main project repository. 23 | - If you deploy this software on a network server, you must make the complete source code of your modifications available to all users of that server. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeInterviewAssist 2 | 3 | > ## ⚠️ IMPORTANT NOTICE TO THE COMMUNITY ⚠️ 4 | > 5 | > **This is a free, open-source initiative - NOT a full-service product!** 6 | > 7 | > There are numerous paid interview preparation tools charging hundreds of dollars for comprehensive features like live audio capture, automated answer generation, and more. This project is fundamentally different: 8 | > 9 | > - This is a **small, non-profit, community-driven project** with zero financial incentive behind it 10 | > - The entire codebase is freely available for anyone to use, modify, or extend 11 | > - Want features like voice support? You're welcome to integrate tools like OpenAI's Whisper or other APIs 12 | > - New features should come through **community contributions** - it's unreasonable to expect a single maintainer to implement premium features for free 13 | > - The maintainer receives no portfolio benefit, monetary compensation, or recognition for this work 14 | > 15 | > **Before submitting feature requests or expecting personalized support, please understand this project exists purely as a community resource.** If you value what's been created, the best way to show appreciation is by contributing code, documentation, or helping other users. 16 | 17 | > ## 🔑 API KEY INFORMATION - UPDATED 18 | > 19 | > We have tested and confirmed that **both Gemini and OpenAI APIs work properly** with the current version. If you are experiencing issues with your API keys: 20 | > 21 | > - Try deleting your API key entry from the config file located in your user data directory 22 | > - Log out and log back in to the application 23 | > - Check your API key dashboard to verify the key is active and has sufficient credits 24 | > - Ensure you're using the correct API key format (OpenAI keys start with "sk-") 25 | > 26 | > The configuration file is stored at: `C:\Users\[USERNAME]\AppData\Roaming\interview-coder-v1\config.json` (on Windows) or `/Users/[USERNAME]/Library/Application Support/interview-coder-v1/config.json` (on macOS) 27 | 28 | ## Free, Open-Source AI-Powered Interview Preparation Tool 29 | 30 | This project provides a powerful alternative to premium coding interview platforms. It delivers the core functionality of paid interview preparation tools but in a free, open-source package. Using your own OpenAI API key, you get access to advanced features like AI-powered problem analysis, solution generation, and debugging assistance - all running locally on your machine. 31 | 32 | ### Why This Exists 33 | 34 | The best coding interview tools are often behind expensive paywalls, making them inaccessible to many students and job seekers. This project provides the same powerful functionality without the cost barrier, letting you: 35 | 36 | - Use your own API key (pay only for what you use) 37 | - Run everything locally on your machine with complete privacy 38 | - Make customizations to suit your specific needs 39 | - Learn from and contribute to an open-source tool 40 | 41 | ### Customization Possibilities 42 | 43 | The codebase is designed to be adaptable: 44 | 45 | - **AI Models**: Though currently using OpenAI's models, you can modify the code to integrate with other providers like Claude, Deepseek, Llama, or any model with an API. All integration code is in `electron/ProcessingHelper.ts` and UI settings are in `src/components/Settings/SettingsDialog.tsx`. 46 | - **Languages**: Add support for additional programming languages 47 | - **Features**: Extend the functionality with new capabilities 48 | - **UI**: Customize the interface to your preferences 49 | 50 | All it takes is modest JavaScript/TypeScript knowledge and understanding of the API you want to integrate. 51 | 52 | ## Features 53 | 54 | - 🎯 99% Invisibility: Undetectable window that bypasses most screen capture methods 55 | - 📸 Smart Screenshot Capture: Capture both question text and code separately for better analysis 56 | - 🤖 AI-Powered Analysis: Automatically extracts and analyzes coding problems using GPT-4o 57 | - 💡 Solution Generation: Get detailed explanations and solutions with time/space complexity analysis 58 | - 🔧 Real-time Debugging: Debug your code with AI assistance and structured feedback 59 | - 🎨 Advanced Window Management: Freely move, resize, change opacity, and zoom the window 60 | - 🔄 Model Selection: Choose between GPT-4o and GPT-4o-mini for different processing stages 61 | - 🔒 Privacy-Focused: Your API key and data never leave your computer except for OpenAI API calls 62 | 63 | ## Global Commands 64 | 65 | The application uses unidentifiable global keyboard shortcuts that won't be detected by browsers or other applications: 66 | 67 | - Toggle Window Visibility: [Control or Cmd + B] 68 | - Move Window: [Control or Cmd + Arrow keys] 69 | - Take Screenshot: [Control or Cmd + H] 70 | - Delete Last Screenshot: [Control or Cmd + L] 71 | - Process Screenshots: [Control or Cmd + Enter] 72 | - Start New Problem: [Control or Cmd + R] 73 | - Quit: [Control or Cmd + Q] 74 | - Decrease Opacity: [Control or Cmd + [] 75 | - Increase Opacity: [Control or Cmd + ]] 76 | - Zoom Out: [Control or Cmd + -] 77 | - Reset Zoom: [Control or Cmd + 0] 78 | - Zoom In: [Control or Cmd + =] 79 | 80 | ## Invisibility Compatibility 81 | 82 | The application is invisible to: 83 | 84 | - Zoom versions below 6.1.6 (inclusive) 85 | - All browser-based screen recording software 86 | - All versions of Discord 87 | - Mac OS _screenshot_ functionality (Command + Shift + 3/4) 88 | 89 | Note: The application is **NOT** invisible to: 90 | 91 | - Zoom versions 6.1.6 and above 92 | - https://zoom.en.uptodown.com/mac/versions (link to downgrade Zoom if needed) 93 | - Mac OS native screen _recording_ (Command + Shift + 5) 94 | 95 | ## Prerequisites 96 | 97 | - Node.js (v16 or higher) 98 | - npm or bun package manager 99 | - OpenAI API Key 100 | - Screen Recording Permission for Terminal/IDE 101 | - On macOS: 102 | 1. Go to System Preferences > Security & Privacy > Privacy > Screen Recording 103 | 2. Ensure that CodeInterviewAssist has screen recording permission enabled 104 | 3. Restart CodeInterviewAssist after enabling permissions 105 | - On Windows: 106 | - No additional permissions needed 107 | - On Linux: 108 | - May require `xhost` access depending on your distribution 109 | 110 | ## Running the Application 111 | 112 | ### Quick Start 113 | 114 | 1. Clone the repository: 115 | 116 | ```bash 117 | git clone https://github.com/greeneu/interview-coder-withoupaywall-opensource.git 118 | cd interview-coder-withoupaywall-opensource 119 | ``` 120 | 121 | 2. Install dependencies: 122 | 123 | ```bash 124 | npm install 125 | ``` 126 | 127 | 3. **RECOMMENDED**: Clean any previous builds: 128 | 129 | ```bash 130 | npm run clean 131 | ``` 132 | 133 | 4. Run the appropriate script for your platform: 134 | 135 | **For Windows:** 136 | ```bash 137 | stealth-run.bat 138 | ``` 139 | 140 | **For macOS/Linux:** 141 | ```bash 142 | # Make the script executable first 143 | chmod +x stealth-run.sh 144 | ./stealth-run.sh 145 | ``` 146 | 147 | **IMPORTANT**: The application window will be invisible by default! Use Ctrl+B (or Cmd+B on Mac) to toggle visibility. 148 | 149 | ### Building Distributable Packages 150 | 151 | To create installable packages for distribution: 152 | 153 | **For macOS (DMG):** 154 | ```bash 155 | # Using npm 156 | npm run package-mac 157 | 158 | # Or using yarn 159 | yarn package-mac 160 | ``` 161 | 162 | **For Windows (Installer):** 163 | ```bash 164 | # Using npm 165 | npm run package-win 166 | 167 | # Or using yarn 168 | yarn package-win 169 | ``` 170 | 171 | The packaged applications will be available in the `release` directory. 172 | 173 | **What the scripts do:** 174 | - Create necessary directories for the application 175 | - Clean previous builds to ensure a fresh start 176 | - Build the application in production mode 177 | - Launch the application in invisible mode 178 | 179 | ### Notes & Troubleshooting 180 | 181 | - **Window Manager Compatibility**: Some window management tools (like Rectangle Pro on macOS) may interfere with the app's window movement. Consider disabling them temporarily. 182 | 183 | - **API Usage**: Be mindful of your OpenAI API key's rate limits and credit usage. Vision API calls are more expensive than text-only calls. 184 | 185 | - **LLM Customization**: You can easily customize the app to include LLMs like Claude, Deepseek, or Grok by modifying the API calls in `ProcessingHelper.ts` and related UI components. 186 | 187 | - **Common Issues**: 188 | - Run `npm run clean` before starting the app for a fresh build 189 | - Use Ctrl+B/Cmd+B multiple times if the window doesn't appear 190 | - Adjust window opacity with Ctrl+[/]/Cmd+[/] if needed 191 | - For macOS: ensure script has execute permissions (`chmod +x stealth-run.sh`) 192 | 193 | ## Comparison with Paid Interview Tools 194 | 195 | | Feature | Premium Tools (Paid) | CodeInterviewAssist (This Project) | 196 | |---------|------------------------|----------------------------------------| 197 | | Price | $60/month subscription | Free (only pay for your API usage) | 198 | | Solution Generation | ✅ | ✅ | 199 | | Debugging Assistance | ✅ | ✅ | 200 | | Invisibility | ✅ | ✅ | 201 | | Multi-language Support | ✅ | ✅ | 202 | | Time/Space Complexity Analysis | ✅ | ✅ | 203 | | Window Management | ✅ | ✅ | 204 | | Auth System | Required | None (Simplified) | 205 | | Payment Processing | Required | None (Use your own API key) | 206 | | Privacy | Server-processed | 100% Local Processing | 207 | | Customization | Limited | Full Source Code Access | 208 | | Model Selection | Limited | Choice Between Models | 209 | 210 | ## Tech Stack 211 | 212 | - Electron 213 | - React 214 | - TypeScript 215 | - Vite 216 | - Tailwind CSS 217 | - Radix UI Components 218 | - OpenAI API 219 | 220 | ## How It Works 221 | 222 | 1. **Initial Setup** 223 | - Launch the invisible window 224 | - Enter your OpenAI API key in the settings 225 | - Choose your preferred model for extraction, solution generation, and debugging 226 | 227 | 2. **Capturing Problem** 228 | - Use global shortcut [Control or Cmd + H] to take screenshots of code problems 229 | - Screenshots are automatically added to the queue of up to 2 230 | - If needed, remove the last screenshot with [Control or Cmd + L] 231 | 232 | 3. **Processing** 233 | - Press [Control or Cmd + Enter] to analyze the screenshots 234 | - AI extracts problem requirements from the screenshots using GPT-4 Vision API 235 | - The model generates an optimal solution based on the extracted information 236 | - All analysis is done using your personal OpenAI API key 237 | 238 | 4. **Solution & Debugging** 239 | - View the generated solutions with detailed explanations 240 | - Use debugging feature by taking more screenshots of error messages or code 241 | - Get structured analysis with identified issues, corrections, and optimizations 242 | - Toggle between solutions and queue views as needed 243 | 244 | 5. **Window Management** 245 | - Move window using [Control or Cmd + Arrow keys] 246 | - Toggle visibility with [Control or Cmd + B] 247 | - Adjust opacity with [Control or Cmd + [] and [Control or Cmd + ]] 248 | - Window remains invisible to specified screen sharing applications 249 | - Start a new problem using [Control or Cmd + R] 250 | 251 | 6. **Language Selection 252 | 253 | - Easily switch between programming languages with a single click 254 | - Use arrow keys for keyboard navigation through available languages 255 | - The system dynamically adapts to any languages added or removed from the codebase 256 | - Your language preference is saved between sessions 257 | 258 | ## Adding More AI Models 259 | 260 | This application is built with extensibility in mind. You can easily add support for additional LLMs alongside the existing OpenAI integration: 261 | 262 | - You can add Claude, Deepseek, Grok, or any other AI model as alternative options 263 | - The application architecture allows for multiple LLM backends to coexist 264 | - Users can have the freedom to choose their preferred AI provider 265 | 266 | To add new models, simply extend the API integration in `electron/ProcessingHelper.ts` and add the corresponding UI options in `src/components/Settings/SettingsDialog.tsx`. The modular design makes this straightforward without disrupting existing functionality. 267 | 268 | ## Configuration 269 | 270 | - **OpenAI API Key**: Your personal API key is stored locally and only used for API calls to OpenAI 271 | - **Model Selection**: You can choose between GPT-4o and GPT-4o-mini for each stage of processing: 272 | - Problem Extraction: Analyzes screenshots to understand the coding problem 273 | - Solution Generation: Creates optimized solutions with explanations 274 | - Debugging: Provides detailed analysis of errors and improvement suggestions 275 | - **Language**: Select your preferred programming language for solutions 276 | - **Window Controls**: Adjust opacity, position, and zoom level using keyboard shortcuts 277 | - **All settings are stored locally** in your user data directory and persist between sessions 278 | 279 | ## License 280 | 281 | This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). 282 | 283 | ### What This Means 284 | 285 | - You are free to use, modify, and distribute this software 286 | - If you modify the code, you must make your changes available under the same license 287 | - If you run a modified version on a network server, you must make the source code available to users 288 | - We strongly encourage you to contribute improvements back to the main project 289 | 290 | See the [LICENSE-SHORT](LICENSE-SHORT) file for a summary of terms or visit [GNU AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) for the full license text. 291 | 292 | ### Contributing 293 | 294 | We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information. 295 | 296 | ## Disclaimer and Ethical Usage 297 | 298 | This tool is intended as a learning aid and practice assistant. While it can help you understand problems and solution approaches during interviews, consider these ethical guidelines: 299 | 300 | - Be honest about using assistance tools if asked directly in an interview 301 | - Use this tool to learn concepts, not just to get answers 302 | - Recognize that understanding solutions is more valuable than simply presenting them 303 | - In take-home assignments, make sure you thoroughly understand any solutions you submit 304 | 305 | Remember that the purpose of technical interviews is to assess your problem-solving skills and understanding. This tool works best when used to enhance your learning, not as a substitute for it. 306 | 307 | ## Support and Questions 308 | 309 | If you have questions or need support, please open an issue on the GitHub repository. 310 | 311 | --- 312 | 313 | > **Remember:** This is a community resource. If you find it valuable, consider contributing rather than just requesting features. The project grows through collective effort, not individual demands. 314 | -------------------------------------------------------------------------------- /assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.debugger 10 | 11 | com.apple.security.device.camera 12 | 13 | com.apple.security.device.microphone 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | com.apple.security.network.client 18 | 19 | 20 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/bun.lockb -------------------------------------------------------------------------------- /cleanup-for-github.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Cleaning up project for GitHub... 3 | 4 | echo Removing node_modules directory... 5 | if exist node_modules rmdir /s /q node_modules 6 | 7 | echo Removing dist directory... 8 | if exist dist rmdir /s /q dist 9 | 10 | echo Removing dist-electron directory... 11 | if exist dist-electron rmdir /s /q dist-electron 12 | 13 | echo Removing release directory... 14 | if exist release rmdir /s /q release 15 | 16 | echo Removing package-lock.json... 17 | if exist package-lock.json del package-lock.json 18 | 19 | echo Removing any .env files... 20 | if exist .env del .env 21 | if exist .env.local del .env.local 22 | if exist .env.development del .env.development 23 | if exist .env.production del .env.production 24 | 25 | echo Cleanup complete! 26 | echo Ready to commit to GitHub. 27 | echo Run 'npm install' after cloning to install dependencies. 28 | pause 29 | -------------------------------------------------------------------------------- /cleanup-for-github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Cleaning up project for GitHub..." 3 | 4 | echo "Removing node_modules directory..." 5 | rm -rf node_modules 6 | 7 | echo "Removing dist directory..." 8 | rm -rf dist 9 | 10 | echo "Removing dist-electron directory..." 11 | rm -rf dist-electron 12 | 13 | echo "Removing release directory..." 14 | rm -rf release 15 | 16 | echo "Removing package-lock.json..." 17 | rm -f package-lock.json 18 | 19 | echo "Removing any .env files..." 20 | rm -f .env .env.local .env.development .env.production 21 | 22 | echo "Cleanup complete!" 23 | echo "Ready to commit to GitHub." 24 | echo "Run 'npm install' after cloning to install dependencies." 25 | echo "Press Enter to continue..." 26 | read 27 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/config.js -------------------------------------------------------------------------------- /electron/ConfigHelper.ts: -------------------------------------------------------------------------------- 1 | // ConfigHelper.ts 2 | import fs from "node:fs" 3 | import path from "node:path" 4 | import { app } from "electron" 5 | import { EventEmitter } from "events" 6 | import { OpenAI } from "openai" 7 | 8 | interface Config { 9 | apiKey: string; 10 | apiProvider: "openai" | "gemini" | "anthropic"; // Added provider selection 11 | extractionModel: string; 12 | solutionModel: string; 13 | debuggingModel: string; 14 | language: string; 15 | opacity: number; 16 | } 17 | 18 | export class ConfigHelper extends EventEmitter { 19 | private configPath: string; 20 | private defaultConfig: Config = { 21 | apiKey: "", 22 | apiProvider: "gemini", // Default to Gemini 23 | extractionModel: "gemini-2.0-flash", // Default to Flash for faster responses 24 | solutionModel: "gemini-2.0-flash", 25 | debuggingModel: "gemini-2.0-flash", 26 | language: "python", 27 | opacity: 1.0 28 | }; 29 | 30 | constructor() { 31 | super(); 32 | // Use the app's user data directory to store the config 33 | try { 34 | this.configPath = path.join(app.getPath('userData'), 'config.json'); 35 | console.log('Config path:', this.configPath); 36 | } catch (err) { 37 | console.warn('Could not access user data path, using fallback'); 38 | this.configPath = path.join(process.cwd(), 'config.json'); 39 | } 40 | 41 | // Ensure the initial config file exists 42 | this.ensureConfigExists(); 43 | } 44 | 45 | /** 46 | * Ensure config file exists 47 | */ 48 | private ensureConfigExists(): void { 49 | try { 50 | if (!fs.existsSync(this.configPath)) { 51 | this.saveConfig(this.defaultConfig); 52 | } 53 | } catch (err) { 54 | console.error("Error ensuring config exists:", err); 55 | } 56 | } 57 | 58 | /** 59 | * Validate and sanitize model selection to ensure only allowed models are used 60 | */ 61 | private sanitizeModelSelection(model: string, provider: "openai" | "gemini" | "anthropic"): string { 62 | if (provider === "openai") { 63 | // Only allow gpt-4o and gpt-4o-mini for OpenAI 64 | const allowedModels = ['gpt-4o', 'gpt-4o-mini']; 65 | if (!allowedModels.includes(model)) { 66 | console.warn(`Invalid OpenAI model specified: ${model}. Using default model: gpt-4o`); 67 | return 'gpt-4o'; 68 | } 69 | return model; 70 | } else if (provider === "gemini") { 71 | // Only allow gemini-1.5-pro and gemini-2.0-flash for Gemini 72 | const allowedModels = ['gemini-1.5-pro', 'gemini-2.0-flash']; 73 | if (!allowedModels.includes(model)) { 74 | console.warn(`Invalid Gemini model specified: ${model}. Using default model: gemini-2.0-flash`); 75 | return 'gemini-2.0-flash'; // Changed default to flash 76 | } 77 | return model; 78 | } else if (provider === "anthropic") { 79 | // Only allow Claude models 80 | const allowedModels = ['claude-3-7-sonnet-20250219', 'claude-3-5-sonnet-20241022', 'claude-3-opus-20240229']; 81 | if (!allowedModels.includes(model)) { 82 | console.warn(`Invalid Anthropic model specified: ${model}. Using default model: claude-3-7-sonnet-20250219`); 83 | return 'claude-3-7-sonnet-20250219'; 84 | } 85 | return model; 86 | } 87 | // Default fallback 88 | return model; 89 | } 90 | 91 | public loadConfig(): Config { 92 | try { 93 | if (fs.existsSync(this.configPath)) { 94 | const configData = fs.readFileSync(this.configPath, 'utf8'); 95 | const config = JSON.parse(configData); 96 | 97 | // Ensure apiProvider is a valid value 98 | if (config.apiProvider !== "openai" && config.apiProvider !== "gemini" && config.apiProvider !== "anthropic") { 99 | config.apiProvider = "gemini"; // Default to Gemini if invalid 100 | } 101 | 102 | // Sanitize model selections to ensure only allowed models are used 103 | if (config.extractionModel) { 104 | config.extractionModel = this.sanitizeModelSelection(config.extractionModel, config.apiProvider); 105 | } 106 | if (config.solutionModel) { 107 | config.solutionModel = this.sanitizeModelSelection(config.solutionModel, config.apiProvider); 108 | } 109 | if (config.debuggingModel) { 110 | config.debuggingModel = this.sanitizeModelSelection(config.debuggingModel, config.apiProvider); 111 | } 112 | 113 | return { 114 | ...this.defaultConfig, 115 | ...config 116 | }; 117 | } 118 | 119 | // If no config exists, create a default one 120 | this.saveConfig(this.defaultConfig); 121 | return this.defaultConfig; 122 | } catch (err) { 123 | console.error("Error loading config:", err); 124 | return this.defaultConfig; 125 | } 126 | } 127 | 128 | /** 129 | * Save configuration to disk 130 | */ 131 | public saveConfig(config: Config): void { 132 | try { 133 | // Ensure the directory exists 134 | const configDir = path.dirname(this.configPath); 135 | if (!fs.existsSync(configDir)) { 136 | fs.mkdirSync(configDir, { recursive: true }); 137 | } 138 | // Write the config file 139 | fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2)); 140 | } catch (err) { 141 | console.error("Error saving config:", err); 142 | } 143 | } 144 | 145 | /** 146 | * Update specific configuration values 147 | */ 148 | public updateConfig(updates: Partial): Config { 149 | try { 150 | const currentConfig = this.loadConfig(); 151 | let provider = updates.apiProvider || currentConfig.apiProvider; 152 | 153 | // Auto-detect provider based on API key format if a new key is provided 154 | if (updates.apiKey && !updates.apiProvider) { 155 | // If API key starts with "sk-", it's likely an OpenAI key 156 | if (updates.apiKey.trim().startsWith('sk-')) { 157 | provider = "openai"; 158 | console.log("Auto-detected OpenAI API key format"); 159 | } else if (updates.apiKey.trim().startsWith('sk-ant-')) { 160 | provider = "anthropic"; 161 | console.log("Auto-detected Anthropic API key format"); 162 | } else { 163 | provider = "gemini"; 164 | console.log("Using Gemini API key format (default)"); 165 | } 166 | 167 | // Update the provider in the updates object 168 | updates.apiProvider = provider; 169 | } 170 | 171 | // If provider is changing, reset models to the default for that provider 172 | if (updates.apiProvider && updates.apiProvider !== currentConfig.apiProvider) { 173 | if (updates.apiProvider === "openai") { 174 | updates.extractionModel = "gpt-4o"; 175 | updates.solutionModel = "gpt-4o"; 176 | updates.debuggingModel = "gpt-4o"; 177 | } else if (updates.apiProvider === "anthropic") { 178 | updates.extractionModel = "claude-3-7-sonnet-20250219"; 179 | updates.solutionModel = "claude-3-7-sonnet-20250219"; 180 | updates.debuggingModel = "claude-3-7-sonnet-20250219"; 181 | } else { 182 | updates.extractionModel = "gemini-2.0-flash"; 183 | updates.solutionModel = "gemini-2.0-flash"; 184 | updates.debuggingModel = "gemini-2.0-flash"; 185 | } 186 | } 187 | 188 | // Sanitize model selections in the updates 189 | if (updates.extractionModel) { 190 | updates.extractionModel = this.sanitizeModelSelection(updates.extractionModel, provider); 191 | } 192 | if (updates.solutionModel) { 193 | updates.solutionModel = this.sanitizeModelSelection(updates.solutionModel, provider); 194 | } 195 | if (updates.debuggingModel) { 196 | updates.debuggingModel = this.sanitizeModelSelection(updates.debuggingModel, provider); 197 | } 198 | 199 | const newConfig = { ...currentConfig, ...updates }; 200 | this.saveConfig(newConfig); 201 | 202 | // Only emit update event for changes other than opacity 203 | // This prevents re-initializing the AI client when only opacity changes 204 | if (updates.apiKey !== undefined || updates.apiProvider !== undefined || 205 | updates.extractionModel !== undefined || updates.solutionModel !== undefined || 206 | updates.debuggingModel !== undefined || updates.language !== undefined) { 207 | this.emit('config-updated', newConfig); 208 | } 209 | 210 | return newConfig; 211 | } catch (error) { 212 | console.error('Error updating config:', error); 213 | return this.defaultConfig; 214 | } 215 | } 216 | 217 | /** 218 | * Check if the API key is configured 219 | */ 220 | public hasApiKey(): boolean { 221 | const config = this.loadConfig(); 222 | return !!config.apiKey && config.apiKey.trim().length > 0; 223 | } 224 | 225 | /** 226 | * Validate the API key format 227 | */ 228 | public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini" | "anthropic" ): boolean { 229 | // If provider is not specified, attempt to auto-detect 230 | if (!provider) { 231 | if (apiKey.trim().startsWith('sk-')) { 232 | if (apiKey.trim().startsWith('sk-ant-')) { 233 | provider = "anthropic"; 234 | } else { 235 | provider = "openai"; 236 | } 237 | } else { 238 | provider = "gemini"; 239 | } 240 | } 241 | 242 | if (provider === "openai") { 243 | // Basic format validation for OpenAI API keys 244 | return /^sk-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); 245 | } else if (provider === "gemini") { 246 | // Basic format validation for Gemini API keys (usually alphanumeric with no specific prefix) 247 | return apiKey.trim().length >= 10; // Assuming Gemini keys are at least 10 chars 248 | } else if (provider === "anthropic") { 249 | // Basic format validation for Anthropic API keys 250 | return /^sk-ant-[a-zA-Z0-9]{32,}$/.test(apiKey.trim()); 251 | } 252 | 253 | return false; 254 | } 255 | 256 | /** 257 | * Get the stored opacity value 258 | */ 259 | public getOpacity(): number { 260 | const config = this.loadConfig(); 261 | return config.opacity !== undefined ? config.opacity : 1.0; 262 | } 263 | 264 | /** 265 | * Set the window opacity value 266 | */ 267 | public setOpacity(opacity: number): void { 268 | // Ensure opacity is between 0.1 and 1.0 269 | const validOpacity = Math.min(1.0, Math.max(0.1, opacity)); 270 | this.updateConfig({ opacity: validOpacity }); 271 | } 272 | 273 | /** 274 | * Get the preferred programming language 275 | */ 276 | public getLanguage(): string { 277 | const config = this.loadConfig(); 278 | return config.language || "python"; 279 | } 280 | 281 | /** 282 | * Set the preferred programming language 283 | */ 284 | public setLanguage(language: string): void { 285 | this.updateConfig({ language }); 286 | } 287 | 288 | /** 289 | * Test API key with the selected provider 290 | */ 291 | public async testApiKey(apiKey: string, provider?: "openai" | "gemini" | "anthropic"): Promise<{valid: boolean, error?: string}> { 292 | // Auto-detect provider based on key format if not specified 293 | if (!provider) { 294 | if (apiKey.trim().startsWith('sk-')) { 295 | if (apiKey.trim().startsWith('sk-ant-')) { 296 | provider = "anthropic"; 297 | console.log("Auto-detected Anthropic API key format for testing"); 298 | } else { 299 | provider = "openai"; 300 | console.log("Auto-detected OpenAI API key format for testing"); 301 | } 302 | } else { 303 | provider = "gemini"; 304 | console.log("Using Gemini API key format for testing (default)"); 305 | } 306 | } 307 | 308 | if (provider === "openai") { 309 | return this.testOpenAIKey(apiKey); 310 | } else if (provider === "gemini") { 311 | return this.testGeminiKey(apiKey); 312 | } else if (provider === "anthropic") { 313 | return this.testAnthropicKey(apiKey); 314 | } 315 | 316 | return { valid: false, error: "Unknown API provider" }; 317 | } 318 | 319 | /** 320 | * Test OpenAI API key 321 | */ 322 | private async testOpenAIKey(apiKey: string): Promise<{valid: boolean, error?: string}> { 323 | try { 324 | const openai = new OpenAI({ apiKey }); 325 | // Make a simple API call to test the key 326 | await openai.models.list(); 327 | return { valid: true }; 328 | } catch (error: any) { 329 | console.error('OpenAI API key test failed:', error); 330 | 331 | // Determine the specific error type for better error messages 332 | let errorMessage = 'Unknown error validating OpenAI API key'; 333 | 334 | if (error.status === 401) { 335 | errorMessage = 'Invalid API key. Please check your OpenAI key and try again.'; 336 | } else if (error.status === 429) { 337 | errorMessage = 'Rate limit exceeded. Your OpenAI API key has reached its request limit or has insufficient quota.'; 338 | } else if (error.status === 500) { 339 | errorMessage = 'OpenAI server error. Please try again later.'; 340 | } else if (error.message) { 341 | errorMessage = `Error: ${error.message}`; 342 | } 343 | 344 | return { valid: false, error: errorMessage }; 345 | } 346 | } 347 | 348 | /** 349 | * Test Gemini API key 350 | * Note: This is a simplified implementation since we don't have the actual Gemini client 351 | */ 352 | private async testGeminiKey(apiKey: string): Promise<{valid: boolean, error?: string}> { 353 | try { 354 | // For now, we'll just do a basic check to ensure the key exists and has valid format 355 | // In production, you would connect to the Gemini API and validate the key 356 | if (apiKey && apiKey.trim().length >= 20) { 357 | // Here you would actually validate the key with a Gemini API call 358 | return { valid: true }; 359 | } 360 | return { valid: false, error: 'Invalid Gemini API key format.' }; 361 | } catch (error: any) { 362 | console.error('Gemini API key test failed:', error); 363 | let errorMessage = 'Unknown error validating Gemini API key'; 364 | 365 | if (error.message) { 366 | errorMessage = `Error: ${error.message}`; 367 | } 368 | 369 | return { valid: false, error: errorMessage }; 370 | } 371 | } 372 | 373 | /** 374 | * Test Anthropic API key 375 | * Note: This is a simplified implementation since we don't have the actual Anthropic client 376 | */ 377 | private async testAnthropicKey(apiKey: string): Promise<{valid: boolean, error?: string}> { 378 | try { 379 | // For now, we'll just do a basic check to ensure the key exists and has valid format 380 | // In production, you would connect to the Anthropic API and validate the key 381 | if (apiKey && /^sk-ant-[a-zA-Z0-9]{32,}$/.test(apiKey.trim())) { 382 | // Here you would actually validate the key with an Anthropic API call 383 | return { valid: true }; 384 | } 385 | return { valid: false, error: 'Invalid Anthropic API key format.' }; 386 | } catch (error: any) { 387 | console.error('Anthropic API key test failed:', error); 388 | let errorMessage = 'Unknown error validating Anthropic API key'; 389 | 390 | if (error.message) { 391 | errorMessage = `Error: ${error.message}`; 392 | } 393 | 394 | return { valid: false, error: errorMessage }; 395 | } 396 | } 397 | } 398 | 399 | // Export a singleton instance 400 | export const configHelper = new ConfigHelper(); 401 | -------------------------------------------------------------------------------- /electron/ScreenshotHelper.ts: -------------------------------------------------------------------------------- 1 | // ScreenshotHelper.ts 2 | 3 | import path from "node:path" 4 | import fs from "node:fs" 5 | import { app } from "electron" 6 | import { v4 as uuidv4 } from "uuid" 7 | import { execFile } from "child_process" 8 | import { promisify } from "util" 9 | import screenshot from "screenshot-desktop" 10 | import os from "os" 11 | 12 | const execFileAsync = promisify(execFile) 13 | 14 | export class ScreenshotHelper { 15 | private screenshotQueue: string[] = [] 16 | private extraScreenshotQueue: string[] = [] 17 | private readonly MAX_SCREENSHOTS = 5 18 | 19 | private readonly screenshotDir: string 20 | private readonly extraScreenshotDir: string 21 | private readonly tempDir: string 22 | 23 | private view: "queue" | "solutions" | "debug" = "queue" 24 | 25 | constructor(view: "queue" | "solutions" | "debug" = "queue") { 26 | this.view = view 27 | 28 | // Initialize directories 29 | this.screenshotDir = path.join(app.getPath("userData"), "screenshots") 30 | this.extraScreenshotDir = path.join( 31 | app.getPath("userData"), 32 | "extra_screenshots" 33 | ) 34 | this.tempDir = path.join(app.getPath("temp"), "interview-coder-screenshots") 35 | 36 | // Create directories if they don't exist 37 | this.ensureDirectoriesExist(); 38 | 39 | // Clean existing screenshot directories when starting the app 40 | this.cleanScreenshotDirectories(); 41 | } 42 | 43 | private ensureDirectoriesExist(): void { 44 | const directories = [this.screenshotDir, this.extraScreenshotDir, this.tempDir]; 45 | 46 | for (const dir of directories) { 47 | if (!fs.existsSync(dir)) { 48 | try { 49 | fs.mkdirSync(dir, { recursive: true }); 50 | console.log(`Created directory: ${dir}`); 51 | } catch (err) { 52 | console.error(`Error creating directory ${dir}:`, err); 53 | } 54 | } 55 | } 56 | } 57 | 58 | // This method replaces loadExistingScreenshots() to ensure we start with empty queues 59 | private cleanScreenshotDirectories(): void { 60 | try { 61 | // Clean main screenshots directory 62 | if (fs.existsSync(this.screenshotDir)) { 63 | const files = fs.readdirSync(this.screenshotDir) 64 | .filter(file => file.endsWith('.png')) 65 | .map(file => path.join(this.screenshotDir, file)); 66 | 67 | // Delete each screenshot file 68 | for (const file of files) { 69 | try { 70 | fs.unlinkSync(file); 71 | console.log(`Deleted existing screenshot: ${file}`); 72 | } catch (err) { 73 | console.error(`Error deleting screenshot ${file}:`, err); 74 | } 75 | } 76 | } 77 | 78 | // Clean extra screenshots directory 79 | if (fs.existsSync(this.extraScreenshotDir)) { 80 | const files = fs.readdirSync(this.extraScreenshotDir) 81 | .filter(file => file.endsWith('.png')) 82 | .map(file => path.join(this.extraScreenshotDir, file)); 83 | 84 | // Delete each screenshot file 85 | for (const file of files) { 86 | try { 87 | fs.unlinkSync(file); 88 | console.log(`Deleted existing extra screenshot: ${file}`); 89 | } catch (err) { 90 | console.error(`Error deleting extra screenshot ${file}:`, err); 91 | } 92 | } 93 | } 94 | 95 | console.log("Screenshot directories cleaned successfully"); 96 | } catch (err) { 97 | console.error("Error cleaning screenshot directories:", err); 98 | } 99 | } 100 | 101 | public getView(): "queue" | "solutions" | "debug" { 102 | return this.view 103 | } 104 | 105 | public setView(view: "queue" | "solutions" | "debug"): void { 106 | console.log("Setting view in ScreenshotHelper:", view) 107 | console.log( 108 | "Current queues - Main:", 109 | this.screenshotQueue, 110 | "Extra:", 111 | this.extraScreenshotQueue 112 | ) 113 | this.view = view 114 | } 115 | 116 | public getScreenshotQueue(): string[] { 117 | return this.screenshotQueue 118 | } 119 | 120 | public getExtraScreenshotQueue(): string[] { 121 | console.log("Getting extra screenshot queue:", this.extraScreenshotQueue) 122 | return this.extraScreenshotQueue 123 | } 124 | 125 | public clearQueues(): void { 126 | // Clear screenshotQueue 127 | this.screenshotQueue.forEach((screenshotPath) => { 128 | fs.unlink(screenshotPath, (err) => { 129 | if (err) 130 | console.error(`Error deleting screenshot at ${screenshotPath}:`, err) 131 | }) 132 | }) 133 | this.screenshotQueue = [] 134 | 135 | // Clear extraScreenshotQueue 136 | this.extraScreenshotQueue.forEach((screenshotPath) => { 137 | fs.unlink(screenshotPath, (err) => { 138 | if (err) 139 | console.error( 140 | `Error deleting extra screenshot at ${screenshotPath}:`, 141 | err 142 | ) 143 | }) 144 | }) 145 | this.extraScreenshotQueue = [] 146 | } 147 | 148 | private async captureScreenshot(): Promise { 149 | try { 150 | console.log("Starting screenshot capture..."); 151 | 152 | // For Windows, try multiple methods 153 | if (process.platform === 'win32') { 154 | return await this.captureWindowsScreenshot(); 155 | } 156 | 157 | // For macOS and Linux, use buffer directly 158 | console.log("Taking screenshot on non-Windows platform"); 159 | const buffer = await screenshot({ format: 'png' }); 160 | console.log(`Screenshot captured successfully, size: ${buffer.length} bytes`); 161 | return buffer; 162 | } catch (error) { 163 | console.error("Error capturing screenshot:", error); 164 | throw new Error(`Failed to capture screenshot: ${error.message}`); 165 | } 166 | } 167 | 168 | /** 169 | * Windows-specific screenshot capture with multiple fallback mechanisms 170 | */ 171 | private async captureWindowsScreenshot(): Promise { 172 | console.log("Attempting Windows screenshot with multiple methods"); 173 | 174 | // Method 1: Try screenshot-desktop with filename first 175 | try { 176 | const tempFile = path.join(this.tempDir, `temp-${uuidv4()}.png`); 177 | console.log(`Taking Windows screenshot to temp file (Method 1): ${tempFile}`); 178 | 179 | await screenshot({ filename: tempFile }); 180 | 181 | if (fs.existsSync(tempFile)) { 182 | const buffer = await fs.promises.readFile(tempFile); 183 | console.log(`Method 1 successful, screenshot size: ${buffer.length} bytes`); 184 | 185 | // Cleanup temp file 186 | try { 187 | await fs.promises.unlink(tempFile); 188 | } catch (cleanupErr) { 189 | console.warn("Failed to clean up temp file:", cleanupErr); 190 | } 191 | 192 | return buffer; 193 | } else { 194 | console.log("Method 1 failed: File not created"); 195 | throw new Error("Screenshot file not created"); 196 | } 197 | } catch (error) { 198 | console.warn("Windows screenshot Method 1 failed:", error); 199 | 200 | // Method 2: Try using PowerShell 201 | try { 202 | console.log("Attempting Windows screenshot with PowerShell (Method 2)"); 203 | const tempFile = path.join(this.tempDir, `ps-temp-${uuidv4()}.png`); 204 | 205 | // PowerShell command to take screenshot using .NET classes 206 | const psScript = ` 207 | Add-Type -AssemblyName System.Windows.Forms,System.Drawing 208 | $screens = [System.Windows.Forms.Screen]::AllScreens 209 | $top = ($screens | ForEach-Object {$_.Bounds.Top} | Measure-Object -Minimum).Minimum 210 | $left = ($screens | ForEach-Object {$_.Bounds.Left} | Measure-Object -Minimum).Minimum 211 | $width = ($screens | ForEach-Object {$_.Bounds.Right} | Measure-Object -Maximum).Maximum 212 | $height = ($screens | ForEach-Object {$_.Bounds.Bottom} | Measure-Object -Maximum).Maximum 213 | $bounds = [System.Drawing.Rectangle]::FromLTRB($left, $top, $width, $height) 214 | $bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height 215 | $graphics = [System.Drawing.Graphics]::FromImage($bmp) 216 | $graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size) 217 | $bmp.Save('${tempFile.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png) 218 | $graphics.Dispose() 219 | $bmp.Dispose() 220 | `; 221 | 222 | // Execute PowerShell 223 | await execFileAsync('powershell', [ 224 | '-NoProfile', 225 | '-ExecutionPolicy', 'Bypass', 226 | '-Command', psScript 227 | ]); 228 | 229 | // Check if file exists and read it 230 | if (fs.existsSync(tempFile)) { 231 | const buffer = await fs.promises.readFile(tempFile); 232 | console.log(`Method 2 successful, screenshot size: ${buffer.length} bytes`); 233 | 234 | // Cleanup 235 | try { 236 | await fs.promises.unlink(tempFile); 237 | } catch (err) { 238 | console.warn("Failed to clean up PowerShell temp file:", err); 239 | } 240 | 241 | return buffer; 242 | } else { 243 | throw new Error("PowerShell screenshot file not created"); 244 | } 245 | } catch (psError) { 246 | console.warn("Windows PowerShell screenshot failed:", psError); 247 | 248 | // Method 3: Last resort - create a tiny placeholder image 249 | console.log("All screenshot methods failed, creating placeholder image"); 250 | 251 | // Create a 1x1 transparent PNG as fallback 252 | const fallbackBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', 'base64'); 253 | console.log("Created placeholder image as fallback"); 254 | 255 | // Show the error but return a valid buffer so the app doesn't crash 256 | throw new Error("Could not capture screenshot with any method. Please check your Windows security settings and try again."); 257 | } 258 | } 259 | } 260 | 261 | public async takeScreenshot( 262 | hideMainWindow: () => void, 263 | showMainWindow: () => void 264 | ): Promise { 265 | console.log("Taking screenshot in view:", this.view) 266 | hideMainWindow() 267 | 268 | // Increased delay for window hiding on Windows 269 | const hideDelay = process.platform === 'win32' ? 500 : 300; 270 | await new Promise((resolve) => setTimeout(resolve, hideDelay)) 271 | 272 | let screenshotPath = "" 273 | try { 274 | // Get screenshot buffer using cross-platform method 275 | const screenshotBuffer = await this.captureScreenshot(); 276 | 277 | if (!screenshotBuffer || screenshotBuffer.length === 0) { 278 | throw new Error("Screenshot capture returned empty buffer"); 279 | } 280 | 281 | // Save and manage the screenshot based on current view 282 | if (this.view === "queue") { 283 | screenshotPath = path.join(this.screenshotDir, `${uuidv4()}.png`) 284 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 285 | console.log("Adding screenshot to main queue:", screenshotPath) 286 | this.screenshotQueue.push(screenshotPath) 287 | if (this.screenshotQueue.length > this.MAX_SCREENSHOTS) { 288 | const removedPath = this.screenshotQueue.shift() 289 | if (removedPath) { 290 | try { 291 | await fs.promises.unlink(removedPath) 292 | console.log( 293 | "Removed old screenshot from main queue:", 294 | removedPath 295 | ) 296 | } catch (error) { 297 | console.error("Error removing old screenshot:", error) 298 | } 299 | } 300 | } 301 | } else { 302 | // In solutions view, only add to extra queue 303 | screenshotPath = path.join(this.extraScreenshotDir, `${uuidv4()}.png`) 304 | await fs.promises.writeFile(screenshotPath, screenshotBuffer) 305 | console.log("Adding screenshot to extra queue:", screenshotPath) 306 | this.extraScreenshotQueue.push(screenshotPath) 307 | if (this.extraScreenshotQueue.length > this.MAX_SCREENSHOTS) { 308 | const removedPath = this.extraScreenshotQueue.shift() 309 | if (removedPath) { 310 | try { 311 | await fs.promises.unlink(removedPath) 312 | console.log( 313 | "Removed old screenshot from extra queue:", 314 | removedPath 315 | ) 316 | } catch (error) { 317 | console.error("Error removing old screenshot:", error) 318 | } 319 | } 320 | } 321 | } 322 | } catch (error) { 323 | console.error("Screenshot error:", error) 324 | throw error 325 | } finally { 326 | // Increased delay for showing window again 327 | await new Promise((resolve) => setTimeout(resolve, 200)) 328 | showMainWindow() 329 | } 330 | 331 | return screenshotPath 332 | } 333 | 334 | public async getImagePreview(filepath: string): Promise { 335 | try { 336 | if (!fs.existsSync(filepath)) { 337 | console.error(`Image file not found: ${filepath}`); 338 | return ''; 339 | } 340 | 341 | const data = await fs.promises.readFile(filepath) 342 | return `data:image/png;base64,${data.toString("base64")}` 343 | } catch (error) { 344 | console.error("Error reading image:", error) 345 | return '' 346 | } 347 | } 348 | 349 | public async deleteScreenshot( 350 | path: string 351 | ): Promise<{ success: boolean; error?: string }> { 352 | try { 353 | if (fs.existsSync(path)) { 354 | await fs.promises.unlink(path) 355 | } 356 | 357 | if (this.view === "queue") { 358 | this.screenshotQueue = this.screenshotQueue.filter( 359 | (filePath) => filePath !== path 360 | ) 361 | } else { 362 | this.extraScreenshotQueue = this.extraScreenshotQueue.filter( 363 | (filePath) => filePath !== path 364 | ) 365 | } 366 | return { success: true } 367 | } catch (error) { 368 | console.error("Error deleting file:", error) 369 | return { success: false, error: error.message } 370 | } 371 | } 372 | 373 | public clearExtraScreenshotQueue(): void { 374 | // Clear extraScreenshotQueue 375 | this.extraScreenshotQueue.forEach((screenshotPath) => { 376 | if (fs.existsSync(screenshotPath)) { 377 | fs.unlink(screenshotPath, (err) => { 378 | if (err) 379 | console.error( 380 | `Error deleting extra screenshot at ${screenshotPath}:`, 381 | err 382 | ) 383 | }) 384 | } 385 | }) 386 | this.extraScreenshotQueue = [] 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /electron/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from "electron-updater" 2 | import { BrowserWindow, ipcMain, app } from "electron" 3 | import log from "electron-log" 4 | 5 | export function initAutoUpdater() { 6 | console.log("Initializing auto-updater...") 7 | 8 | // Skip update checks in development 9 | if (!app.isPackaged) { 10 | console.log("Skipping auto-updater in development mode") 11 | return 12 | } 13 | 14 | if (!process.env.GH_TOKEN) { 15 | console.error("GH_TOKEN environment variable is not set") 16 | return 17 | } 18 | 19 | // Configure auto updater 20 | autoUpdater.autoDownload = true 21 | autoUpdater.autoInstallOnAppQuit = true 22 | autoUpdater.allowDowngrade = true 23 | autoUpdater.allowPrerelease = true 24 | 25 | // Enable more verbose logging 26 | autoUpdater.logger = log 27 | log.transports.file.level = "debug" 28 | console.log( 29 | "Auto-updater logger configured with level:", 30 | log.transports.file.level 31 | ) 32 | 33 | // Log all update events 34 | autoUpdater.on("checking-for-update", () => { 35 | console.log("Checking for updates...") 36 | }) 37 | 38 | autoUpdater.on("update-available", (info) => { 39 | console.log("Update available:", info) 40 | // Notify renderer process about available update 41 | BrowserWindow.getAllWindows().forEach((window) => { 42 | console.log("Sending update-available to window") 43 | window.webContents.send("update-available", info) 44 | }) 45 | }) 46 | 47 | autoUpdater.on("update-not-available", (info) => { 48 | console.log("Update not available:", info) 49 | }) 50 | 51 | autoUpdater.on("download-progress", (progressObj) => { 52 | console.log("Download progress:", progressObj) 53 | }) 54 | 55 | autoUpdater.on("update-downloaded", (info) => { 56 | console.log("Update downloaded:", info) 57 | // Notify renderer process that update is ready to install 58 | BrowserWindow.getAllWindows().forEach((window) => { 59 | console.log("Sending update-downloaded to window") 60 | window.webContents.send("update-downloaded", info) 61 | }) 62 | }) 63 | 64 | autoUpdater.on("error", (err) => { 65 | console.error("Auto updater error:", err) 66 | }) 67 | 68 | // Check for updates immediately 69 | console.log("Checking for updates...") 70 | autoUpdater 71 | .checkForUpdates() 72 | .then((result) => { 73 | console.log("Update check result:", result) 74 | }) 75 | .catch((err) => { 76 | console.error("Error checking for updates:", err) 77 | }) 78 | 79 | // Set up update checking interval (every 1 hour) 80 | setInterval(() => { 81 | console.log("Checking for updates (interval)...") 82 | autoUpdater 83 | .checkForUpdates() 84 | .then((result) => { 85 | console.log("Update check result (interval):", result) 86 | }) 87 | .catch((err) => { 88 | console.error("Error checking for updates (interval):", err) 89 | }) 90 | }, 60 * 60 * 1000) 91 | 92 | // Handle IPC messages from renderer 93 | ipcMain.handle("start-update", async () => { 94 | console.log("Start update requested") 95 | try { 96 | await autoUpdater.downloadUpdate() 97 | console.log("Update download completed") 98 | return { success: true } 99 | } catch (error) { 100 | console.error("Failed to start update:", error) 101 | return { success: false, error: error.message } 102 | } 103 | }) 104 | 105 | ipcMain.handle("install-update", () => { 106 | console.log("Install update requested") 107 | autoUpdater.quitAndInstall() 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /electron/ipcHandlers.ts: -------------------------------------------------------------------------------- 1 | // ipcHandlers.ts 2 | 3 | import { ipcMain, shell, dialog } from "electron" 4 | import { randomBytes } from "crypto" 5 | import { IIpcHandlerDeps } from "./main" 6 | import { configHelper } from "./ConfigHelper" 7 | 8 | export function initializeIpcHandlers(deps: IIpcHandlerDeps): void { 9 | console.log("Initializing IPC handlers") 10 | 11 | // Configuration handlers 12 | ipcMain.handle("get-config", () => { 13 | return configHelper.loadConfig(); 14 | }) 15 | 16 | ipcMain.handle("update-config", (_event, updates) => { 17 | return configHelper.updateConfig(updates); 18 | }) 19 | 20 | ipcMain.handle("check-api-key", () => { 21 | return configHelper.hasApiKey(); 22 | }) 23 | 24 | ipcMain.handle("validate-api-key", async (_event, apiKey) => { 25 | // First check the format 26 | if (!configHelper.isValidApiKeyFormat(apiKey)) { 27 | return { 28 | valid: false, 29 | error: "Invalid API key format. OpenAI API keys start with 'sk-'" 30 | }; 31 | } 32 | 33 | // Then test the API key with OpenAI 34 | const result = await configHelper.testApiKey(apiKey); 35 | return result; 36 | }) 37 | 38 | // Credits handlers 39 | ipcMain.handle("set-initial-credits", async (_event, credits: number) => { 40 | const mainWindow = deps.getMainWindow() 41 | if (!mainWindow) return 42 | 43 | try { 44 | // Set the credits in a way that ensures atomicity 45 | await mainWindow.webContents.executeJavaScript( 46 | `window.__CREDITS__ = ${credits}` 47 | ) 48 | mainWindow.webContents.send("credits-updated", credits) 49 | } catch (error) { 50 | console.error("Error setting initial credits:", error) 51 | throw error 52 | } 53 | }) 54 | 55 | ipcMain.handle("decrement-credits", async () => { 56 | const mainWindow = deps.getMainWindow() 57 | if (!mainWindow) return 58 | 59 | try { 60 | const currentCredits = await mainWindow.webContents.executeJavaScript( 61 | "window.__CREDITS__" 62 | ) 63 | if (currentCredits > 0) { 64 | const newCredits = currentCredits - 1 65 | await mainWindow.webContents.executeJavaScript( 66 | `window.__CREDITS__ = ${newCredits}` 67 | ) 68 | mainWindow.webContents.send("credits-updated", newCredits) 69 | } 70 | } catch (error) { 71 | console.error("Error decrementing credits:", error) 72 | } 73 | }) 74 | 75 | // Screenshot queue handlers 76 | ipcMain.handle("get-screenshot-queue", () => { 77 | return deps.getScreenshotQueue() 78 | }) 79 | 80 | ipcMain.handle("get-extra-screenshot-queue", () => { 81 | return deps.getExtraScreenshotQueue() 82 | }) 83 | 84 | ipcMain.handle("delete-screenshot", async (event, path: string) => { 85 | return deps.deleteScreenshot(path) 86 | }) 87 | 88 | ipcMain.handle("get-image-preview", async (event, path: string) => { 89 | return deps.getImagePreview(path) 90 | }) 91 | 92 | // Screenshot processing handlers 93 | ipcMain.handle("process-screenshots", async () => { 94 | // Check for API key before processing 95 | if (!configHelper.hasApiKey()) { 96 | const mainWindow = deps.getMainWindow(); 97 | if (mainWindow) { 98 | mainWindow.webContents.send(deps.PROCESSING_EVENTS.API_KEY_INVALID); 99 | } 100 | return; 101 | } 102 | 103 | await deps.processingHelper?.processScreenshots() 104 | }) 105 | 106 | // Window dimension handlers 107 | ipcMain.handle( 108 | "update-content-dimensions", 109 | async (event, { width, height }: { width: number; height: number }) => { 110 | if (width && height) { 111 | deps.setWindowDimensions(width, height) 112 | } 113 | } 114 | ) 115 | 116 | ipcMain.handle( 117 | "set-window-dimensions", 118 | (event, width: number, height: number) => { 119 | deps.setWindowDimensions(width, height) 120 | } 121 | ) 122 | 123 | // Screenshot management handlers 124 | ipcMain.handle("get-screenshots", async () => { 125 | try { 126 | let previews = [] 127 | const currentView = deps.getView() 128 | 129 | if (currentView === "queue") { 130 | const queue = deps.getScreenshotQueue() 131 | previews = await Promise.all( 132 | queue.map(async (path) => ({ 133 | path, 134 | preview: await deps.getImagePreview(path) 135 | })) 136 | ) 137 | } else { 138 | const extraQueue = deps.getExtraScreenshotQueue() 139 | previews = await Promise.all( 140 | extraQueue.map(async (path) => ({ 141 | path, 142 | preview: await deps.getImagePreview(path) 143 | })) 144 | ) 145 | } 146 | 147 | return previews 148 | } catch (error) { 149 | console.error("Error getting screenshots:", error) 150 | throw error 151 | } 152 | }) 153 | 154 | // Screenshot trigger handlers 155 | ipcMain.handle("trigger-screenshot", async () => { 156 | const mainWindow = deps.getMainWindow() 157 | if (mainWindow) { 158 | try { 159 | const screenshotPath = await deps.takeScreenshot() 160 | const preview = await deps.getImagePreview(screenshotPath) 161 | mainWindow.webContents.send("screenshot-taken", { 162 | path: screenshotPath, 163 | preview 164 | }) 165 | return { success: true } 166 | } catch (error) { 167 | console.error("Error triggering screenshot:", error) 168 | return { error: "Failed to trigger screenshot" } 169 | } 170 | } 171 | return { error: "No main window available" } 172 | }) 173 | 174 | ipcMain.handle("take-screenshot", async () => { 175 | try { 176 | const screenshotPath = await deps.takeScreenshot() 177 | const preview = await deps.getImagePreview(screenshotPath) 178 | return { path: screenshotPath, preview } 179 | } catch (error) { 180 | console.error("Error taking screenshot:", error) 181 | return { error: "Failed to take screenshot" } 182 | } 183 | }) 184 | 185 | // Auth-related handlers removed 186 | 187 | ipcMain.handle("open-external-url", (event, url: string) => { 188 | shell.openExternal(url) 189 | }) 190 | 191 | // Open external URL handler 192 | ipcMain.handle("openLink", (event, url: string) => { 193 | try { 194 | console.log(`Opening external URL: ${url}`); 195 | shell.openExternal(url); 196 | return { success: true }; 197 | } catch (error) { 198 | console.error(`Error opening URL ${url}:`, error); 199 | return { success: false, error: `Failed to open URL: ${error}` }; 200 | } 201 | }) 202 | 203 | // Settings portal handler 204 | ipcMain.handle("open-settings-portal", () => { 205 | const mainWindow = deps.getMainWindow(); 206 | if (mainWindow) { 207 | mainWindow.webContents.send("show-settings-dialog"); 208 | return { success: true }; 209 | } 210 | return { success: false, error: "Main window not available" }; 211 | }) 212 | 213 | // Window management handlers 214 | ipcMain.handle("toggle-window", () => { 215 | try { 216 | deps.toggleMainWindow() 217 | return { success: true } 218 | } catch (error) { 219 | console.error("Error toggling window:", error) 220 | return { error: "Failed to toggle window" } 221 | } 222 | }) 223 | 224 | ipcMain.handle("reset-queues", async () => { 225 | try { 226 | deps.clearQueues() 227 | return { success: true } 228 | } catch (error) { 229 | console.error("Error resetting queues:", error) 230 | return { error: "Failed to reset queues" } 231 | } 232 | }) 233 | 234 | // Process screenshot handlers 235 | ipcMain.handle("trigger-process-screenshots", async () => { 236 | try { 237 | // Check for API key before processing 238 | if (!configHelper.hasApiKey()) { 239 | const mainWindow = deps.getMainWindow(); 240 | if (mainWindow) { 241 | mainWindow.webContents.send(deps.PROCESSING_EVENTS.API_KEY_INVALID); 242 | } 243 | return { success: false, error: "API key required" }; 244 | } 245 | 246 | await deps.processingHelper?.processScreenshots() 247 | return { success: true } 248 | } catch (error) { 249 | console.error("Error processing screenshots:", error) 250 | return { error: "Failed to process screenshots" } 251 | } 252 | }) 253 | 254 | // Reset handlers 255 | ipcMain.handle("trigger-reset", () => { 256 | try { 257 | // First cancel any ongoing requests 258 | deps.processingHelper?.cancelOngoingRequests() 259 | 260 | // Clear all queues immediately 261 | deps.clearQueues() 262 | 263 | // Reset view to queue 264 | deps.setView("queue") 265 | 266 | // Get main window and send reset events 267 | const mainWindow = deps.getMainWindow() 268 | if (mainWindow && !mainWindow.isDestroyed()) { 269 | // Send reset events in sequence 270 | mainWindow.webContents.send("reset-view") 271 | mainWindow.webContents.send("reset") 272 | } 273 | 274 | return { success: true } 275 | } catch (error) { 276 | console.error("Error triggering reset:", error) 277 | return { error: "Failed to trigger reset" } 278 | } 279 | }) 280 | 281 | // Window movement handlers 282 | ipcMain.handle("trigger-move-left", () => { 283 | try { 284 | deps.moveWindowLeft() 285 | return { success: true } 286 | } catch (error) { 287 | console.error("Error moving window left:", error) 288 | return { error: "Failed to move window left" } 289 | } 290 | }) 291 | 292 | ipcMain.handle("trigger-move-right", () => { 293 | try { 294 | deps.moveWindowRight() 295 | return { success: true } 296 | } catch (error) { 297 | console.error("Error moving window right:", error) 298 | return { error: "Failed to move window right" } 299 | } 300 | }) 301 | 302 | ipcMain.handle("trigger-move-up", () => { 303 | try { 304 | deps.moveWindowUp() 305 | return { success: true } 306 | } catch (error) { 307 | console.error("Error moving window up:", error) 308 | return { error: "Failed to move window up" } 309 | } 310 | }) 311 | 312 | ipcMain.handle("trigger-move-down", () => { 313 | try { 314 | deps.moveWindowDown() 315 | return { success: true } 316 | } catch (error) { 317 | console.error("Error moving window down:", error) 318 | return { error: "Failed to move window down" } 319 | } 320 | }) 321 | 322 | // Delete last screenshot handler 323 | ipcMain.handle("delete-last-screenshot", async () => { 324 | try { 325 | const queue = deps.getView() === "queue" 326 | ? deps.getScreenshotQueue() 327 | : deps.getExtraScreenshotQueue() 328 | 329 | if (queue.length === 0) { 330 | return { success: false, error: "No screenshots to delete" } 331 | } 332 | 333 | // Get the last screenshot in the queue 334 | const lastScreenshot = queue[queue.length - 1] 335 | 336 | // Delete it 337 | const result = await deps.deleteScreenshot(lastScreenshot) 338 | 339 | // Notify the renderer about the change 340 | const mainWindow = deps.getMainWindow() 341 | if (mainWindow && !mainWindow.isDestroyed()) { 342 | mainWindow.webContents.send("screenshot-deleted", { path: lastScreenshot }) 343 | } 344 | 345 | return result 346 | } catch (error) { 347 | console.error("Error deleting last screenshot:", error) 348 | return { success: false, error: "Failed to delete last screenshot" } 349 | } 350 | }) 351 | } 352 | -------------------------------------------------------------------------------- /electron/preload.ts: -------------------------------------------------------------------------------- 1 | console.log("Preload script starting...") 2 | import { contextBridge, ipcRenderer } from "electron" 3 | const { shell } = require("electron") 4 | 5 | export const PROCESSING_EVENTS = { 6 | //global states 7 | UNAUTHORIZED: "procesing-unauthorized", 8 | NO_SCREENSHOTS: "processing-no-screenshots", 9 | OUT_OF_CREDITS: "out-of-credits", 10 | API_KEY_INVALID: "api-key-invalid", 11 | 12 | //states for generating the initial solution 13 | INITIAL_START: "initial-start", 14 | PROBLEM_EXTRACTED: "problem-extracted", 15 | SOLUTION_SUCCESS: "solution-success", 16 | INITIAL_SOLUTION_ERROR: "solution-error", 17 | RESET: "reset", 18 | 19 | //states for processing the debugging 20 | DEBUG_START: "debug-start", 21 | DEBUG_SUCCESS: "debug-success", 22 | DEBUG_ERROR: "debug-error" 23 | } as const 24 | 25 | // At the top of the file 26 | console.log("Preload script is running") 27 | 28 | const electronAPI = { 29 | // Original methods 30 | openSubscriptionPortal: async (authData: { id: string; email: string }) => { 31 | return ipcRenderer.invoke("open-subscription-portal", authData) 32 | }, 33 | openSettingsPortal: () => ipcRenderer.invoke("open-settings-portal"), 34 | updateContentDimensions: (dimensions: { width: number; height: number }) => 35 | ipcRenderer.invoke("update-content-dimensions", dimensions), 36 | clearStore: () => ipcRenderer.invoke("clear-store"), 37 | getScreenshots: () => ipcRenderer.invoke("get-screenshots"), 38 | deleteScreenshot: (path: string) => 39 | ipcRenderer.invoke("delete-screenshot", path), 40 | toggleMainWindow: async () => { 41 | console.log("toggleMainWindow called from preload") 42 | try { 43 | const result = await ipcRenderer.invoke("toggle-window") 44 | console.log("toggle-window result:", result) 45 | return result 46 | } catch (error) { 47 | console.error("Error in toggleMainWindow:", error) 48 | throw error 49 | } 50 | }, 51 | // Event listeners 52 | onScreenshotTaken: ( 53 | callback: (data: { path: string; preview: string }) => void 54 | ) => { 55 | const subscription = (_: any, data: { path: string; preview: string }) => 56 | callback(data) 57 | ipcRenderer.on("screenshot-taken", subscription) 58 | return () => { 59 | ipcRenderer.removeListener("screenshot-taken", subscription) 60 | } 61 | }, 62 | onResetView: (callback: () => void) => { 63 | const subscription = () => callback() 64 | ipcRenderer.on("reset-view", subscription) 65 | return () => { 66 | ipcRenderer.removeListener("reset-view", subscription) 67 | } 68 | }, 69 | onSolutionStart: (callback: () => void) => { 70 | const subscription = () => callback() 71 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_START, subscription) 72 | return () => { 73 | ipcRenderer.removeListener(PROCESSING_EVENTS.INITIAL_START, subscription) 74 | } 75 | }, 76 | onDebugStart: (callback: () => void) => { 77 | const subscription = () => callback() 78 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_START, subscription) 79 | return () => { 80 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_START, subscription) 81 | } 82 | }, 83 | onDebugSuccess: (callback: (data: any) => void) => { 84 | ipcRenderer.on("debug-success", (_event, data) => callback(data)) 85 | return () => { 86 | ipcRenderer.removeListener("debug-success", (_event, data) => 87 | callback(data) 88 | ) 89 | } 90 | }, 91 | onDebugError: (callback: (error: string) => void) => { 92 | const subscription = (_: any, error: string) => callback(error) 93 | ipcRenderer.on(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 94 | return () => { 95 | ipcRenderer.removeListener(PROCESSING_EVENTS.DEBUG_ERROR, subscription) 96 | } 97 | }, 98 | onSolutionError: (callback: (error: string) => void) => { 99 | const subscription = (_: any, error: string) => callback(error) 100 | ipcRenderer.on(PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, subscription) 101 | return () => { 102 | ipcRenderer.removeListener( 103 | PROCESSING_EVENTS.INITIAL_SOLUTION_ERROR, 104 | subscription 105 | ) 106 | } 107 | }, 108 | onProcessingNoScreenshots: (callback: () => void) => { 109 | const subscription = () => callback() 110 | ipcRenderer.on(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 111 | return () => { 112 | ipcRenderer.removeListener(PROCESSING_EVENTS.NO_SCREENSHOTS, subscription) 113 | } 114 | }, 115 | onOutOfCredits: (callback: () => void) => { 116 | const subscription = () => callback() 117 | ipcRenderer.on(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 118 | return () => { 119 | ipcRenderer.removeListener(PROCESSING_EVENTS.OUT_OF_CREDITS, subscription) 120 | } 121 | }, 122 | onProblemExtracted: (callback: (data: any) => void) => { 123 | const subscription = (_: any, data: any) => callback(data) 124 | ipcRenderer.on(PROCESSING_EVENTS.PROBLEM_EXTRACTED, subscription) 125 | return () => { 126 | ipcRenderer.removeListener( 127 | PROCESSING_EVENTS.PROBLEM_EXTRACTED, 128 | subscription 129 | ) 130 | } 131 | }, 132 | onSolutionSuccess: (callback: (data: any) => void) => { 133 | const subscription = (_: any, data: any) => callback(data) 134 | ipcRenderer.on(PROCESSING_EVENTS.SOLUTION_SUCCESS, subscription) 135 | return () => { 136 | ipcRenderer.removeListener( 137 | PROCESSING_EVENTS.SOLUTION_SUCCESS, 138 | subscription 139 | ) 140 | } 141 | }, 142 | onUnauthorized: (callback: () => void) => { 143 | const subscription = () => callback() 144 | ipcRenderer.on(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 145 | return () => { 146 | ipcRenderer.removeListener(PROCESSING_EVENTS.UNAUTHORIZED, subscription) 147 | } 148 | }, 149 | // External URL handler 150 | openLink: (url: string) => shell.openExternal(url), 151 | triggerScreenshot: () => ipcRenderer.invoke("trigger-screenshot"), 152 | triggerProcessScreenshots: () => 153 | ipcRenderer.invoke("trigger-process-screenshots"), 154 | triggerReset: () => ipcRenderer.invoke("trigger-reset"), 155 | triggerMoveLeft: () => ipcRenderer.invoke("trigger-move-left"), 156 | triggerMoveRight: () => ipcRenderer.invoke("trigger-move-right"), 157 | triggerMoveUp: () => ipcRenderer.invoke("trigger-move-up"), 158 | triggerMoveDown: () => ipcRenderer.invoke("trigger-move-down"), 159 | onSubscriptionUpdated: (callback: () => void) => { 160 | const subscription = () => callback() 161 | ipcRenderer.on("subscription-updated", subscription) 162 | return () => { 163 | ipcRenderer.removeListener("subscription-updated", subscription) 164 | } 165 | }, 166 | onSubscriptionPortalClosed: (callback: () => void) => { 167 | const subscription = () => callback() 168 | ipcRenderer.on("subscription-portal-closed", subscription) 169 | return () => { 170 | ipcRenderer.removeListener("subscription-portal-closed", subscription) 171 | } 172 | }, 173 | onReset: (callback: () => void) => { 174 | const subscription = () => callback() 175 | ipcRenderer.on(PROCESSING_EVENTS.RESET, subscription) 176 | return () => { 177 | ipcRenderer.removeListener(PROCESSING_EVENTS.RESET, subscription) 178 | } 179 | }, 180 | startUpdate: () => ipcRenderer.invoke("start-update"), 181 | installUpdate: () => ipcRenderer.invoke("install-update"), 182 | onUpdateAvailable: (callback: (info: any) => void) => { 183 | const subscription = (_: any, info: any) => callback(info) 184 | ipcRenderer.on("update-available", subscription) 185 | return () => { 186 | ipcRenderer.removeListener("update-available", subscription) 187 | } 188 | }, 189 | onUpdateDownloaded: (callback: (info: any) => void) => { 190 | const subscription = (_: any, info: any) => callback(info) 191 | ipcRenderer.on("update-downloaded", subscription) 192 | return () => { 193 | ipcRenderer.removeListener("update-downloaded", subscription) 194 | } 195 | }, 196 | decrementCredits: () => ipcRenderer.invoke("decrement-credits"), 197 | onCreditsUpdated: (callback: (credits: number) => void) => { 198 | const subscription = (_event: any, credits: number) => callback(credits) 199 | ipcRenderer.on("credits-updated", subscription) 200 | return () => { 201 | ipcRenderer.removeListener("credits-updated", subscription) 202 | } 203 | }, 204 | getPlatform: () => process.platform, 205 | 206 | // New methods for OpenAI API integration 207 | getConfig: () => ipcRenderer.invoke("get-config"), 208 | updateConfig: (config: { apiKey?: string; model?: string; language?: string; opacity?: number }) => 209 | ipcRenderer.invoke("update-config", config), 210 | onShowSettings: (callback: () => void) => { 211 | const subscription = () => callback() 212 | ipcRenderer.on("show-settings-dialog", subscription) 213 | return () => { 214 | ipcRenderer.removeListener("show-settings-dialog", subscription) 215 | } 216 | }, 217 | checkApiKey: () => ipcRenderer.invoke("check-api-key"), 218 | validateApiKey: (apiKey: string) => 219 | ipcRenderer.invoke("validate-api-key", apiKey), 220 | openExternal: (url: string) => 221 | ipcRenderer.invoke("openExternal", url), 222 | onApiKeyInvalid: (callback: () => void) => { 223 | const subscription = () => callback() 224 | ipcRenderer.on(PROCESSING_EVENTS.API_KEY_INVALID, subscription) 225 | return () => { 226 | ipcRenderer.removeListener(PROCESSING_EVENTS.API_KEY_INVALID, subscription) 227 | } 228 | }, 229 | removeListener: (eventName: string, callback: (...args: any[]) => void) => { 230 | ipcRenderer.removeListener(eventName, callback) 231 | }, 232 | onDeleteLastScreenshot: (callback: () => void) => { 233 | const subscription = () => callback() 234 | ipcRenderer.on("delete-last-screenshot", subscription) 235 | return () => { 236 | ipcRenderer.removeListener("delete-last-screenshot", subscription) 237 | } 238 | }, 239 | deleteLastScreenshot: () => ipcRenderer.invoke("delete-last-screenshot") 240 | } 241 | 242 | // Before exposing the API 243 | console.log( 244 | "About to expose electronAPI with methods:", 245 | Object.keys(electronAPI) 246 | ) 247 | 248 | // Expose the API 249 | contextBridge.exposeInMainWorld("electronAPI", electronAPI) 250 | 251 | console.log("electronAPI exposed to window") 252 | 253 | // Add this focus restoration handler 254 | ipcRenderer.on("restore-focus", () => { 255 | // Try to focus the active element if it exists 256 | const activeElement = document.activeElement as HTMLElement 257 | if (activeElement && typeof activeElement.focus === "function") { 258 | activeElement.focus() 259 | } 260 | }) 261 | 262 | // Remove auth-callback handling - no longer needed 263 | -------------------------------------------------------------------------------- /electron/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut, app } from "electron" 2 | import { IShortcutsHelperDeps } from "./main" 3 | import { configHelper } from "./ConfigHelper" 4 | 5 | export class ShortcutsHelper { 6 | private deps: IShortcutsHelperDeps 7 | 8 | constructor(deps: IShortcutsHelperDeps) { 9 | this.deps = deps 10 | } 11 | 12 | private adjustOpacity(delta: number): void { 13 | const mainWindow = this.deps.getMainWindow(); 14 | if (!mainWindow) return; 15 | 16 | let currentOpacity = mainWindow.getOpacity(); 17 | let newOpacity = Math.max(0.1, Math.min(1.0, currentOpacity + delta)); 18 | console.log(`Adjusting opacity from ${currentOpacity} to ${newOpacity}`); 19 | 20 | mainWindow.setOpacity(newOpacity); 21 | 22 | // Save the opacity setting to config without re-initializing the client 23 | try { 24 | const config = configHelper.loadConfig(); 25 | config.opacity = newOpacity; 26 | configHelper.saveConfig(config); 27 | } catch (error) { 28 | console.error('Error saving opacity to config:', error); 29 | } 30 | 31 | // If we're making the window visible, also make sure it's shown and interaction is enabled 32 | if (newOpacity > 0.1 && !this.deps.isVisible()) { 33 | this.deps.toggleMainWindow(); 34 | } 35 | } 36 | 37 | public registerGlobalShortcuts(): void { 38 | globalShortcut.register("CommandOrControl+H", async () => { 39 | const mainWindow = this.deps.getMainWindow() 40 | if (mainWindow) { 41 | console.log("Taking screenshot...") 42 | try { 43 | const screenshotPath = await this.deps.takeScreenshot() 44 | const preview = await this.deps.getImagePreview(screenshotPath) 45 | mainWindow.webContents.send("screenshot-taken", { 46 | path: screenshotPath, 47 | preview 48 | }) 49 | } catch (error) { 50 | console.error("Error capturing screenshot:", error) 51 | } 52 | } 53 | }) 54 | 55 | globalShortcut.register("CommandOrControl+Enter", async () => { 56 | await this.deps.processingHelper?.processScreenshots() 57 | }) 58 | 59 | globalShortcut.register("CommandOrControl+R", () => { 60 | console.log( 61 | "Command + R pressed. Canceling requests and resetting queues..." 62 | ) 63 | 64 | // Cancel ongoing API requests 65 | this.deps.processingHelper?.cancelOngoingRequests() 66 | 67 | // Clear both screenshot queues 68 | this.deps.clearQueues() 69 | 70 | console.log("Cleared queues.") 71 | 72 | // Update the view state to 'queue' 73 | this.deps.setView("queue") 74 | 75 | // Notify renderer process to switch view to 'queue' 76 | const mainWindow = this.deps.getMainWindow() 77 | if (mainWindow && !mainWindow.isDestroyed()) { 78 | mainWindow.webContents.send("reset-view") 79 | mainWindow.webContents.send("reset") 80 | } 81 | }) 82 | 83 | // New shortcuts for moving the window 84 | globalShortcut.register("CommandOrControl+Left", () => { 85 | console.log("Command/Ctrl + Left pressed. Moving window left.") 86 | this.deps.moveWindowLeft() 87 | }) 88 | 89 | globalShortcut.register("CommandOrControl+Right", () => { 90 | console.log("Command/Ctrl + Right pressed. Moving window right.") 91 | this.deps.moveWindowRight() 92 | }) 93 | 94 | globalShortcut.register("CommandOrControl+Down", () => { 95 | console.log("Command/Ctrl + down pressed. Moving window down.") 96 | this.deps.moveWindowDown() 97 | }) 98 | 99 | globalShortcut.register("CommandOrControl+Up", () => { 100 | console.log("Command/Ctrl + Up pressed. Moving window Up.") 101 | this.deps.moveWindowUp() 102 | }) 103 | 104 | globalShortcut.register("CommandOrControl+B", () => { 105 | console.log("Command/Ctrl + B pressed. Toggling window visibility.") 106 | this.deps.toggleMainWindow() 107 | }) 108 | 109 | globalShortcut.register("CommandOrControl+Q", () => { 110 | console.log("Command/Ctrl + Q pressed. Quitting application.") 111 | app.quit() 112 | }) 113 | 114 | // Adjust opacity shortcuts 115 | globalShortcut.register("CommandOrControl+[", () => { 116 | console.log("Command/Ctrl + [ pressed. Decreasing opacity.") 117 | this.adjustOpacity(-0.1) 118 | }) 119 | 120 | globalShortcut.register("CommandOrControl+]", () => { 121 | console.log("Command/Ctrl + ] pressed. Increasing opacity.") 122 | this.adjustOpacity(0.1) 123 | }) 124 | 125 | // Zoom controls 126 | globalShortcut.register("CommandOrControl+-", () => { 127 | console.log("Command/Ctrl + - pressed. Zooming out.") 128 | const mainWindow = this.deps.getMainWindow() 129 | if (mainWindow) { 130 | const currentZoom = mainWindow.webContents.getZoomLevel() 131 | mainWindow.webContents.setZoomLevel(currentZoom - 0.5) 132 | } 133 | }) 134 | 135 | globalShortcut.register("CommandOrControl+0", () => { 136 | console.log("Command/Ctrl + 0 pressed. Resetting zoom.") 137 | const mainWindow = this.deps.getMainWindow() 138 | if (mainWindow) { 139 | mainWindow.webContents.setZoomLevel(0) 140 | } 141 | }) 142 | 143 | globalShortcut.register("CommandOrControl+=", () => { 144 | console.log("Command/Ctrl + = pressed. Zooming in.") 145 | const mainWindow = this.deps.getMainWindow() 146 | if (mainWindow) { 147 | const currentZoom = mainWindow.webContents.getZoomLevel() 148 | mainWindow.webContents.setZoomLevel(currentZoom + 0.5) 149 | } 150 | }) 151 | 152 | // Delete last screenshot shortcut 153 | globalShortcut.register("CommandOrControl+L", () => { 154 | console.log("Command/Ctrl + L pressed. Deleting last screenshot.") 155 | const mainWindow = this.deps.getMainWindow() 156 | if (mainWindow) { 157 | // Send an event to the renderer to delete the last screenshot 158 | mainWindow.webContents.send("delete-last-screenshot") 159 | } 160 | }) 161 | 162 | // Unregister shortcuts when quitting 163 | app.on("will-quit", () => { 164 | globalShortcut.unregisterAll() 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /electron/store.ts: -------------------------------------------------------------------------------- 1 | import Store from "electron-store" 2 | 3 | interface StoreSchema { 4 | // Empty for now, we can add other store items here later 5 | } 6 | 7 | const store = new Store({ 8 | defaults: {}, 9 | encryptionKey: "your-encryption-key" 10 | }) as Store & { 11 | store: StoreSchema 12 | get: (key: K) => StoreSchema[K] 13 | set: (key: K, value: StoreSchema[K]) => void 14 | } 15 | 16 | export { store } 17 | -------------------------------------------------------------------------------- /electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": true, 9 | "sourceMap": true, 10 | "jsx": "react-jsx", 11 | "baseUrl": ".", 12 | "outDir": "../dist-electron", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["*.ts", "ConfigHelper.ts", "../src/types/electron.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | readonly VITE_SUPABASE_ANON_KEY: string 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import tseslintPlugin from "@typescript-eslint/eslint-plugin"; 4 | import tseslintParser from "@typescript-eslint/parser"; 5 | import json from "@eslint/json"; 6 | import markdown from "@eslint/markdown"; 7 | import css from "@eslint/css"; 8 | 9 | export default [ 10 | js.configs.recommended, 11 | 12 | { 13 | files: ["**/*.{js,mjs,cjs,ts,tsx}"], 14 | languageOptions: { 15 | parser: tseslintParser, 16 | parserOptions: { 17 | ecmaVersion: "latest", 18 | sourceType: "module", 19 | }, 20 | globals: { 21 | ...globals.browser, 22 | ...globals.node, 23 | }, 24 | }, 25 | plugins: { 26 | "@typescript-eslint": tseslintPlugin, 27 | }, 28 | rules: { 29 | ...tseslintPlugin.configs.recommended.rules, 30 | }, 31 | }, 32 | 33 | { 34 | files: ["**/*.json"], 35 | plugins: { json }, 36 | rules: { ...json.configs.recommended.rules }, 37 | }, 38 | { 39 | files: ["**/*.jsonc"], 40 | plugins: { json }, 41 | rules: { ...json.configs.recommended.rules }, 42 | }, 43 | { 44 | files: ["**/*.json5"], 45 | plugins: { json }, 46 | rules: { ...json.configs.recommended.rules }, 47 | }, 48 | { 49 | files: ["**/*.md"], 50 | plugins: { markdown }, 51 | rules: { ...markdown.configs.recommended.rules }, 52 | }, 53 | { 54 | files: ["**/*.css"], 55 | plugins: { css }, 56 | rules: { ...css.configs.recommended.rules }, 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Interview Coder 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /invisible_launcher.vbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/invisible_launcher.vbs -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interview-coder-v1", 3 | "version": "1.0.19", 4 | "main": "./dist-electron/main.js", 5 | "scripts": { 6 | "clean": "npx rimraf dist dist-electron", 7 | "dev": "cross-env NODE_ENV=development npm run clean && concurrently \"tsc -w -p tsconfig.electron.json\" \"vite\" \"wait-on -t 30000 http://localhost:54321 && electron ./dist-electron/main.js\"", 8 | "test": "echo \"No tests defined. Please contribute if you like\" && exit 0", 9 | "lint": "npx eslint .", 10 | "start": "cross-env NODE_ENV=development concurrently \"tsc -p tsconfig.electron.json\" \"vite\" \"wait-on -t 30000 http://localhost:54321 && electron ./dist-electron/main.js\"", 11 | "build": "cross-env NODE_ENV=production npm run clean && vite build && tsc -p tsconfig.electron.json", 12 | "run-prod": "cross-env NODE_ENV=production electron ./dist-electron/main.js", 13 | "package": "npm run build && electron-builder build", 14 | "package-mac": "npm run build && electron-builder build --mac", 15 | "package-win": "npm run build && electron-builder build --win" 16 | }, 17 | "build": { 18 | "appId": "com.chunginlee.interviewcoder", 19 | "productName": "Interview Coder", 20 | "files": [ 21 | "dist/**/*", 22 | "dist-electron/**/*", 23 | "package.json", 24 | "electron/**/*" 25 | ], 26 | "directories": { 27 | "output": "release", 28 | "buildResources": "assets" 29 | }, 30 | "asar": true, 31 | "compression": "maximum", 32 | "generateUpdatesFilesForAllChannels": true, 33 | "mac": { 34 | "category": "public.app-category.developer-tools", 35 | "target": [ 36 | { 37 | "target": "dmg", 38 | "arch": [ 39 | "x64", 40 | "arm64" 41 | ] 42 | }, 43 | { 44 | "target": "zip", 45 | "arch": [ 46 | "x64", 47 | "arm64" 48 | ] 49 | } 50 | ], 51 | "artifactName": "Interview-Coder-${arch}.${ext}", 52 | "icon": "assets/icons/mac/icon.icns", 53 | "hardenedRuntime": true, 54 | "gatekeeperAssess": false, 55 | "entitlements": "build/entitlements.mac.plist", 56 | "entitlementsInherit": "build/entitlements.mac.plist", 57 | "identity": "Developer ID Application", 58 | "notarize": true, 59 | "protocols": { 60 | "name": "interview-coder-protocol", 61 | "schemes": [ 62 | "interview-coder" 63 | ] 64 | } 65 | }, 66 | "win": { 67 | "target": [ 68 | "nsis" 69 | ], 70 | "icon": "assets/icons/win/icon.ico", 71 | "artifactName": "${productName}-Windows-${version}.${ext}", 72 | "protocols": { 73 | "name": "interview-coder-protocol", 74 | "schemes": [ 75 | "interview-coder" 76 | ] 77 | } 78 | }, 79 | "linux": { 80 | "target": [ 81 | "AppImage" 82 | ], 83 | "icon": "assets/icons/png/icon-256x256.png", 84 | "artifactName": "${productName}-Linux-${version}.${ext}", 85 | "protocols": { 86 | "name": "interview-coder-protocol", 87 | "schemes": [ 88 | "interview-coder" 89 | ] 90 | } 91 | }, 92 | "publish": [ 93 | { 94 | "provider": "github", 95 | "owner": "ibttf", 96 | "repo": "interview-coder", 97 | "private": false, 98 | "releaseType": "release" 99 | } 100 | ], 101 | "extraResources": [ 102 | { 103 | "from": ".env", 104 | "to": ".env", 105 | "filter": [ 106 | "**/*" 107 | ] 108 | } 109 | ], 110 | "extraMetadata": { 111 | "main": "dist-electron/main.js" 112 | } 113 | }, 114 | "keywords": [ 115 | "interview", 116 | "coding", 117 | "interview prep", 118 | "technical interview", 119 | "tool" 120 | ], 121 | "author": "Interview Coder Contributors", 122 | "license": "AGPL-3.0-or-later", 123 | "description": "An invisible desktop application to help you pass your technical interviews.", 124 | "dependencies": { 125 | "@anthropic-ai/sdk": "^0.39.0", 126 | "@electron/notarize": "^2.3.0", 127 | "@emotion/react": "^11.11.0", 128 | "@emotion/styled": "^11.11.0", 129 | "@radix-ui/react-dialog": "^1.1.2", 130 | "@radix-ui/react-label": "^2.1.0", 131 | "@radix-ui/react-slot": "^1.1.0", 132 | "@radix-ui/react-toast": "^1.2.2", 133 | "@supabase/supabase-js": "^2.49.4", 134 | "@tanstack/react-query": "^5.64.0", 135 | "axios": "^1.7.7", 136 | "class-variance-authority": "^0.7.1", 137 | "clsx": "^2.1.1", 138 | "diff": "^7.0.0", 139 | "dotenv": "^16.4.7", 140 | "electron-log": "^5.2.4", 141 | "electron-store": "^10.0.0", 142 | "electron-updater": "^6.3.9", 143 | "form-data": "^4.0.1", 144 | "lucide-react": "^0.460.0", 145 | "openai": "^4.28.4", 146 | "react": "^18.2.0", 147 | "react-code-blocks": "^0.1.6", 148 | "react-dom": "^18.2.0", 149 | "react-router-dom": "^6.28.1", 150 | "react-syntax-highlighter": "^15.6.1", 151 | "screenshot-desktop": "^1.15.0", 152 | "tailwind-merge": "^2.5.5", 153 | "uuid": "^11.0.3" 154 | }, 155 | "devDependencies": { 156 | "@electron/typescript-definitions": "^8.14.0", 157 | "@eslint/css": "^0.6.0", 158 | "@eslint/js": "^9.24.0", 159 | "@eslint/json": "^0.11.0", 160 | "@eslint/markdown": "^6.3.0", 161 | "@types/color": "^4.2.0", 162 | "@types/diff": "^6.0.0", 163 | "@types/electron-store": "^1.3.1", 164 | "@types/node": "^20.11.30", 165 | "@types/react": "^18.2.67", 166 | "@types/react-dom": "^18.2.22", 167 | "@types/react-syntax-highlighter": "^15.5.13", 168 | "@types/screenshot-desktop": "^1.12.3", 169 | "@types/uuid": "^9.0.8", 170 | "@typescript-eslint/eslint-plugin": "^7.18.0", 171 | "@typescript-eslint/parser": "^7.18.0", 172 | "@typescript-eslint/utils": "^8.29.1", 173 | "@vitejs/plugin-react": "^4.2.1", 174 | "autoprefixer": "^10.4.20", 175 | "concurrently": "^8.2.2", 176 | "cross-env": "^7.0.3", 177 | "electron": "^29.1.4", 178 | "electron-builder": "^24.13.3", 179 | "electron-is-dev": "^3.0.1", 180 | "eslint": "^8.57.1", 181 | "eslint-flat-config-utils": "^2.0.1", 182 | "eslint-plugin-react-hooks": "^4.6.0", 183 | "eslint-plugin-react-refresh": "^0.4.6", 184 | "globals": "^16.0.0", 185 | "postcss": "^8.4.49", 186 | "rimraf": "^6.0.1", 187 | "tailwindcss": "^3.4.15", 188 | "typescript": "^5.4.2", 189 | "vite": "^6.2.5", 190 | "vite-plugin-electron": "^0.28.4", 191 | "vite-plugin-electron-renderer": "^0.14.6", 192 | "wait-on": "^7.2.0" 193 | }, 194 | "browserslist": { 195 | "production": [ 196 | ">0.2%", 197 | "not dead", 198 | "not op_mini all" 199 | ], 200 | "development": [ 201 | "last 1 chrome version", 202 | "last 1 firefox version", 203 | "last 1 safari version" 204 | ] 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /renderer/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /renderer/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /renderer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "renderer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.5.2", 10 | "@types/node": "^16.18.119", 11 | "@types/react": "^18.3.12", 12 | "@types/react-dom": "^18.3.1", 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "react-scripts": "5.0.1", 16 | "typescript": "^4.9.5", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /renderer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/renderer/public/favicon.ico -------------------------------------------------------------------------------- /renderer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /renderer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/renderer/public/logo192.png -------------------------------------------------------------------------------- /renderer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ornithopter-pilot/interview-coder-withoupaywall-opensource/46fe6b22fb6ea1f4da093475db94a3e14779d0fd/renderer/public/logo512.png -------------------------------------------------------------------------------- /renderer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Interview Coder", 3 | "name": "Interview Coder", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /renderer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /renderer/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /renderer/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /renderer/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

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

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /renderer/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /renderer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /renderer/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renderer/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /renderer/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /renderer/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import SubscribedApp from "./_pages/SubscribedApp" 2 | import { UpdateNotification } from "./components/UpdateNotification" 3 | import { 4 | QueryClient, 5 | QueryClientProvider 6 | } from "@tanstack/react-query" 7 | import { useEffect, useState, useCallback } from "react" 8 | import { 9 | Toast, 10 | ToastDescription, 11 | ToastProvider, 12 | ToastTitle, 13 | ToastViewport 14 | } from "./components/ui/toast" 15 | import { ToastContext } from "./contexts/toast" 16 | import { WelcomeScreen } from "./components/WelcomeScreen" 17 | import { SettingsDialog } from "./components/Settings/SettingsDialog" 18 | 19 | // Create a React Query client 20 | const queryClient = new QueryClient({ 21 | defaultOptions: { 22 | queries: { 23 | staleTime: 0, 24 | gcTime: Infinity, 25 | retry: 1, 26 | refetchOnWindowFocus: false 27 | }, 28 | mutations: { 29 | retry: 1 30 | } 31 | } 32 | }) 33 | 34 | // Root component that provides the QueryClient 35 | function App() { 36 | const [toastState, setToastState] = useState({ 37 | open: false, 38 | title: "", 39 | description: "", 40 | variant: "neutral" as "neutral" | "success" | "error" 41 | }) 42 | const [credits, setCredits] = useState(999) // Unlimited credits 43 | const [currentLanguage, setCurrentLanguage] = useState("python") 44 | const [isInitialized, setIsInitialized] = useState(false) 45 | const [hasApiKey, setHasApiKey] = useState(false) 46 | const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false) 47 | // Note: Model selection is now handled via separate extraction/solution/debugging model settings 48 | 49 | const [isSettingsOpen, setIsSettingsOpen] = useState(false) 50 | 51 | // Set unlimited credits 52 | const updateCredits = useCallback(() => { 53 | setCredits(999) // No credit limit in this version 54 | window.__CREDITS__ = 999 55 | }, []) 56 | 57 | // Helper function to safely update language 58 | const updateLanguage = useCallback((newLanguage: string) => { 59 | setCurrentLanguage(newLanguage) 60 | window.__LANGUAGE__ = newLanguage 61 | }, []) 62 | 63 | // Helper function to mark initialization complete 64 | const markInitialized = useCallback(() => { 65 | setIsInitialized(true) 66 | window.__IS_INITIALIZED__ = true 67 | }, []) 68 | 69 | // Show toast method 70 | const showToast = useCallback( 71 | ( 72 | title: string, 73 | description: string, 74 | variant: "neutral" | "success" | "error" 75 | ) => { 76 | setToastState({ 77 | open: true, 78 | title, 79 | description, 80 | variant 81 | }) 82 | }, 83 | [] 84 | ) 85 | 86 | // Check for OpenAI API key and prompt if not found 87 | useEffect(() => { 88 | const checkApiKey = async () => { 89 | try { 90 | const hasKey = await window.electronAPI.checkApiKey() 91 | setHasApiKey(hasKey) 92 | 93 | // If no API key is found, show the settings dialog after a short delay 94 | if (!hasKey) { 95 | setTimeout(() => { 96 | setIsSettingsOpen(true) 97 | }, 1000) 98 | } 99 | } catch (error) { 100 | console.error("Failed to check API key:", error) 101 | } 102 | } 103 | 104 | if (isInitialized) { 105 | checkApiKey() 106 | } 107 | }, [isInitialized]) 108 | 109 | // Initialize dropdown handler 110 | useEffect(() => { 111 | if (isInitialized) { 112 | // Process all types of dropdown elements with a shorter delay 113 | const timer = setTimeout(() => { 114 | // Find both native select elements and custom dropdowns 115 | const selectElements = document.querySelectorAll('select'); 116 | const customDropdowns = document.querySelectorAll('.dropdown-trigger, [role="combobox"], button:has(.dropdown)'); 117 | 118 | // Enable native selects 119 | selectElements.forEach(dropdown => { 120 | dropdown.disabled = false; 121 | }); 122 | 123 | // Enable custom dropdowns by removing any disabled attributes 124 | customDropdowns.forEach(dropdown => { 125 | if (dropdown instanceof HTMLElement) { 126 | dropdown.removeAttribute('disabled'); 127 | dropdown.setAttribute('aria-disabled', 'false'); 128 | } 129 | }); 130 | 131 | console.log(`Enabled ${selectElements.length} select elements and ${customDropdowns.length} custom dropdowns`); 132 | }, 1000); 133 | 134 | return () => clearTimeout(timer); 135 | } 136 | }, [isInitialized]); 137 | 138 | // Listen for settings dialog open requests 139 | useEffect(() => { 140 | const unsubscribeSettings = window.electronAPI.onShowSettings(() => { 141 | console.log("Show settings dialog requested"); 142 | setIsSettingsOpen(true); 143 | }); 144 | 145 | return () => { 146 | unsubscribeSettings(); 147 | }; 148 | }, []); 149 | 150 | // Initialize basic app state 151 | useEffect(() => { 152 | // Load config and set values 153 | const initializeApp = async () => { 154 | try { 155 | // Set unlimited credits 156 | updateCredits() 157 | 158 | // Load config including language and model settings 159 | const config = await window.electronAPI.getConfig() 160 | 161 | // Load language preference 162 | if (config && config.language) { 163 | updateLanguage(config.language) 164 | } else { 165 | updateLanguage("python") 166 | } 167 | 168 | // Model settings are now managed through the settings dialog 169 | // and stored in config as extractionModel, solutionModel, and debuggingModel 170 | 171 | markInitialized() 172 | } catch (error) { 173 | console.error("Failed to initialize app:", error) 174 | // Fallback to defaults 175 | updateLanguage("python") 176 | markInitialized() 177 | } 178 | } 179 | 180 | initializeApp() 181 | 182 | // Event listeners for process events 183 | const onApiKeyInvalid = () => { 184 | showToast( 185 | "API Key Invalid", 186 | "Your OpenAI API key appears to be invalid or has insufficient credits", 187 | "error" 188 | ) 189 | setApiKeyDialogOpen(true) 190 | } 191 | 192 | // Setup API key invalid listener 193 | window.electronAPI.onApiKeyInvalid(onApiKeyInvalid) 194 | 195 | // Define a no-op handler for solution success 196 | const unsubscribeSolutionSuccess = window.electronAPI.onSolutionSuccess( 197 | () => { 198 | console.log("Solution success - no credits deducted in this version") 199 | // No credit deduction in this version 200 | } 201 | ) 202 | 203 | // Cleanup function 204 | return () => { 205 | window.electronAPI.removeListener("API_KEY_INVALID", onApiKeyInvalid) 206 | unsubscribeSolutionSuccess() 207 | window.__IS_INITIALIZED__ = false 208 | setIsInitialized(false) 209 | } 210 | }, [updateCredits, updateLanguage, markInitialized, showToast]) 211 | 212 | // API Key dialog management 213 | const handleOpenSettings = useCallback(() => { 214 | console.log('Opening settings dialog'); 215 | setIsSettingsOpen(true); 216 | }, []); 217 | 218 | const handleCloseSettings = useCallback((open: boolean) => { 219 | console.log('Settings dialog state changed:', open); 220 | setIsSettingsOpen(open); 221 | }, []); 222 | 223 | const handleApiKeySave = useCallback(async (apiKey: string) => { 224 | try { 225 | await window.electronAPI.updateConfig({ apiKey }) 226 | setHasApiKey(true) 227 | showToast("Success", "API key saved successfully", "success") 228 | 229 | // Reload app after a short delay to reinitialize with the new API key 230 | setTimeout(() => { 231 | window.location.reload() 232 | }, 1500) 233 | } catch (error) { 234 | console.error("Failed to save API key:", error) 235 | showToast("Error", "Failed to save API key", "error") 236 | } 237 | }, [showToast]) 238 | 239 | return ( 240 | 241 | 242 | 243 |
244 | {isInitialized ? ( 245 | hasApiKey ? ( 246 | 251 | ) : ( 252 | 253 | ) 254 | ) : ( 255 |
256 |
257 |
258 |

259 | Initializing... 260 |

261 |
262 |
263 | )} 264 | 265 |
266 | 267 | {/* Settings Dialog */} 268 | 272 | 273 | 276 | setToastState((prev) => ({ ...prev, open })) 277 | } 278 | variant={toastState.variant} 279 | duration={1500} 280 | > 281 | {toastState.title} 282 | {toastState.description} 283 | 284 | 285 |
286 |
287 |
288 | ) 289 | } 290 | 291 | export default App -------------------------------------------------------------------------------- /src/_pages/Debug.tsx: -------------------------------------------------------------------------------- 1 | // Debug.tsx 2 | import { useQuery, useQueryClient } from "@tanstack/react-query" 3 | import React, { useEffect, useRef, useState } from "react" 4 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" 5 | import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism" 6 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 7 | import SolutionCommands from "../components/Solutions/SolutionCommands" 8 | import { Screenshot } from "../types/screenshots" 9 | import { ComplexitySection, ContentSection } from "./Solutions" 10 | import { useToast } from "../contexts/toast" 11 | 12 | const CodeSection = ({ 13 | title, 14 | code, 15 | isLoading, 16 | currentLanguage 17 | }: { 18 | title: string 19 | code: React.ReactNode 20 | isLoading: boolean 21 | currentLanguage: string 22 | }) => ( 23 |
24 |

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

29 | Loading solutions... 30 |

31 |
32 |
33 | ) : ( 34 |
35 | 49 | {code as string} 50 | 51 |
52 | )} 53 |
54 | ) 55 | 56 | async function fetchScreenshots(): Promise { 57 | try { 58 | const existing = await window.electronAPI.getScreenshots() 59 | console.log("Raw screenshot data in Debug:", existing) 60 | return (Array.isArray(existing) ? existing : []).map((p) => ({ 61 | id: p.path, 62 | path: p.path, 63 | preview: p.preview, 64 | timestamp: Date.now() 65 | })) 66 | } catch (error) { 67 | console.error("Error loading screenshots:", error) 68 | throw error 69 | } 70 | } 71 | 72 | interface DebugProps { 73 | isProcessing: boolean 74 | setIsProcessing: (isProcessing: boolean) => void 75 | currentLanguage: string 76 | setLanguage: (language: string) => void 77 | } 78 | 79 | const Debug: React.FC = ({ 80 | isProcessing, 81 | setIsProcessing, 82 | currentLanguage, 83 | setLanguage 84 | }) => { 85 | const [tooltipVisible, setTooltipVisible] = useState(false) 86 | const [tooltipHeight, setTooltipHeight] = useState(0) 87 | const { showToast } = useToast() 88 | 89 | const { data: screenshots = [], refetch } = useQuery({ 90 | queryKey: ["screenshots"], 91 | queryFn: fetchScreenshots, 92 | staleTime: Infinity, 93 | gcTime: Infinity, 94 | refetchOnWindowFocus: false 95 | }) 96 | 97 | const [newCode, setNewCode] = useState(null) 98 | const [thoughtsData, setThoughtsData] = useState(null) 99 | const [timeComplexityData, setTimeComplexityData] = useState( 100 | null 101 | ) 102 | const [spaceComplexityData, setSpaceComplexityData] = useState( 103 | null 104 | ) 105 | const [debugAnalysis, setDebugAnalysis] = useState(null) 106 | 107 | const queryClient = useQueryClient() 108 | const contentRef = useRef(null) 109 | 110 | useEffect(() => { 111 | // Try to get the new solution data from cache first 112 | const newSolution = queryClient.getQueryData(["new_solution"]) as { 113 | code: string 114 | debug_analysis: string 115 | thoughts: string[] 116 | time_complexity: string 117 | space_complexity: string 118 | } | null 119 | 120 | // If we have cached data, set all state variables to the cached data 121 | if (newSolution) { 122 | console.log("Found cached debug solution:", newSolution); 123 | 124 | if (newSolution.debug_analysis) { 125 | // Store the debug analysis in its own state variable 126 | setDebugAnalysis(newSolution.debug_analysis); 127 | // Set code separately for the code section 128 | setNewCode(newSolution.code || "// Debug mode - see analysis below"); 129 | 130 | // Process thoughts/analysis points 131 | if (newSolution.debug_analysis.includes('\n\n')) { 132 | const sections = newSolution.debug_analysis.split('\n\n').filter(Boolean); 133 | // Pick first few sections as thoughts 134 | setThoughtsData(sections.slice(0, 3)); 135 | } else { 136 | setThoughtsData(["Debug analysis based on your screenshots"]); 137 | } 138 | } else { 139 | // Fallback to code or default 140 | setNewCode(newSolution.code || "// No analysis available"); 141 | setThoughtsData(newSolution.thoughts || ["Debug analysis based on your screenshots"]); 142 | } 143 | setTimeComplexityData(newSolution.time_complexity || "N/A - Debug mode") 144 | setSpaceComplexityData(newSolution.space_complexity || "N/A - Debug mode") 145 | setIsProcessing(false) 146 | } 147 | 148 | // Set up event listeners 149 | const cleanupFunctions = [ 150 | window.electronAPI.onScreenshotTaken(() => refetch()), 151 | window.electronAPI.onResetView(() => refetch()), 152 | window.electronAPI.onDebugSuccess((data) => { 153 | console.log("Debug success event received with data:", data); 154 | queryClient.setQueryData(["new_solution"], data); 155 | 156 | // Also update local state for immediate rendering 157 | if (data.debug_analysis) { 158 | // Store the debug analysis in its own state variable 159 | setDebugAnalysis(data.debug_analysis); 160 | // Set code separately for the code section 161 | setNewCode(data.code || "// Debug mode - see analysis below"); 162 | 163 | // Process thoughts/analysis points 164 | if (data.debug_analysis.includes('\n\n')) { 165 | const sections = data.debug_analysis.split('\n\n').filter(Boolean); 166 | // Pick first few sections as thoughts 167 | setThoughtsData(sections.slice(0, 3)); 168 | } else if (data.debug_analysis.includes('\n')) { 169 | // Try to find bullet points or numbered lists 170 | const lines = data.debug_analysis.split('\n'); 171 | const bulletPoints = lines.filter(line => 172 | line.trim().match(/^[\d*\-•]+\s/) || 173 | line.trim().match(/^[A-Z][\d\.\)\:]/) || 174 | line.includes(':') && line.length < 100 175 | ); 176 | 177 | if (bulletPoints.length > 0) { 178 | setThoughtsData(bulletPoints.slice(0, 5)); 179 | } else { 180 | setThoughtsData(["Debug analysis based on your screenshots"]); 181 | } 182 | } else { 183 | setThoughtsData(["Debug analysis based on your screenshots"]); 184 | } 185 | } else { 186 | // Fallback to code or default 187 | setNewCode(data.code || "// No analysis available"); 188 | setThoughtsData(data.thoughts || ["Debug analysis based on your screenshots"]); 189 | setDebugAnalysis(null); 190 | } 191 | setTimeComplexityData(data.time_complexity || "N/A - Debug mode"); 192 | setSpaceComplexityData(data.space_complexity || "N/A - Debug mode"); 193 | 194 | setIsProcessing(false); 195 | }), 196 | 197 | window.electronAPI.onDebugStart(() => { 198 | setIsProcessing(true) 199 | }), 200 | window.electronAPI.onDebugError((error: string) => { 201 | showToast( 202 | "Processing Failed", 203 | "There was an error debugging your code.", 204 | "error" 205 | ) 206 | setIsProcessing(false) 207 | console.error("Processing error:", error) 208 | }) 209 | ] 210 | 211 | // Set up resize observer 212 | const updateDimensions = () => { 213 | if (contentRef.current) { 214 | let contentHeight = contentRef.current.scrollHeight 215 | const contentWidth = contentRef.current.scrollWidth 216 | if (tooltipVisible) { 217 | contentHeight += tooltipHeight 218 | } 219 | window.electronAPI.updateContentDimensions({ 220 | width: contentWidth, 221 | height: contentHeight 222 | }) 223 | } 224 | } 225 | 226 | const resizeObserver = new ResizeObserver(updateDimensions) 227 | if (contentRef.current) { 228 | resizeObserver.observe(contentRef.current) 229 | } 230 | updateDimensions() 231 | 232 | return () => { 233 | resizeObserver.disconnect() 234 | cleanupFunctions.forEach((cleanup) => cleanup()) 235 | } 236 | }, [queryClient, setIsProcessing]) 237 | 238 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 239 | setTooltipVisible(visible) 240 | setTooltipHeight(height) 241 | } 242 | 243 | const handleDeleteExtraScreenshot = async (index: number) => { 244 | const screenshotToDelete = screenshots[index] 245 | 246 | try { 247 | const response = await window.electronAPI.deleteScreenshot( 248 | screenshotToDelete.path 249 | ) 250 | 251 | if (response.success) { 252 | refetch() 253 | } else { 254 | console.error("Failed to delete extra screenshot:", response.error) 255 | } 256 | } catch (error) { 257 | console.error("Error deleting extra screenshot:", error) 258 | } 259 | } 260 | 261 | return ( 262 |
263 |
264 | {/* Conditionally render the screenshot queue */} 265 |
266 |
267 |
268 | 273 |
274 |
275 |
276 | 277 | {/* Navbar of commands with the tooltip */} 278 | 287 | 288 | {/* Main Content */} 289 |
290 |
291 |
292 | {/* Thoughts Section */} 293 | 298 |
299 | {thoughtsData.map((thought, index) => ( 300 |
301 |
302 |
{thought}
303 |
304 | ))} 305 |
306 |
307 | ) 308 | } 309 | isLoading={!thoughtsData} 310 | /> 311 | 312 | {/* Code Section */} 313 | 319 | 320 | {/* Debug Analysis Section */} 321 |
322 |

Analysis & Improvements

323 | {!debugAnalysis ? ( 324 |
325 |
326 |

327 | Loading debug analysis... 328 |

329 |
330 |
331 | ) : ( 332 |
333 | {/* Process the debug analysis text by sections and lines */} 334 | {(() => { 335 | // First identify key sections based on common patterns in the debug output 336 | const sections = []; 337 | let currentSection = { title: '', content: [] }; 338 | 339 | // Split by possible section headers (### or ##) 340 | const mainSections = debugAnalysis.split(/(?=^#{1,3}\s|^\*\*\*|^\s*[A-Z][\w\s]+\s*$)/m); 341 | 342 | // Filter out empty sections and process each one 343 | mainSections.filter(Boolean).forEach(sectionText => { 344 | // First line might be a header 345 | const lines = sectionText.split('\n'); 346 | let title = ''; 347 | let startLineIndex = 0; 348 | 349 | // Check if first line is a header 350 | if (lines[0] && (lines[0].startsWith('#') || lines[0].startsWith('**') || 351 | lines[0].match(/^[A-Z][\w\s]+$/) || lines[0].includes('Issues') || 352 | lines[0].includes('Improvements') || lines[0].includes('Optimizations'))) { 353 | title = lines[0].replace(/^#+\s*|\*\*/g, ''); 354 | startLineIndex = 1; 355 | } 356 | 357 | // Add the section 358 | sections.push({ 359 | title, 360 | content: lines.slice(startLineIndex).filter(Boolean) 361 | }); 362 | }); 363 | 364 | // Render the processed sections 365 | return sections.map((section, sectionIndex) => ( 366 |
367 | {section.title && ( 368 |
369 | {section.title} 370 |
371 | )} 372 |
373 | {section.content.map((line, lineIndex) => { 374 | // Handle code blocks - detect full code blocks 375 | if (line.trim().startsWith('```')) { 376 | // If we find the start of a code block, collect all lines until the end 377 | if (line.trim() === '```' || line.trim().startsWith('```')) { 378 | // Find end of this code block 379 | const codeBlockEndIndex = section.content.findIndex( 380 | (l, i) => i > lineIndex && l.trim() === '```' 381 | ); 382 | 383 | if (codeBlockEndIndex > lineIndex) { 384 | // Extract language if specified 385 | const langMatch = line.trim().match(/```(\w+)/); 386 | const language = langMatch ? langMatch[1] : ''; 387 | 388 | // Get the code content 389 | const codeContent = section.content 390 | .slice(lineIndex + 1, codeBlockEndIndex) 391 | .join('\n'); 392 | 393 | // Skip ahead in our loop 394 | lineIndex = codeBlockEndIndex; 395 | 396 | return ( 397 |
398 | {codeContent} 399 |
400 | ); 401 | } 402 | } 403 | } 404 | 405 | // Handle bullet points 406 | if (line.trim().match(/^[\-*•]\s/) || line.trim().match(/^\d+\.\s/)) { 407 | return ( 408 |
409 |
410 |
411 | {line.replace(/^[\-*•]\s|^\d+\.\s/, '')} 412 |
413 |
414 | ); 415 | } 416 | 417 | // Handle inline code 418 | if (line.includes('`')) { 419 | const parts = line.split(/(`[^`]+`)/g); 420 | return ( 421 |
422 | {parts.map((part, partIndex) => { 423 | if (part.startsWith('`') && part.endsWith('`')) { 424 | return {part.slice(1, -1)}; 425 | } 426 | return {part}; 427 | })} 428 |
429 | ); 430 | } 431 | 432 | // Handle sub-headers 433 | if (line.trim().match(/^#+\s/) || (line.trim().match(/^[A-Z][\w\s]+:/) && line.length < 60)) { 434 | return ( 435 |
436 | {line.replace(/^#+\s+/, '')} 437 |
438 | ); 439 | } 440 | 441 | // Regular text 442 | return
{line}
; 443 | })} 444 |
445 |
446 | )); 447 | })()} 448 |
449 | )} 450 |
451 | 452 | {/* Complexity Section */} 453 | 458 |
459 |
460 |
461 |
462 |
463 | ) 464 | } 465 | 466 | export default Debug 467 | -------------------------------------------------------------------------------- /src/_pages/Queue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react" 2 | import { useQuery } from "@tanstack/react-query" 3 | import ScreenshotQueue from "../components/Queue/ScreenshotQueue" 4 | import QueueCommands from "../components/Queue/QueueCommands" 5 | 6 | import { useToast } from "../contexts/toast" 7 | import { Screenshot } from "../types/screenshots" 8 | 9 | async function fetchScreenshots(): Promise { 10 | try { 11 | const existing = await window.electronAPI.getScreenshots() 12 | return existing 13 | } catch (error) { 14 | console.error("Error loading screenshots:", error) 15 | throw error 16 | } 17 | } 18 | 19 | interface QueueProps { 20 | setView: (view: "queue" | "solutions" | "debug") => void 21 | credits: number 22 | currentLanguage: string 23 | setLanguage: (language: string) => void 24 | } 25 | 26 | const Queue: React.FC = ({ 27 | setView, 28 | credits, 29 | currentLanguage, 30 | setLanguage 31 | }) => { 32 | const { showToast } = useToast() 33 | 34 | const [isTooltipVisible, setIsTooltipVisible] = useState(false) 35 | const [tooltipHeight, setTooltipHeight] = useState(0) 36 | const contentRef = useRef(null) 37 | 38 | const { 39 | data: screenshots = [], 40 | isLoading, 41 | refetch 42 | } = useQuery({ 43 | queryKey: ["screenshots"], 44 | queryFn: fetchScreenshots, 45 | staleTime: Infinity, 46 | gcTime: Infinity, 47 | refetchOnWindowFocus: false 48 | }) 49 | 50 | const handleDeleteScreenshot = async (index: number) => { 51 | const screenshotToDelete = screenshots[index] 52 | 53 | try { 54 | const response = await window.electronAPI.deleteScreenshot( 55 | screenshotToDelete.path 56 | ) 57 | 58 | if (response.success) { 59 | refetch() // Refetch screenshots instead of managing state directly 60 | } else { 61 | console.error("Failed to delete screenshot:", response.error) 62 | showToast("Error", "Failed to delete the screenshot file", "error") 63 | } 64 | } catch (error) { 65 | console.error("Error deleting screenshot:", error) 66 | } 67 | } 68 | 69 | useEffect(() => { 70 | // Height update logic 71 | const updateDimensions = () => { 72 | if (contentRef.current) { 73 | let contentHeight = contentRef.current.scrollHeight 74 | const contentWidth = contentRef.current.scrollWidth 75 | if (isTooltipVisible) { 76 | contentHeight += tooltipHeight 77 | } 78 | window.electronAPI.updateContentDimensions({ 79 | width: contentWidth, 80 | height: contentHeight 81 | }) 82 | } 83 | } 84 | 85 | // Initialize resize observer 86 | const resizeObserver = new ResizeObserver(updateDimensions) 87 | if (contentRef.current) { 88 | resizeObserver.observe(contentRef.current) 89 | } 90 | updateDimensions() 91 | 92 | // Set up event listeners 93 | const cleanupFunctions = [ 94 | window.electronAPI.onScreenshotTaken(() => refetch()), 95 | window.electronAPI.onResetView(() => refetch()), 96 | window.electronAPI.onDeleteLastScreenshot(async () => { 97 | if (screenshots.length > 0) { 98 | const lastScreenshot = screenshots[screenshots.length - 1]; 99 | await handleDeleteScreenshot(screenshots.length - 1); 100 | // Toast removed as requested 101 | } else { 102 | showToast("No Screenshots", "There are no screenshots to delete", "neutral"); 103 | } 104 | }), 105 | window.electronAPI.onSolutionError((error: string) => { 106 | showToast( 107 | "Processing Failed", 108 | "There was an error processing your screenshots.", 109 | "error" 110 | ) 111 | setView("queue") // Revert to queue if processing fails 112 | console.error("Processing error:", error) 113 | }), 114 | window.electronAPI.onProcessingNoScreenshots(() => { 115 | showToast( 116 | "No Screenshots", 117 | "There are no screenshots to process.", 118 | "neutral" 119 | ) 120 | }), 121 | // Removed out of credits handler - unlimited credits in this version 122 | ] 123 | 124 | return () => { 125 | resizeObserver.disconnect() 126 | cleanupFunctions.forEach((cleanup) => cleanup()) 127 | } 128 | }, [isTooltipVisible, tooltipHeight, screenshots]) 129 | 130 | const handleTooltipVisibilityChange = (visible: boolean, height: number) => { 131 | setIsTooltipVisible(visible) 132 | setTooltipHeight(height) 133 | } 134 | 135 | const handleOpenSettings = () => { 136 | window.electronAPI.openSettingsPortal(); 137 | }; 138 | 139 | return ( 140 |
141 |
142 |
143 | 148 | 149 | 156 |
157 |
158 |
159 | ) 160 | } 161 | 162 | export default Queue 163 | -------------------------------------------------------------------------------- /src/_pages/SubscribePage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react" 2 | import { supabase } from "../lib/supabase" 3 | import { User } from "@supabase/supabase-js" 4 | 5 | interface SubscribePageProps { 6 | user: User 7 | } 8 | 9 | export default function SubscribePage({ user }: SubscribePageProps) { 10 | const [error, setError] = useState(null) 11 | const containerRef = useRef(null) 12 | 13 | useEffect(() => { 14 | const updateDimensions = () => { 15 | if (containerRef.current) { 16 | window.electronAPI.updateContentDimensions({ 17 | width: 400, // Fixed width 18 | height: 400 // Fixed height 19 | }) 20 | } 21 | } 22 | 23 | updateDimensions() 24 | }, []) 25 | 26 | const handleSignOut = async () => { 27 | try { 28 | const { error: signOutError } = await supabase.auth.signOut() 29 | if (signOutError) throw signOutError 30 | } catch (err) { 31 | console.error("Error signing out:", err) 32 | setError("Failed to sign out. Please try again.") 33 | setTimeout(() => setError(null), 3000) 34 | } 35 | } 36 | 37 | const handleSubscribe = async () => { 38 | if (!user) return 39 | 40 | try { 41 | const result = await window.electronAPI.openSubscriptionPortal({ 42 | id: user.id, 43 | email: user.email! 44 | }) 45 | 46 | if (!result.success) { 47 | throw new Error(result.error || "Failed to open subscription portal") 48 | } 49 | } catch (err) { 50 | console.error("Error opening subscription portal:", err) 51 | setError("Failed to open subscription portal. Please try again.") 52 | setTimeout(() => setError(null), 3000) 53 | } 54 | } 55 | 56 | return ( 57 |
61 |
62 |
63 |

64 | Welcome to Interview Coder 65 |

66 |

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

70 |

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

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

{error}

155 |
156 | )} 157 |
158 |
159 |
160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /src/_pages/SubscribedApp.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/SubscribedApp.tsx 2 | import { useQueryClient } from "@tanstack/react-query" 3 | import { useEffect, useRef, useState } from "react" 4 | import Queue from "../_pages/Queue" 5 | import Solutions from "../_pages/Solutions" 6 | import { useToast } from "../contexts/toast" 7 | 8 | interface SubscribedAppProps { 9 | credits: number 10 | currentLanguage: string 11 | setLanguage: (language: string) => void 12 | } 13 | 14 | const SubscribedApp: React.FC = ({ 15 | credits, 16 | currentLanguage, 17 | setLanguage 18 | }) => { 19 | const queryClient = useQueryClient() 20 | const [view, setView] = useState<"queue" | "solutions" | "debug">("queue") 21 | const containerRef = useRef(null) 22 | const { showToast } = useToast() 23 | 24 | // Let's ensure we reset queries etc. if some electron signals happen 25 | useEffect(() => { 26 | const cleanup = window.electronAPI.onResetView(() => { 27 | queryClient.invalidateQueries({ 28 | queryKey: ["screenshots"] 29 | }) 30 | queryClient.invalidateQueries({ 31 | queryKey: ["problem_statement"] 32 | }) 33 | queryClient.invalidateQueries({ 34 | queryKey: ["solution"] 35 | }) 36 | queryClient.invalidateQueries({ 37 | queryKey: ["new_solution"] 38 | }) 39 | setView("queue") 40 | }) 41 | 42 | return () => { 43 | cleanup() 44 | } 45 | }, []) 46 | 47 | // Dynamically update the window size 48 | useEffect(() => { 49 | if (!containerRef.current) return 50 | 51 | const updateDimensions = () => { 52 | if (!containerRef.current) return 53 | const height = containerRef.current.scrollHeight || 600 54 | const width = containerRef.current.scrollWidth || 800 55 | window.electronAPI?.updateContentDimensions({ width, height }) 56 | } 57 | 58 | // Force initial dimension update immediately 59 | updateDimensions() 60 | 61 | // Set a fallback timer to ensure dimensions are set even if content isn't fully loaded 62 | const fallbackTimer = setTimeout(() => { 63 | window.electronAPI?.updateContentDimensions({ width: 800, height: 600 }) 64 | }, 500) 65 | 66 | const resizeObserver = new ResizeObserver(updateDimensions) 67 | resizeObserver.observe(containerRef.current) 68 | 69 | // Also watch DOM changes 70 | const mutationObserver = new MutationObserver(updateDimensions) 71 | mutationObserver.observe(containerRef.current, { 72 | childList: true, 73 | subtree: true, 74 | attributes: true, 75 | characterData: true 76 | }) 77 | 78 | // Do another update after a delay to catch any late-loading content 79 | const delayedUpdate = setTimeout(updateDimensions, 1000) 80 | 81 | return () => { 82 | resizeObserver.disconnect() 83 | mutationObserver.disconnect() 84 | clearTimeout(fallbackTimer) 85 | clearTimeout(delayedUpdate) 86 | } 87 | }, [view]) 88 | 89 | // Listen for events that might switch views or show errors 90 | useEffect(() => { 91 | const cleanupFunctions = [ 92 | window.electronAPI.onSolutionStart(() => { 93 | setView("solutions") 94 | }), 95 | window.electronAPI.onUnauthorized(() => { 96 | queryClient.removeQueries({ 97 | queryKey: ["screenshots"] 98 | }) 99 | queryClient.removeQueries({ 100 | queryKey: ["solution"] 101 | }) 102 | queryClient.removeQueries({ 103 | queryKey: ["problem_statement"] 104 | }) 105 | setView("queue") 106 | }), 107 | window.electronAPI.onResetView(() => { 108 | queryClient.removeQueries({ 109 | queryKey: ["screenshots"] 110 | }) 111 | queryClient.removeQueries({ 112 | queryKey: ["solution"] 113 | }) 114 | queryClient.removeQueries({ 115 | queryKey: ["problem_statement"] 116 | }) 117 | setView("queue") 118 | }), 119 | window.electronAPI.onResetView(() => { 120 | queryClient.setQueryData(["problem_statement"], null) 121 | }), 122 | window.electronAPI.onProblemExtracted((data: any) => { 123 | if (view === "queue") { 124 | queryClient.invalidateQueries({ 125 | queryKey: ["problem_statement"] 126 | }) 127 | queryClient.setQueryData(["problem_statement"], data) 128 | } 129 | }), 130 | window.electronAPI.onSolutionError((error: string) => { 131 | showToast("Error", error, "error") 132 | }) 133 | ] 134 | return () => cleanupFunctions.forEach((fn) => fn()) 135 | }, [view]) 136 | 137 | return ( 138 |
139 | {view === "queue" ? ( 140 | 146 | ) : view === "solutions" ? ( 147 | 153 | ) : null} 154 |
155 | ) 156 | } 157 | 158 | export default SubscribedApp 159 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Settings, LogOut, ChevronDown, ChevronUp } from 'lucide-react'; 3 | import { Button } from '../ui/button'; 4 | import { useToast } from '../../contexts/toast'; 5 | 6 | interface HeaderProps { 7 | currentLanguage: string; 8 | setLanguage: (language: string) => void; 9 | onOpenSettings: () => void; 10 | } 11 | 12 | // Available programming languages 13 | const LANGUAGES = [ 14 | { value: 'python', label: 'Python' }, 15 | { value: 'javascript', label: 'JavaScript' }, 16 | { value: 'java', label: 'Java' }, 17 | { value: 'cpp', label: 'C++' }, 18 | { value: 'csharp', label: 'C#' }, 19 | { value: 'go', label: 'Go' }, 20 | { value: 'rust', label: 'Rust' }, 21 | { value: 'typescript', label: 'TypeScript' }, 22 | ]; 23 | 24 | export function Header({ currentLanguage, setLanguage, onOpenSettings }: HeaderProps) { 25 | const [dropdownOpen, setDropdownOpen] = useState(false); 26 | const { showToast } = useToast(); 27 | 28 | // Handle logout - clear API key and reload app 29 | const handleLogout = async () => { 30 | try { 31 | // Update config with empty API key 32 | await window.electronAPI.updateConfig({ 33 | apiKey: '', 34 | }); 35 | 36 | showToast('Success', 'Logged out successfully', 'success'); 37 | 38 | // Reload the app after a short delay 39 | setTimeout(() => { 40 | window.location.reload(); 41 | }, 1500); 42 | } catch (error) { 43 | console.error('Error logging out:', error); 44 | showToast('Error', 'Failed to log out', 'error'); 45 | } 46 | }; 47 | 48 | // Handle language selection 49 | const handleLanguageSelect = (lang: string) => { 50 | setLanguage(lang); 51 | setDropdownOpen(false); 52 | 53 | // Also save the language preference to config 54 | window.electronAPI.updateConfig({ 55 | language: lang 56 | }).catch(error => { 57 | console.error('Failed to save language preference:', error); 58 | }); 59 | }; 60 | 61 | const toggleDropdown = () => { 62 | setDropdownOpen(!dropdownOpen); 63 | }; 64 | 65 | // Find the current language object 66 | const currentLangObj = LANGUAGES.find(lang => lang.value === currentLanguage) || LANGUAGES[0]; 67 | 68 | return ( 69 |
70 |
71 | Language: 72 |
73 | 84 | 85 | {dropdownOpen && ( 86 |
87 |
88 | {LANGUAGES.map((lang) => ( 89 | 100 | ))} 101 |
102 |
103 | )} 104 |
105 |
106 | 107 |
108 | 118 | 119 | 129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/components/Queue/ScreenshotItem.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ScreenshotItem.tsx 2 | import React from "react" 3 | import { X } from "lucide-react" 4 | 5 | interface Screenshot { 6 | path: string 7 | preview: string 8 | } 9 | 10 | interface ScreenshotItemProps { 11 | screenshot: Screenshot 12 | onDelete: (index: number) => void 13 | index: number 14 | isLoading: boolean 15 | } 16 | 17 | const ScreenshotItem: React.FC = ({ 18 | screenshot, 19 | onDelete, 20 | index, 21 | isLoading 22 | }) => { 23 | const handleDelete = async () => { 24 | await onDelete(index) 25 | } 26 | 27 | return ( 28 | <> 29 |
34 |
35 | {isLoading && ( 36 |
37 |
38 |
39 | )} 40 | Screenshot 49 |
50 | {!isLoading && ( 51 | 61 | )} 62 |
63 | 64 | ) 65 | } 66 | 67 | export default ScreenshotItem 68 | -------------------------------------------------------------------------------- /src/components/Queue/ScreenshotQueue.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ScreenshotItem from "./ScreenshotItem" 3 | 4 | interface Screenshot { 5 | path: string 6 | preview: string 7 | } 8 | 9 | interface ScreenshotQueueProps { 10 | isLoading: boolean 11 | screenshots: Screenshot[] 12 | onDeleteScreenshot: (index: number) => void 13 | } 14 | const ScreenshotQueue: React.FC = ({ 15 | isLoading, 16 | screenshots, 17 | onDeleteScreenshot 18 | }) => { 19 | if (screenshots.length === 0) { 20 | return <> 21 | } 22 | 23 | const displayScreenshots = screenshots.slice(0, 5) 24 | 25 | return ( 26 |
27 | {displayScreenshots.map((screenshot, index) => ( 28 | 35 | ))} 36 |
37 | ) 38 | } 39 | 40 | export default ScreenshotQueue 41 | -------------------------------------------------------------------------------- /src/components/UpdateNotification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { Dialog, DialogContent } from "./ui/dialog" 3 | import { Button } from "./ui/button" 4 | import { useToast } from "../contexts/toast" 5 | 6 | export const UpdateNotification: React.FC = () => { 7 | const [updateAvailable, setUpdateAvailable] = useState(false) 8 | const [updateDownloaded, setUpdateDownloaded] = useState(false) 9 | const [isDownloading, setIsDownloading] = useState(false) 10 | const { showToast } = useToast() 11 | 12 | useEffect(() => { 13 | console.log("UpdateNotification: Setting up event listeners") 14 | 15 | const unsubscribeAvailable = window.electronAPI.onUpdateAvailable( 16 | (info) => { 17 | console.log("UpdateNotification: Update available received", info) 18 | setUpdateAvailable(true) 19 | } 20 | ) 21 | 22 | const unsubscribeDownloaded = window.electronAPI.onUpdateDownloaded( 23 | (info) => { 24 | console.log("UpdateNotification: Update downloaded received", info) 25 | setUpdateDownloaded(true) 26 | setIsDownloading(false) 27 | } 28 | ) 29 | 30 | return () => { 31 | console.log("UpdateNotification: Cleaning up event listeners") 32 | unsubscribeAvailable() 33 | unsubscribeDownloaded() 34 | } 35 | }, []) 36 | 37 | const handleStartUpdate = async () => { 38 | console.log("UpdateNotification: Starting update download") 39 | setIsDownloading(true) 40 | const result = await window.electronAPI.startUpdate() 41 | console.log("UpdateNotification: Update download result", result) 42 | if (!result.success) { 43 | setIsDownloading(false) 44 | showToast("Error", "Failed to download update", "error") 45 | } 46 | } 47 | 48 | const handleInstallUpdate = () => { 49 | console.log("UpdateNotification: Installing update") 50 | window.electronAPI.installUpdate() 51 | } 52 | 53 | console.log("UpdateNotification: Render state", { 54 | updateAvailable, 55 | updateDownloaded, 56 | isDownloading 57 | }) 58 | if (!updateAvailable && !updateDownloaded) return null 59 | 60 | return ( 61 | 62 | e.preventDefault()} 65 | > 66 |
67 |

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

72 |

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

77 |
78 | {updateDownloaded ? ( 79 | 86 | ) : ( 87 | 95 | )} 96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/components/WelcomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from './ui/button'; 3 | 4 | interface WelcomeScreenProps { 5 | onOpenSettings: () => void; 6 | } 7 | 8 | export const WelcomeScreen: React.FC = ({ onOpenSettings }) => { 9 | return ( 10 |
11 |
12 |

13 | Interview Coder 14 | Unlocked Edition 15 |

16 | 17 |
18 |

Welcome to Interview Coder

19 |

20 | This application helps you ace technical interviews by providing AI-powered 21 | solutions to coding problems. 22 |

23 |
24 |

Global Shortcuts

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

Getting Started

56 |

57 | Before using the application, you need to configure your OpenAI API key. 58 |

59 | 65 |
66 | 67 |
68 | Start by taking screenshots of your coding problem (Ctrl+H / Cmd+H) 69 |
70 |
71 |
72 | ); 73 | }; -------------------------------------------------------------------------------- /src/components/shared/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | interface LanguageSelectorProps { 4 | currentLanguage: string 5 | setLanguage: (language: string) => void 6 | } 7 | 8 | export const LanguageSelector: React.FC = ({ 9 | currentLanguage, 10 | setLanguage 11 | }) => { 12 | const handleLanguageChange = async ( 13 | e: React.ChangeEvent 14 | ) => { 15 | const newLanguage = e.target.value 16 | 17 | try { 18 | // Save language preference to electron store 19 | await window.electronAPI.updateConfig({ language: newLanguage }) 20 | 21 | // Update global language variable 22 | window.__LANGUAGE__ = newLanguage 23 | 24 | // Update state in React 25 | setLanguage(newLanguage) 26 | 27 | console.log(`Language changed to ${newLanguage}`); 28 | } catch (error) { 29 | console.error("Error updating language:", error) 30 | } 31 | } 32 | 33 | return ( 34 |
35 |
36 | Language 37 | 54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { cn } from "../../lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline" 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 rounded-md px-3 text-xs", 25 | lg: "h-10 rounded-md px-8", 26 | icon: "h-9 w-9" 27 | } 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default" 32 | } 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "../../lib/utils" 3 | 4 | const Card = React.forwardRef< 5 | HTMLDivElement, 6 | React.HTMLAttributes 7 | >(({ className, ...props }, ref) => ( 8 |
16 | )) 17 | Card.displayName = "Card" 18 | 19 | const CardHeader = React.forwardRef< 20 | HTMLDivElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 |
28 | )) 29 | CardHeader.displayName = "CardHeader" 30 | 31 | const CardTitle = React.forwardRef< 32 | HTMLParagraphElement, 33 | React.HTMLAttributes 34 | >(({ className, ...props }, ref) => ( 35 |

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

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

60 | )) 61 | CardContent.displayName = "CardContent" 62 | 63 | const CardFooter = React.forwardRef< 64 | HTMLDivElement, 65 | React.HTMLAttributes 66 | >(({ className, ...props }, ref) => ( 67 |
72 | )) 73 | CardFooter.displayName = "CardFooter" 74 | 75 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 76 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | // src/components/ui/dialog.tsx 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { cn } from "../../lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | const DialogTrigger = DialogPrimitive.Trigger 9 | const DialogPortal = DialogPrimitive.Portal 10 | 11 | const DialogOverlay = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 22 | 23 | const DialogContent = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | 29 | 41 | {children} 42 | 43 | 44 | )) 45 | DialogContent.displayName = DialogPrimitive.Content.displayName 46 | 47 | const DialogClose = DialogPrimitive.Close 48 | 49 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 50 |
54 | ) 55 | DialogHeader.displayName = "DialogHeader" 56 | 57 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 58 |
62 | ) 63 | DialogFooter.displayName = "DialogFooter" 64 | 65 | const DialogTitle = React.forwardRef< 66 | HTMLHeadingElement, 67 | React.HTMLAttributes 68 | >(({ className, ...props }, ref) => ( 69 | 74 | )) 75 | DialogTitle.displayName = DialogPrimitive.Title.displayName 76 | 77 | const DialogDescription = React.forwardRef< 78 | HTMLParagraphElement, 79 | React.HTMLAttributes 80 | >(({ className, ...props }, ref) => ( 81 | 86 | )) 87 | DialogDescription.displayName = DialogPrimitive.Description.displayName 88 | 89 | export { 90 | Dialog, 91 | DialogTrigger, 92 | DialogContent, 93 | DialogClose, 94 | DialogHeader, 95 | DialogFooter, 96 | DialogTitle, 97 | DialogDescription 98 | } 99 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cn } from "../../lib/utils" 3 | 4 | export interface InputProps 5 | extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ) 20 | } 21 | ) 22 | Input.displayName = "Input" 23 | 24 | export { Input } 25 | -------------------------------------------------------------------------------- /src/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ToastPrimitive from "@radix-ui/react-toast" 3 | import { cn } from "../../lib/utils" 4 | import { AlertCircle, CheckCircle2, Info, X } from "lucide-react" 5 | 6 | const ToastProvider = ToastPrimitive.Provider 7 | 8 | export type ToastMessage = { 9 | title: string 10 | description: string 11 | variant: ToastVariant 12 | } 13 | 14 | const ToastViewport = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, ...props }, ref) => ( 18 | 26 | )) 27 | ToastViewport.displayName = ToastPrimitive.Viewport.displayName 28 | 29 | type ToastVariant = "neutral" | "success" | "error" 30 | 31 | interface ToastProps 32 | extends React.ComponentPropsWithoutRef { 33 | variant?: ToastVariant 34 | swipeDirection?: "right" | "left" | "up" | "down" 35 | } 36 | 37 | const toastVariants: Record< 38 | ToastVariant, 39 | { icon: React.ReactNode; bgColor: string } 40 | > = { 41 | neutral: { 42 | icon: , 43 | bgColor: "bg-amber-100" 44 | }, 45 | success: { 46 | icon: , 47 | bgColor: "bg-emerald-100" 48 | }, 49 | error: { 50 | icon: , 51 | bgColor: "bg-red-100" 52 | } 53 | } 54 | 55 | const Toast = React.forwardRef< 56 | React.ElementRef, 57 | ToastProps 58 | >(({ className, variant = "neutral", ...props }, ref) => ( 59 | 69 | {toastVariants[variant].icon} 70 |
{props.children}
71 | 72 | 73 | 74 |
75 | )) 76 | Toast.displayName = ToastPrimitive.Root.displayName 77 | 78 | const ToastAction = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | ToastAction.displayName = ToastPrimitive.Action.displayName 92 | 93 | const ToastTitle = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | ToastTitle.displayName = ToastPrimitive.Title.displayName 104 | 105 | const ToastDescription = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | ToastDescription.displayName = ToastPrimitive.Description.displayName 116 | 117 | export type { ToastProps, ToastVariant } 118 | export { 119 | ToastProvider, 120 | ToastViewport, 121 | Toast, 122 | ToastAction, 123 | ToastTitle, 124 | ToastDescription 125 | } 126 | -------------------------------------------------------------------------------- /src/contexts/toast.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react" 2 | 3 | type ToastVariant = "neutral" | "success" | "error" 4 | 5 | interface ToastContextType { 6 | showToast: (title: string, description: string, variant: ToastVariant) => void 7 | } 8 | 9 | export const ToastContext = createContext( 10 | undefined 11 | ) 12 | 13 | export function useToast() { 14 | const context = useContext(ToastContext) 15 | if (!context) { 16 | throw new Error("useToast must be used within a ToastProvider") 17 | } 18 | return context 19 | } 20 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { ToastMessage } from "./components/ui/toast" 4 | 5 | interface ImportMetaEnv { 6 | readonly VITE_SUPABASE_URL: string 7 | readonly VITE_SUPABASE_ANON_KEY: string 8 | readonly NODE_ENV: string 9 | } 10 | 11 | interface ImportMeta { 12 | readonly env: ImportMetaEnv 13 | } 14 | 15 | interface ElectronAPI { 16 | openSubscriptionPortal: (authData: { 17 | id: string 18 | email: string 19 | }) => Promise<{ success: boolean; error?: string }> 20 | updateContentDimensions: (dimensions: { 21 | width: number 22 | height: number 23 | }) => Promise 24 | clearStore: () => Promise<{ success: boolean; error?: string }> 25 | getScreenshots: () => Promise<{ 26 | success: boolean 27 | previews?: Array<{ path: string; preview: string }> | null 28 | error?: string 29 | }> 30 | deleteScreenshot: ( 31 | path: string 32 | ) => Promise<{ success: boolean; error?: string }> 33 | onScreenshotTaken: ( 34 | callback: (data: { path: string; preview: string }) => void 35 | ) => () => void 36 | onResetView: (callback: () => void) => () => void 37 | onSolutionStart: (callback: () => void) => () => void 38 | onDebugStart: (callback: () => void) => () => void 39 | onDebugSuccess: (callback: (data: any) => void) => () => void 40 | onSolutionError: (callback: (error: string) => void) => () => void 41 | onProcessingNoScreenshots: (callback: () => void) => () => void 42 | onProblemExtracted: (callback: (data: any) => void) => () => void 43 | onSolutionSuccess: (callback: (data: any) => void) => () => void 44 | onUnauthorized: (callback: () => void) => () => void 45 | onDebugError: (callback: (error: string) => void) => () => void 46 | openExternal: (url: string) => void 47 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 48 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 49 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 50 | triggerReset: () => Promise<{ success: boolean; error?: string }> 51 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 52 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 53 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 54 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 55 | onSubscriptionUpdated: (callback: () => void) => () => void 56 | onSubscriptionPortalClosed: (callback: () => void) => () => void 57 | // Add update-related methods 58 | startUpdate: () => Promise<{ success: boolean; error?: string }> 59 | installUpdate: () => void 60 | onUpdateAvailable: (callback: (info: any) => void) => () => void 61 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 62 | } 63 | 64 | interface Window { 65 | electronAPI: ElectronAPI 66 | electron: { 67 | ipcRenderer: { 68 | on(channel: string, func: (...args: any[]) => void): void 69 | removeListener(channel: string, func: (...args: any[]) => void): void 70 | } 71 | } 72 | __CREDITS__: number 73 | __LANGUAGE__: string 74 | __IS_INITIALIZED__: boolean 75 | __AUTH_TOKEN__: string 76 | } 77 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Custom animations for UI elements */ 6 | @keyframes fadeIn { 7 | from { opacity: 0; transform: translate(-50%, -48%); } 8 | to { opacity: 0.98; transform: translate(-50%, -50%); } 9 | } 10 | 11 | /* Dialog transition improvements */ 12 | .settings-dialog { 13 | transform-origin: center; 14 | backface-visibility: hidden; 15 | will-change: opacity, transform; 16 | } 17 | 18 | /* Custom styling for dropdowns */ 19 | select { 20 | appearance: menulist; 21 | background-color: #000000 !important; 22 | color: white !important; 23 | } 24 | 25 | option { 26 | background-color: #1a1a1a !important; 27 | color: white !important; 28 | padding: 8px !important; 29 | } 30 | 31 | .frosted-glass { 32 | background: rgba(26, 26, 26, 0.8); 33 | backdrop-filter: blur(8px); 34 | } 35 | 36 | .auth-button { 37 | background: rgba(252, 252, 252, 0.98); 38 | color: rgba(60, 60, 60, 0.9); 39 | transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 40 | position: relative; 41 | z-index: 2; 42 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); 43 | } 44 | 45 | .auth-button:hover { 46 | background: rgba(255, 255, 255, 1); 47 | } 48 | 49 | .auth-button::before { 50 | content: ""; 51 | position: absolute; 52 | inset: -8px; 53 | background: linear-gradient(45deg, #ff000000, #0000ff00); 54 | z-index: -1; 55 | transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 56 | border-radius: inherit; 57 | filter: blur(24px); 58 | opacity: 0; 59 | } 60 | 61 | .auth-button:hover::before { 62 | background: linear-gradient( 63 | 45deg, 64 | rgba(255, 0, 0, 0.4), 65 | rgba(0, 0, 255, 0.4) 66 | ); 67 | filter: blur(48px); 68 | inset: -16px; 69 | opacity: 1; 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | // This file has been emptied to remove Supabase dependencies. 2 | // The open-source version uses local configuration instead. 3 | 4 | // Export empty objects to prevent import errors in case any components still reference this file 5 | export const supabase = { 6 | auth: { 7 | getUser: async () => ({ data: { user: null } }), 8 | onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }), 9 | signOut: async () => ({ error: null }), 10 | exchangeCodeForSession: async () => ({ error: null }), 11 | signInWithOAuth: async () => ({ data: null, error: null }) 12 | }, 13 | from: () => ({ 14 | select: () => ({ 15 | eq: () => ({ 16 | single: async () => null, 17 | maybeSingle: async () => null 18 | }) 19 | }), 20 | update: () => ({ 21 | eq: () => ({ 22 | select: () => ({ 23 | single: async () => null 24 | }) 25 | }) 26 | }) 27 | }), 28 | channel: () => ({ 29 | on: () => ({ 30 | subscribe: () => ({ 31 | unsubscribe: () => {} 32 | }) 33 | }) 34 | }) 35 | }; 36 | 37 | export const signInWithGoogle = async () => { 38 | console.log("Sign in with Google not available in open-source version"); 39 | return { data: null }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | // src/lib/utils.ts 2 | 3 | import { type ClassValue, clsx } from "clsx" 4 | import { twMerge } from "tailwind-merge" 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)) 8 | } 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | export interface ElectronAPI { 2 | // Original methods 3 | openSubscriptionPortal: (authData: { 4 | id: string 5 | email: string 6 | }) => Promise<{ success: boolean; error?: string }> 7 | updateContentDimensions: (dimensions: { 8 | width: number 9 | height: number 10 | }) => Promise 11 | clearStore: () => Promise<{ success: boolean; error?: string }> 12 | getScreenshots: () => Promise<{ 13 | success: boolean 14 | previews?: Array<{ path: string; preview: string }> | null 15 | error?: string 16 | }> 17 | deleteScreenshot: ( 18 | path: string 19 | ) => Promise<{ success: boolean; error?: string }> 20 | onScreenshotTaken: ( 21 | callback: (data: { path: string; preview: string }) => void 22 | ) => () => void 23 | onResetView: (callback: () => void) => () => void 24 | onSolutionStart: (callback: () => void) => () => void 25 | onDebugStart: (callback: () => void) => () => void 26 | onDebugSuccess: (callback: (data: any) => void) => () => void 27 | onSolutionError: (callback: (error: string) => void) => () => void 28 | onProcessingNoScreenshots: (callback: () => void) => () => void 29 | onProblemExtracted: (callback: (data: any) => void) => () => void 30 | onSolutionSuccess: (callback: (data: any) => void) => () => void 31 | onUnauthorized: (callback: () => void) => () => void 32 | onDebugError: (callback: (error: string) => void) => () => void 33 | openExternal: (url: string) => void 34 | toggleMainWindow: () => Promise<{ success: boolean; error?: string }> 35 | triggerScreenshot: () => Promise<{ success: boolean; error?: string }> 36 | triggerProcessScreenshots: () => Promise<{ success: boolean; error?: string }> 37 | triggerReset: () => Promise<{ success: boolean; error?: string }> 38 | triggerMoveLeft: () => Promise<{ success: boolean; error?: string }> 39 | triggerMoveRight: () => Promise<{ success: boolean; error?: string }> 40 | triggerMoveUp: () => Promise<{ success: boolean; error?: string }> 41 | triggerMoveDown: () => Promise<{ success: boolean; error?: string }> 42 | onSubscriptionUpdated: (callback: () => void) => () => void 43 | onSubscriptionPortalClosed: (callback: () => void) => () => void 44 | startUpdate: () => Promise<{ success: boolean; error?: string }> 45 | installUpdate: () => void 46 | onUpdateAvailable: (callback: (info: any) => void) => () => void 47 | onUpdateDownloaded: (callback: (info: any) => void) => () => void 48 | 49 | decrementCredits: () => Promise 50 | setInitialCredits: (credits: number) => Promise 51 | onCreditsUpdated: (callback: (credits: number) => void) => () => void 52 | onOutOfCredits: (callback: () => void) => () => void 53 | openSettingsPortal: () => Promise 54 | getPlatform: () => string 55 | 56 | // New methods for OpenAI integration 57 | getConfig: () => Promise<{ apiKey: string; model: string }> 58 | updateConfig: (config: { apiKey?: string; model?: string }) => Promise 59 | checkApiKey: () => Promise 60 | validateApiKey: (apiKey: string) => Promise<{ valid: boolean; error?: string }> 61 | openLink: (url: string) => void 62 | onApiKeyInvalid: (callback: () => void) => () => void 63 | removeListener: (eventName: string, callback: (...args: any[]) => void) => void 64 | } 65 | 66 | declare global { 67 | interface Window { 68 | electronAPI: ElectronAPI 69 | electron: { 70 | ipcRenderer: { 71 | on: (channel: string, func: (...args: any[]) => void) => void 72 | removeListener: ( 73 | channel: string, 74 | func: (...args: any[]) => void 75 | ) => void 76 | } 77 | } 78 | __CREDITS__: number 79 | __LANGUAGE__: string 80 | __IS_INITIALIZED__: boolean 81 | __AUTH_TOKEN__?: string | null 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | __IS_INITIALIZED__: boolean 3 | __CREDITS__: number 4 | __LANGUAGE__: string 5 | __AUTH_TOKEN__: string | null 6 | supabase: any // Replace with proper Supabase client type if needed 7 | electron: any // Replace with proper Electron type if needed 8 | electronAPI: any // Replace with proper Electron API type if needed 9 | } 10 | -------------------------------------------------------------------------------- /src/types/index.tsx: -------------------------------------------------------------------------------- 1 | export interface Screenshot { 2 | id: string 3 | path: string 4 | timestamp: number 5 | thumbnail: string // Base64 thumbnail 6 | } 7 | 8 | export interface Solution { 9 | initial_thoughts: string[] 10 | thought_steps: string[] 11 | description: string 12 | code: string 13 | } 14 | -------------------------------------------------------------------------------- /src/types/screenshots.ts: -------------------------------------------------------------------------------- 1 | export interface Screenshot { 2 | path: string 3 | preview: string 4 | } 5 | -------------------------------------------------------------------------------- /src/types/solutions.ts: -------------------------------------------------------------------------------- 1 | export interface Solution { 2 | initial_thoughts: string[] 3 | thought_steps: string[] 4 | description: string 5 | code: string 6 | } 7 | 8 | export interface SolutionsResponse { 9 | [key: string]: Solution 10 | } 11 | 12 | export interface ProblemStatementData { 13 | problem_statement: string 14 | input_format: { 15 | description: string 16 | parameters: any[] 17 | } 18 | output_format: { 19 | description: string 20 | type: string 21 | subtype: string 22 | } 23 | complexity: { 24 | time: string 25 | space: string 26 | } 27 | test_cases: any[] 28 | validation_type: string 29 | difficulty: string 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/platform.ts: -------------------------------------------------------------------------------- 1 | // Get the platform safely 2 | const getPlatform = () => { 3 | try { 4 | return window.electronAPI?.getPlatform() || 'win32' // Default to win32 if API is not available 5 | } catch { 6 | return 'win32' // Default to win32 if there's an error 7 | } 8 | } 9 | 10 | // Platform-specific command key symbol 11 | export const COMMAND_KEY = getPlatform() === 'darwin' ? '⌘' : 'Ctrl' 12 | 13 | // Helper to check if we're on Windows 14 | export const isWindows = getPlatform() === 'win32' 15 | 16 | // Helper to check if we're on macOS 17 | export const isMacOS = getPlatform() === 'darwin' -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SUPABASE_URL: string 5 | 6 | readonly VITE_SUPABASE_ANON_KEY: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /stealth-run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo === Interview Coder - Invisible Edition (No Paywall) === 3 | echo. 4 | echo IMPORTANT: This app is designed to be INVISIBLE by default! 5 | echo Use the keyboard shortcuts to control it: 6 | echo. 7 | echo - Toggle Visibility: Ctrl+B (or Cmd+B on Mac) 8 | echo - Take Screenshot: Ctrl+H 9 | echo - Process Screenshots: Ctrl+Enter 10 | echo - Move Window: Ctrl+Arrows (Left/Right/Up/Down) 11 | echo - Adjust Opacity: Ctrl+[ (decrease) / Ctrl+] (increase) 12 | echo - Reset View: Ctrl+R 13 | echo - Quit App: Ctrl+Q 14 | echo. 15 | echo When you press Ctrl+B, the window will toggle between visible and invisible. 16 | echo If movement shortcuts aren't working, try making the window visible first with Ctrl+B. 17 | echo. 18 | 19 | cd /D "%~dp0" 20 | 21 | echo === Step 1: Creating required directories... === 22 | mkdir "%APPDATA%\interview-coder-v1\temp" 2>nul 23 | mkdir "%APPDATA%\interview-coder-v1\cache" 2>nul 24 | mkdir "%APPDATA%\interview-coder-v1\screenshots" 2>nul 25 | mkdir "%APPDATA%\interview-coder-v1\extra_screenshots" 2>nul 26 | 27 | echo === Step 2: Cleaning previous builds... === 28 | echo Removing old build files to ensure a fresh start... 29 | rmdir /s /q dist dist-electron 2>nul 30 | del /q .env 2>nul 31 | 32 | echo === Step 3: Building application... === 33 | echo This may take a moment... 34 | call npm run build 35 | 36 | echo === Step 4: Launching in stealth mode... === 37 | echo Remember: Press Ctrl+B to make it visible, Ctrl+[ and Ctrl+] to adjust opacity! 38 | echo. 39 | set NODE_ENV=production 40 | start /B cmd /c "npx electron ./dist-electron/main.js" 41 | 42 | echo App is now running invisibly! Press Ctrl+B to make it visible. 43 | echo. 44 | echo If you encounter any issues: 45 | echo 1. Make sure you've installed dependencies with 'npm install' 46 | echo 2. Press Ctrl+B multiple times to toggle visibility 47 | echo 3. Check Task Manager to verify the app is running -------------------------------------------------------------------------------- /stealth-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "=== Interview Coder - Invisible Edition (No Paywall) ===" 3 | echo 4 | echo "IMPORTANT: This app is designed to be INVISIBLE by default!" 5 | echo "Use the keyboard shortcuts to control it:" 6 | echo 7 | echo "- Toggle Visibility: Cmd+B" 8 | echo "- Take Screenshot: Cmd+H" 9 | echo "- Process Screenshots: Cmd+Enter" 10 | echo "- Move Window: Cmd+Arrows (Left/Right/Up/Down)" 11 | echo "- Adjust Opacity: Cmd+[ (decrease) / Cmd+] (increase)" 12 | echo "- Reset View: Cmd+R" 13 | echo "- Quit App: Cmd+Q" 14 | echo 15 | echo "When you press Cmd+B, the window will toggle between visible and invisible." 16 | echo "If movement shortcuts aren't working, try making the window visible first with Cmd+B." 17 | echo 18 | 19 | # Navigate to script directory 20 | cd "$(dirname "$0")" 21 | 22 | echo "=== Step 1: Creating required directories... ===" 23 | mkdir -p ~/Library/Application\ Support/interview-coder-v1/temp 24 | mkdir -p ~/Library/Application\ Support/interview-coder-v1/cache 25 | mkdir -p ~/Library/Application\ Support/interview-coder-v1/screenshots 26 | mkdir -p ~/Library/Application\ Support/interview-coder-v1/extra_screenshots 27 | 28 | echo "=== Step 2: Cleaning previous builds... ===" 29 | echo "Removing old build files to ensure a fresh start..." 30 | rm -rf dist dist-electron 31 | rm -f .env 32 | 33 | echo "=== Step 3: Building application... ===" 34 | echo "This may take a moment..." 35 | npm run build 36 | 37 | echo "=== Step 4: Launching in stealth mode... ===" 38 | echo "Remember: Cmd+B to make it visible, Cmd+[ and Cmd+] to adjust opacity!" 39 | echo 40 | export NODE_ENV=production 41 | npx electron ./dist-electron/main.js & 42 | 43 | echo "App is now running invisibly! Press Cmd+B to make it visible." 44 | echo 45 | echo "If you encounter any issues:" 46 | echo "1. Make sure you've installed dependencies with 'npm install'" 47 | echo "2. Make sure this script has execute permissions (chmod +x stealth-run.sh)" 48 | echo "3. Press Cmd+B multiple times to toggle visibility" 49 | echo "4. Check Activity Monitor to verify the app is running" 50 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Inter", "system-ui", "sans-serif"] 8 | }, 9 | animation: { 10 | in: "in 0.2s ease-out", 11 | out: "out 0.2s ease-in", 12 | pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", 13 | shimmer: "shimmer 2s linear infinite", 14 | "text-gradient-wave": "textGradientWave 2s infinite ease-in-out" 15 | }, 16 | keyframes: { 17 | textGradientWave: { 18 | "0%": { backgroundPosition: "0% 50%" }, 19 | "100%": { backgroundPosition: "200% 50%" } 20 | }, 21 | shimmer: { 22 | "0%": { 23 | backgroundPosition: "200% 0" 24 | }, 25 | "100%": { 26 | backgroundPosition: "-200% 0" 27 | } 28 | }, 29 | in: { 30 | "0%": { transform: "translateY(100%)", opacity: 0 }, 31 | "100%": { transform: "translateY(0)", opacity: 1 } 32 | }, 33 | out: { 34 | "0%": { transform: "translateY(0)", opacity: 1 }, 35 | "100%": { transform: "translateY(100%)", opacity: 0 } 36 | }, 37 | pulse: { 38 | "0%, 100%": { 39 | opacity: 1 40 | }, 41 | "50%": { 42 | opacity: 0.5 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | plugins: [] 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.electron.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "noUnusedLocals": false, 9 | "noUnusedParameters": false, 10 | "outDir": "dist-electron", 11 | "esModuleInterop": true, 12 | "noImplicitAny": false, 13 | "strictNullChecks": false, 14 | "baseUrl": ".", 15 | "paths": { 16 | "main": ["electron/main.ts"] 17 | } 18 | }, 19 | "include": ["electron/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ES2020", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react-jsx", 13 | "strict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "allowJs": true, 18 | "esModuleInterop": true, 19 | "allowImportingTsExtensions": true, 20 | "types": ["vite/client"] 21 | }, 22 | "include": ["electron/**/*", "src/**/*"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "vite" 3 | import electron from "vite-plugin-electron" 4 | import react from "@vitejs/plugin-react" 5 | import path from "path" 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | electron([ 11 | { 12 | // main.ts 13 | entry: "electron/main.ts", 14 | vite: { 15 | build: { 16 | outDir: "dist-electron", 17 | sourcemap: true, 18 | minify: false, 19 | rollupOptions: { 20 | external: ["electron"] 21 | } 22 | } 23 | } 24 | }, 25 | { 26 | // preload.ts 27 | entry: "electron/preload.ts", 28 | vite: { 29 | build: { 30 | outDir: "dist-electron", 31 | sourcemap: true, 32 | rollupOptions: { 33 | external: ["electron"] 34 | } 35 | } 36 | } 37 | } 38 | ]) 39 | ], 40 | base: process.env.NODE_ENV === "production" ? "./" : "/", 41 | server: { 42 | port: 54321, 43 | strictPort: true, 44 | watch: { 45 | usePolling: true 46 | } 47 | }, 48 | build: { 49 | outDir: "dist", 50 | emptyOutDir: true, 51 | sourcemap: true 52 | }, 53 | resolve: { 54 | alias: { 55 | "@": path.resolve(__dirname, "./src") 56 | } 57 | } 58 | }) 59 | --------------------------------------------------------------------------------