├── .gitignore ├── README.md ├── build ├── asset-manifest.json ├── background.js ├── content.css ├── content.js ├── images │ ├── logo.png │ └── popup-logo.png ├── index.html ├── manifest.json └── static │ ├── css │ ├── main.0099fad3.css │ └── main.0099fad3.css.map │ └── js │ ├── main.6dc7b37c.js │ ├── main.6dc7b37c.js.LICENSE.txt │ └── main.6dc7b37c.js.map ├── package.json ├── public ├── background.js ├── content.css ├── content.js ├── images │ ├── logo.png │ └── popup-logo.png ├── index.html └── manifest.json ├── src ├── App.tsx ├── components │ ├── Input.tsx │ ├── Select.tsx │ └── Slider.tsx ├── index.tsx └── styles │ ├── input.css │ ├── main.css │ ├── select.css │ └── slider.css ├── tsconfig.json └── yarn.lock /.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 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPT-3 For Gmail 2 | 3 | ## Installation 4 | 5 | ### 1. Get your api key 6 | 7 | Head to [OpenAI's](https://openai.com/api/) website and obtain your API key. 8 | 9 | ### 2. Load the extension 10 | 11 | Once the setup script has finished running, you need to follow these steps: 12 | 13 | 1. (Optional) Re-build the extension with `yarn build` 14 | 15 | 2. Go to your browsers extension page (e.g. [chrome://extensions/](chrome://extensions/)) 16 | 17 | 3. Toggle to activate `Developer Mode` on top right 18 | 19 | 4. Click on the `load unpacked` button 20 | 21 | 5. Select the newly generated `build` folder 22 | 23 | 6. Good to go! 24 | 25 | ### 3. Paste your API key 26 | 27 | To start using the extension do the following: 28 | 29 | 1. Click on the extension's icon on the top right 30 | 31 | 2. Paste your key on the key field 32 | 33 | 3. Configure the extension as desired 34 | 35 | 4. Head to gmail and enjoy 36 | 37 | https://user-images.githubusercontent.com/24496843/194149908-acdf93c8-854f-472a-80ed-8cc61f86a05e.mov 38 | 39 | ## Usage 40 | 41 | ### Text generation 42 | 43 | https://user-images.githubusercontent.com/24496843/193957293-565973ae-cc94-4489-b673-7fd0653d42f7.mov 44 | 45 | ### Text insertion 46 | 47 | https://user-images.githubusercontent.com/24496843/193957303-927762c0-4af4-4684-ad35-c19216b5dea8.mov 48 | 49 | ## Donations 50 | 51 | If you enjoy my work, please donate [here](https://paypal.me/danimelchor) 52 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.0099fad3.css", 4 | "main.js": "/static/js/main.6dc7b37c.js", 5 | "index.html": "/index.html", 6 | "main.0099fad3.css.map": "/static/css/main.0099fad3.css.map", 7 | "main.6dc7b37c.js.map": "/static/js/main.6dc7b37c.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.0099fad3.css", 11 | "static/js/main.6dc7b37c.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /build/background.js: -------------------------------------------------------------------------------- 1 | const getConfig = async () => { 2 | const { 3 | apiKey, 4 | model, 5 | temperature, 6 | maxTokens, 7 | topP, 8 | frequencyPenalty, 9 | presencePenalty, 10 | } = await chrome.storage.sync.get([ 11 | "apiKey", 12 | "model", 13 | "temperature", 14 | "maxTokens", 15 | "topP", 16 | "frequencyPenalty", 17 | "presencePenalty", 18 | ]); 19 | 20 | return { 21 | apiKey: apiKey || "", 22 | model: model || "text-davinci-002", 23 | temperature: temperature || 0.7, 24 | maxTokens: maxTokens || 256, 25 | topP: topP || 1, 26 | frequencyPenalty: frequencyPenalty || 0, 27 | presencePenalty: presencePenalty || 0, 28 | }; 29 | }; 30 | 31 | const getNextTokens = async (prompt, suffix) => { 32 | const url = "https://api.openai.com/v1/completions"; 33 | 34 | // Get config from storage 35 | const { 36 | apiKey, 37 | model, 38 | temperature, 39 | maxTokens, 40 | topP, 41 | frequencyPenalty, 42 | presencePenalty, 43 | } = await getConfig(); 44 | 45 | // Create request body 46 | const data = { 47 | prompt: prompt, 48 | suffix: suffix || null, 49 | model: model, 50 | max_tokens: maxTokens, 51 | temperature: temperature, 52 | top_p: topP, 53 | frequency_penalty: frequencyPenalty, 54 | presence_penalty: presencePenalty, 55 | }; 56 | 57 | // Create headers 58 | const headers = { 59 | "Content-Type": "application/json", 60 | Authorization: "Bearer " + apiKey, 61 | }; 62 | 63 | // Make request 64 | const res = await fetch(url, { 65 | method: "POST", 66 | headers: headers, 67 | body: JSON.stringify(data), 68 | }); 69 | 70 | const json = await res.json(); 71 | 72 | if (json.error) { 73 | return { error: json.error }; 74 | } 75 | 76 | return { text: json.choices[0].text }; 77 | }; 78 | 79 | chrome.runtime.onMessage.addListener(async (request) => { 80 | if (request.text != null) { 81 | // Communicate with content script to get the current text 82 | const [prompt, suffix] = request.text; 83 | const nextTokens = await getNextTokens(prompt, suffix); 84 | 85 | // Communicate with content script to update the text 86 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 87 | chrome.tabs.sendMessage(tabs[0].id, { generate: nextTokens }); 88 | }); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /build/content.css: -------------------------------------------------------------------------------- 1 | .generate-button { 2 | background: #10a37f; 3 | border-radius: 100px; 4 | color: white; 5 | padding: 5px; 6 | border: none; 7 | cursor: pointer; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | position: absolute; 12 | transform: translateY(-100%); 13 | transition: all 100ms ease-in-out; 14 | } 15 | 16 | .generate-button:hover { 17 | background: #0f8e6c; 18 | box-shadow: 0 0 0px 3px #dedede; 19 | } 20 | 21 | .generate-button:focus { 22 | outline: none; 23 | } 24 | 25 | .generate-button img { 26 | width: 17px; 27 | height: 17px; 28 | pointer-events: none; 29 | } 30 | 31 | .generate-button-error { 32 | background: rgb(205, 86, 86); 33 | padding: 5px 10px; 34 | } 35 | 36 | .generate-button-error:hover { 37 | background: rgb(166, 67, 67); 38 | } 39 | 40 | .generate-button-loading .spinner { 41 | width: 13px; 42 | height: 13px; 43 | border: 3px solid white; 44 | border-left-color: transparent; 45 | border-right-color: transparent; 46 | border-radius: 100%; 47 | animation: rotate 1s linear infinite; 48 | } 49 | 50 | @keyframes rotate { 51 | 0% { 52 | transform: rotate(0deg); 53 | } 54 | 100% { 55 | transform: rotate(360deg); 56 | } 57 | } -------------------------------------------------------------------------------- /build/content.js: -------------------------------------------------------------------------------- 1 | var LAST_ACTIVE_EL = null; 2 | const SEP = "[insert]"; 3 | 4 | const extractText = () => { 5 | var txt = LAST_ACTIVE_EL.innerText; 6 | txt = txt.replace(/(\s)+/g, "$1"); 7 | txt = txt.trim(); 8 | txt = txt.split(SEP); 9 | return [txt[0], txt[1] || ""]; 10 | }; 11 | 12 | const insertText = (text) => { 13 | // Insert text as HTML 14 | const spl_text = text.split("\n"); 15 | var res = ""; 16 | 17 | for (const s of spl_text) { 18 | if (s == "") { 19 | res += "

