├── public ├── icon16.png ├── icon32.png ├── icon64.png ├── icon128.png ├── background.js ├── index.html ├── content.css ├── manifest.json ├── README.md └── content.js ├── src ├── fonts │ └── UbuntuMono-Regular.ttf ├── index.css ├── index.js ├── App.css ├── themeOptions.js └── App.js ├── .prettierrc ├── .gitignore ├── package.json └── README.md /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ordinath/Whisper_to_ChatGPT/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ordinath/Whisper_to_ChatGPT/HEAD/public/icon32.png -------------------------------------------------------------------------------- /public/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ordinath/Whisper_to_ChatGPT/HEAD/public/icon64.png -------------------------------------------------------------------------------- /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ordinath/Whisper_to_ChatGPT/HEAD/public/icon128.png -------------------------------------------------------------------------------- /src/fonts/UbuntuMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ordinath/Whisper_to_ChatGPT/HEAD/src/fonts/UbuntuMono-Regular.ttf -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 160, 7 | "endOfLine": "lf" 8 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Ubuntu Mono'; 3 | src: local('UbuntuMono-Regular'), url(./fonts/UbuntuMono-Regular.ttf) format('truetype'); 4 | } 5 | 6 | body { 7 | background-color: rgb(51, 49, 53); 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /public/background.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | chrome.runtime.onInstalled.addListener(() => { 3 | console.log('Extension installed!'); 4 | }); 5 | 6 | // eslint-disable-next-line no-undef 7 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 8 | console.log(message); 9 | }); 10 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Whisper to ChatGPT Config 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /public/content.css: -------------------------------------------------------------------------------- 1 | .snippet_button { 2 | position: absolute; 3 | font-size: 0.8rem; /* Smaller font size */ 4 | padding: 0.3rem 0.5rem; /* Adjust the padding */ 5 | min-width: 1.5rem; /* Set a minimum width */ 6 | text-align: center; /* Center the text */ 7 | background-color: rgba(0, 0, 0, 0.1); 8 | font-weight: bold; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | *.zip 25 | .DS_Store 26 | 27 | clogs/ 28 | logs/ -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } */ 39 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Whisper to ChatGPT", 4 | "version": "1.2.29", 5 | "icons": { 6 | "16": "icon16.png", 7 | "32": "icon32.png", 8 | "64": "icon64.png", 9 | "128": "icon128.png" 10 | }, 11 | "description": "Prompt with your voice to ChatGPT in your Chrome browser using Whisper API with a button click.", 12 | "content_scripts": [ 13 | { 14 | "matches": [ 15 | "https://chat.openai.com/*", 16 | "https://chatgpt.com/*" 17 | ], 18 | "js": [ 19 | "content.js" 20 | ], 21 | "css": [ 22 | "content.css" 23 | ], 24 | "run_at": "document_idle", 25 | "all_frames": false 26 | } 27 | ], 28 | "background": { 29 | "service_worker": "background.js" 30 | }, 31 | "action": { 32 | "default_icon": "icon128.png", 33 | "default_popup": "index.html" 34 | }, 35 | "permissions": [ 36 | "storage" 37 | ] 38 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whisper_to_chatgpt", 3 | "version": "1.2.29", 4 | "description": "Elevate your ChatGPT experience with Voice-to-Text ChatGPT extension! Seamlessly record your voice and transcribe it using OpenAI's Whisper API - all within your Chrome browser. Just click, record, and transcribe! 🎉", 5 | "author": "Ordinath", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@emotion/react": "^11.10.6", 9 | "@emotion/styled": "^11.10.6", 10 | "@mui/material": "^5.11.14", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/README.md: -------------------------------------------------------------------------------- 1 | # 🎙️ V2T ChatGPT: Transcribe with Whisper API 🚀 2 | 3 | Elevate your ChatGPT experience with Voice-to-Text ChatGPT extension! Seamlessly record your voice and transcribe it using OpenAI's Whisper API - all within your Chrome browser. Just click, record, and transcribe! 🎉 4 | 5 | This extension is open-source! 🎉 Check out the repository on GitHub: https://github.com/Ordinath/Whisper_to_ChatGPT 6 | 7 | ## ✨ Features 8 | 9 | - 🎤 Record your voice and transcribe it using the powerful Whisper API 10 | - 🌐 Use the extension with main inputs on chat.openai.com and edit-inputs 11 | - 🔧 Customize the prompt for better API voice recognition results 12 | - 👁️ Clean and user-friendly interface with an eye-catching mic button 13 | 14 | ## 🔧 How to Run Locally 15 | 16 | To run the extension locally in your Chrome browser, follow these steps: 17 | 18 | 1. Clone or download the repository from GitHub: `https://github.com/Ordinath/Whisper_to_ChatGPT` 19 | 2. Open Google Chrome and navigate to `chrome://extensions` 20 | 3. Enable "Developer mode" by toggling the switch in the top-right corner 21 | 4. Click on "Load unpacked" button and select the folder containing the downloaded repository 22 | 5. The extension should now appear in your list of installed extensions 23 | 24 | Now you can use Voice-to-Text ChatGPT extension without downloading it from the Chrome Web Store! 25 | 26 | ## 🔑 API Key Disclaimer 27 | 28 | This extension requires an OpenAI account with a valid API key to function properly. OpenAI provides a small amount of free credits for all accounts, which is more than enough to use the Whisper API in ChatGPT and enjoy the extension's features. 29 | 30 | ## 📣 Feedback and Contributions 31 | 32 | We're dedicated to improving this extension and have exciting plans for its future! Give it a try, and share your thoughts - we're always open to feedback and suggestions. Feel free to open issues, submit pull requests, or just reach out with any ideas you have. 33 | 34 | 😄 Happy recording! 35 | -------------------------------------------------------------------------------- /src/themeOptions.js: -------------------------------------------------------------------------------- 1 | const themeOptions = { 2 | palette: { 3 | mode: 'dark', 4 | primary: { 5 | main: '#0f0', 6 | }, 7 | background: { 8 | default: '#333135', 9 | paper: '#212121', 10 | }, 11 | secondary: { 12 | main: '#4a0c3a', 13 | }, 14 | }, 15 | typography: { 16 | fontFamily: 'Ubuntu Mono, Open Sans', 17 | h1: { 18 | fontFamily: 'Ubuntu Mono', 19 | }, 20 | h2: { 21 | fontFamily: 'Ubuntu Mono', 22 | }, 23 | h3: { 24 | fontFamily: 'Ubuntu Mono', 25 | }, 26 | h4: { 27 | fontFamily: 'Ubuntu Mono', 28 | }, 29 | h6: { 30 | fontFamily: 'Ubuntu Mono', 31 | }, 32 | h5: { 33 | fontFamily: 'Ubuntu Mono', 34 | }, 35 | subtitle1: { 36 | fontFamily: 'Ubuntu Mono', 37 | }, 38 | subtitle2: { 39 | fontFamily: 'Ubuntu Mono', 40 | }, 41 | button: { 42 | fontFamily: 'Ubuntu Mono', 43 | fontWeight: 700, 44 | lineHeight: 1.75, 45 | }, 46 | overline: { 47 | fontFamily: 'Ubuntu Mono', 48 | }, 49 | input: { 50 | fontFamily: 'Ubuntu Mono', 51 | } 52 | }, 53 | overrides: { 54 | MuiButton: { 55 | root: { 56 | background: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)', 57 | border: 0, 58 | borderRadius: 3, 59 | boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)', 60 | color: 'white', 61 | height: 48, 62 | padding: '0 30px', 63 | }, 64 | }, 65 | }, 66 | props: { 67 | MuiButton: { 68 | size: 'small', 69 | }, 70 | MuiButtonGroup: { 71 | size: 'small', 72 | }, 73 | MuiCheckbox: { 74 | size: 'small', 75 | }, 76 | MuiFab: { 77 | size: 'small', 78 | }, 79 | MuiFormControl: { 80 | margin: 'dense', 81 | size: 'small', 82 | }, 83 | MuiFormHelperText: { 84 | margin: 'dense', 85 | }, 86 | MuiIconButton: { 87 | size: 'small', 88 | }, 89 | MuiInputBase: { 90 | margin: 'dense', 91 | // fontFamily: 'Ubuntu Mono', 92 | }, 93 | MuiInputLabel: { 94 | margin: 'dense', 95 | }, 96 | MuiRadio: { 97 | size: 'small', 98 | }, 99 | MuiSwitch: { 100 | size: 'small', 101 | }, 102 | MuiTextField: { 103 | margin: 'dense', 104 | size: 'small', 105 | }, 106 | }, 107 | shape: { 108 | borderRadius: 4, 109 | }, 110 | spacing: 8, 111 | }; 112 | 113 | export default themeOptions; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🎙️ Voice-to-Text ChatGPT: Transcribe with Whisper API 🚀 2 | 3 | Elevate your ChatGPT experience with [Voice-to-Text ChatGPT chrome extension](https://chrome.google.com/webstore/detail/whisper-to-chatgpt/jdmppbmnffdfhjlddebcelhigiomacfl?hl=ru&authuser=0)! Seamlessly record your voice and transcribe it using OpenAI's Whisper API - all within your Chrome browser. Just click, record, and transcribe! 🎉 4 | 5 | This extension is now a React application and open-source! 🎉 Check out the repository on GitHub: https://github.com/Ordinath/Whisper_to_ChatGPT 6 | 7 | ## ✨ Features 8 | 9 | - 🎤 Record your voice and transcribe it using the powerful Whisper API 10 | - ⚡ Configurable shortcut button to quickly activate the microphone 11 | - 🌐 Use the extension with main inputs on chat.openai.com and edit-inputs 12 | - 🔧 Customize the prompt for better API voice recognition results 13 | - 👁️ Clean and user-friendly interface with an eye-catching mic button 14 | - 🔄 Support for multiple Whisper API prompts for versatile transcription contexts 15 | - 🌍 Implicit translation support for transcribing and translating your input 16 | - 💾 Download your transcriptions as sound files for further use 17 | - 📌 Snippets feature (in beta) for quickly pasting frequently used text in the ChatGPT text area 18 | 19 | ## 🔧 How to Run Locally 20 | 21 | To run the extension locally in your Chrome browser, follow these steps: 22 | 23 | 1. Clone or download the repository from GitHub: https://github.com/Ordinath/Whisper_to_ChatGPT 24 | 2. Install the dependencies by running `npm install` in the project folder 25 | 3. Run `npm run build` to build the app for production to the build folder 26 | 4. Open Google Chrome and navigate to chrome://extensions 27 | 5. Enable "Developer mode" by toggling the switch in the top-right corner 28 | 6. Click on "Load unpacked" button and select the build folder created in step 3 29 | 7. The extension should now appear in your list of installed extensions 30 | 31 | Now you can use Voice-to-Text ChatGPT extension without downloading it from the Chrome Web Store! 32 | 33 | ## 🔑 API Key Disclaimer 34 | 35 | This extension requires an OpenAI account with a valid API key to function properly. OpenAI provides a small amount of free credits for all accounts, which is more than enough to use the Whisper API in ChatGPT and enjoy the extension's features. 36 | 37 | ## 📣 Feedback and Contributions 38 | 39 | We're dedicated to improving this extension and have exciting plans for its future! Give it a try, and share your thoughts - we're always open to feedback and suggestions. Feel free to open issues, submit pull requests, or just reach out with any ideas you have. 40 | 41 | 😄 Happy recording! 42 | 43 | ## 🛠️ Development 44 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 45 | 46 | # Available Scripts 47 | 1. `npm start` : Runs the app in development mode, open http://localhost:3000 to view it in your browser 48 | 2. `npm run build` : Builds the app for production to the build folder 49 | For more information, refer to the [Create React App documentation](https://create-react-app.dev/docs/getting-started/) and [React documentation](https://facebook.github.io/create-react-app/docs/getting-started). 50 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import React, { useState, useEffect } from 'react'; 3 | import Link from '@mui/material/Link'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import Box from '@mui/material/Box'; 6 | import TextField from '@mui/material/TextField'; 7 | import Button from '@mui/material/Button'; 8 | import Typography from '@mui/material/Typography'; 9 | import FormControlLabel from '@mui/material/FormControlLabel'; 10 | import Switch from '@mui/material/Switch'; 11 | import FormHelperText from '@mui/material/FormHelperText'; 12 | import MenuItem from '@mui/material/MenuItem'; 13 | import Select from '@mui/material/Select'; 14 | import FormControl from '@mui/material/FormControl'; 15 | import InputLabel from '@mui/material/InputLabel'; 16 | import Divider from '@mui/material/Divider'; 17 | import ThemeProvider from '@mui/material/styles/ThemeProvider'; 18 | import createTheme from '@mui/material/styles/createTheme'; 19 | import themeOptions from './themeOptions'; 20 | 21 | const darkTheme = createTheme(themeOptions); 22 | 23 | function App() { 24 | const [token, setToken] = useState(''); 25 | const [prompts, setPrompts] = useState([]); 26 | const [selectedPrompt, setSelectedPrompt] = useState(''); 27 | const [snippetFields, setSnippetFields] = useState([]); 28 | const [translationEnabled, setTranslationEnabled] = useState(false); 29 | const [downloadEnabled, setDownloadEnabled] = useState(false); 30 | const [snippetsEnabled, setSnippetsEnabled] = useState(false); 31 | 32 | // overdone shortcut feature 33 | const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); // hardcoded for now, we can adjust later if alphabet changes 34 | const [shortcutEnabled, setShortcutEnabled] = useState(false); 35 | const [shortcutFirstKey, setShortcutFirstKey] = useState('none'); 36 | // const [shortcutSecondKey, setShortcutSecondKey] = useState('none'); 37 | const [shortcutFirstModifier, setShortcutFirstModifier] = useState('none'); 38 | const [shortcutSecondModifier, setShortcutSecondModifier] = useState('none'); 39 | 40 | const [promptTitle, setPromptTitle] = useState(''); 41 | const [promptContent, setPromptContent] = useState(''); 42 | 43 | // retrieve stored data (backwards compatibilly with old version) 44 | useEffect(() => { 45 | // remove test data 46 | // chrome.storage?.sync.remove(['whisper_popup_dismissed', 'whisper_popup_last_shown', 'whisper_popup_close_count']); 47 | 48 | const retrieveState = async () => { 49 | await chrome.storage?.sync.get( 50 | [ 51 | 'openai_token', 52 | 'openai_prompts', 53 | 'openai_selected_prompt', 54 | 'openai_prompt', 55 | 'config_enable_translation', 56 | 'config_enable_download', 57 | 'config_enable_snippets', 58 | 'snippets', 59 | 'config_enable_shortcut', 60 | 'config_shortcut_first_key', 61 | // 'config_shortcut_second_key', 62 | 'config_shortcut_first_modifier', 63 | 'config_shortcut_second_modifier', 64 | ], 65 | async (result) => { 66 | console.log('Config retrieved:', { ...result, openai_token: result.openai_token ? '***' : '' }); 67 | if (result.openai_token) { 68 | setToken(result.openai_token); 69 | } 70 | 71 | // backwards compatibility with old version (only one prompt) 72 | // first launch of new version 73 | if (result.openai_prompt && !result.openai_prompts && !result.openai_selected_prompt) { 74 | setPrompts([{ title: 'Initial prompt', content: result.openai_prompt }]); 75 | setSelectedPrompt(0); 76 | setPromptTitle('Initial prompt'); 77 | setPromptContent(result.openai_prompt); 78 | await chrome.storage?.sync.set( 79 | { 80 | openai_prompts: [{ title: 'Initial prompt', content: result.openai_prompt }], 81 | openai_selected_prompt: 0, 82 | }, 83 | () => { 84 | // console.log('Config stored'); 85 | } 86 | ); 87 | } 88 | 89 | // first launch ever 90 | if (!result.openai_prompt && !result.openai_prompts && !result.openai_selected_prompt) { 91 | const initialPrompt = `The transcript is about OpenAI which makes technology like DALL·E, GPT-3, and ChatGPT with the hope of one day building an AGI system that benefits all of humanity.`; 92 | setPrompts([{ title: 'Initial prompt', content: initialPrompt }]); 93 | setSelectedPrompt(0); 94 | setPromptTitle('Initial prompt'); 95 | setPromptContent(initialPrompt); 96 | await chrome.storage?.sync.set( 97 | { 98 | openai_prompts: [{ title: 'Initial prompt', content: initialPrompt }], 99 | openai_selected_prompt: 0, 100 | }, 101 | () => { 102 | // console.log('Config stored'); 103 | } 104 | ); 105 | } 106 | 107 | // regular use with saved prompts in new version 108 | if (result.openai_prompts && (result.openai_selected_prompt || result.openai_selected_prompt === 0)) { 109 | setPrompts(result.openai_prompts); 110 | setSelectedPrompt(result.openai_selected_prompt); 111 | setPromptTitle(result.openai_prompts[result.openai_selected_prompt]?.title || ''); 112 | setPromptContent(result.openai_prompts[result.openai_selected_prompt]?.content || ''); 113 | } 114 | 115 | // shortcuts config on first launch 116 | if (!result.config_shortcut_first_key && !result.config_shortcut_first_modifier && !result.config_shortcut_second_modifier) { 117 | // we set default for windows and mac separately 118 | if (navigator.userAgentData.platform.toLowerCase().indexOf('mac') > -1) { 119 | setShortcutFirstModifier('ctrlKey'); 120 | setShortcutFirstKey('r'); 121 | await chrome.storage?.sync.set( 122 | { 123 | config_shortcut_first_modifier: 'ctrlKey', 124 | config_shortcut_first_key: 'r', 125 | }, 126 | () => { 127 | // console.log('Config stored'); 128 | } 129 | ); 130 | } else if (navigator.userAgentData.platform.toLowerCase().indexOf('win') > -1) { 131 | setShortcutFirstModifier('shiftKey'); 132 | setShortcutSecondModifier('altKey'); 133 | setShortcutFirstKey('r'); 134 | await chrome.storage?.sync.set( 135 | { 136 | config_shortcut_first_modifier: 'shiftKey', 137 | config_shortcut_second_modifier: 'altKey', 138 | config_shortcut_first_key: 'r', 139 | }, 140 | () => { 141 | // console.log('Config stored'); 142 | } 143 | ); 144 | } 145 | } 146 | 147 | if (result.config_enable_translation) { 148 | setTranslationEnabled(result.config_enable_translation); 149 | } 150 | if (result.config_enable_download) { 151 | setDownloadEnabled(result.config_enable_download); 152 | } 153 | if (result.config_enable_snippets) { 154 | setSnippetsEnabled(result.config_enable_snippets); 155 | } 156 | if (result.snippets) { 157 | setSnippetFields(result.snippets.map((snippet, index) => ({ id: index, value: snippet }))); 158 | } 159 | if (result.config_enable_shortcut) { 160 | setShortcutEnabled(result.config_enable_shortcut); 161 | } 162 | if (result.config_shortcut_first_key) { 163 | setShortcutFirstKey(result.config_shortcut_first_key); 164 | } 165 | // if (result.config_shortcut_second_key) { 166 | // setShortcutSecondKey(result.config_shortcut_second_key); 167 | // } 168 | if (result.config_shortcut_first_modifier) { 169 | setShortcutFirstModifier(result.config_shortcut_first_modifier); 170 | } 171 | if (result.config_shortcut_second_modifier) { 172 | setShortcutSecondModifier(result.config_shortcut_second_modifier); 173 | } 174 | } 175 | ); 176 | }; 177 | retrieveState(); 178 | }, []); 179 | 180 | // update prompt title and content when selected prompt changes (just in case you forget to click save) 181 | useEffect(() => { 182 | if (selectedPrompt >= 0) { 183 | const promptsCopy = [...prompts]; 184 | promptsCopy[selectedPrompt] = { title: promptTitle, content: promptContent }; 185 | setPrompts(promptsCopy); 186 | // console.log('Prompt changed:', { prompts }); 187 | const timeout = setTimeout(() => { 188 | chrome.storage?.sync.set( 189 | { 190 | openai_prompts: prompts, 191 | }, 192 | () => { 193 | // console.log('Config stored:', { prompts, selectedPrompt }); 194 | } 195 | ); 196 | }, 500); 197 | return () => clearTimeout(timeout); 198 | } 199 | }, [promptTitle, promptContent]); 200 | 201 | // update chrome storage when snippet fields change 202 | // we need to debounce this because it should not be called on every keystroke 203 | useEffect(() => { 204 | const snippetsTexts = snippetFields.map((field) => field.value); 205 | const timeout = setTimeout(() => { 206 | chrome.storage?.sync.set( 207 | { 208 | snippets: snippetsTexts, 209 | }, 210 | () => { 211 | // console.log('Config stored:', { snippets: snippetsTexts }); 212 | } 213 | ); 214 | }, 500); 215 | return () => clearTimeout(timeout); 216 | }, [snippetFields]); 217 | 218 | const handleSelectedPromptChange = (event) => { 219 | setSelectedPrompt(event.target.value); 220 | setPromptTitle(prompts[event.target.value]?.title || ''); 221 | setPromptContent(prompts[event.target.value]?.content || ''); 222 | const timeout = setTimeout(() => { 223 | chrome.storage?.sync.set( 224 | { 225 | openai_selected_prompt: event.target.value, 226 | }, 227 | () => { 228 | // console.log('Config stored:', { selectedPrompt: event.target.value }); 229 | } 230 | ); 231 | }, 500); 232 | return () => clearTimeout(timeout); 233 | }; 234 | 235 | const handleSavePrompt = () => { 236 | const promptsCopy = [...prompts]; 237 | promptsCopy[selectedPrompt] = { title: promptTitle, content: promptContent }; 238 | setPrompts(promptsCopy); 239 | chrome.storage?.sync.set( 240 | { 241 | openai_prompts: promptsCopy, 242 | openai_selected_prompt: selectedPrompt, 243 | }, 244 | () => { 245 | // console.log('Config stored:', { token, prompts: promptsCopy }); 246 | } 247 | ); 248 | }; 249 | 250 | const handleRemovePrompt = () => { 251 | if (selectedPrompt >= 0) { 252 | const promptsCopy = prompts.filter((_, i) => i !== selectedPrompt); 253 | setPrompts(promptsCopy); 254 | setSelectedPrompt(''); 255 | setPromptTitle(''); 256 | setPromptContent(''); 257 | 258 | chrome.storage?.sync.set( 259 | { 260 | openai_prompts: promptsCopy, 261 | }, 262 | () => { 263 | // console.log('Config stored:', { token, prompts: promptsCopy }); 264 | } 265 | ); 266 | } 267 | }; 268 | 269 | const handleAddSnippet = () => { 270 | setSnippetFields([...snippetFields, { id: snippetFields.length, value: '' }]); 271 | }; 272 | 273 | const handleRemoveSnippet = (id) => { 274 | setSnippetFields(snippetFields.filter((field) => field.id !== id)); 275 | }; 276 | 277 | const handleChangeSnippet = (id, value) => { 278 | setSnippetFields(snippetFields.map((field) => (field.id === id ? { ...field, value } : field))); 279 | }; 280 | 281 | const handleToggleTranslation = (event) => { 282 | setTranslationEnabled(event.target.checked); 283 | chrome.storage?.sync.set({ config_enable_translation: event.target.checked }, () => { 284 | // console.log('Config stored:', { config_enable_translation: event.target.checked }); 285 | }); 286 | }; 287 | 288 | const handleToggleShortcut = (event) => { 289 | setShortcutEnabled(event.target.checked); 290 | chrome.storage?.sync.set({ config_enable_shortcut: event.target.checked }, () => { 291 | // console.log('Config stored:', { config_enable_translation: event.target.checked }); 292 | }); 293 | }; 294 | 295 | const handleToggleDownload = (event) => { 296 | setDownloadEnabled(event.target.checked); 297 | chrome.storage?.sync.set({ config_enable_download: event.target.checked }, () => { 298 | // console.log('Config stored:', { config_enable_download: event.target.checked }); 299 | }); 300 | }; 301 | 302 | const handleToggleSnippets = (event) => { 303 | setSnippetsEnabled(event.target.checked); 304 | chrome.storage?.sync.set({ config_enable_snippets: event.target.checked }, () => { 305 | // console.log('Config stored:', { config_enable_snippets: event.target.checked }); 306 | }); 307 | }; 308 | 309 | const handleTokenChange = (event) => { 310 | setToken(event.target.value); 311 | chrome.storage?.sync.set({ openai_token: event.target.value }, () => { 312 | // console.log('Config stored:', { openai_token: event.target.value }); 313 | }); 314 | }; 315 | 316 | const handleShortcutFirstModifierChange = (event) => { 317 | setShortcutFirstModifier(event.target.value); 318 | chrome.storage?.sync.set({ config_shortcut_first_modifier: event.target.value }, () => { 319 | // console.log('Config stored:', { config_shortcut_first_modifier: event.target.value }); 320 | }); 321 | }; 322 | 323 | const handleShortcutSecondModifierChange = (event) => { 324 | setShortcutSecondModifier(event.target.value); 325 | chrome.storage?.sync.set({ config_shortcut_second_modifier: event.target.value }, () => { 326 | // console.log('Config stored:', { config_shortcut_second_modifier: event.target.value }); 327 | }); 328 | }; 329 | const handleShortcutFirstKeyChange = (event) => { 330 | setShortcutFirstKey(event.target.value); 331 | chrome.storage?.sync.set({ config_shortcut_first_key: event.target.value }, () => { 332 | // console.log('Config stored:', { config_shortcut_first_key: event.target.value }); 333 | }); 334 | }; 335 | 336 | // const handleShortcutSecondKeyChange = (event) => { 337 | // setShortcutSecondKey(event.target.value); 338 | // chrome.storage?.sync.set({ config_shortcut_second_key: event.target.value }, () => { 339 | // // console.log('Config stored:', { config_shortcut_second_key: event.target.value }); 340 | // }); 341 | // }; 342 | 343 | return ( 344 | <> 345 | 346 | 347 | 348 | 349 | 350 | Enjoying Whisper to ChatGPT?
351 | Try our desktop application to transcribe and paste across any desktop apps with a shortcut!
352 | Get one month free with promo code: THANKUWHISPER
353 | 354 | https://sonascript.com 355 | 356 |
357 |
358 | 359 | 0 ? 'password' : 'text'} 368 | /> 369 | 370 | 377 | 378 | 379 | Select Prompt 380 | 394 | 395 | 396 | 397 | setPromptTitle(e.target.value)} size="small" /> 398 | 399 | 400 | setPromptContent(e.target.value)} 406 | helperText="Boost words recognition according to provided context." 407 | size="small" 408 | multiline 409 | minRows={10} 410 | /> 411 | 412 | 413 | 416 | 417 | 418 | 421 | 422 | 429 | 430 | 431 | Config: 432 | 433 | 434 | 435 | } 437 | label="Enable Microphone Toggle Shortcut" 438 | /> 439 | Requires page refresh on an any change. 440 | 441 | {shortcutEnabled && ( 442 | <> 443 | 444 | 445 | Modifier 1 446 | 459 | 460 | 461 | 462 | 463 | Modifier 2 464 | 477 | 478 | 479 | 480 | 481 | Key 482 | 497 | 498 | 499 | 500 | )} 501 | 502 | } label="Enable Translation" /> 503 | Translate any input language to English 504 | 505 | 506 | } label="Enable File Download" /> 507 | Download sound file with recording 508 | 509 | {/* 510 | } label="Enable Snippets (Beta)" /> 511 | Requires page refresh on toggle. Only tested on desktop version. 512 | */} 513 | {/* {snippetsEnabled && ( 514 | <> 515 | 516 | Predefined Snippets: 517 | 518 | {snippetFields.map((field) => ( 519 | <> 520 | 521 | handleChangeSnippet(field.id, e.target.value)} 525 | size="small" 526 | fullWidth 527 | multiline 528 | maxRows={5} 529 | /> 530 | 531 | 532 | 535 | 536 | 537 | ))} 538 | 539 | 542 | 543 | 544 | )} */} 545 |
546 |
547 | 548 | ); 549 | } 550 | 551 | export default App; 552 | -------------------------------------------------------------------------------- /public/content.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | const SVG_MIC_HTML = ` 3 | 4 | `; 5 | const SVG_MIC_SPINNING_HTML = ` 6 | 10 | 11 | `; 12 | const SVG_SPINNER_HTML = 13 | ' '; 14 | 15 | const TRANSCRIPTION_URL = 'https://api.openai.com/v1/audio/transcriptions'; 16 | const TRANSLATION_URL = 'https://api.openai.com/v1/audio/translations'; 17 | const PRO_MAIN_MICROPHONE_BUTTON_CLASSES = 18 | 'flex h-9 w-9 items-center justify-center rounded-full transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary bg-black text-white dark:bg-white dark:text-black disabled:bg-[#D7D7D7]'; 19 | const NON_PRO_MAIN_MICROPHONE_BUTTON_CLASSES = 20 | 'flex h-9 w-9 items-center justify-center rounded-full bg-black text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary'; 21 | const SECONDARY_MICROPHONE_BUTTON_CLASSES = 22 | 'flex h-9 w-9 items-center justify-center rounded-full bg-black text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary'; 23 | 24 | // --- Updated Constants for Selectors --- 25 | const INPUT_SELECTOR = 'div[contenteditable="true"][id="prompt-textarea"]'; 26 | const PARENT_CONTAINER_SELECTOR = '.grid.grid-cols-\\[auto_1fr_auto\\]'; 27 | const BUTTONS_AREA_SELECTOR = '.\\[grid-area\\:trailing\\] .ms-auto.flex.items-center'; 28 | const MIC_BUTTON_SELECTOR = '.microphone_button'; 29 | // --- End Updated Constants --- 30 | 31 | const TESTING = false; 32 | 33 | const USAGE_COUNT_KEY = 'whisper_usage_count'; 34 | const POPUP_THRESHOLD = 5; 35 | const POPUP_FREQUENCY = 10; 36 | const POPUP_DISMISSED_KEY = 'whisper_popup_dismissed'; 37 | const POPUP_LAST_SHOWN_KEY = 'whisper_popup_last_shown'; 38 | const POPUP_CLOSE_COUNT_KEY = 'whisper_popup_close_count'; 39 | const POPUP_MIN_CLOSES_FOR_DONT_SHOW = 2; 40 | 41 | const getPopupHtml = (showDontShowOption = false) => { 42 | if (showDontShowOption) { 43 | return ` 44 |
45 |
46 | 47 | 48 |
49 | 54 |
`; 55 | } 56 | 57 | return ` 58 |
59 |
60 |
61 | Enjoying Whisper To ChatGPT? 62 |
63 |
64 | Try our Desktop App and dictate anywhere! 65 |
66 |
67 | 72 |
`; 73 | }; 74 | 75 | function logError(message, error) { 76 | console.error(`[Whisper to ChatGPT] ${message}`, error); 77 | } 78 | 79 | async function retrieveFromStorage(key) { 80 | return new Promise((resolve) => { 81 | chrome.storage.sync.get(key, function (result) { 82 | resolve(result[key]); 83 | }); 84 | }); 85 | } 86 | 87 | class AudioRecorder { 88 | constructor() { 89 | this.recording = false; 90 | this.mediaRecorder = null; 91 | this.textarea = null; 92 | this.micButton = null; 93 | this.token = null; 94 | this.snippetButtons = []; 95 | this.popupContainer = null; 96 | this.activePopup = null; 97 | } 98 | 99 | async listenForKeyboardShortcut() { 100 | if (await this.shortcutEnabled()) { 101 | const shortcutFirstKey = await retrieveFromStorage('config_shortcut_first_key'); 102 | const shortcutFirstModifier = await retrieveFromStorage('config_shortcut_first_modifier'); 103 | const shortcutSecondModifier = await retrieveFromStorage('config_shortcut_second_modifier'); 104 | document.addEventListener('keydown', (event) => { 105 | if (event.code === `Key${shortcutFirstKey.toUpperCase()}`) { 106 | // console.log(event); 107 | if (shortcutFirstModifier && shortcutFirstModifier !== 'none' && !event[shortcutFirstModifier]) return; 108 | if (shortcutSecondModifier && shortcutSecondModifier !== 'none' && !event[shortcutSecondModifier]) return; 109 | 110 | event.preventDefault(); 111 | 112 | // Find our microphone button based on the new UI structure 113 | const micButton = document.querySelector('.microphone_button'); 114 | if (micButton) { 115 | micButton.click(); 116 | } else { 117 | // If our button doesn't exist yet, try to find the input and add it 118 | const promptTextarea = document.querySelector('div[contenteditable="true"][id="prompt-textarea"]'); 119 | if (promptTextarea) { 120 | addMicrophoneButton(promptTextarea, 'main'); 121 | // Give a small delay to ensure the button is added 122 | setTimeout(() => { 123 | const newMicButton = document.querySelector('.microphone_button'); 124 | if (newMicButton) { 125 | newMicButton.click(); 126 | } 127 | }, 100); 128 | } 129 | } 130 | } 131 | }); 132 | } 133 | } 134 | 135 | createMicButton(inputType, version) { 136 | this.micButton = document.createElement('button'); 137 | if (inputType === 'main') { 138 | this.micButton.className = `microphone_button ${version === 'PRO' ? PRO_MAIN_MICROPHONE_BUTTON_CLASSES : NON_PRO_MAIN_MICROPHONE_BUTTON_CLASSES}`; 139 | } else { 140 | this.micButton.className = `microphone_button ${SECONDARY_MICROPHONE_BUTTON_CLASSES}`; 141 | } 142 | this.micButton.innerHTML = SVG_MIC_HTML; 143 | this.micButton.addEventListener('click', (e) => { 144 | e.preventDefault(); 145 | this.toggleRecording(); 146 | }); 147 | } 148 | 149 | updateButtonGridPosition() { 150 | const textareaRows = this.textarea.clientHeight / 24; 151 | 152 | if (this.snippetButtons) { 153 | this.snippetButtons.forEach((buttonObj, index) => { 154 | buttonObj.y = buttonObj.initialY - (textareaRows - 1) * 1.5; 155 | buttonObj.button.style.transform = `translate(${buttonObj.x}rem, ${buttonObj.y}rem)`; 156 | }); 157 | } 158 | } 159 | 160 | observeTextareaResize() { 161 | this.resizeObserver = new ResizeObserver(() => { 162 | this.updateButtonGridPosition(); 163 | }); 164 | this.resizeObserver.observe(this.textarea); 165 | } 166 | 167 | async downloadEnabled() { 168 | return await retrieveFromStorage('config_enable_download'); 169 | } 170 | 171 | async translationEnabled() { 172 | return await retrieveFromStorage('config_enable_translation'); 173 | } 174 | 175 | // async snippetsEnabled() { 176 | // return await retrieveFromStorage('config_enable_snippets'); 177 | // } 178 | 179 | async shortcutEnabled() { 180 | const shortcutEnabled = await retrieveFromStorage('config_enable_shortcut'); 181 | // initialize the shortcut keys if they are not set (first time user) 182 | const shortcutFirstKey = await retrieveFromStorage('config_shortcut_first_key'); 183 | const shortcutFirstModifier = await retrieveFromStorage('config_shortcut_first_modifier'); 184 | const shortcutSecondModifier = await retrieveFromStorage('config_shortcut_second_modifier'); 185 | if (!shortcutFirstKey && !shortcutFirstModifier && !shortcutSecondModifier) { 186 | const platform = navigator.userAgentData.platform.toLowerCase(); 187 | if (platform.indexOf('mac') > -1) { 188 | await chrome.storage?.sync.set( 189 | { 190 | config_shortcut_first_modifier: 'ctrlKey', 191 | config_shortcut_first_key: 'r', 192 | }, 193 | () => {} 194 | ); 195 | } else if (platform.indexOf('win') > -1) { 196 | await chrome.storage?.sync.set( 197 | { 198 | config_shortcut_first_modifier: 'shiftKey', 199 | config_shortcut_second_modifier: 'altKey', 200 | config_shortcut_first_key: 'r', 201 | }, 202 | () => {} 203 | ); 204 | } 205 | } 206 | return shortcutEnabled; 207 | } 208 | 209 | async retrieveToken() { 210 | return await retrieveFromStorage('openai_token'); 211 | } 212 | 213 | async getSelectedPrompt() { 214 | const selectedPrompt = await retrieveFromStorage('openai_selected_prompt'); 215 | const prompts = await retrieveFromStorage('openai_prompts'); 216 | // if (!prompts) we initialize the prompts (first time user) 217 | if (!prompts || !selectedPrompt) { 218 | // backwards compatibility with 1.0 version 219 | const previousVersionPrompt = await retrieveFromStorage('openai_prompt'); 220 | 221 | const initialPrompt = { 222 | title: 'Initial prompt', 223 | content: previousVersionPrompt 224 | ? previousVersionPrompt 225 | : `The transcript is about OpenAI which makes technology like DALL·E, GPT-3, and ChatGPT with the hope of one day building an AGI system that benefits all of humanity.`, 226 | }; 227 | await chrome.storage?.sync.set( 228 | { 229 | openai_prompts: [initialPrompt], 230 | openai_selected_prompt: 0, 231 | }, 232 | () => {} 233 | ); 234 | return initialPrompt; 235 | } else { 236 | return prompts[selectedPrompt]; 237 | } 238 | } 239 | 240 | async incrementUsageCount() { 241 | // console.log('incrementUsageCount'); 242 | const currentCount = (await retrieveFromStorage(USAGE_COUNT_KEY)) || 0; 243 | const newCount = currentCount + 1; 244 | await chrome.storage.sync.set({ [USAGE_COUNT_KEY]: newCount }); 245 | 246 | const dismissed = await retrieveFromStorage(POPUP_DISMISSED_KEY); 247 | const lastShown = (await retrieveFromStorage(POPUP_LAST_SHOWN_KEY)) || 0; 248 | 249 | if (!dismissed) { 250 | // Show popup for first time users at threshold 251 | if (newCount >= POPUP_THRESHOLD && lastShown === 0) { 252 | this.showPopup(true); 253 | await chrome.storage.sync.set({ [POPUP_LAST_SHOWN_KEY]: newCount }); 254 | } 255 | // After threshold, show popup every POPUP_FREQUENCY uses 256 | else if (newCount >= POPUP_THRESHOLD && newCount - lastShown >= POPUP_FREQUENCY) { 257 | this.showPopup(false); 258 | await chrome.storage.sync.set({ [POPUP_LAST_SHOWN_KEY]: newCount }); 259 | } 260 | } 261 | } 262 | 263 | async showPopup(firstTime = false) { 264 | // If there's already an active popup, don't show another one 265 | if (this.activePopup && document.contains(this.activePopup)) { 266 | return; 267 | } 268 | 269 | // Get the close count 270 | const closeCount = (await retrieveFromStorage(POPUP_CLOSE_COUNT_KEY)) || 0; 271 | 272 | const popupElement = document.createElement('div'); 273 | popupElement.className = 'whisper-popup'; 274 | 275 | // If we've closed it 3 times and this is after closing the promo message 276 | const showDontShowOption = closeCount > 0 && closeCount % POPUP_MIN_CLOSES_FOR_DONT_SHOW === 0; 277 | popupElement.innerHTML = getPopupHtml(showDontShowOption); 278 | const popup = popupElement.firstElementChild; 279 | 280 | if (this.popupContainer) { 281 | this.popupContainer.appendChild(popup); 282 | this.activePopup = popup; // Store reference to the active popup 283 | } 284 | 285 | // Handle popup close and checkbox 286 | const closeButton = popup.querySelector('.whisper-popup-close'); 287 | if (closeButton) { 288 | closeButton.addEventListener('click', async (e) => { 289 | // Prevent event propagation 290 | e.preventDefault(); 291 | e.stopPropagation(); 292 | 293 | if (showDontShowOption) { 294 | // If this is the "don't show again" popup, check if the checkbox is checked 295 | const checkbox = popup.querySelector('#whisper-dont-show'); 296 | if (checkbox && checkbox.checked) { 297 | await chrome.storage.sync.set({ [POPUP_DISMISSED_KEY]: true }); 298 | } 299 | // Reset close count after showing "don't show again" option 300 | await chrome.storage.sync.set({ [POPUP_CLOSE_COUNT_KEY]: 0 }); 301 | } else { 302 | // Increment and store close count 303 | const newCloseCount = closeCount + 1; 304 | await chrome.storage.sync.set({ [POPUP_CLOSE_COUNT_KEY]: newCloseCount }); 305 | 306 | // If we've just hit the threshold, show the "don't show again" popup 307 | if (newCloseCount % POPUP_MIN_CLOSES_FOR_DONT_SHOW === 0) { 308 | this.activePopup = null; // Clear the active popup reference 309 | popup.remove(); 310 | this.showPopup(false); // Show the "don't show again" popup 311 | return; 312 | } 313 | } 314 | 315 | // Update last shown count and remove popup 316 | const currentCount = (await retrieveFromStorage(USAGE_COUNT_KEY)) || 0; 317 | await chrome.storage.sync.set({ [POPUP_LAST_SHOWN_KEY]: currentCount }); 318 | this.activePopup = null; // Clear the active popup reference 319 | popup.remove(); 320 | }); 321 | } 322 | 323 | // Add cleanup when popup is removed from DOM 324 | const observer = new MutationObserver((mutations) => { 325 | mutations.forEach((mutation) => { 326 | mutation.removedNodes.forEach((node) => { 327 | if (node === popup) { 328 | this.activePopup = null; 329 | observer.disconnect(); 330 | } 331 | }); 332 | }); 333 | }); 334 | 335 | if (popup.parentNode) { 336 | observer.observe(popup.parentNode, { childList: true }); 337 | } 338 | } 339 | 340 | async startRecording() { 341 | try { 342 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 343 | this.mediaRecorder = new MediaRecorder(stream); 344 | let chunks = []; 345 | this.mediaRecorder.addEventListener('dataavailable', (event) => chunks.push(event.data)); 346 | 347 | this.mediaRecorder.addEventListener('stop', async () => { 348 | this.setButtonState('loading'); 349 | const audioBlob = new Blob(chunks, { type: 'audio/webm' }); 350 | if (await this.downloadEnabled()) { 351 | downloadFile(audioBlob); 352 | } 353 | 354 | const storedToken = await this.retrieveToken(); 355 | const storedPrompt = await this.getSelectedPrompt(); 356 | const headers = new Headers({ 357 | Authorization: `Bearer ${storedToken}`, 358 | }); 359 | const formData = new FormData(); 360 | formData.append('file', audioBlob, 'recording.webm'); 361 | formData.append('model', 'whisper-1'); 362 | formData.append('prompt', storedPrompt.content); 363 | 364 | const requestOptions = { 365 | method: 'POST', 366 | headers, 367 | body: formData, 368 | redirect: 'follow', 369 | }; 370 | 371 | const requestUrl = (await this.translationEnabled()) ? TRANSLATION_URL : TRANSCRIPTION_URL; 372 | 373 | try { 374 | const response = await fetch(requestUrl, requestOptions); 375 | this.setButtonState('ready'); 376 | if (response.status === 200) { 377 | const result = await response.json(); 378 | this.insertTextResult(result.text); 379 | } else { 380 | const errorMessage = getErrorMessage(response.status); 381 | this.insertTextResult(errorMessage); 382 | } 383 | } catch (error) { 384 | this.insertTextResult('Network error! Please check your internet connection and try again.'); 385 | } finally { 386 | this.recording = false; 387 | stream.getTracks().forEach((track) => track.stop()); 388 | } 389 | 390 | await this.incrementUsageCount(); 391 | }); 392 | 393 | this.mediaRecorder.start(); 394 | this.setButtonState('recording'); 395 | this.recording = true; 396 | } catch (error) { 397 | console.error(error); 398 | } 399 | } 400 | 401 | stopRecording() { 402 | this.mediaRecorder.stop(); 403 | this.micButton.innerHTML = SVG_MIC_HTML; 404 | this.recording = false; 405 | } 406 | 407 | toggleRecording() { 408 | if (!this.recording) { 409 | this.startRecording(); 410 | } else { 411 | this.stopRecording(); 412 | } 413 | } 414 | 415 | insertTextResult(resultText) { 416 | const inputElement = this.textarea; 417 | 418 | // If this is a contenteditable div rather than a textarea 419 | if (inputElement.isContentEditable) { 420 | // Set focus to the input element 421 | inputElement.focus(); 422 | 423 | // Insert text at current cursor position or at the end 424 | const selection = window.getSelection(); 425 | const range = selection.getRangeAt(0); 426 | 427 | // Create a text node with the result 428 | const textNode = document.createTextNode(resultText); 429 | 430 | // Insert the text node 431 | range.insertNode(textNode); 432 | 433 | // Move cursor to end of inserted text 434 | range.setStartAfter(textNode); 435 | range.setEndAfter(textNode); 436 | selection.removeAllRanges(); 437 | selection.addRange(range); 438 | 439 | // Trigger an input event to notify ChatGPT that content has changed 440 | const inputEvent = new Event('input', { bubbles: true, cancelable: true }); 441 | inputElement.dispatchEvent(inputEvent); 442 | } else { 443 | // Original logic for standard textareas 444 | // Check if the input element is focused 445 | const isInputFocused = document.activeElement === inputElement; 446 | 447 | if (isInputFocused) { 448 | // If focused, insert at cursor position 449 | const selection = window.getSelection(); 450 | const range = selection.getRangeAt(0); 451 | 452 | // Create a new text node with the result text 453 | const textNode = document.createTextNode(resultText); 454 | 455 | // Insert the new text node at the current cursor position 456 | range.insertNode(textNode); 457 | 458 | // Move the cursor to the end of the inserted text 459 | range.setStartAfter(textNode); 460 | range.setEndAfter(textNode); 461 | selection.removeAllRanges(); 462 | selection.addRange(range); 463 | } else { 464 | // If not focused, append to the end 465 | const lastParagraph = inputElement.querySelector('p:last-child') || inputElement; 466 | 467 | // Create a new text node with the result text 468 | const textNode = document.createTextNode(resultText); 469 | 470 | // Append the new text node to the last paragraph 471 | lastParagraph.appendChild(textNode); 472 | 473 | // Move the cursor to the end of the appended text 474 | const range = document.createRange(); 475 | range.selectNodeContents(lastParagraph); 476 | range.collapse(false); 477 | const selection = window.getSelection(); 478 | selection.removeAllRanges(); 479 | selection.addRange(range); 480 | } 481 | 482 | // Trigger an input event to notify any listeners 483 | const inputEvent = new Event('input', { bubbles: true, cancelable: true }); 484 | inputElement.dispatchEvent(inputEvent); 485 | 486 | // Set focus to the input element 487 | inputElement.focus(); 488 | } 489 | } 490 | 491 | setButtonState(state) { 492 | const hoverClasses = ['hover:bg-gray-100', 'dark:hover:text-gray-400', 'dark:hover:bg-gray-900']; 493 | switch (state) { 494 | case 'recording': 495 | this.micButton.disabled = false; 496 | this.micButton.innerHTML = SVG_MIC_SPINNING_HTML; 497 | break; 498 | case 'loading': 499 | this.micButton.disabled = true; 500 | this.micButton.innerHTML = SVG_SPINNER_HTML; 501 | this.micButton.classList.remove(...hoverClasses); 502 | break; 503 | case 'ready': 504 | default: 505 | this.micButton.disabled = false; 506 | this.micButton.innerHTML = SVG_MIC_HTML; 507 | this.micButton.classList.add(...hoverClasses); 508 | break; 509 | } 510 | } 511 | } 512 | 513 | // First, let's create a singleton recorder instance 514 | let globalRecorder = null; 515 | 516 | function addMicrophoneButton(inputElement, inputType) { 517 | try { 518 | // Check if button already exists using the constant selector 519 | if (document.querySelector(MIC_BUTTON_SELECTOR)) { 520 | return; 521 | } 522 | 523 | // Find the parent container using the constant selector 524 | // This parent is crucial for correctly locating the buttonsArea 525 | const overallInputAndButtonContainer = inputElement.closest(PARENT_CONTAINER_SELECTOR); 526 | if (!overallInputAndButtonContainer) { 527 | return; 528 | } 529 | 530 | // Find the new buttons area within the overall container 531 | const buttonsArea = overallInputAndButtonContainer.querySelector(BUTTONS_AREA_SELECTOR); 532 | if (!buttonsArea) { 533 | return; 534 | } 535 | 536 | // Create or reuse the global recorder 537 | if (!globalRecorder) { 538 | globalRecorder = new AudioRecorder(); 539 | globalRecorder.textarea = inputElement; 540 | globalRecorder.listenForKeyboardShortcut(); 541 | } else { 542 | globalRecorder.textarea = inputElement; 543 | } 544 | 545 | // Create the microphone button 546 | globalRecorder.createMicButton(inputType, 'NON-PRO'); 547 | 548 | // Create the wrapper for the mic button and popup 549 | const micWrapper = document.createElement('div'); 550 | micWrapper.className = 'relative flex items-center'; 551 | 552 | // Create container for popup messages, positioned absolutely using inline styles 553 | const popupContainer = document.createElement('div'); 554 | popupContainer.className = 'whitespace-nowrap z-10'; 555 | popupContainer.style.position = 'absolute'; 556 | popupContainer.style.bottom = '0'; 557 | popupContainer.style.right = '100%'; 558 | popupContainer.style.marginRight = '0.5rem'; 559 | 560 | // Create the container for just the mic button 561 | const micContainer = document.createElement('div'); 562 | micContainer.className = 'min-w-9'; 563 | micContainer.appendChild(globalRecorder.micButton); 564 | 565 | // Append popup and mic containers to the wrapper 566 | micWrapper.appendChild(popupContainer); 567 | micWrapper.appendChild(micContainer); 568 | 569 | // Insert the complete wrapper into the buttons area 570 | buttonsArea.insertBefore(micWrapper, buttonsArea.firstChild); 571 | globalRecorder.popupContainer = popupContainer; 572 | } catch (error) { 573 | console.log('[Whisper to ChatGPT] Non-critical error in button addition:', error); 574 | } 575 | } 576 | 577 | // --- Updated Helper function for adding the button --- 578 | function tryAddButton() { 579 | try { 580 | if (document.querySelector(MIC_BUTTON_SELECTOR)) { 581 | return; 582 | } 583 | 584 | const inputElement = document.querySelector(INPUT_SELECTOR); 585 | if (inputElement) { 586 | const overallInputAndButtonContainer = inputElement.closest(PARENT_CONTAINER_SELECTOR); 587 | if (overallInputAndButtonContainer) { 588 | const buttonContainer = overallInputAndButtonContainer.querySelector(BUTTONS_AREA_SELECTOR); 589 | if (buttonContainer) { 590 | addMicrophoneButton(inputElement, 'main'); 591 | } 592 | } 593 | } 594 | } catch (error) { 595 | console.log('[Whisper to ChatGPT] Non-critical error in tryAddButton:', error); 596 | } 597 | } 598 | // --- End Updated Helper --- 599 | 600 | function observeDOM() { 601 | try { 602 | const targetNode = document.body; 603 | const config = { childList: true, subtree: true }; 604 | 605 | const callback = function (mutationsList, observer) { 606 | tryAddButton(); 607 | }; 608 | 609 | const observer = new MutationObserver(callback); 610 | observer.observe(targetNode, config); 611 | 612 | tryAddButton(); 613 | } catch (error) { 614 | logError('Failed to observe DOM', error); 615 | } 616 | } 617 | 618 | async function init() { 619 | try { 620 | if (TESTING) { 621 | chrome.storage.sync.clear(); 622 | } 623 | 624 | observeDOM(); 625 | document.addEventListener('click', (event) => { 626 | const target = event.target; 627 | if (target.closest(INPUT_SELECTOR)) { 628 | tryAddButton(); 629 | } 630 | }); 631 | 632 | console.log('[Whisper to ChatGPT] Extension initialized successfully'); 633 | } catch (error) { 634 | logError('Failed to initialize extension', error); 635 | } 636 | } 637 | 638 | function downloadFile(file) { 639 | // set a fileName containing the current date and time in readable format (e.g. `Recording 24.03.2023 13:00.webm` for German locale, but `Recording 03/24/2023 01:00 PM.webm` for English locale) 640 | const fileName = `Recording ${new Date().toLocaleString('en-US', { 641 | year: 'numeric', 642 | month: '2-digit', 643 | day: '2-digit', 644 | hour: '2-digit', 645 | minute: '2-digit', 646 | hour12: true, 647 | })}.webm`; 648 | 649 | // download file 650 | const a = document.createElement('a'); 651 | a.href = URL.createObjectURL(file); 652 | a.download = fileName; 653 | a.click(); 654 | } 655 | 656 | const getErrorMessage = (status) => { 657 | switch (status) { 658 | case 401: 659 | return 'Authentication error! Please check if your OpenAI API key is valid in the extension settings.'; 660 | case 429: 661 | return 'Too many requests to OpenAI server. Please wait a moment and try again.'; 662 | case 400: 663 | return 'Bad request! The audio file may be too large or in an unsupported format.'; 664 | case 500: 665 | return 'OpenAI server error. Please try again later.'; 666 | case 503: 667 | return 'OpenAI service is temporarily unavailable. Please try again later.'; 668 | default: 669 | return `Error ${status}: Unable to process audio. Please check your API key or try again later.`; 670 | } 671 | }; 672 | 673 | init(); 674 | --------------------------------------------------------------------------------