├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── prettier.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.png ├── icon.svg ├── icon@2x.png ├── iconTemplate.png ├── iconTemplate@2x.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png ├── build └── .gitignore ├── images ├── OpenAI_Logo.svg ├── minimized.jpg ├── send.svg ├── smol_chicken.svg └── vertical-grip.png ├── package-lock.json ├── package.json ├── release └── app │ ├── package-lock.json │ └── package.json ├── scripts ├── configs │ ├── .eslintrc │ ├── postcss.config.js │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ ├── erb-logo.png │ └── palette-sponsor-banner.svg ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── copy-version-number.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── src ├── __tests__ │ └── App.test.tsx ├── components │ ├── pane.tsx │ └── sidebar.tsx ├── images │ ├── OpenAI_Logo.svg │ ├── godmode.icns │ ├── godmode.ico │ ├── godmode.png │ ├── godmodeicon.icns │ ├── icon.icns │ ├── icon.png │ ├── icon@2x.png │ ├── iconTemplate.png │ ├── iconTemplate@2x.png │ ├── minimized.jpg │ ├── send.svg │ ├── smol_chicken.svg │ └── vertical-grip.png ├── lib │ ├── constants.ts │ ├── types.ts │ └── utils.ts ├── main │ ├── apify.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ └── util.ts ├── providers │ ├── bard.js │ ├── bing.js │ ├── claude.js │ ├── claude2.js │ ├── falcon180bspace.js │ ├── huggingchat.js │ ├── inflection.js │ ├── lepton-llama.js │ ├── oobabooga.js │ ├── openai.js │ ├── openrouter.js │ ├── perplexity-labs.js │ ├── perplexity.js │ ├── phind.js │ ├── poe.js │ ├── provider.js │ ├── smol.js │ ├── stablechat.js │ ├── together.js │ ├── vercel.js │ └── you.js ├── renderer │ ├── App.css │ ├── App.tsx │ ├── TitleBar.tsx │ ├── browserPane.tsx │ ├── components │ │ ├── settings.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input.tsx │ │ │ └── tooltip.tsx │ ├── index.ejs │ ├── index.tsx │ ├── layout.tsx │ ├── preload.d.ts │ └── promptCritic.tsx └── settings │ ├── archive │ └── settings.js │ ├── settings.css │ ├── settings.html │ └── settings.tsx ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | APPLE_ID=xxxxxx@gmail.com 2 | APPLE_PASSWORD=xxxxx-asdadw-sxxxxxx-xsxsxsxs 3 | APPLE_TEAM_ID=xxxxxxxxx -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | screenshots/Steps to reproduce the behavior, or a video/loom recording 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | 21 | **Desktop (please complete the following information):** 22 | 23 | - OS: [e.g. iOS] 24 | - GodMode Version [e.g. v1 beta 5] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Request]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | jobs: 7 | prettier: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v3 12 | with: 13 | ref: '${{ github.head_ref }}' 14 | token: '${{ secrets.GH_PAT }}' 15 | - name: Prettify code 16 | uses: creyD/prettier_action@v4.3 17 | with: 18 | prettier_options: --write . 19 | only_changed: True 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Apply prettier changes 24 | branch: ${{ github.head_ref }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | dist 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | .DS_Store 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # TypeScript cache 45 | *.tsbuildinfo 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | .env.test 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless/ 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | # DynamoDB Local files 85 | .dynamodb/ 86 | 87 | # Webpack 88 | .webpack/ 89 | 90 | # Electron-Forge 91 | out/ 92 | forge.config.js 93 | release/ 94 | scripts/dll 95 | 96 | # First run flag 97 | first-run.flag 98 | 99 | todo.md 100 | .env 101 | 102 | # VSCode 103 | .vscode/* 104 | 105 | tmp.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | scripts/dll/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at electronreactboilerplate@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contributors guide 2 | 3 | ## seeking contributors! 4 | 5 | this is a FOSS effort and will never be commercialized (no tracking, will not be a paid app (but the chat apps within might), etc). please check out the feature requests (https://github.com/smol-ai/GodMode/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) if you'd like to help! in particular, we'd like: 6 | 7 | - **a PR to make the 3 panels customizable to different URLs and input targets!** 8 | - a "Windows maintainer" to handle Windows user needs and beta test every release to make sure we didn't break something for them 9 | 10 | ## Install and Run from Source 11 | 12 | To install and run from source, follow these steps: 13 | 14 | 1. Clone the repository and navigate to the project folder: 15 | 16 | ```bash 17 | git clone https://github.com/smol-ai/GodMode.git 18 | cd GodMode 19 | ``` 20 | 21 | 2. Install dependencies: 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | 3. Start the application and generate the platform-specific desktop file: 28 | 29 | ```bash 30 | npm run start 31 | ``` 32 | 33 | This command will launch the application and automatically generate the appropriate desktop file or shortcut for your operating system (Linux, macOS, or Windows). 34 | 35 | ## adding/fixing providers 36 | 37 | The best way to contribute is to add new providers or improve existing ones. Check out the [providers folder](https://github.com/smol-ai/GodMode/tree/main/src/providers) to see how they work. 38 | 39 | The Provider base class is https://github.com/smol-ai/GodMode/blob/main/src/providers/provider.js and mostly you have to do 3 things to add a provider: 40 | 41 | - edit the `var inputElement = document.querySelector('div.ProseMirror')` to target the right element for chat input 42 | - edit the `var btn = document.querySelector("button[aria-label*='Send Message']");` to target the right button element to send the message 43 | - (optional) edit the `handleCss` to clean up the UI for a small window view 44 | 45 | See https://github.com/smol-ai/GodMode/blob/main/src/providers/claude2.js for a simple reference. Sometimes it gets more complicated, like [Bing provider](https://github.com/smol-ai/GodMode/blob/main/src/providers/bing.js), because of the DOM structure 46 | 47 | ## debugging 48 | 49 | I have the devtools up all the time while in development. You can disable them by commenting this line. 50 | 51 | ```js 52 | window.webContents.openDevTools(); 53 | ``` 54 | 55 | ## building and notarizing 56 | 57 | Apple is a piece of sht. 58 | 59 | copy `.env.example` to `.env` and follow https://www.electronforge.io/guides/code-signing/code-signing-macos (we tried option 1, but eventually ended up with option 2 as you see below) 60 | 61 | then you have to generate a bunch of stuff 62 | https://medium.com/ascentic-technology/getting-an-electron-app-ready-for-macos-distribution-2941fce27450 63 | 64 | ```bash 65 | $ spctl -a -vvv -t install smolmenubar.app 66 | smolmenubar.app: accepted 67 | source=Notarized Developer ID 68 | origin=Developer ID Application: Shawn Wang (7SVH735GV7) 69 | ``` 70 | 71 | all of this has now been packaged into a script called `npm run buildAndSign`. Note that for now, this script ONLY runs on swyx's macbook air inside the terminal (and somehow NOT in vsocde, i dont know why). If you are experienced with electron signing and notarizing, please help us make this work for all contributors! 72 | 73 | ```bash 74 | npm run buildAndSign 75 | 76 | > smolmenubar@0.0.16 buildAndSign 77 | > NODE_ENV=sign npm run make 78 | 79 | 80 | > smolmenubar@0.0.16 make 81 | > electron-forge make --arch arm64,x64 82 | 83 | ✔ Checking your system 84 | ✔ Loading configuration 85 | ✔ Resolving make targets 86 | › Making for the following targets: dmg 87 | ✔ Loading configuration 88 | ✔ Resolving make targets 89 | › Making for the following targets: dmg 90 | ✔ Running package command 91 | ✔ Preparing to package application 92 | ✔ Running packaging hooks 93 | ✔ Running generateAssets hook 94 | ✔ Running prePackage hook 95 | ✔ Packaging application 96 | ✔ Packaging for arm64 on darwin [2m15s] 97 | ✔ Packaging for x64 on darwin [2m27s] 98 | ✔ Running postPackage hook 99 | ✔ Running preMake hook 100 | ✔ Making distributables 101 | ✔ Making a dmg distributable for darwin/arm64 [7s] 102 | ✔ Making a dmg distributable for darwin/x64 [8s] 103 | ✔ Running postMake hook 104 | › Artifacts available at: /Users/swyx/Documents/Work/smol-menubar/out/make 105 | ``` 106 | 107 | ## publishing to app store 108 | 109 | > NOTE: we havent actually gotten this working yet, i just straight up gave up 110 | 111 | the below from https://developer.apple.com/help/app-store-connect/manage-builds/upload-builds is something like what we want 112 | 113 | ```bash 114 | xcrun altool --validate-app -f smolmenubar.app -t macos -u shawnthe1@gmail.com 115 | ``` 116 | 117 | but doesnt work 118 | 119 | ```bash 120 | xcrun altool --validate-app -f smolmenubar.app --type macos -u shawnthe1@gmail.com 121 | shawnthe1@gmail.com's password: 122 | 123 | 2023-07-04 00:09:32.533 *** Error: Validation failed for 'smolmenubar.app'. 124 | 2023-07-04 00:09:32.534 *** Error: Unable to determine app platform for 'Undefined' software type. App Store operation failed. (1194) 125 | { 126 | NSLocalizedDescription = "Unable to determine app platform for 'Undefined' software type."; 127 | NSLocalizedFailureReason = "App Store operation failed."; 128 | NSLocalizedRecoverySuggestion = "Unable to determine app platform for 'Undefined' software type."; 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Electron React Boilerplate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | import React = require('react'); 5 | 6 | export const ReactComponent: React.FC>; 7 | 8 | const content: string; 9 | export default content; 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string; 14 | export default content; 15 | } 16 | 17 | declare module '*.jpg' { 18 | const content: string; 19 | export default content; 20 | } 21 | 22 | declare module '*.scss' { 23 | const content: Styles; 24 | export default content; 25 | } 26 | 27 | declare module '*.sass' { 28 | const content: Styles; 29 | export default content; 30 | } 31 | 32 | declare module '*.css' { 33 | const content: Styles; 34 | export default content; 35 | } 36 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.debugger 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icon@2x.png -------------------------------------------------------------------------------- /assets/iconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/iconTemplate.png -------------------------------------------------------------------------------- /assets/iconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/iconTemplate@2x.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/assets/icons/96x96.png -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | build-keys.json 2 | AuthKey_*.p8 -------------------------------------------------------------------------------- /images/OpenAI_Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 13 | 14 | 31 | 33 | 38 | 39 | 43 | 48 | 49 | 51 | 53 | 58 | 60 | 61 | 63 | 78 | 79 | 81 | 83 | 85 | 87 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /images/minimized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/images/minimized.jpg -------------------------------------------------------------------------------- /images/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/smol_chicken.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /images/vertical-grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/images/vertical-grip.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "godmode", 3 | "productName": "GodMode", 4 | "version": "1.0.0-beta.9", 5 | "author": "smol-ai", 6 | "description": "Simultaneously chat with ChatGPT, Claude, Bing, Bard, Llama2, et al in a full chat browser, NO API", 7 | "main": "src/main/main.ts", 8 | "scripts": { 9 | "build": "npm run build:copy && concurrently \"npm run build:main\" \"npm run build:renderer\"", 10 | "build:copy": "node scripts/scripts/copy-version-number.js", 11 | "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./scripts/configs/webpack.config.main.prod.ts", 12 | "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./scripts/configs/webpack.config.renderer.prod.ts", 13 | "postinstall": "ts-node scripts/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./scripts/configs/webpack.config.renderer.dev.dll.ts", 14 | "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", 15 | "package": "npm run package-mac && npm run package-lin && npm run package-win", 16 | "package-mac": "ts-node ./scripts/scripts/clean.js dist && npm run build && electron-builder build --mac --publish never", 17 | "package-lin": "ts-node ./scripts/scripts/clean.js dist && npm run build && electron-builder build --linux --publish never", 18 | "package-win": "ts-node ./scripts/scripts/clean.js dist && npm run build && electron-builder build --win --x64 --publish never", 19 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", 20 | "start": "ts-node ./scripts/scripts/check-port-in-use.js && npm run start:renderer", 21 | "start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .", 22 | "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./scripts/configs/webpack.config.preload.dev.ts", 23 | "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./scripts/configs/webpack.config.renderer.dev.ts", 24 | "format": "prettier '**/*.{js,jsx,ts,tsx}' --write" 25 | }, 26 | "config": { 27 | "forge": "./forge.config.js" 28 | }, 29 | "keywords": [ 30 | "chatgpt", 31 | "openai", 32 | "mac" 33 | ], 34 | "license": "MIT", 35 | "dependencies": { 36 | "@headlessui/react": "^1.7.17", 37 | "@heroicons/react": "^2.0.18", 38 | "@radix-ui/react-dialog": "^1.0.5", 39 | "@radix-ui/react-icons": "^1.3.0", 40 | "@radix-ui/react-slot": "^1.0.2", 41 | "@radix-ui/react-tooltip": "^1.0.7", 42 | "chalk": "4.1.2", 43 | "class-variance-authority": "^0.7.0", 44 | "clsx": "^2.0.0", 45 | "electron-context-menu": "^3.6.1", 46 | "electron-debug": "^3.2.0", 47 | "electron-log": "^4.4.8", 48 | "electron-store": "^8.1.0", 49 | "electron-updater": "^6.1.4", 50 | "react": "^18.2.0", 51 | "react-beautiful-dnd": "^13.1.1", 52 | "react-dom": "^18.2.0", 53 | "react-router-dom": "^6.16.0", 54 | "react-split": "^2.0.14", 55 | "tailwind-merge": "^1.14.0", 56 | "vex-js": "^4.1.0" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/smol-ai/GodMode" 61 | }, 62 | "devDependencies": { 63 | "@electron/notarize": "^2.1.0", 64 | "@electron/rebuild": "^3.3.0", 65 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", 66 | "@svgr/webpack": "^8.1.0", 67 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.2", 68 | "@testing-library/jest-dom": "^6.1.3", 69 | "@testing-library/react": "^14.0.0", 70 | "@types/jest": "^29.5.5", 71 | "@types/node": "20.8.2", 72 | "@types/react": "^18.2.25", 73 | "@types/react-beautiful-dnd": "^13.1.5", 74 | "@types/react-dom": "^18.2.10", 75 | "@types/react-test-renderer": "^18.0.3", 76 | "@types/terser-webpack-plugin": "^5.0.4", 77 | "@types/webpack-bundle-analyzer": "^4.6.1", 78 | "@typescript-eslint/eslint-plugin": "^6.7.4", 79 | "@typescript-eslint/parser": "^6.7.4", 80 | "autoprefixer": "^10.4.16", 81 | "browserslist-config-erb": "^0.0.3", 82 | "concurrently": "^8.2.1", 83 | "core-js": "^3.33.0", 84 | "cross-env": "^7.0.3", 85 | "css-loader": "^6.8.1", 86 | "css-minimizer-webpack-plugin": "^5.0.1", 87 | "detect-port": "^1.5.1", 88 | "electron": "^26.3.0", 89 | "electron-builder": "^24.6.4", 90 | "electron-devtools-installer": "^3.2.0", 91 | "electronmon": "^2.0.2", 92 | "eslint": "^8.50.0", 93 | "eslint-config-airbnb-base": "^15.0.0", 94 | "eslint-config-erb": "^4.1.0", 95 | "eslint-import-resolver-typescript": "^3.6.1", 96 | "eslint-import-resolver-webpack": "^0.13.7", 97 | "eslint-plugin-compat": "^4.2.0", 98 | "eslint-plugin-import": "^2.28.1", 99 | "eslint-plugin-jest": "^27.4.2", 100 | "eslint-plugin-jsx-a11y": "^6.7.1", 101 | "eslint-plugin-promise": "^6.1.1", 102 | "eslint-plugin-react": "^7.33.2", 103 | "eslint-plugin-react-hooks": "^4.6.0", 104 | "file-loader": "^6.2.0", 105 | "html-webpack-plugin": "^5.5.3", 106 | "identity-obj-proxy": "^3.0.0", 107 | "jest": "^29.7.0", 108 | "jest-environment-jsdom": "^29.7.0", 109 | "mini-css-extract-plugin": "^2.7.6", 110 | "postcss": "^8.4.31", 111 | "postcss-loader": "^7.3.3", 112 | "prettier": "^3.0.3", 113 | "react-refresh": "^0.14.0", 114 | "react-test-renderer": "^18.2.0", 115 | "rimraf": "^5.0.5", 116 | "sass": "^1.68.0", 117 | "sass-loader": "^13.3.2", 118 | "style-loader": "^3.3.3", 119 | "tailwindcss": "^3.3.3", 120 | "terser-webpack-plugin": "^5.3.9", 121 | "ts-jest": "^29.1.1", 122 | "ts-loader": "^9.4.4", 123 | "ts-node": "^10.9.1", 124 | "tsconfig-paths-webpack-plugin": "^4.1.0", 125 | "typescript": "^5.2.2", 126 | "url-loader": "^4.1.1", 127 | "webpack": "^5.88.2", 128 | "webpack-bundle-analyzer": "^4.9.1", 129 | "webpack-cli": "^5.1.4", 130 | "webpack-dev-server": "^4.15.1", 131 | "webpack-merge": "^5.9.0" 132 | }, 133 | "build": { 134 | "productName": "GodMode", 135 | "appId": "org.smol.GodMode", 136 | "asar": true, 137 | "asarUnpack": "**\\*.{node,dll}", 138 | "copyright": "Copyright 2023 smol.ai/godmode", 139 | "files": [ 140 | "dist", 141 | "node_modules", 142 | "package.json" 143 | ], 144 | "afterSign": "scripts/scripts/notarize.js", 145 | "mac": { 146 | "target": { 147 | "target": "default", 148 | "arch": [ 149 | "arm64", 150 | "universal", 151 | "x64" 152 | ] 153 | }, 154 | "category": "public.app-category.developer-tools", 155 | "type": "distribution", 156 | "hardenedRuntime": true, 157 | "entitlements": "assets/entitlements.mac.plist", 158 | "entitlementsInherit": "assets/entitlements.mac.plist", 159 | "gatekeeperAssess": false 160 | }, 161 | "dmg": { 162 | "contents": [ 163 | { 164 | "x": 130, 165 | "y": 220 166 | }, 167 | { 168 | "x": 410, 169 | "y": 220, 170 | "type": "link", 171 | "path": "/Applications" 172 | } 173 | ] 174 | }, 175 | "win": { 176 | "target": [ 177 | { 178 | "target": "nsis", 179 | "arch": [ 180 | "x64", 181 | "arm64" 182 | ] 183 | } 184 | ] 185 | }, 186 | "linux": { 187 | "target": [ 188 | { 189 | "target": "AppImage", 190 | "arch": [ 191 | "x64", 192 | "arm64" 193 | ] 194 | } 195 | ], 196 | "category": "Development" 197 | }, 198 | "directories": { 199 | "app": "release/app", 200 | "buildResources": "assets", 201 | "output": "release/build" 202 | }, 203 | "extraResources": [ 204 | "./assets/**" 205 | ], 206 | "publish": { 207 | "provider": "github", 208 | "owner": "smol-ai", 209 | "repo": "GodMode" 210 | } 211 | }, 212 | "devEngines": { 213 | "node": ">=18.x", 214 | "npm": ">=7.x" 215 | }, 216 | "electronmon": { 217 | "patterns": [ 218 | "!**/**", 219 | "src/main/**" 220 | ], 221 | "logLevel": "verbose" 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /release/app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GodMode", 3 | "version": "1.0.0-beta.9", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "GodMode", 9 | "version": "1.0.0-beta.9", 10 | "hasInstallScript": true, 11 | "license": "MIT" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GodMode", 3 | "version": "1.0.0-beta.9", 4 | "description": "A foundation for scalable desktop apps", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Electron React Boilerplate Maintainers", 8 | "email": "electronreactboilerplate@gmail.com", 9 | "url": "https://github.com/electron-react-boilerplate" 10 | }, 11 | "main": "./dist/main/main.js", 12 | "scripts": { 13 | "rebuild": "node -r ts-node/register ../../scripts/scripts/electron-rebuild.js", 14 | "postinstall": "npm run rebuild && npm run link-modules", 15 | "link-modules": "node -r ts-node/register ../../scripts/scripts/link-modules.ts" 16 | }, 17 | "dependencies": {} 18 | } -------------------------------------------------------------------------------- /scripts/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "prefer-const": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/configs/postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | module.exports = { 5 | plugins: [tailwindcss, autoprefixer], 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 7 | import webpackPaths from './webpack.paths'; 8 | import { dependencies as externals } from '../../release/app/package.json'; 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: 'errors-only', 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'ts-loader', 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | compilerOptions: { 26 | module: 'esnext', 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: 'commonjs2', 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 47 | modules: [webpackPaths.srcPath, 'node_modules'], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [new TsconfigPathsPlugins()], 50 | }, 51 | 52 | plugins: [ 53 | new webpack.EnvironmentPlugin({ 54 | NODE_ENV: 'production', 55 | }), 56 | ], 57 | }; 58 | 59 | export default configuration; 60 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | console.log('webpackPaths.distMainPath', webpackPaths.distMainPath); 19 | 20 | const configuration: webpack.Configuration = { 21 | devtool: 'source-map', 22 | 23 | mode: 'production', 24 | 25 | target: 'electron-main', 26 | 27 | entry: { 28 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 29 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 30 | }, 31 | 32 | output: { 33 | path: webpackPaths.distMainPath, 34 | filename: '[name].js', 35 | library: { 36 | type: 'umd', 37 | }, 38 | }, 39 | 40 | optimization: { 41 | minimizer: [ 42 | new TerserPlugin({ 43 | parallel: true, 44 | }), 45 | ], 46 | }, 47 | 48 | plugins: [ 49 | new BundleAnalyzerPlugin({ 50 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 51 | analyzerPort: 8888, 52 | }), 53 | 54 | /** 55 | * Create global constants which can be configured at compile time. 56 | * 57 | * Useful for allowing different behaviour between development builds and 58 | * release builds 59 | * 60 | * NODE_ENV should be production so that modules do not perform certain 61 | * development checks 62 | */ 63 | new webpack.EnvironmentPlugin({ 64 | NODE_ENV: 'production', 65 | DEBUG_PROD: false, 66 | START_MINIMIZED: false, 67 | }), 68 | 69 | new webpack.DefinePlugin({ 70 | 'process.type': '"browser"', 71 | }), 72 | ], 73 | 74 | /** 75 | * Disables webpack processing of __dirname and __filename. 76 | * If you run the bundle in node.js it falls back to these values of node.js. 77 | * https://github.com/webpack/webpack/issues/2010 78 | */ 79 | node: { 80 | __dirname: false, 81 | __filename: false, 82 | }, 83 | }; 84 | 85 | export default merge(baseConfig, configuration); 86 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | library: { 28 | type: 'umd', 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: 'development', 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.renderer.dev.ts: -------------------------------------------------------------------------------- 1 | import 'webpack-dev-server'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import webpack from 'webpack'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import chalk from 'chalk'; 7 | import { merge } from 'webpack-merge'; 8 | import { execSync, spawn } from 'child_process'; 9 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | 14 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 15 | // at the dev webpack config is not accidentally run in a production environment 16 | if (process.env.NODE_ENV === 'production') { 17 | checkNodeEnv('development'); 18 | } 19 | 20 | const port = process.env.PORT || 1212; 21 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json'); 22 | const skipDLLs = 23 | module.parent?.filename.includes('webpack.config.renderer.dev.dll') || 24 | module.parent?.filename.includes('webpack.config.eslint'); 25 | 26 | /** 27 | * Warn if the DLL is not built 28 | */ 29 | if ( 30 | !skipDLLs && 31 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest)) 32 | ) { 33 | console.log( 34 | chalk.black.bgYellow.bold( 35 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"', 36 | ), 37 | ); 38 | execSync('npm run postinstall'); 39 | } 40 | 41 | const configuration: webpack.Configuration = { 42 | devtool: 'inline-source-map', 43 | 44 | mode: 'development', 45 | 46 | target: ['web', 'electron-renderer'], 47 | 48 | entry: [ 49 | `webpack-dev-server/client?http://localhost:${port}/dist`, 50 | 'webpack/hot/only-dev-server', 51 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 52 | ], 53 | 54 | output: { 55 | path: webpackPaths.distRendererPath, 56 | publicPath: '/', 57 | filename: 'renderer.dev.js', 58 | library: { 59 | type: 'umd', 60 | }, 61 | }, 62 | 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.s?(c|a)ss$/, 67 | use: [ 68 | 'style-loader', 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | modules: true, 73 | sourceMap: true, 74 | importLoaders: 1, 75 | }, 76 | }, 77 | 'sass-loader', 78 | ], 79 | include: /\.module\.s?(c|a)ss$/, 80 | }, 81 | { 82 | test: /\.s?css$/, 83 | use: [ 84 | 'style-loader', 85 | 'css-loader', 86 | 'sass-loader', 87 | { 88 | loader: 'postcss-loader', 89 | options: { 90 | postcssOptions: { 91 | plugins: [require('tailwindcss'), require('autoprefixer')], 92 | }, 93 | }, 94 | }, 95 | ], 96 | exclude: /\.module\.s?(c|a)ss$/, 97 | }, 98 | // Fonts 99 | { 100 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 101 | type: 'asset/resource', 102 | }, 103 | // Images 104 | { 105 | test: /\.(png|jpg|jpeg|gif)$/i, 106 | type: 'asset/resource', 107 | }, 108 | // SVG 109 | { 110 | test: /\.svg$/, 111 | use: [ 112 | { 113 | loader: '@svgr/webpack', 114 | options: { 115 | prettier: false, 116 | svgo: false, 117 | svgoConfig: { 118 | plugins: [{ removeViewBox: false }], 119 | }, 120 | titleProp: true, 121 | ref: true, 122 | }, 123 | }, 124 | 'file-loader', 125 | ], 126 | }, 127 | ], 128 | }, 129 | plugins: [ 130 | ...(skipDLLs 131 | ? [] 132 | : [ 133 | new webpack.DllReferencePlugin({ 134 | context: webpackPaths.dllPath, 135 | manifest: require(manifest), 136 | sourceType: 'var', 137 | }), 138 | ]), 139 | 140 | new webpack.NoEmitOnErrorsPlugin(), 141 | 142 | /** 143 | * Create global constants which can be configured at compile time. 144 | * 145 | * Useful for allowing different behaviour between development builds and 146 | * release builds 147 | * 148 | * NODE_ENV should be production so that modules do not perform certain 149 | * development checks 150 | * 151 | * By default, use 'development' as NODE_ENV. This can be overriden with 152 | * 'staging', for example, by changing the ENV variables in the npm scripts 153 | */ 154 | new webpack.EnvironmentPlugin({ 155 | NODE_ENV: 'development', 156 | }), 157 | 158 | new webpack.LoaderOptionsPlugin({ 159 | debug: true, 160 | }), 161 | 162 | new ReactRefreshWebpackPlugin(), 163 | 164 | new HtmlWebpackPlugin({ 165 | filename: path.join('index.html'), 166 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 167 | minify: { 168 | collapseWhitespace: true, 169 | removeAttributeQuotes: true, 170 | removeComments: true, 171 | }, 172 | isBrowser: false, 173 | env: process.env.NODE_ENV, 174 | isDevelopment: process.env.NODE_ENV !== 'production', 175 | nodeModules: webpackPaths.appNodeModulesPath, 176 | }), 177 | ], 178 | 179 | node: { 180 | __dirname: false, 181 | __filename: false, 182 | }, 183 | 184 | devServer: { 185 | port, 186 | compress: true, 187 | hot: true, 188 | headers: { 'Access-Control-Allow-Origin': '*' }, 189 | static: { 190 | publicPath: '/', 191 | }, 192 | historyApiFallback: { 193 | verbose: true, 194 | }, 195 | setupMiddlewares(middlewares) { 196 | console.log('Starting preload.js builder...'); 197 | const preloadProcess = spawn('npm', ['run', 'start:preload'], { 198 | shell: true, 199 | stdio: 'inherit', 200 | }) 201 | .on('close', (code: number) => process.exit(code!)) 202 | .on('error', (spawnError) => console.error(spawnError)); 203 | 204 | console.log('Starting Main Process...'); 205 | let args = ['run', 'start:main']; 206 | if (process.env.MAIN_ARGS) { 207 | args = args.concat( 208 | ['--', ...process.env.MAIN_ARGS.matchAll(/"[^"]+"|[^\s"]+/g)].flat(), 209 | ); 210 | } 211 | spawn('npm', args, { 212 | shell: true, 213 | stdio: 'inherit', 214 | }) 215 | .on('close', (code: number) => { 216 | preloadProcess.kill(); 217 | process.exit(code!); 218 | }) 219 | .on('error', (spawnError) => console.error(spawnError)); 220 | return middlewares; 221 | }, 222 | }, 223 | }; 224 | 225 | export default merge(baseConfig, configuration); 226 | -------------------------------------------------------------------------------- /scripts/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const configuration: webpack.Configuration = { 22 | devtool: 'source-map', 23 | 24 | mode: 'production', 25 | 26 | target: ['web', 'electron-renderer'], 27 | 28 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')], 29 | 30 | output: { 31 | path: webpackPaths.distRendererPath, 32 | publicPath: './', 33 | filename: 'renderer.js', 34 | library: { 35 | type: 'umd', 36 | }, 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.s?(a|c)ss$/, 43 | use: [ 44 | MiniCssExtractPlugin.loader, 45 | { 46 | loader: 'css-loader', 47 | options: { 48 | modules: true, 49 | sourceMap: true, 50 | importLoaders: 1, 51 | }, 52 | }, 53 | 'sass-loader', 54 | ], 55 | include: /\.module\.s?(c|a)ss$/, 56 | }, 57 | { 58 | test: /\.s?(a|c)ss$/, 59 | use: [ 60 | MiniCssExtractPlugin.loader, 61 | 'css-loader', 62 | 'sass-loader', 63 | { 64 | loader: 'postcss-loader', 65 | options: { 66 | postcssOptions: { 67 | plugins: [require('tailwindcss'), require('autoprefixer')], 68 | }, 69 | }, 70 | }, 71 | ], 72 | exclude: /\.module\.s?(c|a)ss$/, 73 | }, 74 | // Fonts 75 | { 76 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 77 | type: 'asset/resource', 78 | }, 79 | // Images 80 | { 81 | test: /\.(png|jpg|jpeg|gif)$/i, 82 | type: 'asset/resource', 83 | }, 84 | // SVG 85 | { 86 | test: /\.svg$/, 87 | use: [ 88 | { 89 | loader: '@svgr/webpack', 90 | options: { 91 | prettier: false, 92 | svgo: false, 93 | svgoConfig: { 94 | plugins: [{ removeViewBox: false }], 95 | }, 96 | titleProp: true, 97 | ref: true, 98 | }, 99 | }, 100 | 'file-loader', 101 | ], 102 | }, 103 | ], 104 | }, 105 | 106 | optimization: { 107 | minimize: true, 108 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], 109 | }, 110 | 111 | plugins: [ 112 | /** 113 | * Create global constants which can be configured at compile time. 114 | * 115 | * Useful for allowing different behaviour between development builds and 116 | * release builds 117 | * 118 | * NODE_ENV should be production so that modules do not perform certain 119 | * development checks 120 | */ 121 | new webpack.EnvironmentPlugin({ 122 | NODE_ENV: 'production', 123 | DEBUG_PROD: false, 124 | }), 125 | 126 | new MiniCssExtractPlugin({ 127 | filename: 'style.css', 128 | }), 129 | 130 | new BundleAnalyzerPlugin({ 131 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 132 | analyzerPort: 8889, 133 | }), 134 | 135 | new HtmlWebpackPlugin({ 136 | filename: 'index.html', 137 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 138 | minify: { 139 | collapseWhitespace: true, 140 | removeAttributeQuotes: true, 141 | removeComments: true, 142 | }, 143 | isBrowser: false, 144 | isDevelopment: false, 145 | }), 146 | 147 | new webpack.DefinePlugin({ 148 | 'process.type': '"renderer"', 149 | }), 150 | ], 151 | }; 152 | 153 | export default merge(baseConfig, configuration); 154 | -------------------------------------------------------------------------------- /scripts/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /scripts/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/scripts/img/erb-logo.png -------------------------------------------------------------------------------- /scripts/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /scripts/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /scripts/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(), 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency), 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.', 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package', 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure', 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, 11 | ), 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rimrafSync } from 'rimraf'; 2 | import fs from 'fs'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | const foldersToRemove = [ 6 | webpackPaths.distPath, 7 | // webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/scripts/copy-version-number.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // Get package.json from root 5 | const rootPackageJsonPath = path.join(__dirname, '../../package.json'); 6 | const rootPackageJson = require(rootPackageJsonPath); 7 | 8 | // Get package.json from release/app 9 | const appPackageJsonPath = path.join( 10 | __dirname, 11 | '../../release', 12 | 'app', 13 | 'package.json', 14 | ); 15 | const appPackageJson = require(appPackageJsonPath); 16 | 17 | // Copy version from root to app 18 | appPackageJson.version = rootPackageJson.version; 19 | 20 | // Write updated app package.json 21 | fs.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2)); 22 | 23 | console.log( 24 | `Copied version ${appPackageJson.version} from root package.json to release/app/package.json`, 25 | ); 26 | -------------------------------------------------------------------------------- /scripts/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { rimrafSync } from 'rimraf'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { 9 | glob: true, 10 | }); 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { 13 | glob: true, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /scripts/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import { dependencies } from '../../release/app/package.json'; 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 12 | const cmd = 13 | process.platform === 'win32' 14 | ? electronRebuildCmd.replace(/\//g, '\\') 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: 'inherit', 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /scripts/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /scripts/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn( 17 | 'Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set', 18 | ); 19 | return; 20 | } 21 | 22 | const appName = context.packager.appInfo.productFilename; 23 | 24 | const appPath = `${appOutDir}/${appName}.app`; 25 | await notarize({ 26 | appPath, 27 | // appleId: process.env.APPLE_ID, 28 | // appleIdPassword: process.env.APPLE_ID_PASS, 29 | tool: 'notarytool', 30 | // https://www.electronforge.io/guides/code-signing/code-signing-macos 31 | appleApiKey: `build/AuthKey_QRJN4Y5JG6.p8`, 32 | appleApiKeyId: 'QRJN4Y5JG6', 33 | appleApiIssuer: '58a60bb5-5495-4a08-b180-2c106e11bc66', 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render } from '@testing-library/react'; 3 | import App from '../renderer/App'; 4 | 5 | describe('App', () => { 6 | it('should render', () => { 7 | expect(render()).toBeTruthy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/pane.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'renderer/components/ui/button'; 2 | import { 3 | Dialog, 4 | DialogXContent, 5 | DialogHeader, 6 | DialogTitle, 7 | } from 'renderer/components/ui/dialog'; 8 | import { ProviderInterface } from 'lib/types'; 9 | import { CmdOrCtrlKey } from 'lib/utils'; 10 | import React from 'react'; 11 | import { 12 | ArrowLeftIcon, 13 | ArrowRightIcon, 14 | ReloadIcon, 15 | ResetIcon, 16 | ZoomInIcon, 17 | ZoomOutIcon, 18 | MagnifyingGlassIcon, 19 | } from '@radix-ui/react-icons'; 20 | import { Input } from 'renderer/components/ui/input'; 21 | import { 22 | Tooltip, 23 | TooltipContent, 24 | TooltipProvider, 25 | TooltipTrigger, 26 | } from 'renderer/components/ui/tooltip'; 27 | 28 | export default function Pane({ 29 | provider, 30 | number, 31 | currentlyOpenPreviewPane, 32 | setOpenPreviewPane, 33 | }: { 34 | provider: ProviderInterface; 35 | number: number; 36 | currentlyOpenPreviewPane: number; 37 | setOpenPreviewPane: (num: number) => void; 38 | }) { 39 | const isPreviewOpen = currentlyOpenPreviewPane === number; 40 | const contentRef = React.useRef(null); 41 | 42 | const [shownUrl, setShownUrl] = React.useState(null); 43 | // this did not work not sure why 44 | // set a timer effect every second to check if the webview is can go back 45 | React.useEffect(() => { 46 | const interval = setInterval(() => { 47 | // @ts-ignore 48 | const newUrl = provider.getWebview()?.src; 49 | if (newUrl !== shownUrl) setShownUrl(newUrl); 50 | }, 1000); 51 | return () => clearInterval(interval); 52 | }); 53 | 54 | // this did not work not sure why 55 | // // set a timer effect every second to check if the webview is can go back 56 | // const [canGoBack, setCanGoBack] = React.useState(false); 57 | // const [canGoFwd, setCanGoFwd] = React.useState(false); 58 | // React.useEffect(() => { 59 | // const interval = setInterval(() => { 60 | // console.log( 61 | // 'provider.getWebview()?.canGoBack()', 62 | // provider.getWebview(), 63 | // provider.getWebview()?.canGoBack() 64 | // ); 65 | // // @ts-ignore 66 | // if (provider.getWebview()?.canGoBack()) { 67 | // setCanGoBack(true); 68 | // } else { 69 | // setCanGoBack(false); 70 | // } 71 | // // @ts-ignore 72 | // if (provider.getWebview()?.canGoForward()) { 73 | // setCanGoFwd(true); 74 | // } else { 75 | // setCanGoFwd(true); 76 | // } 77 | // }, 1000); 78 | // return () => clearInterval(interval); 79 | // }); 80 | 81 | function XButton({ children, tooltip, onClick, className = '' }: any) { 82 | return ( 83 | 84 | 85 | 86 | 93 | 94 | 95 |

{tooltip}

96 |
97 |
98 |
99 | ); 100 | } 101 | 102 | return ( 103 |
104 |
105 | 133 |
134 | { 137 | setOpenPreviewPane(0); 138 | // zoom out when dropping out of preview 139 | provider 140 | .getWebview() 141 | // @ts-ignore 142 | .setZoomLevel(provider.getWebview().getZoomLevel() - 2); 143 | }} 144 | > 145 | 149 | 150 | 151 | {provider.fullName} 152 |
153 | { 156 | provider 157 | .getWebview() 158 | // @ts-ignore 159 | .setZoomLevel(provider.getWebview().getZoomLevel() + 1); 160 | }} 161 | > 162 | 163 | 164 | { 167 | provider 168 | .getWebview() 169 | // @ts-ignore 170 | .setZoomLevel(provider.getWebview().getZoomLevel() - 1); 171 | }} 172 | > 173 | 174 | 175 | { 178 | provider 179 | .getWebview() 180 | // @ts-ignore 181 | .setZoomLevel(0); 182 | }} 183 | > 184 | 185 | 186 |
187 | 188 |
189 | { 193 | const webview = provider.getWebview(); 194 | if (typeof webview?.refresh === 'function') { 195 | webview?.refresh(); 196 | } else { 197 | webview?.reload(); 198 | } 199 | }} 200 | > 201 | 202 | 203 | { 206 | provider.getWebview()?.goBack(); 207 | }} 208 | > 209 | 210 | 211 | { 214 | provider.getWebview()?.goForward(); 215 | }} 216 | > 217 | 218 | 219 | {provider.clearCookies && ( 220 | { 223 | provider.clearCookies(); 224 | }} 225 | > 226 | Clear Cookies 227 | 228 | )} 229 |
230 |
231 |
232 |
233 |
234 | 248 |
249 | ); 250 | } 251 | -------------------------------------------------------------------------------- /src/components/sidebar.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/components/sidebar.tsx -------------------------------------------------------------------------------- /src/images/OpenAI_Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 13 | 14 | 31 | 33 | 38 | 39 | 43 | 48 | 49 | 51 | 53 | 58 | 60 | 61 | 63 | 78 | 79 | 81 | 83 | 85 | 87 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/images/godmode.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/godmode.icns -------------------------------------------------------------------------------- /src/images/godmode.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/godmode.ico -------------------------------------------------------------------------------- /src/images/godmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/godmode.png -------------------------------------------------------------------------------- /src/images/godmodeicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/godmodeicon.icns -------------------------------------------------------------------------------- /src/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/icon.icns -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/icon.png -------------------------------------------------------------------------------- /src/images/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/icon@2x.png -------------------------------------------------------------------------------- /src/images/iconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/iconTemplate.png -------------------------------------------------------------------------------- /src/images/iconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/iconTemplate@2x.png -------------------------------------------------------------------------------- /src/images/minimized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/minimized.jpg -------------------------------------------------------------------------------- /src/images/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/smol_chicken.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/images/vertical-grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smol-ai/GodMode/8ac3ffde96ab72d992b5d5cf1743673e610cb07b/src/images/vertical-grip.png -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import Bard from '../providers/bard'; 2 | import Bing from '../providers/bing'; 3 | import Claude from '../providers/claude'; 4 | import Claude2 from '../providers/claude2'; 5 | import HuggingChat from '../providers/huggingchat'; 6 | import OobaBooga from '../providers/oobabooga'; 7 | import OpenAi from '../providers/openai'; 8 | import Perplexity from '../providers/perplexity'; 9 | import YouChat from '../providers/you'; 10 | import PerplexityLlama from '../providers/perplexity-labs.js'; 11 | import LeptonLlama from '../providers/lepton-llama.js'; 12 | import Phind from '../providers/phind'; 13 | import Smol from '../providers/smol'; 14 | import Together from '../providers/together'; 15 | import Vercel from 'providers/vercel'; 16 | import OpenRouter from '../providers/openrouter'; 17 | import Poe from 'providers/poe'; 18 | import InflectionPi from 'providers/inflection'; 19 | import StableChat from 'providers/stablechat'; 20 | import Falcon180BSpace from 'providers/falcon180bspace'; 21 | 22 | export const allProviders = [ 23 | OpenAi, 24 | Bard, 25 | Bing, 26 | Claude, 27 | Claude2, 28 | YouChat, 29 | Perplexity, 30 | Phind, 31 | Poe, 32 | InflectionPi, 33 | HuggingChat, 34 | StableChat, 35 | Falcon180BSpace, 36 | OobaBooga, 37 | Together, 38 | OpenRouter, 39 | PerplexityLlama, 40 | LeptonLlama, 41 | Vercel, 42 | Smol, 43 | ]; 44 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import Provider from 'providers/provider'; 2 | 3 | export interface ProviderInterface { 4 | new (): Provider; 5 | fullName: string; 6 | shortName: string; 7 | webviewId: string; 8 | getWebview(): HTMLElement | null; 9 | url: string; 10 | paneId(): string; 11 | setupCustomPasteBehavior(): void; 12 | handleInput(input: string): void; 13 | handleSubmit(input?: string): void; 14 | handleCss(): void; 15 | handleDarkMode(bool: boolean): void; 16 | getUserAgent(): string; 17 | isEnabled(): boolean; 18 | setEnabled(enabled: boolean): void; 19 | clearCookies?(): void; 20 | 21 | codeForInputElement?: string; 22 | codeForSetInputElementValue?(prompt: string): void; 23 | codeForClickingSubmit?: string; 24 | codeForExtractingResponse?: string; 25 | } 26 | 27 | export interface Settings { 28 | getGlobalShortcut: () => Promise; 29 | setGlobalShortcut: (shortcut: string) => Promise; 30 | getFocusSuperprompt: () => Promise; 31 | setFocusSuperprompt: (state: boolean) => Promise; 32 | getPlatform: () => Promise; 33 | } 34 | 35 | // Tell typescript that the window object has a property called settings 36 | declare global { 37 | interface Window { 38 | settings: Settings; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ProviderInterface } from './types'; 2 | import { type ClassValue, clsx } from 'clsx'; 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export function getEnabledProviders(allProviders: ProviderInterface[]) { 6 | return allProviders.filter((provider) => provider.isEnabled()); 7 | } 8 | 9 | export function cn(...inputs: ClassValue[]) { 10 | return twMerge(clsx(inputs)); 11 | } 12 | export function convertKeyCode(code: string) { 13 | return code 14 | .toUpperCase() 15 | .replace('KEY', '') 16 | .replace('DIGIT', '') 17 | .replace('NUMPAD', 'NUM') 18 | .replace('COMMA', ','); 19 | } 20 | 21 | export type ShortcutKey = 22 | | 'Command' 23 | | 'Cmd' 24 | | 'Control' 25 | | 'Ctrl' 26 | | 'CommandOrControl' 27 | | 'CmdOrCtrl' 28 | | 'Alt' 29 | | 'Option' 30 | | 'AltGr' 31 | | 'Shift' 32 | | 'Super' 33 | | 'Meta'; 34 | 35 | export const modifierKeys: Set = new Set([ 36 | 'Command', 37 | 'Control', 38 | 'Cmd', 39 | 'Ctrl', 40 | 'CmdOrCtrl', 41 | 'CommandOrControl', 42 | 'Alt', 43 | 'Option', 44 | 'AltGr', 45 | 'Shift', 46 | 'Super', 47 | 'Meta', 48 | ]); 49 | 50 | export function convertModifierKey(key: ShortcutKey | string): string { 51 | const shortcuts: Record = { 52 | Command: CmdOrCtrlKey, 53 | Cmd: CmdOrCtrlKey, 54 | CmdOrCtrl: CmdOrCtrlKey, 55 | CommandOrControl: CmdOrCtrlKey, 56 | Control: 'Ctrl', 57 | Ctrl: 'Ctrl', 58 | Alt: 'Alt', 59 | Option: 'Option', 60 | AltGr: 'AltGr', 61 | Shift: 'Shift', 62 | Super: 'CmdOrCtrl', 63 | Meta: 'CmdOrCtrl', 64 | }; 65 | 66 | const result = shortcuts[key as ShortcutKey] || key; 67 | return result; 68 | } 69 | 70 | // Iterate through shortcut array and confirm there is at least 1 modifier 71 | // and no more than 1 non-modifier key 72 | 73 | export function isValidShortcut(...keys: (string | string[])[]): boolean { 74 | // Track the count of modifier and non-modifier keys 75 | let modifierCount = 0; 76 | let nonModifierCount = 0; 77 | let shiftCount = 0; // Track the count of shift keys 78 | 79 | const shortcut = keys.flatMap((value) => 80 | typeof value === 'string' ? value.split('+') : value.flat(), 81 | ); 82 | 83 | shortcut.forEach((key) => { 84 | if (key === 'Shift') { 85 | shiftCount++; 86 | } else if (modifierKeys.has(key as ShortcutKey)) { 87 | modifierCount++; 88 | } else { 89 | nonModifierCount++; 90 | } 91 | }); 92 | 93 | return modifierCount >= 1 && nonModifierCount === 1; // Modify this based on the specific rules for a valid shortcut 94 | } 95 | 96 | // This is here to avoid a circular dependency in constants.ts 97 | export const CmdOrCtrlKey = getCurrentPlatform() === 'mac' ? 'Cmd' : 'Ctrl'; 98 | 99 | export function getCurrentPlatform(): string { 100 | const platform = ( 101 | typeof process !== 'undefined' ? process.platform : navigator.platform 102 | ) // navigator.platform is technically deprecated, but still works 103 | .toLowerCase(); 104 | if (['darwin', 'macintel'].includes(platform)) { 105 | return 'mac'; 106 | } else if (platform === 'win32') { 107 | return 'win'; 108 | } else { 109 | return 'linux'; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/apify.ts: -------------------------------------------------------------------------------- 1 | import { ProviderInterface } from 'lib/types'; 2 | import { BrowserWindow } from 'electron'; 3 | 4 | export async function streamChatResponse(opts: { 5 | provider: ProviderInterface; 6 | prompt: string; 7 | sendFn: (...args: any[]) => void | undefined; 8 | }) { 9 | const win = new BrowserWindow({ 10 | // show: true, 11 | show: false, 12 | // titleBarStyle: 'hidden', 13 | // width: 800, 14 | // height: 600, 15 | // webPreferences: { 16 | // webviewTag: true, 17 | // nodeIntegration: true, 18 | // }, 19 | }); 20 | win.loadURL(opts.provider.url); 21 | 22 | return new Promise((resolve, reject) => { 23 | win.webContents.on('dom-ready', async () => { 24 | try { 25 | // check if logged in (and inputElement exists) 26 | await win.webContents.executeJavaScript( 27 | `{${opts.provider.codeForInputElement}}`, 28 | ); 29 | } catch (err) { 30 | console.error( 31 | 'input element doesnt exist: ', 32 | opts.provider.codeForInputElement, 33 | ); 34 | return reject(err); 35 | } 36 | await timeout(500); 37 | const script = `{ 38 | ${opts.provider.codeForInputElement} 39 | ${opts.provider.codeForSetInputElementValue!(opts.prompt)} 40 | ${opts.provider.codeForClickingSubmit} 41 | }`; 42 | await win.webContents.executeJavaScript(script); 43 | console.log('script', script); 44 | 45 | // Define two variables to store the previous responses 46 | let lastResponseHTML = null; 47 | let secondLastResponseHTML = null; 48 | 49 | console.log('looping'); 50 | // Loop until our condition is met 51 | await timeout(300); 52 | while (true) { 53 | await timeout(300); 54 | // await win.webContents.executeJavaScript( 55 | // `console.log('hiii', [...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')]);`, 56 | // ); 57 | var responseHTML = await win.webContents.executeJavaScript( 58 | `${opts.provider.codeForExtractingResponse}.innerHTML`, 59 | ); 60 | var responseText = await win.webContents.executeJavaScript( 61 | `${opts.provider.codeForExtractingResponse}.innerText`, 62 | ); 63 | 64 | console.log({ responseHTML, secondLastResponseHTML }); 65 | // If responseHTML hasn't changed for 2 invocations, break 66 | if ( 67 | responseHTML === lastResponseHTML && 68 | responseHTML === secondLastResponseHTML 69 | ) { 70 | console.log('prompting'); 71 | break; 72 | } 73 | 74 | // Shift our stored responses for the next loop iteration 75 | secondLastResponseHTML = lastResponseHTML; 76 | lastResponseHTML = responseHTML; 77 | 78 | console.log('sendFn', responseText); 79 | opts.sendFn(responseHTML, responseText); // stream incomplete responses back 80 | } 81 | console.log('closing'); 82 | win.close(); 83 | return resolve({ responseHTML, responseText }); 84 | }); 85 | }); 86 | } 87 | // thanks claude 88 | 89 | function timeout(ms: number) { 90 | return new Promise((resolve) => setTimeout(resolve, ms)); 91 | } 92 | -------------------------------------------------------------------------------- /src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | shell, 5 | BrowserWindow, 6 | MenuItemConstructorOptions, 7 | ipcRenderer, 8 | } from 'electron'; 9 | 10 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { 11 | selector?: string; 12 | submenu?: DarwinMenuItemConstructorOptions[] | Menu; 13 | } 14 | 15 | function openSettingsWindow() { 16 | ipcRenderer.send('open-settings-window'); 17 | } 18 | 19 | export default class MenuBuilder { 20 | mainWindow: BrowserWindow; 21 | 22 | constructor(mainWindow: BrowserWindow) { 23 | this.mainWindow = mainWindow; 24 | } 25 | 26 | buildMenu(): Menu { 27 | if ( 28 | process.env.NODE_ENV === 'development' || 29 | process.env.DEBUG_PROD === 'true' 30 | ) { 31 | this.setupDevelopmentEnvironment(); 32 | } 33 | 34 | const template = 35 | process.platform === 'darwin' 36 | ? this.buildDarwinTemplate() 37 | : this.buildDefaultTemplate(); 38 | 39 | const menu = Menu.buildFromTemplate(template); 40 | Menu.setApplicationMenu(menu); 41 | 42 | return menu; 43 | } 44 | 45 | setupDevelopmentEnvironment(): void { 46 | this.mainWindow.webContents.on('context-menu', (_, props) => { 47 | const { x, y } = props; 48 | 49 | Menu.buildFromTemplate([ 50 | { 51 | label: 'Inspect element', 52 | click: () => { 53 | this.mainWindow.webContents.inspectElement(x, y); 54 | }, 55 | }, 56 | ]).popup({ window: this.mainWindow }); 57 | }); 58 | } 59 | 60 | buildDarwinTemplate(): MenuItemConstructorOptions[] { 61 | const subMenuAbout: DarwinMenuItemConstructorOptions = { 62 | label: 'God Mode', 63 | submenu: [ 64 | { 65 | label: 'About God Mode', 66 | selector: 'orderFrontStandardAboutPanel:', 67 | }, 68 | { type: 'separator' }, 69 | { label: 'Services', submenu: [] }, 70 | { type: 'separator' }, 71 | { 72 | label: 'Hide God Mode', 73 | accelerator: 'Command+H', 74 | selector: 'hide:', 75 | }, 76 | { 77 | label: 'Hide Others', 78 | accelerator: 'Command+Shift+H', 79 | selector: 'hideOtherApplications:', 80 | }, 81 | { label: 'Show All', selector: 'unhideAllApplications:' }, 82 | { type: 'separator' }, 83 | { 84 | label: 'Quit', 85 | accelerator: 'Command+Q', 86 | click: () => { 87 | app.quit(); 88 | }, 89 | }, 90 | ], 91 | }; 92 | const subMenuEdit: DarwinMenuItemConstructorOptions = { 93 | label: 'Edit', 94 | submenu: [ 95 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 96 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 97 | { type: 'separator' }, 98 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 99 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 100 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 101 | { 102 | label: 'Select All', 103 | accelerator: 'Command+A', 104 | selector: 'selectAll:', 105 | }, 106 | ], 107 | }; 108 | const subMenuViewDev: MenuItemConstructorOptions = { 109 | label: 'View', 110 | submenu: [ 111 | { 112 | label: 'Reload', 113 | accelerator: 'Command+R', 114 | click: () => { 115 | this.mainWindow.webContents.reload(); 116 | }, 117 | }, 118 | { 119 | label: 'Toggle Full Screen', 120 | accelerator: 'Ctrl+Command+F', 121 | click: () => { 122 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 123 | }, 124 | }, 125 | { 126 | label: 'Toggle Developer Tools', 127 | accelerator: 'Alt+Command+I', 128 | click: () => { 129 | this.mainWindow.webContents.toggleDevTools(); 130 | }, 131 | }, 132 | ], 133 | }; 134 | const subMenuViewProd: MenuItemConstructorOptions = { 135 | label: 'View', 136 | submenu: [ 137 | { 138 | label: 'Toggle Full Screen', 139 | accelerator: 'Ctrl+Command+F', 140 | click: () => { 141 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 142 | }, 143 | }, 144 | ], 145 | }; 146 | const subMenuWindow: DarwinMenuItemConstructorOptions = { 147 | label: 'Window', 148 | submenu: [ 149 | { 150 | label: 'Minimize', 151 | accelerator: 'Command+M', 152 | selector: 'performMiniaturize:', 153 | }, 154 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 155 | { type: 'separator' }, 156 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 157 | ], 158 | }; 159 | const subMenuHelp: MenuItemConstructorOptions = { 160 | label: 'Help', 161 | submenu: [ 162 | { 163 | label: 'Learn More', 164 | click() { 165 | shell.openExternal('https://electronjs.org'); 166 | }, 167 | }, 168 | { 169 | label: 'Documentation', 170 | click() { 171 | shell.openExternal( 172 | 'https://github.com/electron/electron/tree/main/docs#readme', 173 | ); 174 | }, 175 | }, 176 | { 177 | label: 'Community Discussions', 178 | click() { 179 | shell.openExternal('https://www.electronjs.org/community'); 180 | }, 181 | }, 182 | { 183 | label: 'Search Issues', 184 | click() { 185 | shell.openExternal('https://github.com/electron/electron/issues'); 186 | }, 187 | }, 188 | ], 189 | }; 190 | 191 | const subMenuView = 192 | process.env.NODE_ENV === 'development' || 193 | process.env.DEBUG_PROD === 'true' 194 | ? subMenuViewDev 195 | : subMenuViewProd; 196 | 197 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 198 | } 199 | 200 | buildDefaultTemplate() { 201 | const templateDefault = [ 202 | { 203 | label: '&File', 204 | submenu: [ 205 | { 206 | label: '&Open', 207 | accelerator: 'Ctrl+O', 208 | }, 209 | { 210 | label: '&Close', 211 | accelerator: 'Ctrl+W', 212 | click: () => { 213 | this.mainWindow.close(); 214 | }, 215 | }, 216 | ], 217 | }, 218 | { 219 | label: '&View', 220 | submenu: 221 | process.env.NODE_ENV === 'development' || 222 | process.env.DEBUG_PROD === 'true' 223 | ? [ 224 | { 225 | label: '&Reload', 226 | accelerator: 'Ctrl+R', 227 | click: () => { 228 | this.mainWindow.webContents.reload(); 229 | }, 230 | }, 231 | { 232 | label: 'Toggle &Full Screen', 233 | accelerator: 'F11', 234 | click: () => { 235 | this.mainWindow.setFullScreen( 236 | !this.mainWindow.isFullScreen(), 237 | ); 238 | }, 239 | }, 240 | { 241 | label: 'Toggle &Developer Tools', 242 | accelerator: 'Alt+Ctrl+I', 243 | click: () => { 244 | this.mainWindow.webContents.toggleDevTools(); 245 | }, 246 | }, 247 | ] 248 | : [ 249 | { 250 | label: 'Toggle &Full Screen', 251 | accelerator: 'F11', 252 | click: () => { 253 | this.mainWindow.setFullScreen( 254 | !this.mainWindow.isFullScreen(), 255 | ); 256 | }, 257 | }, 258 | ], 259 | }, 260 | { 261 | label: 'Help', 262 | submenu: [ 263 | { 264 | label: 'Learn More', 265 | click() { 266 | shell.openExternal('https://electronjs.org'); 267 | }, 268 | }, 269 | { 270 | label: 'Documentation', 271 | click() { 272 | shell.openExternal( 273 | 'https://github.com/electron/electron/tree/main/docs#readme', 274 | ); 275 | }, 276 | }, 277 | { 278 | label: 'Community Discussions', 279 | click() { 280 | shell.openExternal('https://www.electronjs.org/community'); 281 | }, 282 | }, 283 | { 284 | label: 'Search Issues', 285 | click() { 286 | shell.openExternal('https://github.com/electron/electron/issues'); 287 | }, 288 | }, 289 | ], 290 | }, 291 | ]; 292 | 293 | return templateDefault; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | // Disable no-unused-vars, broken for spread args 2 | /* eslint no-unused-vars: off */ 3 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 4 | 5 | export type Channels = 'ipc-example' | 'perplexity-llama2'; 6 | 7 | const electronHandler = { 8 | ipcRenderer: { 9 | sendMessage(channel: Channels, ...args: unknown[]) { 10 | ipcRenderer.send(channel, ...args); 11 | }, 12 | on(channel: Channels, func: (...args: unknown[]) => void) { 13 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 14 | func(...args); 15 | ipcRenderer.on(channel, subscription); 16 | 17 | return () => { 18 | ipcRenderer.removeListener(channel, subscription); 19 | }; 20 | }, 21 | once(channel: Channels, func: (...args: unknown[]) => void) { 22 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 23 | }, 24 | }, 25 | // https://gist.github.com/samcodee/d4320006d366a2c47048014644ddc375 26 | electronStore: { 27 | get(val: any, def: any) { 28 | const x = ipcRenderer.sendSync('electron-store-get', val, def); 29 | return x; 30 | }, 31 | set(property: any, val: any) { 32 | ipcRenderer.send('electron-store-set', property, val); 33 | }, 34 | // Other method you want to add like has(), reset(), etc. 35 | }, 36 | browserWindow: { 37 | reload() { 38 | ipcRenderer.send('reload-browser'); 39 | }, 40 | getAlwaysOnTop() { 41 | const x = ipcRenderer.sendSync('get-always-on-top'); 42 | return x; 43 | }, 44 | setAlwaysOnTop(val: any) { 45 | ipcRenderer.send('set-always-on-top', val); 46 | }, 47 | promptHiddenChat(prompt: string) { 48 | ipcRenderer.send('prompt-hidden-chat', 'perplexity-llama2', prompt); 49 | }, 50 | enableOpenAtLogin(prompt: string) { 51 | ipcRenderer.send('enable-open-at-login'); 52 | }, 53 | disableOpenAtLogin(prompt: string) { 54 | ipcRenderer.send('disable-open-at-login'); 55 | }, 56 | }, 57 | }; 58 | 59 | contextBridge.exposeInMainWorld('electron', electronHandler); 60 | 61 | contextBridge.exposeInMainWorld('settings', { 62 | getGlobalShortcut: () => { 63 | return ipcRenderer.invoke('get-global-shortcut'); 64 | }, 65 | setGlobalShortcut: (shortcut: string) => { 66 | return ipcRenderer.invoke('set-global-shortcut', shortcut); 67 | }, 68 | getFocusSuperprompt: () => { 69 | return ipcRenderer.invoke('get-focus-superprompt'); 70 | }, 71 | setFocusSuperprompt: (state: boolean) => { 72 | return ipcRenderer.invoke('set-focus-superprompt', state); 73 | }, 74 | getPlatform: () => { 75 | return ipcRenderer.invoke('get-platform'); 76 | }, 77 | getOpenAtLogin: () => { 78 | return ipcRenderer.invoke('get-open-at-login'); 79 | }, 80 | }); 81 | 82 | export type ElectronHandler = typeof electronHandler; 83 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export function resolveHtmlPath(htmlFileName: string) { 6 | if (process.env.NODE_ENV === 'development') { 7 | const port = process.env.PORT || 1212; 8 | const url = new URL(`http://localhost:${port}`); 9 | url.pathname = htmlFileName; 10 | return url.href; 11 | } 12 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 13 | } 14 | -------------------------------------------------------------------------------- /src/providers/bard.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Bard extends Provider { 4 | static webviewId = 'webviewBARD'; 5 | static fullName = 'Google Bard'; 6 | static shortName = 'Bard'; 7 | 8 | static url = 'https://bard.google.com'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector(".ql-editor.textarea"); 14 | if (inputElement) { 15 | const inputEvent = new Event('input', { bubbles: true }); 16 | inputElement.value = \`${input}\`; // must be escaped backticks to support multiline 17 | inputElement.dispatchEvent(inputEvent); 18 | // bard is weird 19 | inputElement.querySelector('p').textContent = \`${input}\` 20 | } 21 | } 22 | `); 23 | } 24 | 25 | static handleSubmit() { 26 | this.getWebview().executeJavaScript(`{ 27 | var btn = document.querySelector("button[aria-label*='Send message']"); 28 | if (btn) { 29 | btn.setAttribute("aria-disabled", "false"); // doesnt work alone 30 | btn.focus(); 31 | btn.click(); 32 | } 33 | }`); 34 | } 35 | 36 | static handleCss() { 37 | this.getWebview().addEventListener('dom-ready', () => { 38 | // hide message below text input, sidebar, suggestions on new chat 39 | this.getWebview().insertCSS(` 40 | .chat-history, .conversation-container, .input-area, .mdc-text-area { 41 | margin: 0 !important; 42 | } 43 | /* hide the bard greeting */ 44 | response-container { 45 | display: none; 46 | } 47 | model-response response-container { 48 | display: block !important; 49 | } 50 | /* hide header and footer - disabled for now https://github.com/smol-ai/GodMode/issues/202 51 | .gmat-caption { 52 | opacity: 0; 53 | height: 0; 54 | } 55 | header { 56 | display: none !important; 57 | } 58 | header + div { 59 | display: none !important; 60 | } 61 | */ 62 | 63 | .capabilities-disclaimer { 64 | display: none !important; 65 | } 66 | .input-area-container .input-area { 67 | padding: 0; 68 | } 69 | /* hide the bard avatar in response */ 70 | .logo-gutter { 71 | display: none !important; 72 | } 73 | `); 74 | }); 75 | } 76 | 77 | static handleDarkMode(isDarkMode) { 78 | // Toggle the dark mode setting in the store 79 | window.electron.electronStore.set('isDarkMode', isDarkMode); 80 | 81 | if (isDarkMode) { 82 | this.getWebview().executeJavaScript(`{ 83 | document.querySelector('body').classList.add('dark-theme'); 84 | document.querySelector('body').classList.remove('light-theme'); 85 | }`); 86 | } else { 87 | this.getWebview().executeJavaScript(`{ 88 | document.querySelector('body').classList.add('light-theme'); 89 | document.querySelector('body').classList.remove('dark-theme'); 90 | }`); 91 | } 92 | } 93 | 94 | static getUserAgent() { 95 | // bard does not accept Electron mentions. 96 | // also must be realistic UA string, not just "Chrome", or this happens https://github.com/smol-ai/GodMode/pull/231 97 | return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; 98 | } 99 | 100 | static isEnabled() { 101 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 102 | } 103 | } 104 | 105 | module.exports = Bard; 106 | -------------------------------------------------------------------------------- /src/providers/bing.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Bing extends Provider { 4 | static webviewId = 'webviewBING'; 5 | static fullName = 'Microsoft Bing'; 6 | static shortName = 'Bing'; 7 | 8 | static url = 'https://bing.com/chat'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | // Simulate user input 13 | function simulateUserInput(element, text) { 14 | const inputEvent = new Event('input', { bubbles: true }); 15 | element.focus(); 16 | element.value = text; 17 | element.dispatchEvent(inputEvent); 18 | } 19 | 20 | // SERP Shadow DOM 21 | var serpDOM = document.querySelector('.cib-serp-main'); 22 | 23 | // Action Bar Shadow DOM 24 | var inputDOM = serpDOM.shadowRoot.querySelector('#cib-action-bar-main'); 25 | 26 | // Text Input Shadow DOM 27 | var textInputDOM = inputDOM.shadowRoot.querySelector('cib-text-input'); 28 | 29 | // This inner cib-text-input Shadow DOM is not always present 30 | var inputElement = textInputDOM ? textInputDOM.shadowRoot.querySelector('#searchbox') : inputDOM.shadowRoot.querySelector('#searchbox'); 31 | 32 | if (inputElement) { 33 | simulateUserInput(inputElement, \`${input}\`); 34 | } 35 | } 36 | `); 37 | } 38 | 39 | static handleSubmit() { 40 | this.getWebview().executeJavaScript(`{ 41 | try { 42 | // Access SERP Shadow DOM 43 | var serpDOM = document.querySelector('.cib-serp-main'); 44 | 45 | // Conversation Shadow DOM - Contains 46 | var conversationDOM = serpDOM.shadowRoot.querySelector('#cib-conversation-main'); 47 | 48 | // Action Bar Shadow DOM 49 | var actionDOM = serpDOM.shadowRoot.querySelector('#cib-action-bar-main'); 50 | 51 | // Icon Shadow DOM 52 | var iconDOM = actionDOM.shadowRoot.querySelector('div.submit > cib-icon-button'); 53 | 54 | // Submit Button 55 | var submitButton = iconDOM.shadowRoot.querySelector('button'); 56 | if (submitButton) { 57 | submitButton.click(); 58 | 59 | submitButton.focus(); 60 | setTimeout(() => { 61 | submitButton.click(); 62 | }, 100) 63 | } 64 | } catch (e) { 65 | console.error('Bing submit error', e); 66 | } 67 | 68 | } 69 | `); 70 | } 71 | 72 | /** Bing requires MS Edge user agent. */ 73 | static getUserAgent() { 74 | return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.37'; 75 | } 76 | 77 | static handleCss() { 78 | this.getWebview().addEventListener('dom-ready', () => { 79 | // hide message below text input, sidebar, suggestions on new chat 80 | setTimeout(() => { 81 | // .b_sydConvMode::after { 82 | this.getWebview().insertCSS(` 83 | html, body { 84 | overflow: hidden; 85 | scrollbar-width: none; 86 | zoom: 80%; 87 | font-size: small; 88 | } 89 | header { 90 | display: none !important; 91 | } 92 | #b_sydWelcomeTemplate { 93 | display: none !important; 94 | } 95 | .preview-container { 96 | display: none !important; 97 | } 98 | `); 99 | }, 1000); 100 | // this.getWebview().setZoomLevel(this.getWebview().getZoomLevel() - 2); 101 | // setTimeout(() => { 102 | // this.getWebview().executeJavaScript(`{ 103 | 104 | // // Access SERP Shadow DOM 105 | // var serpDOM = document.querySelector('.cib-serp-main').shadowRoot; 106 | 107 | // // Conversation Shadow DOM 108 | // var conversationDOM = serpDOM.querySelector('#cib-conversation-main').shadowRoot; 109 | 110 | // // Action Bar Shadow DOM 111 | // var actionBarDOM = serpDOM.querySelector('#cib-action-bar-main').shadowRoot; 112 | 113 | // // Text Input Shadow DOM 114 | // var textInputDOM = actionBarDOM.querySelector('cib-text-input').shadowRoot; 115 | 116 | // // Welcome Container Shadow DOM 117 | // var welcomeDOM = conversationDOM.querySelector('cib-welcome-container').shadowRoot; 118 | 119 | // // Hide all welcome container items except tone selector 120 | // // welcomeDOM.querySelector('div.preview-container').setAttribute('style', 'display: none !important;'); 121 | 122 | // // Hide all welcome container items except tone selector 123 | // welcomeDOM.querySelector('div.container-logo').setAttribute('style', 'display: none !important'); 124 | // welcomeDOM.querySelector('div.container-title').setAttribute('style', 'color: white !important'); 125 | // welcomeDOM.querySelector('div.container-subTitle').setAttribute('style', 'display: none !important'); 126 | // welcomeDOM.querySelector('div.container-item').setAttribute('style', 'display: none !important'); 127 | // welcomeDOM.querySelector('div.learn-tag-item').setAttribute('style', 'display: none !important'); 128 | // welcomeDOM.querySelector('div.privacy-statement').setAttribute('style', 'display: none !important'); 129 | 130 | // // Remove feedback widget 131 | // serpDOM.querySelector('cib-serp-feedback').setAttribute('style', 'display: none !important'); 132 | 133 | // // Remove background gradients 134 | // serpDOM.querySelector('cib-background').remove(); 135 | // conversationDOM.querySelector('.fade.top').remove(); 136 | // conversationDOM.querySelector('.fade.bottom').remove(); 137 | 138 | // } 139 | // `); 140 | // }, 1000); 141 | }); 142 | } 143 | 144 | // Some providers will have their own dark mode implementation 145 | static handleDarkMode(isDarkMode) { 146 | // Implement dark or light mode using prodiver-specific code 147 | if (isDarkMode) { 148 | this.getWebview().executeJavaScript(`{ 149 | document.getElementById("rdiodark").click(); 150 | } 151 | `); 152 | } else { 153 | this.getWebview().executeJavaScript(`{ 154 | document?.getElementById("rdiolight").click(); 155 | } 156 | `); 157 | } 158 | } 159 | 160 | static isEnabled() { 161 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); 162 | } 163 | } 164 | 165 | module.exports = Bing; 166 | -------------------------------------------------------------------------------- /src/providers/claude.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Claude extends Provider { 4 | static webviewId = 'webviewCLAUDE'; 5 | static fullName = 'Anthropic Claude'; 6 | static shortName = 'Claude1'; 7 | 8 | static url = 'https://console.anthropic.com/chat/new'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('div.ProseMirror') 14 | if (inputElement) { 15 | inputElement.innerHTML = \`${input}\` 16 | } 17 | }`); 18 | } 19 | 20 | static handleSubmit() { 21 | this.getWebview().executeJavaScript(`{ 22 | var btn = document.querySelector('div.group.grid.p-3 button:has(svg)'); // YES we are using the has selector!!!! 23 | if (btn) { 24 | btn.focus(); 25 | btn.disabled = false; 26 | btn.click() 27 | } 28 | }`); 29 | } 30 | 31 | static handleCss() { 32 | this.getWebview().addEventListener('dom-ready', () => { 33 | // hide message below text input, sidebar, suggestions on new chat 34 | setTimeout(() => { 35 | this.getWebview().insertCSS(` 36 | header, .container { 37 | background-color: white; 38 | /* single line dark mode ftw */ 39 | filter: invert(100%) hue-rotate(180deg); 40 | } 41 | /* hide the claude avatar in response */ 42 | .p-1.w-9.h-9.shrink-0 { 43 | display: none; 44 | } 45 | /* reduce claude prompt margins */ 46 | .mx-4.md\:mx-12.mb-2.md\:mb-4.mt-2.w-auto { 47 | margin: 0 !important; 48 | } 49 | `); 50 | }, 100); 51 | }); 52 | } 53 | 54 | static toggleDarkMode() { 55 | if (isDarkMode) { 56 | this.getWebview().insertCSS(` 57 | body { 58 | background-color: #1d1d1d !important; 59 | filter: invert(100%) hue-rotate(180deg); 60 | } 61 | `); 62 | } else { 63 | this.getWebview().insertCSS(` 64 | body { 65 | background-color: #ffffff !important; 66 | filter: none; 67 | } 68 | `); 69 | } 70 | } 71 | 72 | static isEnabled() { 73 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 74 | } 75 | } 76 | 77 | module.exports = Claude; 78 | -------------------------------------------------------------------------------- /src/providers/claude2.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Claude2 extends Provider { 4 | static webviewId = 'webviewCLAUDE2'; 5 | static fullName = 'Anthropic Claude 2'; 6 | static shortName = 'Claude2'; 7 | 8 | static url = 'https://claude.ai/chats/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('div.ProseMirror') 14 | if (inputElement) { 15 | inputElement.innerHTML = \`${input}\` 16 | } 17 | }`); 18 | } 19 | 20 | static handleSubmit() { 21 | this.getWebview().executeJavaScript(`{ 22 | var btn = document.querySelector("button[aria-label*='Send Message']"); // subsequent screens use this 23 | if (!btn) var btn = document.querySelector('button:has(div svg)'); // new chats use this 24 | if (!btn) var btn = document.querySelector('button:has(svg)'); // last ditch attempt 25 | if (btn) { 26 | btn.focus(); 27 | btn.disabled = false; 28 | btn.click(); 29 | } 30 | }`); 31 | } 32 | 33 | static handleCss() { 34 | this.getWebview().addEventListener('dom-ready', () => { 35 | // hide message below text input, sidebar, suggestions on new chat 36 | setTimeout(() => { 37 | this.getWebview().insertCSS(` 38 | /* hide the claude avatar in response */ 39 | .p-1.w-9.h-9.shrink-0 { 40 | display: none; 41 | } 42 | /* reduce claude prompt margins */ 43 | .mx-4.md\:mx-12.mb-2.md\:mb-4.mt-2.w-auto { 44 | margin: 0 !important; 45 | } 46 | 47 | `); 48 | }, 1000); 49 | setTimeout(() => { 50 | this.getWebview().executeJavaScript(`{ 51 | // hide welcome back title 52 | document.querySelector('h2').style.display = 'none'; 53 | }`); 54 | }, 1000); 55 | }); 56 | } 57 | 58 | static handleDarkMode(isDarkMode) { 59 | if (isDarkMode) { 60 | this.getWebview().insertCSS(` 61 | body { 62 | background-color: #1d1d1d !important; 63 | filter: invert(100%) hue-rotate(180deg); 64 | } 65 | `); 66 | } else { 67 | this.getWebview().insertCSS(` 68 | body { 69 | background-color: #ffffff !important; 70 | filter: none; 71 | } 72 | `); 73 | } 74 | } 75 | 76 | static getUserAgent() { 77 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; 78 | } 79 | 80 | static isEnabled() { 81 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); 82 | } 83 | } 84 | 85 | module.exports = Claude2; 86 | -------------------------------------------------------------------------------- /src/providers/falcon180bspace.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Falcon180BSpace extends Provider { 4 | static webviewId = 'webviewFalcon180BSpace'; 5 | static fullName = 'Falcon 180B (HF Space, temporary)'; 6 | static shortName = 'Falcon180BSpace'; 7 | 8 | static url = 'https://tiiuae-falcon-180b-demo.hf.space/?__theme=dark'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | var inputElement = document.querySelector('textarea[data-testid="textbox"]'); 13 | if (inputElement) { 14 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 15 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 16 | 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } 22 | 23 | static handleSubmit() { 24 | this.getWebview().executeJavaScript(`{ 25 | var btn = document.querySelector('button.primary'); 26 | if (btn) { 27 | btn.focus(); 28 | btn.disabled = false; 29 | btn.click(); 30 | } 31 | }`); 32 | } 33 | static handleCss() { 34 | this.getWebview().addEventListener('dom-ready', () => { 35 | // Hide the "Try asking" segment 36 | setTimeout(() => { 37 | this.getWebview().insertCSS(` 38 | div[data-testid="markdown"] { 39 | display: none; 40 | } 41 | #banner-image { 42 | height: 30px; 43 | } 44 | `); 45 | }, 100); 46 | }); 47 | } 48 | static handleDarkMode(isDarkMode) { 49 | if (isDarkMode) { 50 | this.getWebview().executeJavaScript(`{ 51 | if(document.querySelector('html').dataset.theme === 'light'){ 52 | document.querySelector('.menu > ul > div:nth-child(2) > button').click() 53 | } 54 | } 55 | `); 56 | } else { 57 | this.getWebview().executeJavaScript(`{ 58 | if(document.querySelector('html').dataset.theme === 'business'){ 59 | document.querySelector('.menu > ul > div:nth-child(2) > button').click() 60 | } 61 | } 62 | `); 63 | } 64 | } 65 | 66 | static isEnabled() { 67 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 68 | } 69 | } 70 | 71 | module.exports = Falcon180BSpace; 72 | -------------------------------------------------------------------------------- /src/providers/huggingchat.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class HuggingChat extends Provider { 4 | static webviewId = 'webviewHuggingChat'; 5 | static fullName = 'HuggingChat (Llama2, OpenAssistant)'; 6 | static shortName = 'HuggingChat'; 7 | 8 | static url = 'https://huggingface.co/chat/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(` 13 | var inputElement = document.querySelector('textarea[placeholder*="Ask anything"]'); 14 | if (inputElement) { 15 | const inputEvent = new Event('input', { bubbles: true }); 16 | inputElement.value = \`${input}\`; // must be escaped backticks to support multiline 17 | inputElement.dispatchEvent(inputEvent); 18 | } 19 | `); 20 | } 21 | 22 | static handleSubmit() { 23 | this.getWebview().executeJavaScript(` 24 | var btn = document.querySelector('form.relative > div > button[type="submit"]'); 25 | if (btn) { 26 | btn.click(); 27 | } 28 | `); 29 | } 30 | 31 | static handleCss() { 32 | this.getWebview().addEventListener('dom-ready', () => { 33 | // hide message below text input, sidebar, suggestions on new chat 34 | setTimeout(() => { 35 | this.getWebview().executeJavaScript(` 36 | // Hide Examples Box 37 | var elements = Array.from(document.querySelectorAll('div[class]')); 38 | var targetElement; 39 | 40 | for (var i = 0; i < elements.length; i++) { 41 | var classes = elements[i].className.split(' '); 42 | if (classes.includes('lg:col-span-3') && classes.includes('lg:mt-12') && elements[i].textContent.includes('Examples')) { 43 | targetElement = elements[i]; 44 | break; 45 | } 46 | } 47 | 48 | if (targetElement) { 49 | targetElement.style.display = 'none'; 50 | } 51 | 52 | // Hide HuggingChat Logo 53 | var elements = Array.from(document.querySelectorAll('div object')); 54 | 55 | elements.forEach(element => { 56 | if (element.parentElement.textContent.includes('HuggingChat')) { 57 | element.parentElement.parentElement.style.display = 'none'; 58 | } 59 | }); 60 | 61 | // Same loop for the other text. 62 | var pElements = Array.from(document.querySelectorAll('p')); 63 | 64 | pElements.forEach(element => { 65 | if (element.textContent.includes('Examples')) { 66 | element.parentElement.style.display = 'none'; 67 | } 68 | }); 69 | 70 | 71 | `); 72 | }, 100); 73 | }); 74 | } 75 | 76 | static isEnabled() { 77 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 78 | } 79 | } 80 | 81 | module.exports = HuggingChat; 82 | -------------------------------------------------------------------------------- /src/providers/inflection.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class InflectionPi extends Provider { 4 | static webviewId = 'webviewInflection'; 5 | static fullName = 'Inflection Pi'; 6 | static shortName = 'InflectionPi'; 7 | 8 | static url = 'https://pi.ai/talk/'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | var inputElement = document.querySelector('textarea[placeholder="Talk with Pi"]'); 13 | if (inputElement) { 14 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 15 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 16 | 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } 22 | 23 | static handleSubmit() { 24 | this.getWebview().executeJavaScript(`{ 25 | var inputElement = document.querySelector('textarea[placeholder="Talk with Pi"]'); 26 | if (inputElement) { 27 | const event = new KeyboardEvent('keydown', { 28 | key: 'Enter', 29 | view: window, 30 | bubbles: true 31 | }); 32 | inputElement.dispatchEvent(event); 33 | } 34 | }`); 35 | } 36 | static handleCss() { 37 | // this.getWebview().addEventListener('dom-ready', () => { 38 | // // Hide the "Try asking" segment 39 | // setTimeout(() => { 40 | // this.getWebview().insertCSS(` 41 | // .mt-lg { 42 | // display: none; 43 | // } 44 | // `); 45 | // }, 100); 46 | // }); 47 | } 48 | 49 | static isEnabled() { 50 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 51 | } 52 | } 53 | 54 | module.exports = InflectionPi; 55 | -------------------------------------------------------------------------------- /src/providers/lepton-llama.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class LeptonLlama extends Provider { 4 | static webviewId = 'webiewLeptonLlama'; 5 | static fullName = 'Llama 2 (via Lepton)'; 6 | static shortName = 'Llama2-Lepton'; 7 | 8 | static url = 'https://llama2.lepton.run/'; 9 | 10 | static handleInput(input) { 11 | try { 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('textarea[placeholder*="Send a message"]'); 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } catch (e) { 22 | console.debug('Error in LeptonLlama.handleInput():', e); 23 | } 24 | } 25 | 26 | static codeForInputElement = `var inputElement = document.querySelector('textarea[placeholder*="Ask"]');`; 27 | static codeForSetInputElementValue(prompt) { 28 | return ` 29 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 30 | nativeTextAreaValueSetter.call(inputElement, \`${prompt}\`); 31 | var event = new Event('input', { bubbles: true}); 32 | inputElement.dispatchEvent(event); 33 | `; 34 | } 35 | static codeForClickingSubmit = ` 36 | var buttons = Array.from(document.querySelectorAll('button.ant-btn-primary')); 37 | var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); 38 | 39 | var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; 40 | 41 | button.click(); 42 | `; 43 | static codeForExtractingResponse = `[...document.querySelectorAll('.ant-space.ant-space-horizontal .ant-typography pre')].slice(-1)[0]`; // dont append semicolon, we will append innerhtml etc 44 | 45 | static handleSubmit() { 46 | try { 47 | this.getWebview().executeJavaScript(`{ 48 | var buttons = Array.from(document.querySelectorAll('button.ant-btn-primary')); 49 | if (buttons[0]) { 50 | var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); 51 | 52 | var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; 53 | 54 | button.click(); 55 | } 56 | } 57 | `); 58 | } catch (e) { 59 | console.debug('Error in LeptonLlama.handleSubmit():', e); 60 | } 61 | } 62 | 63 | static handleCss() { 64 | this.getWebview().addEventListener('dom-ready', () => { 65 | // hide message below text input, sidebar, suggestions on new chat 66 | // try { 67 | // // setTimeout(() => { 68 | // // this.getWebview().executeJavaScript(`{ 69 | // // // Add Dark Mode 70 | // // document.documentElement.classList.add('dark'); 71 | // // }`); 72 | // // }, 100); 73 | // setTimeout(() => { 74 | // this.getWebview().executeJavaScript(`{ 75 | // // Dispatch the change event manually if there are any event listeners 76 | // var event = new Event('change'); 77 | // selectElement.dispatchEvent(event); 78 | // }`); 79 | // }, 1000); 80 | // } catch (e) { 81 | // console.debug('Error in LeptonLlama.handleCss():', e); 82 | // } 83 | setTimeout(() => { 84 | // hide temperature/length settings 85 | this.getWebview().insertCSS(` 86 | div.ant-col.ant-col-24.css-11zb6yo.ant-col-sm-24.ant-col-md-7.ant-col-xl-5.ant-col-xxl-4.css-lqewvt { 87 | display: none; 88 | } 89 | `); 90 | }, 100); 91 | }); 92 | } 93 | 94 | static isEnabled() { 95 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 96 | } 97 | } 98 | 99 | module.exports = LeptonLlama; 100 | -------------------------------------------------------------------------------- /src/providers/oobabooga.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class OobaBooga extends Provider { 4 | static webviewId = 'webviewOoba'; 5 | static fullName = 'Local Models (OobaBooga)'; 6 | static shortName = 'Oobabooga'; 7 | 8 | static url = 'http://127.0.0.1:7860/'; 9 | 10 | // todo: let user customize their preferred template. 11 | static templateFn = (input) => `Common sense questions and answers 12 | 13 | Question: ${input} 14 | Factual answer:`; 15 | 16 | static handleInput(input) { 17 | this.getWebview().executeJavaScript(`{ 18 | var inputElement = document.querySelector('#main textarea'); 19 | if (inputElement) { 20 | const inputEvent = new Event('input', { bubbles: true }); 21 | inputElement.value = \`${this.templateFn( 22 | input, 23 | )}\`; // must be escaped backticks to support multiline 24 | inputElement.dispatchEvent(inputEvent); 25 | } 26 | }`); 27 | } 28 | 29 | static handleSubmit() { 30 | this.getWebview().executeJavaScript(`{ 31 | var btn = document.querySelector("button.primary") 32 | if (btn) { 33 | btn.focus(); 34 | btn.disabled = false; 35 | btn.click(); 36 | } 37 | }`); 38 | } 39 | 40 | static handleCss() { 41 | this.getWebview().addEventListener('dom-ready', () => { 42 | // hide message below text input, sidebar, suggestions on new chat 43 | this.getWebview().insertCSS(` 44 | `); 45 | }); 46 | } 47 | 48 | static isEnabled() { 49 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 50 | } 51 | } 52 | 53 | module.exports = OobaBooga; 54 | -------------------------------------------------------------------------------- /src/providers/openai.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class OpenAI extends Provider { 4 | static webviewId = 'webviewOAI'; 5 | static fullName = 'OpenAI ChatGPT'; 6 | static shortName = 'ChatGPT'; 7 | 8 | static url = 'https://chat.openai.com/?model=gpt-4-code-interpreter'; // TODO - let people switch 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('#prompt-textarea'); 14 | if (inputElement) { 15 | const inputEvent = new Event('input', { bubbles: true }); 16 | inputElement.value = \`${input}\`; // must be escaped backticks to support multiline 17 | inputElement.dispatchEvent(inputEvent); 18 | } 19 | }`); 20 | } 21 | 22 | static handleSubmit() { 23 | this.getWebview().executeJavaScript(`{ 24 | // var btn = document.querySelector("textarea[placeholder*='Send a message']+button"); // this one broke recently .. note that they add another div (for the file upload) in code interpreter mode 25 | var btn = document.querySelector('button[data-testid="send-button"]'); 26 | if (btn) { 27 | btn.focus(); 28 | btn.disabled = false; 29 | btn.click(); 30 | } 31 | } 32 | `); 33 | } 34 | 35 | static handleCss() { 36 | this.getWebview().addEventListener('dom-ready', () => { 37 | // hide message below text input, sidebar, suggestions on new chat 38 | this.getWebview().insertCSS(` 39 | body { 40 | scrollbar-width: none; 41 | } 42 | .text-xs.text-center { 43 | opacity: 0; 44 | height: 0; 45 | margin-bottom: -10px; 46 | } 47 | 48 | [class*="shared__Wrapper"] { 49 | align-items: center; 50 | justify-content: center; 51 | text-align: center; 52 | margin-top: 15vh; 53 | } 54 | 55 | [class*="shared__Wrapper"] h3 { 56 | margin-top: -40px; 57 | font-size: 20px; 58 | } 59 | 60 | 61 | `); 62 | }); 63 | } 64 | 65 | static isEnabled() { 66 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); 67 | } 68 | } 69 | 70 | module.exports = OpenAI; 71 | -------------------------------------------------------------------------------- /src/providers/openrouter.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class OpenRouter extends Provider { 4 | static webviewId = 'webviewOpenRouter'; 5 | static fullName = 'OpenRouter Playground'; 6 | static shortName = 'OpenRouter'; 7 | 8 | static url = 'https://openrouter.ai/playground'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | var inputElement = document.querySelector('textarea[placeholder*="Chat or prompt"]'); // can be "Ask anything" or "Ask follow-up" 13 | if (inputElement) { 14 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 15 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 16 | var event = new Event('input', { bubbles: true}); 17 | inputElement.dispatchEvent(event); 18 | } 19 | }`); 20 | } 21 | 22 | static handleSubmit() { 23 | this.getWebview().executeJavaScript(`{ 24 | var buttons = Array.from(document.querySelectorAll('button')); 25 | if (buttons[0]) { 26 | var buttonsWithSVGOnly = buttons.filter(button => { 27 | var svg = button.querySelector('svg'); 28 | return !!svg; 29 | }); 30 | 31 | if (buttonsWithSrOnly.length == 1){ 32 | var button = buttonsWithSrOnly[0]; 33 | button.click(); 34 | } 35 | } 36 | }`); 37 | } 38 | static handleCss() { 39 | this.getWebview().addEventListener('dom-ready', () => { 40 | // Hide the "Try asking" segment 41 | setTimeout(() => { 42 | this.getWebview().insertCSS(` 43 | .mt-lg { 44 | display: none; 45 | } 46 | `); 47 | }, 100); 48 | }); 49 | } 50 | 51 | static isEnabled() { 52 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 53 | } 54 | } 55 | 56 | module.exports = OpenRouter; 57 | -------------------------------------------------------------------------------- /src/providers/perplexity-labs.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class PerplexityLabs extends Provider { 4 | static webviewId = 'webiewPerplexityLabs'; 5 | static fullName = 'Perplexity Labs (Llama, Mistral)'; 6 | static shortName = 'Perplexity-Labs'; 7 | 8 | static url = 'https://labs.perplexity.ai/'; 9 | 10 | static handleInput(input) { 11 | try { 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('textarea[placeholder*="Ask"]'); // can be "Ask anything" or "Ask follow-up" 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } catch (e) { 22 | console.debug('Error in PerplexityLabs.handleInput():', e); 23 | } 24 | } 25 | 26 | static codeForInputElement = `var inputElement = document.querySelector('textarea[placeholder*="Ask"]');`; 27 | static codeForSetInputElementValue(prompt) { 28 | return ` 29 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 30 | nativeTextAreaValueSetter.call(inputElement, \`${prompt}\`); 31 | var event = new Event('input', { bubbles: true}); 32 | inputElement.dispatchEvent(event); 33 | `; 34 | } 35 | static codeForClickingSubmit = ` 36 | var buttons = Array.from(document.querySelectorAll('button.bg-super')); 37 | var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); 38 | 39 | var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; 40 | 41 | button.click(); 42 | `; 43 | static codeForExtractingResponse = `[...document.querySelectorAll('.default.font-sans.text-base.text-textMain .prose')].slice(-1)[0]`; // dont append semicolon, we will append innerhtml etc 44 | 45 | static handleSubmit() { 46 | try { 47 | this.getWebview().executeJavaScript(`{ 48 | var buttons = Array.from(document.querySelectorAll('button.bg-super')); 49 | if (buttons[0]) { 50 | var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); 51 | var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; 52 | button.click(); 53 | } 54 | } 55 | `); 56 | } catch (e) { 57 | console.debug('Error in PerplexityLabs.handleSubmit():', e); 58 | } 59 | } 60 | 61 | static handleCss() { 62 | this.getWebview().addEventListener('dom-ready', () => { 63 | // hide message below text input, sidebar, suggestions on new chat 64 | try { 65 | setTimeout(() => { 66 | this.getWebview().executeJavaScript(`{ 67 | // Add Dark Mode 68 | document.documentElement.classList.add('dark'); 69 | }`); 70 | }, 100); 71 | setTimeout(() => { 72 | this.getWebview().executeJavaScript(`{ 73 | // pick llama 70b 74 | var selectElement = document.querySelector('#lamma-select'); 75 | selectElement.value = 'llama-2-70b-chat'; 76 | // Dispatch the change event manually if there are any event listeners 77 | var event = new Event('change'); 78 | selectElement.dispatchEvent(event); 79 | }`); 80 | }, 1000); 81 | } catch (e) { 82 | console.debug('Error in PerplexityLabs.handleCss():', e); 83 | } 84 | // Hide the "Try asking" segment 85 | setTimeout(() => { 86 | this.getWebview().insertCSS(` 87 | .mt-lg { 88 | display: none; 89 | } 90 | `); 91 | }, 100); 92 | }); 93 | } 94 | 95 | static getUserAgent() { 96 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; 97 | } 98 | 99 | static isEnabled() { 100 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 101 | } 102 | } 103 | 104 | module.exports = PerplexityLabs; 105 | -------------------------------------------------------------------------------- /src/providers/perplexity.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Perplexity extends Provider { 4 | static webviewId = 'webviewPerplexity'; 5 | static fullName = 'Perplexity'; 6 | static shortName = 'Perplexity'; 7 | 8 | static url = 'https://www.perplexity.ai/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(` 13 | var inputElement = document.querySelector('textarea[placeholder*="Ask"]'); // can be "Ask anything" or "Ask follow-up" 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | 18 | var event = new Event('input', { bubbles: true}); 19 | inputElement.dispatchEvent(event); 20 | } 21 | `); 22 | } 23 | 24 | static handleSubmit() { 25 | this.getWebview().executeJavaScript(`{ 26 | var buttons = Array.from(document.querySelectorAll('button.bg-super')); 27 | if (buttons[0]) { 28 | var buttonsWithSvgPath = buttons.filter(button => button.querySelector('svg path')); 29 | var button = buttonsWithSvgPath[buttonsWithSvgPath.length - 1]; 30 | button.click(); 31 | } 32 | }`); 33 | } 34 | 35 | static handleCss() { 36 | this.getWebview().addEventListener('dom-ready', () => { 37 | // hide message below text input, sidebar, suggestions on new chat 38 | setTimeout(() => { 39 | this.getWebview().executeJavaScript(` 40 | 41 | 42 | `); 43 | }, 100); 44 | // Hide the "Try asking" segment 45 | setTimeout(() => { 46 | this.getWebview().insertCSS(` 47 | body { 48 | zoom: 80%; 49 | font-size: small; 50 | } 51 | 52 | .mt-lg { 53 | display: none; 54 | } 55 | `); 56 | }, 100); 57 | }); 58 | // this.getWebview().setZoomLevel(this.getWebview().getZoomLevel() - 2); 59 | } 60 | 61 | static getUserAgent() { 62 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; 63 | } 64 | 65 | static isEnabled() { 66 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); 67 | } 68 | } 69 | 70 | module.exports = Perplexity; 71 | -------------------------------------------------------------------------------- /src/providers/phind.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Phind extends Provider { 4 | static webviewId = 'webviewPhind'; 5 | static fullName = 'Phind'; 6 | static shortName = 'Phind'; 7 | 8 | static url = 'https://www.phind.com/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('textarea[placeholder*="Describe your task in detail. What are you stuck on"]'); 14 | if (!inputElement) { 15 | inputElement = document.querySelector('textarea[placeholder*="Send message"]'); 16 | } 17 | if (inputElement) { 18 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 19 | var event = new Event('input', { bubbles: true}); 20 | 21 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 22 | inputElement.dispatchEvent(event); 23 | } 24 | 25 | }`); 26 | } 27 | 28 | static handleSubmit(superprompt) { 29 | // url encode superprompt and navigate webview 30 | const encodedSuperprompt = encodeURIComponent(superprompt); 31 | this.getWebview().loadURL( 32 | `https://www.phind.com/search?q=${encodedSuperprompt}&source=searchbox`, 33 | ); 34 | 35 | // doesnt work 36 | // this.getWebview().executeJavaScript(`{ 37 | // var button = document.querySelector('button[type="submit"]'); 38 | // button.click(); 39 | // }`); 40 | } 41 | 42 | static handleCss() { 43 | this.getWebview().addEventListener('dom-ready', () => { 44 | // briefly commented out in order to get Phind to work - swyx 45 | // setTimeout(() => { 46 | // this.getWebview().executeJavaScript(`{ 47 | // // Hide Phind Logo 48 | // const images = document.querySelectorAll('img[src*="phind"]'); 49 | // if (images) images[images.length - 1].setAttribute('style', 'display: none;'); 50 | // // Hide Tagline 51 | // const tagline = document.querySelector('h1'); 52 | // if (tagline) tagline.setAttribute('style', 'display: none;'); 53 | // // Hide Explore Options 54 | // const exploreOptions = document.querySelector('div.container:has(h4)'); 55 | // if (exploreOptions) exploreOptions.setAttribute('style', 'display: none;'); 56 | // }`); 57 | // }, 100); 58 | }); 59 | } 60 | 61 | // Some providers will have their own dark mode implementation 62 | static handleDarkMode(isDarkMode) { 63 | // briefly commented out in order to get Phind to work - swyx 64 | // Implement dark or light mode using prodiver-specific code 65 | if (isDarkMode) { 66 | this.getWebview().executeJavaScript(`{ 67 | document.documentElement.setAttribute('data-theme', 'dark'); 68 | }`); 69 | } else { 70 | this.getWebview().executeJavaScript(`{ 71 | document.documentElement.setAttribute('data-theme', 'light'); 72 | }`); 73 | } 74 | } 75 | 76 | static isEnabled() { 77 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 78 | } 79 | } 80 | 81 | module.exports = Phind; 82 | -------------------------------------------------------------------------------- /src/providers/poe.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Poe extends Provider { 4 | static webviewId = 'webviewPoe'; 5 | static fullName = 'Quora Poe'; 6 | static shortName = 'Poe'; 7 | 8 | static url = 'https://poe.com/'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | var inputElement = document.querySelector('textarea'); 13 | if (inputElement) { 14 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 15 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 16 | 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } 22 | 23 | static handleSubmit() { 24 | this.getWebview().executeJavaScript(`{ 25 | var button = document.querySelectorAll('button[class*="ChatMessageSendButton_sendButton"]')[0] 26 | if (button) { 27 | button.click(); 28 | } 29 | }`); 30 | } 31 | 32 | static handleCss() { 33 | this.getWebview().addEventListener('dom-ready', () => { 34 | // hide message below text input, sidebar, suggestions on new chat 35 | setTimeout(() => { 36 | this.getWebview().executeJavaScript(` 37 | `); 38 | }, 100); 39 | // Hide the "Try asking" segment 40 | setTimeout(() => { 41 | this.getWebview().insertCSS(` 42 | .mt-lg { 43 | display: none; 44 | } 45 | `); 46 | }, 100); 47 | }); 48 | } 49 | 50 | static isEnabled() { 51 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, true); 52 | } 53 | } 54 | 55 | module.exports = Poe; 56 | -------------------------------------------------------------------------------- /src/providers/provider.js: -------------------------------------------------------------------------------- 1 | // const { ipcRenderer } = require('electron'); 2 | // const log = require('electron-log'); 3 | 4 | class Provider { 5 | static webviewId = ''; 6 | 7 | static getWebview() { 8 | // log('Provider.getWebview()', document.getElementById(this.webviewId)); 9 | return document.getElementById(this.webviewId); 10 | } 11 | 12 | static url = ''; 13 | 14 | static paneId() { 15 | return `${this.name.toLowerCase()}Pane`; 16 | } 17 | 18 | static setupCustomPasteBehavior() { 19 | this.getWebview().addEventListener('dom-ready', () => { 20 | this.getWebview().executeJavaScript(`{ 21 | document.addEventListener('paste', (event) => { 22 | event.preventDefault(); 23 | var text = event.clipboardData.getData('text'); 24 | var activeElement = document.activeElement; 25 | 26 | // sometimes the active element needs a "wake up" before paste (swyx: not entirely sure this works...) 27 | // Create a KeyboardEvent 28 | var event = new KeyboardEvent('keydown', { 29 | key: ' ', 30 | code: 'Space', 31 | which: 32, 32 | keyCode: 32, 33 | bubbles: true 34 | }); 35 | 36 | // Dispatch the event to the active element 37 | activeElement.dispatchEvent(event); 38 | 39 | var start = activeElement.selectionStart; 40 | var end = activeElement.selectionEnd; 41 | activeElement.value = activeElement.value.slice(0, start) + text + activeElement.value.slice(end); 42 | activeElement.selectionStart = activeElement.selectionEnd = start + text.length; 43 | }); 44 | }`); 45 | }); 46 | } 47 | 48 | static handleInput(input) { 49 | throw new Error(`Provider ${this.name} must implement handleInput()`); 50 | } 51 | 52 | static handleSubmit() { 53 | throw new Error(`Provider ${this.name} must implement handleSubmit()`); 54 | } 55 | 56 | static handleCss() { 57 | throw new Error(`Provider ${this.name} must implement handleCss()`); 58 | } 59 | 60 | // Some providers will have their own dark mode implementation 61 | static handleDarkMode(isDarkMode) { 62 | // Implement dark or light mode using prodiver-specific code 63 | if (isDarkMode) { 64 | this.getWebview().executeJavaScript(`{ 65 | document.documentElement.classList.add('dark'); 66 | document.documentElement.classList.remove('light'); 67 | }`); 68 | } else { 69 | this.getWebview().executeJavaScript(`{ 70 | document.documentElement.classList.add('light'); 71 | document.documentElement.classList.remove('dark'); 72 | }`); 73 | } 74 | } 75 | 76 | static getUserAgent() { 77 | return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; 78 | } 79 | 80 | static isEnabled() { 81 | return window.electron.electronStore.get(`${this.webviewId}Enabled`); 82 | } 83 | 84 | static setEnabled(state) { 85 | window.electron.electronStore.set(`${this.webviewId}Enabled`, state); 86 | } 87 | } 88 | 89 | module.exports = Provider; 90 | -------------------------------------------------------------------------------- /src/providers/smol.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class SmolTalk extends Provider { 4 | static webviewId = 'webviewSMOL'; 5 | static fullName = 'Smol Talk (WIP)'; 6 | static shortName = 'Smol'; 7 | 8 | static url = 'https://smoltalk.vercel.app/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('#smol-inputbox') 14 | if (inputElement) { 15 | const inputEvent = new Event('input', { bubbles: true }); 16 | inputElement.dispatchEvent(inputEvent); 17 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 18 | var event = new Event('input', { bubbles: true}); 19 | 20 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 21 | inputElement.dispatchEvent(event); 22 | } 23 | }`); 24 | } 25 | 26 | static clearCookies() { 27 | this.getWebview().executeJavaScript(`{ 28 | const cookies = document.cookie.split(";"); 29 | 30 | for (let i = 0; i < cookies.length; i++) { 31 | const cookie = cookies[i]; 32 | const eqPos = cookie.indexOf("="); 33 | const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; 34 | document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; 35 | } 36 | }`); 37 | } 38 | 39 | static handleSubmit() { 40 | this.getWebview().executeJavaScript(`{ 41 | 42 | var btn = document.querySelector('#smol-submitbtn'); 43 | if (btn) { 44 | btn.focus(); 45 | btn.setAttribute("aria-disabled", "false"); // doesnt work alone 46 | btn.disabled = false; 47 | btn.click() 48 | } 49 | 50 | }`); 51 | } 52 | 53 | static handleCss() { 54 | this.getWebview().addEventListener('dom-ready', () => { 55 | // // hide message below text input, sidebar, suggestions on new chat 56 | // setTimeout(() => { 57 | // this.getWebview().insertCSS(` 58 | // header, .container { 59 | // background-color: white; 60 | // /* single line dark mode ftw */ 61 | // filter: invert(100%) hue-rotate(180deg); 62 | // } 63 | // /* hide the claude avatar in response */ 64 | // .p-1.w-9.h-9.shrink-0 { 65 | // display: none; 66 | // } 67 | // /* reduce claude prompt margins */ 68 | // .mx-4.md\:mx-12.mb-2.md\:mb-4.mt-2.w-auto { 69 | // margin: 0 !important; 70 | // } 71 | // `); 72 | // }, 1000); 73 | }); 74 | } 75 | 76 | static isEnabled() { 77 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 78 | } 79 | } 80 | 81 | module.exports = SmolTalk; 82 | -------------------------------------------------------------------------------- /src/providers/stablechat.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class StableChat extends Provider { 4 | static webviewId = 'webviewStableChat'; 5 | static fullName = 'Stable Chat (Stability AI)'; 6 | static shortName = 'StableChat'; 7 | 8 | static url = 'https://chat.stability.ai'; 9 | 10 | static handleInput(input) { 11 | this.getWebview().executeJavaScript(`{ 12 | var inputElement = document.querySelector('textarea[placeholder="Type something here..."]'); 13 | if (inputElement) { 14 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 15 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 16 | 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } 22 | 23 | static handleSubmit() { 24 | this.getWebview().executeJavaScript(`{ 25 | var btn = document.querySelector('textarea[placeholder="Type something here..."] + button'); 26 | if (btn) { 27 | btn.focus(); 28 | btn.disabled = false; 29 | btn.click(); 30 | } 31 | }`); 32 | } 33 | static handleCss() { 34 | // this.getWebview().addEventListener('dom-ready', () => { 35 | // // Hide the "Try asking" segment 36 | // setTimeout(() => { 37 | // this.getWebview().insertCSS(` 38 | // .mt-lg { 39 | // display: none; 40 | // } 41 | // `); 42 | // }, 100); 43 | // }); 44 | } 45 | static handleDarkMode(isDarkMode) { 46 | if (isDarkMode) { 47 | this.getWebview().executeJavaScript(`{ 48 | if(document.querySelector('html').dataset.theme === 'light'){ 49 | document.querySelector('.menu > ul > div:nth-child(2) > button').click() 50 | } 51 | } 52 | `); 53 | } else { 54 | this.getWebview().executeJavaScript(`{ 55 | if(document.querySelector('html').dataset.theme === 'business'){ 56 | document.querySelector('.menu > ul > div:nth-child(2) > button').click() 57 | } 58 | } 59 | `); 60 | } 61 | } 62 | 63 | static isEnabled() { 64 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 65 | } 66 | } 67 | 68 | module.exports = StableChat; 69 | -------------------------------------------------------------------------------- /src/providers/together.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Together extends Provider { 4 | static webviewId = 'webviewTOGETHER'; 5 | static fullName = 'Together (RedPajama, StarCoder, Falcon, etc)'; 6 | static shortName = 'Together'; 7 | 8 | static url = 'https://api.together.xyz/playground/chat'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('form textarea[placeholder*="Enter text here"]'); 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | var event = new Event('input', { bubbles: true}); 18 | inputElement.dispatchEvent(event); 19 | } 20 | }`); 21 | } 22 | 23 | static handleSubmit() { 24 | this.getWebview().executeJavaScript(`{ 25 | var btn = document.querySelector('button[data-cy="run-inference-button"]'); // YES we are using the has selector!!!! 26 | if (btn) { 27 | btn.focus(); 28 | btn.disabled = false; 29 | btn.click() 30 | } 31 | }`); 32 | } 33 | 34 | static handleCss() { 35 | this.getWebview().addEventListener('dom-ready', () => { 36 | // hide message below text input, sidebar, suggestions on new chat 37 | setTimeout(() => { 38 | this.getWebview().insertCSS(` 39 | header, header + div { 40 | background-color: white; 41 | /* single line dark mode ftw */ 42 | filter: invert(100%) hue-rotate(180deg); 43 | } 44 | header { 45 | height: 10px; 46 | margin-top: -5px; 47 | padding-top: 0px; 48 | padding-bottom: 0px; 49 | } 50 | /* the "chat" header is pretty big */ 51 | .mui-style-qe6v0i { 52 | padding-top: 0px; 53 | } 54 | div + h1, h1, h1 + div { 55 | display: none; 56 | } 57 | `); 58 | }, 100); 59 | }); 60 | } 61 | 62 | static isEnabled() { 63 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 64 | } 65 | } 66 | 67 | module.exports = Together; 68 | -------------------------------------------------------------------------------- /src/providers/vercel.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class Vercel extends Provider { 4 | static webviewId = 'webviewVercelAI'; 5 | static fullName = 'Vercel AI Chatbot'; 6 | static shortName = 'Vercel'; 7 | 8 | static url = 'https://chat.vercel.ai/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('textarea[placeholder*="Send a message."]'); // can be "Ask anything" or "Ask follow-up" 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | 18 | var event = new Event('input', { bubbles: true}); 19 | inputElement.dispatchEvent(event); 20 | } 21 | }`); 22 | } 23 | 24 | static handleSubmit() { 25 | this.getWebview().executeJavaScript(`{ 26 | var buttons = Array.from(document.querySelectorAll('button[type="submit"]')); 27 | if (buttons[0]) { 28 | var buttonsWithSrOnly = buttons.filter(button => { 29 | var span = button.querySelector('span'); 30 | return span && span.textContent.trim() === 'Send message'; 31 | }); 32 | 33 | if (buttonsWithSrOnly.length == 1){ 34 | var button = buttonsWithSrOnly[0]; 35 | button.click(); 36 | } 37 | } 38 | }`); 39 | } 40 | static handleCss() { 41 | this.getWebview().addEventListener('dom-ready', () => { 42 | // Hide the "Try asking" segment 43 | setTimeout(() => { 44 | this.getWebview().insertCSS(` 45 | .mt-lg { 46 | display: none; 47 | } 48 | `); 49 | }, 100); 50 | }); 51 | } 52 | 53 | static isEnabled() { 54 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 55 | } 56 | } 57 | 58 | module.exports = Vercel; 59 | -------------------------------------------------------------------------------- /src/providers/you.js: -------------------------------------------------------------------------------- 1 | const Provider = require('./provider'); 2 | 3 | class YouChat extends Provider { 4 | static webviewId = 'webviewYoudotcom'; 5 | static fullName = 'You.com Chat'; 6 | static shortName = 'You.com'; 7 | 8 | static url = 'https://you.com/chat/'; 9 | 10 | static handleInput(input) { 11 | const fullName = this.fullName; 12 | this.getWebview().executeJavaScript(`{ 13 | var inputElement = document.querySelector('textarea[placeholder*="Ask me anything..."]'); 14 | if (inputElement) { 15 | var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set; 16 | nativeTextAreaValueSetter.call(inputElement, \`${input}\`); 17 | 18 | var event = new Event('input', { bubbles: true}); 19 | inputElement.dispatchEvent(event); 20 | } 21 | }`); 22 | } 23 | 24 | static handleSubmit() { 25 | this.getWebview().executeJavaScript(`{ 26 | var buttons = Array.from(document.querySelectorAll('button[type="submit"]')); 27 | if (buttons[0]) { 28 | buttons[0].click(); 29 | } 30 | }`); 31 | } 32 | static handleCss() { 33 | // this.getWebview().addEventListener('dom-ready', () => { 34 | // // Hide the "Try asking" segment 35 | // setTimeout(() => { 36 | // this.getWebview().insertCSS(` 37 | // .mt-lg { 38 | // display: none; 39 | // } 40 | // `); 41 | // }, 100); 42 | // }); 43 | } 44 | 45 | static isEnabled() { 46 | return window.electron.electronStore.get(`${this.webviewId}Enabled`, false); 47 | } 48 | } 49 | 50 | module.exports = YouChat; 51 | -------------------------------------------------------------------------------- /src/renderer/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 13 | Arial, sans-serif; 14 | padding: 0; 15 | margin: 0; 16 | overflow: hidden; 17 | } 18 | #root { 19 | height: 100vh; 20 | } 21 | 22 | :root { 23 | --superprompt-height: 4rem; 24 | --titlebar-height: 26px; 25 | --powerbar-height: 2rem; 26 | --allverticalheight: calc( 27 | var(--titlebar-height) + var(--powerbar-height) + var(--superprompt-height) 28 | ); 29 | --actual-height: calc(100vh - var(--titlebar-height)); 30 | } 31 | 32 | #titlebar { 33 | height: calc(var(--titlebar-height)); 34 | -webkit-app-region: drag; 35 | } 36 | 37 | .vex-dialog-message ol { 38 | all: revert; /* locally remove tailwind styles */ 39 | } 40 | .vex.vex-theme-os .vex-content { 41 | width: 90vw !important; 42 | } 43 | /* https://github.com/HubSpot/vex/issues/51 */ 44 | .vex-theme-os .title-bar { 45 | margin: -1em; 46 | background: #ccc; 47 | padding: 1em; 48 | margin-bottom: 1em; 49 | border-radius: 5px 5px 0 0; 50 | } 51 | .vex-theme-os .title-bar h1 { 52 | font-size: 1.3em; 53 | font-weight: normal; 54 | line-height: 1.2em; 55 | margin: 0; 56 | } 57 | .powerbar { 58 | /* height: var(--powerbar-height) */ 59 | position: absolute; 60 | z-index: 10; 61 | left: calc(50% - 3rem); 62 | @apply rounded-b-lg bg-white/50; 63 | } 64 | .powerbar:hover { 65 | backdrop-filter: blur(10px); 66 | background: #fff; 67 | animation: blur 1s linear 0s infinite; 68 | /* box-shadow: 69 | 0 0 60px 30px rgba(255, 255, 255, 0.8), 70 | 0 0 100px 60px rgba(255, 255, 255, 0.5); */ 71 | } 72 | 73 | #titlebar button { 74 | -webkit-app-region: no-drag; 75 | } 76 | 77 | @media (prefers-color-scheme: dark) { 78 | .myarrow:before { 79 | border-color: transparent transparent #343541 transparent !important; 80 | } 81 | } 82 | 83 | .page { 84 | background: #eeeeee; 85 | /* overflow: auto; */ 86 | /* disabled because its quite buggy */ 87 | /* resize: both; */ 88 | width: 100%; 89 | height: calc( 90 | 100vh - var(--titlebar-height) - var(--superprompt-height) - 2rem 91 | ); 92 | /* margin: 0 auto; */ 93 | position: relative; 94 | } 95 | 96 | webview { 97 | /* overflow: hidden; */ 98 | position: absolute; 99 | left: 0; 100 | width: 100%; 101 | height: calc( 102 | 100vh - var(--titlebar-height) - var(--superprompt-height) - 2rem 103 | ); 104 | display: inline-flex !important; 105 | } 106 | 107 | /* .flex { 108 | display: flex; 109 | border-radius: 8px; 110 | overflow: hidden; 111 | } */ 112 | #form { 113 | @apply flex items-center justify-center bg-gray-600 border-0; 114 | } 115 | 116 | #form-wrapper { 117 | @apply flex w-screen; 118 | } 119 | 120 | #form textarea { 121 | @apply w-full h-full px-4 py-2 overflow-y-auto font-mono text-sm transition-colors bg-gray-600 border-0 shadow-inner outline-none focus:bg-gray-200; 122 | } 123 | 124 | #form svg { 125 | @apply stroke-white; /* Adding a margin to center the svg inside the button */ 126 | } 127 | 128 | #webviewContainer { 129 | @apply flex h-max; 130 | } 131 | 132 | .titlebar { 133 | display: flex; 134 | justify-content: space-between; 135 | @apply absolute top-0 left-0 right-0 z-10 items-center h-16 text-xs text-white bg-gray-600 shadow-xl; 136 | } 137 | 138 | .titlebar p { 139 | @apply ml-4 font-mono font-semibold; 140 | } 141 | 142 | .titlebar button { 143 | @apply p-2 mr-4 font-mono text-white bg-gray-700 rounded hover:bg-gray-500; 144 | } 145 | 146 | .gutter { 147 | background-color: #a9a9a9; 148 | background-repeat: no-repeat; 149 | background-position: 50%; 150 | } 151 | 152 | .gutter:hover { 153 | background-color: #929292; 154 | } 155 | 156 | .gutter.gutter-horizontal { 157 | background-image: url('../images/vertical-grip.png'); 158 | cursor: col-resize; 159 | } 160 | 161 | /* this doesnt color the bar properly */ 162 | 163 | /* Customize the bar color for Split.js */ 164 | .split .split-content::after { 165 | background: linear-gradient(to right, #4c6ef5, #9059ff); 166 | } 167 | 168 | /* Customize the bar color on hover */ 169 | .split .split-bar:hover { 170 | background: linear-gradient(to right, #4c6fff, #9f59ff); 171 | } 172 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { Route, MemoryRouter as Router, Routes } from 'react-router-dom'; 2 | // import icon from '../../assets/icon.svg'; 3 | // https://electron-react-boilerplate.js.org/docs/styling#tailwind-integration 4 | import 'tailwindcss/tailwind.css'; 5 | import './App.css'; 6 | import Layout from './layout'; 7 | 8 | export default function App() { 9 | return ( 10 | 11 | 12 | } /> 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | export function TitleBar({ isAlwaysOnTop, toggleIsAlwaysOnTop }: any) { 2 | return ( 3 |
7 |