"; 20 | } else { 21 | res += "
" + s + "
"; 22 | } 23 | } 24 | 25 | // Split text by separator 26 | var prev_txt = LAST_ACTIVE_EL.innerHTML; 27 | var spl_prev_txt = prev_txt.split(SEP); 28 | 29 | // Insert text 30 | const before = spl_prev_txt[0]; 31 | const after = spl_prev_txt[1] || ""; 32 | LAST_ACTIVE_EL.innerHTML = before + res + after; 33 | }; 34 | 35 | const createButton = async () => { 36 | // Create button 37 | const button = document.createElement("button"); 38 | button.style.top = LAST_ACTIVE_EL.offsetHeight + "px"; 39 | button.style.left = "10px"; 40 | button.style.zIndex = 1000; 41 | button.id = "generate-button"; 42 | button.classList.add("generate-button"); 43 | 44 | // Add image inside button 45 | const img = document.createElement("img"); 46 | img.src = chrome.runtime.getURL("images/logo.png"); 47 | img.style.pointerEvents = "none"; 48 | button.appendChild(img); 49 | 50 | // Add onclick event 51 | button.addEventListener("click", () => { 52 | const text = extractText(); 53 | LAST_ACTIVE_EL.focus(); 54 | setButtonLoading(); 55 | chrome.runtime.sendMessage({ text }); 56 | }); 57 | 58 | // Append button to parent of input 59 | LAST_ACTIVE_EL.parentNode.appendChild(button); 60 | }; 61 | 62 | const deleteButton = () => { 63 | const button = document.getElementById("generate-button"); 64 | if (button != null) button.remove(); 65 | }; 66 | 67 | const getAllEditable = () => { 68 | return document.querySelectorAll("div[contenteditable=true]"); 69 | }; 70 | 71 | const setButtonLoading = () => { 72 | const button = document.getElementById("generate-button"); 73 | button.innerHTML = "
"; 74 | 75 | // Remove all classes 76 | button.classList.remove("generate-button-error"); 77 | 78 | // add loading class to button 79 | button.classList.add("generate-button-loading"); 80 | }; 81 | 82 | const setButtonError = (err) => { 83 | const button = document.getElementById("generate-button"); 84 | console.log(err); 85 | button.innerHTML = err; 86 | 87 | // Remove all classes 88 | button.classList.remove("generate-button-loading"); 89 | 90 | // Add error class to button 91 | button.classList.add("generate-button-error"); 92 | }; 93 | 94 | const setButtonLoaded = () => { 95 | const button = document.getElementById("generate-button"); 96 | 97 | // Remove all classes 98 | button.classList.remove("generate-button-loading"); 99 | button.classList.remove("generate-button-error"); 100 | 101 | // Add image inside button 102 | const img = document.createElement("img"); 103 | img.src = chrome.runtime.getURL("images/logo.png"); 104 | button.innerHTML = ""; 105 | button.appendChild(img); 106 | }; 107 | 108 | const handleClick = (e) => { 109 | // If element is GPT-3 button, do nothing 110 | if (e.target.id == "generate-button") { 111 | return; 112 | } 113 | 114 | // If element is in editable parent, create button 115 | const editableDivs = getAllEditable(); 116 | for (const div of editableDivs) { 117 | if (div.contains(e.target)) { 118 | deleteButton(); 119 | LAST_ACTIVE_EL = div; 120 | createButton(); 121 | break; 122 | } 123 | } 124 | }; 125 | 126 | // Add event listeners 127 | document.body.addEventListener("click", handleClick); 128 | document.body.addEventListener("resize", deleteButton); 129 | document.body.addEventListener("scroll", deleteButton); 130 | 131 | // Listen for messages from the background script 132 | chrome.runtime.onMessage.addListener((request) => { 133 | if (request.generate) { 134 | if (request.generate.error) { 135 | setButtonError(request.generate.error.message); 136 | } else if (request.generate.text) { 137 | insertText(request.generate.text); 138 | setButtonLoaded(); 139 | } 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /build/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danimelchor/gpt3-email/07ada504e573ec16e5890dcc91618c44a3ccb34f/build/images/logo.png -------------------------------------------------------------------------------- /build/images/popup-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danimelchor/gpt3-email/07ada504e573ec16e5890dcc91618c44a3ccb34f/build/images/popup-logo.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Daniel Melchor", 3 | "version": "1.0.0", 4 | "description": "GPT-3 Autocomplete for GMail", 5 | "manifest_version": 3, 6 | "name": "GTP3Mail", 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "permissions": [ 11 | "tabs", 12 | "storage" 13 | ], 14 | "host_permissions": [ 15 | "https://mail.google.com/*" 16 | ], 17 | "content_scripts": [ 18 | { 19 | "matches": [ 20 | "https://mail.google.com/*" 21 | ], 22 | "css": [ 23 | "content.css" 24 | ], 25 | "js": [ 26 | "content.js" 27 | ] 28 | } 29 | ], 30 | "web_accessible_resources": [ 31 | { 32 | "resources": [ 33 | "images/logo.png" 34 | ], 35 | "matches": [ 36 | "https://mail.google.com/*" 37 | ] 38 | } 39 | ], 40 | "action": { 41 | "default_icon": { 42 | "16": "images/popup-logo.png", 43 | "24": "images/popup-logo.png", 44 | "32": "images/popup-logo.png" 45 | }, 46 | "default_popup": "index.html" 47 | }, 48 | "icons": { 49 | "16": "images/popup-logo.png", 50 | "32": "images/popup-logo.png", 51 | "48": "images/popup-logo.png", 52 | "128": "images/popup-logo.png" 53 | } 54 | } -------------------------------------------------------------------------------- /build/static/css/main.0099fad3.css: -------------------------------------------------------------------------------- 1 | .input-label{color:#353740;font-family:Helvetica,sans-serif;font-size:14px;width:100%}.input-input{border:1px solid #d9d9e3;border-radius:2px;box-sizing:border-box;padding:7px 14px;transition:border-color .1s;width:100%}.input-input:focus{border-color:#10a37f;border-width:2px;outline:none;padding:6px 13px}.select-label{align-items:center;color:#353740;display:flex;font-family:Helvetica,sans-serif;font-size:14px;justify-content:space-between;width:100%}.select-select{border:1px solid #d9d9e3;border-radius:2px;box-sizing:border-box;padding:7px 14px;transition:border-color .1s;width:100%}.select-select:focus{border-color:#10a37f;border-width:2px;outline:none;padding:6px 13px}body{margin:0;padding:0;width:300px}#main{box-sizing:border-box;gap:20px;padding:40px}#main,.setting{align-items:center;display:flex;flex-direction:column;width:100%}.setting{gap:10px;justify-content:center}h1{color:#10a37f;font-size:22px;font-weight:700;margin:0;padding:0;text-align:center}.slider-label,h1{font-family:Helvetica,sans-serif;width:100%}.slider-label{align-items:center;color:#353740;display:flex;font-size:14px;justify-content:space-between}.slider-input{border:1px solid transparent;border-radius:2px;box-sizing:border-box;padding:3px 5px;text-align:right;transition:border-color .1s;width:46px}.slider-input:hover{border-color:#d9d9e3}.slider-input:focus{border-color:#10a37f;border-width:2px;outline:none;padding:2px 4px}.slider-range{-webkit-appearance:none;appearance:none;background:#c5c5d2;border-radius:100px;height:4px;opacity:.7;outline:none;transition:opacity .2s;width:100%}.slider-range:hover{opacity:1}.slider-range::-moz-range-thumb{background:#fff;border:2px solid #888;border-radius:100px;cursor:pointer;height:14px;-moz-transition:height .1s,width .1s;transition:height .1s,width .1s;width:14px}.slider-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;border:2px solid #888;border-radius:100px;cursor:pointer;height:14px;-webkit-transition:height .1s,width .1s;transition:height .1s,width .1s;width:14px}.slider-range::-moz-range-thumb:hover{border-color:#10a37f;height:18px;width:18px}.slider-range::-webkit-slider-thumb:hover{border-color:#10a37f;height:18px;width:18px} 2 | /*# sourceMappingURL=main.0099fad3.css.map*/ -------------------------------------------------------------------------------- /build/static/css/main.0099fad3.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.0099fad3.css","mappings":"AAAA,aAII,aAAc,CADd,gCAAkC,CAFlC,cAAe,CACf,UAGJ,CAEA,aAEI,wBAAyB,CACzB,iBAAkB,CAElB,qBAAsB,CAJtB,gBAAiB,CAGjB,2BAA8B,CAE9B,UACJ,CAEA,mBAEI,oBAAqB,CACrB,gBAAiB,CACjB,YAAa,CAHb,gBAIJ,CCrBA,cAGI,kBAAmB,CAInB,aAAc,CANd,YAAa,CAKb,gCAAkC,CAFlC,cAAe,CAFf,6BAA8B,CAG9B,UAGJ,CAEA,eAEI,wBAAyB,CACzB,iBAAkB,CAElB,qBAAsB,CAJtB,gBAAiB,CAGjB,2BAA8B,CAE9B,UACJ,CAEA,qBAEI,oBAAqB,CACrB,gBAAiB,CACjB,YAAa,CAHb,gBAIJ,CCxBA,KAEI,QAAS,CADT,SAAU,CAEV,WACJ,CAEA,MAOI,qBAAsB,CADtB,QAAS,CADT,YAGJ,CAEA,eANI,kBAAmB,CAFnB,YAAa,CACb,qBAAsB,CAFtB,UAgBJ,CAPA,SAMI,QAAS,CAJT,sBAKJ,CAEA,GAGI,aAAc,CAFd,cAAe,CAGf,eAAiB,CAGjB,QAAS,CACT,SAAU,CAFV,iBAGJ,CClCA,iBD2BI,gCAAkC,CAGlC,UCtBJ,CARA,cAGI,kBAAmB,CAInB,aAAc,CANd,YAAa,CAGb,cAAe,CAFf,6BAMJ,CAEA,cAEI,4BAA6B,CAC7B,iBAAkB,CAElB,qBAAsB,CAJtB,eAAgB,CAMhB,gBAAiB,CAHjB,2BAA8B,CAE9B,UAEJ,CAEA,oBACI,oBACJ,CAEA,oBAEI,oBAAqB,CACrB,gBAAiB,CACjB,YAAa,CAHb,eAIJ,CAEA,cACI,uBAAwB,CACxB,eAAgB,CAGhB,kBAAmB,CAKnB,mBAAoB,CANpB,UAAW,CAGX,UAAY,CADZ,YAAa,CAGb,sBAAwB,CANxB,UAQJ,CAEA,oBACI,SACJ,CAEA,gCAGI,eAAiB,CACjB,qBAAsB,CAEtB,mBAAoB,CADpB,cAAe,CAHf,WAAY,CAKZ,oCAAqC,CAArC,+BAAqC,CANrC,UAOJ,CAEA,oCACI,uBAAwB,CACxB,eAAgB,CAEhB,eAAiB,CACjB,qBAAsB,CAEtB,mBAAoB,CADpB,cAAe,CAHf,WAAY,CAKZ,uCAAqC,CAArC,+BAAqC,CANpB,UAOrB,CAEA,sCACI,oBAAqB,CACrB,WAAY,CACZ,UACJ,CAEA,0CACI,oBAAqB,CACrB,WAAY,CACZ,UACJ","sources":["styles/input.css","styles/select.css","styles/main.css","styles/slider.css"],"sourcesContent":[".input-label {\n font-size: 14px;\n width: 100%;\n font-family: Helvetica, sans-serif;\n color: #353740;\n}\n\n.input-input {\n padding: 7px 14px;\n border: 1px solid #d9d9e3;\n border-radius: 2px;\n transition: border-color 100ms;\n box-sizing: border-box;\n width: 100%;\n}\n\n.input-input:focus {\n padding: 6px 13px;\n border-color: #10a37f;\n border-width: 2px;\n outline: none;\n}",".select-label {\n display: flex;\n justify-content: space-between;\n align-items: center;\n font-size: 14px;\n width: 100%;\n font-family: Helvetica, sans-serif;\n color: #353740;\n}\n\n.select-select {\n padding: 7px 14px;\n border: 1px solid #d9d9e3;\n border-radius: 2px;\n transition: border-color 100ms;\n box-sizing: border-box;\n width: 100%;\n}\n\n.select-select:focus {\n padding: 6px 13px;\n border-color: #10a37f;\n border-width: 2px;\n outline: none;\n}","body {\n padding: 0;\n margin: 0;\n width: 300px;\n}\n\n#main {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 40px;\n gap: 20px;\n box-sizing: border-box;\n}\n\n.setting {\n display: flex;\n justify-content: center;\n align-items: center;\n flex-direction: column;\n width: 100%;\n gap: 10px;\n}\n\nh1 {\n font-size: 22px;\n font-family: Helvetica, sans-serif;\n color: #10a37f;\n font-weight: bold;\n width: 100%;\n text-align: center;\n margin: 0;\n padding: 0;\n}\n",".slider-label {\n display: flex;\n justify-content: space-between;\n align-items: center;\n font-size: 14px;\n width: 100%;\n font-family: Helvetica, sans-serif;\n color: #353740;\n}\n\n.slider-input {\n padding: 3px 5px;\n border: 1px solid transparent;\n border-radius: 2px;\n transition: border-color 100ms;\n box-sizing: border-box;\n width: 46px;\n text-align: right;\n}\n\n.slider-input:hover {\n border-color:#d9d9e3;\n}\n\n.slider-input:focus {\n padding: 2px 4px;\n border-color: #10a37f;\n border-width: 2px;\n outline: none;\n}\n\n.slider-range {\n -webkit-appearance: none;\n appearance: none;\n width: 100%;\n height: 4px;\n background: #c5c5d2;\n outline: none;\n opacity: 0.7;\n -webkit-transition: 0.2s;\n transition: opacity 0.2s;\n border-radius: 100px;\n}\n\n.slider-range:hover {\n opacity: 1;\n}\n\n.slider-range::-moz-range-thumb {\n width: 14px;\n height: 14px;\n background: white;\n border: 2px solid #888;\n cursor: pointer;\n border-radius: 100px;\n transition: height 100ms, width 100ms;\n}\n\n.slider-range::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;width: 14px;\n height: 14px;\n background: white;\n border: 2px solid #888;\n cursor: pointer;\n border-radius: 100px;\n transition: height 100ms, width 100ms;\n}\n\n.slider-range::-moz-range-thumb:hover {\n border-color: #10a37f;\n height: 18px;\n width: 18px;\n}\n\n.slider-range::-webkit-slider-thumb:hover {\n border-color: #10a37f;\n height: 18px;\n width: 18px;\n}\n\n\n\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /build/static/js/main.6dc7b37c.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see main.6dc7b37c.js.LICENSE.txt */ 2 | !function(){"use strict";var e={463:function(e,n,t){var r=t(791),l=t(296);function a(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,t=1;t