🐣 GodMode

8 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/components/settings.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 2 | import { useEffect, useState } from 'react'; 3 | import { Settings } from 'lib/types'; 4 | import { Button } from './ui/button'; 5 | import { Switch } from '@headlessui/react'; 6 | import { 7 | convertKeyCode, 8 | convertModifierKey, 9 | modifierKeys, 10 | isValidShortcut, 11 | type ShortcutKey, 12 | } from 'lib/utils'; 13 | 14 | export default function SettingsMenu({ 15 | open, 16 | onClose, 17 | }: { 18 | open: boolean; 19 | onClose: () => void; 20 | }) { 21 | const [shortcut, setShortcut] = useState([]); 22 | const [validShortcut, setValidShortcut] = useState([]); 23 | const [isRecording, setIsRecording] = useState(false); 24 | const [metaKey, setMetaKey] = useState(''); 25 | const [openAtLogin, setOpenAtLogin] = useState(false); 26 | const [superpromptFocus, setSuperpromptFocus] = useState(false); 27 | 28 | function classNames(...classes) { 29 | return classes.filter(Boolean).join(' '); 30 | } 31 | 32 | const settings = window.settings as Settings; 33 | let pressedKeys = new Set(); 34 | 35 | function recordShortcut(event: KeyboardEvent) { 36 | event.preventDefault(); 37 | if (!isRecording) return; 38 | 39 | let workingShortcut = shortcut; 40 | const { key } = event; 41 | 42 | // const fetchPlatform = async () => { 43 | // const platform = await settings.getPlatform(); 44 | // return platform; 45 | // }; 46 | 47 | const pressedKey = modifierKeys.has(key as ShortcutKey) 48 | ? convertModifierKey(key as ShortcutKey) 49 | : convertKeyCode(event.code); 50 | 51 | pressedKeys.add(pressedKey); 52 | workingShortcut = Array.from(pressedKeys); 53 | 54 | if (isValidShortcut(workingShortcut)) { 55 | pressedKeys.clear(); 56 | setIsRecording(false); 57 | 58 | setValidShortcut([...workingShortcut]); 59 | } 60 | 61 | setShortcut([...workingShortcut]); 62 | } 63 | 64 | function keyUp(event: KeyboardEvent) { 65 | event.preventDefault(); 66 | // if (!isRecording) return; 67 | const { key } = event; 68 | if (modifierKeys.has(key as ShortcutKey)) { 69 | pressedKeys.delete(convertModifierKey(key as ShortcutKey)); 70 | } else { 71 | pressedKeys.delete(convertKeyCode(event.code)); 72 | } 73 | if (key === 'Escape') setIsRecording(false); 74 | } 75 | 76 | // Set the meta key on mount based on platform (cmd on mac, ctrl on windows) 77 | useEffect(() => { 78 | const fetchPlatform = async () => { 79 | const platform = await settings.getPlatform(); 80 | setMetaKey(platform === 'darwin' ? 'CmdOrCtrl' : 'Control'); 81 | }; 82 | if (isValidShortcut(shortcut)) fetchPlatform(); 83 | }, []); 84 | 85 | // Set the initial super prompt focus and shortcut states from the main process on mount 86 | useEffect(() => { 87 | const displayShortcut = async () => { 88 | const initialShortcut = await settings.getGlobalShortcut(); 89 | console.debug('initialShortcut', initialShortcut); 90 | setShortcut(initialShortcut?.split('+')); 91 | }; 92 | const setInitialSuperpromptFocus = async () => { 93 | const initialState = await settings.getFocusSuperprompt(); 94 | setSuperpromptFocus(initialState); 95 | }; 96 | displayShortcut(); 97 | setInitialSuperpromptFocus(); 98 | }, []); 99 | 100 | // Whenever shortcut is updated, update it in the electron store in the main process via IPC 101 | useEffect(() => { 102 | if (!isValidShortcut(validShortcut)) return; 103 | const updateShortcut = async (shortcut: string[]) => { 104 | const newShortcut = shortcut.join('+'); 105 | const sc = await settings.setGlobalShortcut(newShortcut); 106 | setValidShortcut([]); 107 | }; 108 | updateShortcut(validShortcut); 109 | }, [validShortcut]); 110 | // Toggle superprompt focus setting in electron store 111 | useEffect(() => { 112 | const updateSuperpromptFocus = async () => { 113 | await settings.setFocusSuperprompt(superpromptFocus); 114 | }; 115 | updateSuperpromptFocus(); 116 | }, [superpromptFocus]); 117 | // Turn on key listeners when recording shortcuts 118 | useEffect(() => { 119 | if (isRecording && validShortcut.length === 0) { 120 | console.log('inside recording'); 121 | window.addEventListener('keydown', recordShortcut); 122 | window.addEventListener('keyup', keyUp); 123 | } else { 124 | console.log('inside not recording'); 125 | window.removeEventListener('keydown', recordShortcut); 126 | window.removeEventListener('keyup', keyUp); 127 | } 128 | return () => { 129 | window.removeEventListener('keydown', recordShortcut); 130 | window.removeEventListener('keyup', keyUp); 131 | }; 132 | }, [isRecording, validShortcut]); 133 | 134 | // Turn off recording when the dialog is closed 135 | useEffect(() => { 136 | if (!open) setIsRecording(false); 137 | }, [open]); 138 | 139 | useEffect(() => { 140 | const fetchOpenAtLogin = async () => { 141 | const isOpen = await settings.getOpenAtLogin(); 142 | setOpenAtLogin(isOpen); 143 | }; 144 | fetchOpenAtLogin(); 145 | }, []); 146 | 147 | useEffect(() => { 148 | openAtLogin 149 | ? window.electron.browserWindow.enableOpenAtLogin() 150 | : window.electron.browserWindow.disableOpenAtLogin(); 151 | }, [openAtLogin]); 152 | 153 | return ( 154 | 155 | 156 | 157 | Settings 158 | 159 | 160 | 161 | 166 | Automatically Open GodMode at Login 167 | 168 | 169 | 177 | 185 | 186 | 187 | 188 | 193 | Focus superprompt input on quick open shortcut 194 | 195 | 196 | 204 | 212 | 213 | 214 |
219 | Change Shortcut 220 |
221 |
222 | 223 |
227 | {shortcut?.map((key, index) => ( 228 |
229 |
230 | {key} 231 |
232 |
233 | {index < shortcut.length - 1 && '+'} 234 |
235 |
236 | ))} 237 |
238 | {isRecording ? ( 239 | 246 | ) : ( 247 | 254 | )} 255 |
256 |
257 | ); 258 | } 259 | -------------------------------------------------------------------------------- /src/renderer/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from 'lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center 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', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 14 | destructive: 15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 16 | outline: 17 | 'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground', 18 | secondary: 19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 20 | ghost: 'hover:bg-accent hover:text-accent-foreground', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | }, 23 | size: { 24 | default: 'h-9 px-4 py-2', 25 | sm: 'h-8 rounded-md px-3 text-xs', 26 | lg: 'h-10 rounded-md px-8', 27 | icon: 'h-9 w-9', 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: 'default', 32 | size: 'default', 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : 'button'; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = 'Button'; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/renderer/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 5 | import { Cross2Icon } from '@radix-ui/react-icons'; 6 | 7 | import { cn } from 'lib/utils'; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = ({ 14 | className, 15 | ...props 16 | }: DialogPrimitive.DialogPortalProps) => ( 17 | 18 | ); 19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 20 | 21 | const DialogOverlay = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 33 | )); 34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 35 | 36 | const DialogContent = React.forwardRef< 37 | React.ElementRef, 38 | React.ComponentPropsWithoutRef 39 | >(({ className, children, ...props }, ref) => ( 40 | 41 | 42 | 50 | {children} 51 | 52 | 53 | Close 54 | 55 | 56 | 57 | )); 58 | DialogContent.displayName = DialogPrimitive.Content.displayName; 59 | 60 | const DialogXContent = React.forwardRef< 61 | React.ElementRef, 62 | React.ComponentPropsWithoutRef 63 | >(({ className, children, ...props }, ref) => ( 64 | 65 | 66 | 74 | {children} 75 | 76 | 77 | Close 78 | 79 | 80 | 81 | )); 82 | DialogXContent.displayName = DialogPrimitive.Content.displayName; 83 | 84 | const DialogHeader = ({ 85 | className, 86 | ...props 87 | }: React.HTMLAttributes) => ( 88 |
95 | ); 96 | DialogHeader.displayName = 'DialogHeader'; 97 | 98 | const DialogFooter = ({ 99 | className, 100 | ...props 101 | }: React.HTMLAttributes) => ( 102 |
109 | ); 110 | DialogFooter.displayName = 'DialogFooter'; 111 | 112 | const DialogTitle = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, ...props }, ref) => ( 116 | 124 | )); 125 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 126 | 127 | const DialogDescription = React.forwardRef< 128 | React.ElementRef, 129 | React.ComponentPropsWithoutRef 130 | >(({ className, ...props }, ref) => ( 131 | 136 | )); 137 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 138 | 139 | export { 140 | Dialog, 141 | DialogTrigger, 142 | DialogContent, 143 | DialogXContent, 144 | DialogHeader, 145 | DialogFooter, 146 | DialogTitle, 147 | DialogDescription, 148 | }; 149 | -------------------------------------------------------------------------------- /src/renderer/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from 'lib/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/renderer/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 5 | 6 | import { cn } from '/lib/utils'; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | 4 | const container = document.getElementById('root') as HTMLElement; 5 | const root = createRoot(container); 6 | root.render(); 7 | 8 | // calling IPC exposed from preload script 9 | window.electron.ipcRenderer.once('ipc-example', (arg) => { 10 | // eslint-disable-next-line no-console 11 | console.log(arg); 12 | }); 13 | window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']); 14 | 15 | window.electron.ipcRenderer.on('perplexity-llama2', (args) => { 16 | // comes from main.ts ipcMain.on('prompt-hidden-chat',...) sendFn(responseHTML, responseText) 17 | var target = document.getElementById('streamingPromptResponseContainer'); 18 | if (target) { 19 | target.innerHTML = args as string; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler } from 'main/preload'; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-unused-vars 5 | interface Window { 6 | electron: ElectronHandler; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /src/renderer/promptCritic.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesIcon } from '@heroicons/react/20/solid'; 2 | 3 | // @ts-ignore 4 | import vex from 'vex-js'; 5 | // Main css 6 | import 'vex-js/dist/css/vex.css'; 7 | // Themes (Import all themes you want to use here) 8 | import 'vex-js/dist/css/vex-theme-default.css'; 9 | import 'vex-js/dist/css/vex-theme-os.css'; 10 | vex.registerPlugin(require('vex-dialog')); 11 | vex.defaultOptions.className = 'vex-theme-os'; 12 | 13 | function _promptCritic(originalPrompt: string) { 14 | return `I need to improve the original prompt: 15 | 16 | --- Original Prompt --- 17 | ${originalPrompt} 18 | --- End Original Prompt --- 19 | 20 | There are known ways to improve prompts for better LLM performance. 21 | Can you please briefly list ways to improve (3
  1. bullet points with bolded headings per bullet) and then suggest one improved prompt? 22 | Do not attempt to answer the prompt yourself. 23 | `; 24 | // , for example adding "Let's think step by step to get to the right answer" or adding comments before each line of code. 25 | } 26 | 27 | function _promptImprover(originalPrompt: string, modifyInstructions?: string) { 28 | return `I need to add more detail to the original prompt: 29 | 30 | --- Original Prompt --- 31 | ${originalPrompt} 32 | --- End Original Prompt --- 33 | 34 | ${ 35 | modifyInstructions 36 | ? `My modification instructions are: ${modifyInstructions}` 37 | : '' 38 | } 39 | Please suggest a newer, more detailed (still <300 words) version of this prompt that improves LLM performance. 40 | Do not preface with any conversation or small talk, only reply with the improved prompt. 41 | `; 42 | 43 | // For example: 44 | // - for general knowledge questions, appending "Let's think step by step to get to the right answer." is known to do well. 45 | // - for creative writing, setting temperature=200 and adding exciting adjectives, writing in the style of Hunter S Thompson and Winston Churchill and other well known authors. 46 | // - for code generation, first ask for the high level implementation plan in comments, then make sure each non-trivial line of code is preceded by a comment explaining what it does. 47 | 48 | // Do not preface with any conversation or small talk, only reply with the improved prompt. 49 | // `; 50 | } 51 | 52 | export function PromptCritic(props: { 53 | active: boolean; 54 | superprompt: string; 55 | setSuperprompt: (p: string) => void; 56 | }) { 57 | const { active, superprompt, setSuperprompt } = props; 58 | async function runPromptCritic() { 59 | if (superprompt.length < 10) { 60 | alert( 61 | 'superprompt is too short. write a longer one! e.g. "write a receipe for scrambled eggs"', 62 | ); 63 | return; 64 | } 65 | if (superprompt.length > 60) { 66 | alert( 67 | 'superprompt is too long. it can only currently handle low effort prompts. e.g. "write a receipe for scrambled eggs". we are working on extending it to 100k tokens!', 68 | ); 69 | return; 70 | } 71 | console.log('promptCritic', superprompt); 72 | window.electron.browserWindow.promptHiddenChat(_promptCritic(superprompt)); 73 | var promptChangeStr = await new Promise((res) => 74 | vex.dialog.prompt({ 75 | unsafeMessage: ` 76 |
    77 |

    PromptCritic analysis

    78 |
    79 |
    80 |
    `, 81 | placeholder: `Write your new prompt here`, 82 | callback: res, 83 | }), 84 | ); 85 | if (!promptChangeStr) return; 86 | console.log('stage 2 response', promptChangeStr); 87 | 88 | console.log('finalPrompt', promptChangeStr); 89 | if (promptChangeStr != null) { 90 | setSuperprompt(promptChangeStr); 91 | } 92 | // window.electron.browserWindow.promptHiddenChat(_promptImprover(superprompt, promptChangeStr)); 93 | // console.log('stage 3 response', prospectivePrompt); 94 | // var finalPrompt: string | null = await new Promise((res) => 95 | // vex.dialog.prompt({ 96 | // unsafeMessage: ` 97 | //
    98 | //

    PromptCritic's Improved suggestion

    99 | //
    100 | //
    101 | //
    `, 102 | // // value: prospectivePrompt.responseText, 103 | // input: ``, 107 | // placeholder: `your final prompt; copy and paste from above if it helps`, 108 | // callback: (data: any) => { 109 | // console.log({ data }); 110 | // if (!data) { 111 | // console.log('Cancelled'); 112 | // } else { 113 | // res(data); 114 | // } 115 | // }, 116 | // }), 117 | // ); 118 | 119 | // const textareavalue = prospectivePrompt.responseText.replace( 120 | // /\r|\n/, 121 | // '
    ' 122 | // ); 123 | // console.log('finalPrompt', finalPrompt); 124 | // if (finalPrompt != null) { 125 | // setSuperprompt(finalPrompt); 126 | // } 127 | } 128 | return ( 129 | 139 | ); 140 | } 141 | 142 | // https://tailwindui.com/components/application-ui/elements/dropdowns 143 | function classNames(...classes: string[]) { 144 | return classes.filter(Boolean).join(' '); 145 | } 146 | -------------------------------------------------------------------------------- /src/settings/archive/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {"darwin" | "win32" | "linux"} 3 | */ 4 | let currentPlatform = 'darwin'; 5 | /** 6 | * @type {"Command" | "Super"} 7 | */ 8 | let metaKey = 'Command'; 9 | window.settings?.getPlatform?.().then((platform) => { 10 | // Get the platform from the main process 11 | currentPlatform = platform; 12 | metaKey = currentPlatform === 'darwin' ? 'Command' : 'Super'; 13 | }); 14 | 15 | /** 16 | * @type {string[]} 17 | */ 18 | let shortcut = []; 19 | window.settings?.getQuickOpenShortcut?.().then((accelerator) => { 20 | // Get the accelerator from the store in the main process 21 | shortcut = convertAcceleratorToTokens(accelerator); 22 | updateDOM(); 23 | }); 24 | 25 | /** 26 | * @type {Set} 27 | */ 28 | let modifierKeySet = new Set(); 29 | let interimShift = false; 30 | let isRecording = false; 31 | const modifierKeys = new Set(['Control', 'Shift', 'Alt', 'Meta']); 32 | 33 | function convertAcceleratorToTokens(accelerator) { 34 | return accelerator.split('+').filter(Boolean); 35 | } 36 | 37 | function convertTokensToAccelerator(tokens) { 38 | return tokens.join('+'); 39 | } 40 | 41 | /** 42 | * Convert a key code from a Web keyboard event to an Electron key code 43 | * @param {string} code 44 | * @returns {string} 45 | */ 46 | function mapWebKeyCodeToElectronKeyCode(code) { 47 | return code 48 | .toUpperCase() 49 | .replace('KEY', '') 50 | .replace('DIGIT', '') 51 | .replace('NUMPAD', 'NUM') 52 | .replace('COMMA', ','); 53 | } 54 | 55 | function onlyPressedModifierKeyIsShift() { 56 | return ( 57 | modifierKeySet.size === 1 && modifierKeySet.has('Shift') && interimShift 58 | ); 59 | } 60 | 61 | /** 62 | * Handle the recording of a keyboard shortcut 63 | * @param {KeyboardEvent} event 64 | */ 65 | function recordShortcut(event) { 66 | event.preventDefault(); 67 | if (!isRecording) { 68 | return; 69 | } 70 | const { key } = event; 71 | if (key === 'Shift') { 72 | interimShift = true; 73 | } 74 | if (interimShift && modifierKeys.has(key)) { 75 | modifierKeySet.add('Shift'); 76 | if (key === 'Meta') { 77 | modifierKeySet.add(metaKey); 78 | } else { 79 | modifierKeySet.add(key); 80 | } 81 | } else if (modifierKeys.has(key)) { 82 | if (key === 'Meta') { 83 | modifierKeySet.add(metaKey); 84 | } else { 85 | modifierKeySet.add(key); 86 | } 87 | } else if ( 88 | key.length === 1 && 89 | modifierKeySet.size > 0 && 90 | !onlyPressedModifierKeyIsShift() 91 | ) { 92 | const nonModifiedKey = mapWebKeyCodeToElectronKeyCode(event.code); 93 | if (interimShift === false) { 94 | modifierKeySet.delete('Shift'); 95 | } 96 | const finalShortcut = Array.from(modifierKeySet).concat(nonModifiedKey); 97 | shortcut = finalShortcut; 98 | modifierKeySet = new Set(); 99 | interimShift = false; 100 | isRecording = false; 101 | window.settings?.setQuickOpenShortcut?.( 102 | convertTokensToAccelerator(shortcut), 103 | ); 104 | } 105 | 106 | updateDOM(); 107 | } 108 | 109 | /** 110 | * Handle the release of a key 111 | * @param {KeyboardEvent} event 112 | */ 113 | function keyUp(event) { 114 | if (!isRecording) return; 115 | if (event.key === 'Escape') { 116 | isRecording = false; 117 | } 118 | const { key } = event; 119 | if (event.key === 'Shift') { 120 | interimShift = false; 121 | } else if (modifierKeys.has(key)) { 122 | modifierKeySet.delete(key); 123 | } 124 | } 125 | 126 | function toggleRecording() { 127 | isRecording = !isRecording; 128 | updateDOM(); 129 | } 130 | 131 | function turnRecordingOff() { 132 | isRecording = false; 133 | updateDOM(); 134 | } 135 | 136 | window.addEventListener('keydown', recordShortcut); 137 | window.addEventListener('keyup', keyUp); 138 | 139 | // Manually manage the DOM based on state 140 | function updateDOM() { 141 | const container = document.getElementById('accelerator-container'); 142 | container.innerHTML = ''; 143 | 144 | if (!shortcut || shortcut.length === 0) { 145 | const btn = document.createElement('button'); 146 | btn.textContent = isRecording 147 | ? 'Recording shortcut...' 148 | : 'Click to record shortcut'; 149 | btn.addEventListener('click', toggleRecording); 150 | container.appendChild(btn); 151 | } else { 152 | const wrapper = document.createElement('div'); 153 | wrapper.classList.add('accelerator-wrapper'); 154 | const shortcutDiv = document.createElement('div'); 155 | shortcutDiv.classList.add('accelerator'); 156 | if (isRecording) { 157 | const tagDiv = document.createElement('div'); 158 | tagDiv.classList.add('tag', 'in-progress'); 159 | tagDiv.textContent = 'Type shortcut...'; 160 | shortcutDiv.appendChild(tagDiv); 161 | } else { 162 | shortcut.forEach((token) => { 163 | const tagDiv = document.createElement('div'); 164 | tagDiv.classList.add('tag'); 165 | tagDiv.textContent = token; 166 | shortcutDiv.appendChild(tagDiv); 167 | }); 168 | } 169 | const btn = document.createElement('button'); 170 | btn.textContent = isRecording 171 | ? 'Cancel recording' 172 | : 'Click to record new shortcut'; 173 | btn.addEventListener('click', toggleRecording); 174 | wrapper.appendChild(shortcutDiv); 175 | wrapper.appendChild(btn); 176 | container.appendChild(wrapper); 177 | } 178 | } 179 | 180 | // Initial render 181 | updateDOM(); 182 | -------------------------------------------------------------------------------- /src/settings/settings.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | margin: 0; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | .titlebar { 14 | height: 28px; 15 | -webkit-app-region: drag; 16 | color: #666; 17 | font-size: 12px; 18 | font-weight: bold; 19 | text-align: center; 20 | line-height: 28px; 21 | margin-bottom: 20px; 22 | border-bottom: 1px #eee solid; 23 | cursor: default; 24 | user-select: none; 25 | } 26 | 27 | .container { 28 | padding: 20px; 29 | } 30 | 31 | .accelerator-wrapper { 32 | display: flex; 33 | flex-direction: column; 34 | gap: 8px; 35 | } 36 | 37 | .accelerator { 38 | color: rgba(0, 0, 0, 0.8) !important; 39 | background: transparent; 40 | border-radius: 4px; 41 | padding: 0px 8px; 42 | transition: var(--theme-transitions-primary); 43 | border: 1px solid; 44 | border-color: rgba(0, 0, 0, 0.1); 45 | display: flex; 46 | flex-wrap: wrap; 47 | align-items: center; 48 | padding: 8px; 49 | gap: 4px; 50 | font-size: 12px; 51 | } 52 | 53 | .accelerator:hover { 54 | border-color: rgba(0, 0, 0, 0.5); 55 | } 56 | 57 | .accelerator-token { 58 | display: flex; 59 | align-items: center; 60 | } 61 | 62 | .tag { 63 | padding: 2px 4px; 64 | border-radius: 2px; 65 | width: fit-content; 66 | font-size: 12px; 67 | background-color: #eee; 68 | color: #222; 69 | } 70 | 71 | .tag.in-progress { 72 | background-color: #e0e7ff; 73 | color: #3730a3; 74 | } 75 | 76 | button { 77 | padding: 8px; 78 | border-radius: 5px; 79 | border: 1px solid rgba(0, 0, 0, 0.2); 80 | } 81 | -------------------------------------------------------------------------------- /src/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    Change Quick Open Shortcut
    8 |
    9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import SettingsComponent from './SettingsComponent'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors'); 2 | 3 | module.exports = { 4 | content: [ 5 | './src/renderer/**/*.{js,jsx,ts,tsx,ejs}', 6 | './src/components/**/*.{js,jsx,ts,tsx,ejs}', 7 | ], 8 | darkMode: 'media', // or 'class' 9 | theme: { 10 | extend: { 11 | colors: { 12 | sky: colors.sky, 13 | cyan: colors.cyan, 14 | }, 15 | }, 16 | }, 17 | variants: { 18 | extend: {}, 19 | }, 20 | plugins: [], 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2021"], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "sourceMap": true, 10 | "baseUrl": "./src", 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | "outDir": "scripts/dll" 17 | }, 18 | "exclude": ["test", "release/build", "release/app/dist", "scripts/dll"] 19 | } 20 | --------------------------------------------------------------------------------