a(c,t)))break e;e[r]=c,e[s]=t,r=s}}}return n}function a(e,n){var t=e.sortIndex-n.sortIndex;return 0!==t?t:e.id-n.id}if("object"===typeof performance&&"function"===typeof performance.now){var o=performance;n.unstable_now=function(){return o.now()}}else{var u=Date,i=u.now();n.unstable_now=function(){return u.now()-i}}var s=[],c=[],f=1,d=null,p=3,m=!1,h=!1,v=!1,g="function"===typeof setTimeout?setTimeout:null,y="function"===typeof clearTimeout?clearTimeout:null,b="undefined"!==typeof setImmediate?setImmediate:null;function k(e){for(var n=r(c);null!==n;){if(null===n.callback)l(c);else{if(!(n.startTime<=e))break;l(c),n.sortIndex=n.expirationTime,t(s,n)}n=r(c)}}function w(e){if(v=!1,k(e),!h)if(null!==r(s))h=!0,R(S);else{var n=r(c);null!==n&&M(w,n.startTime-e)}}function S(e,t){h=!1,v&&(v=!1,y(_),_=-1),m=!0;var a=p;try{for(k(t),d=r(s);null!==d&&(!(d.expirationTime>t)||e&&!z());){var o=d.callback;if("function"===typeof o){d.callback=null,p=d.priorityLevel;var u=o(d.expirationTime<=t);t=n.unstable_now(),"function"===typeof u?d.callback=u:d===r(s)&&l(s),k(t)}else l(s);d=r(s)}if(null!==d)var i=!0;else{var f=r(c);null!==f&&M(w,f.startTime-t),i=!1}return i}finally{d=null,p=a,m=!1}}"undefined"!==typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var x,E=!1,C=null,_=-1,P=5,N=-1;function z(){return!(n.unstable_now()-Ne||125o?(e.sortIndex=a,t(c,e),null===r(s)&&e===r(c)&&(v?(y(_),_=-1):v=!0,M(w,a-o))):(e.sortIndex=u,t(s,e),h||m||(h=!0,R(S))),e},n.unstable_shouldYield=z,n.unstable_wrapCallback=function(e){var n=p;return function(){var t=p;p=n;try{return e.apply(this,arguments)}finally{p=t}}}},296:function(e,n,t){e.exports=t(813)}},n={};function t(r){var l=n[r];if(void 0!==l)return l.exports;var a=n[r]={exports:{}};return e[r](a,a.exports,t),a.exports}!function(){var e=t(250);function n(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function r(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function l(e){for(var t=1;te.length)&&(n=e.length);for(var t=0,r=new Array(n);t0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
--------------------------------------------------------------------------------
/public/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 |
--------------------------------------------------------------------------------
/public/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 | }
--------------------------------------------------------------------------------
/public/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 |
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danimelchor/gpt3-email/07ada504e573ec16e5890dcc91618c44a3ccb34f/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/popup-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danimelchor/gpt3-email/07ada504e573ec16e5890dcc91618c44a3ccb34f/public/images/popup-logo.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/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 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import Input from "components/Input";
2 | import Select from "components/Select";
3 | import { useEffect } from "react";
4 | import { useState } from "react";
5 | import "styles/main.css";
6 |
7 | import Slider from "./components/Slider";
8 |
9 | type ConfigState = {
10 | apiKey: string;
11 | model: string;
12 | temperature: number;
13 | maxTokens: number;
14 | topP: number;
15 | frequencyPenalty: number;
16 | presencePenalty: number;
17 | };
18 |
19 | function App() {
20 | const [models, setModels] = useState(["text-davinci-002"]);
21 | const [loggedIn, setLoggedIn] = useState(true);
22 | const [loaded, setLoaded] = useState(false);
23 | const [config, setConfig] = useState({
24 | apiKey: "",
25 | model: "text-davinci-002",
26 | temperature: 0.7,
27 | maxTokens: 256,
28 | topP: 1,
29 | frequencyPenalty: 0.0,
30 | presencePenalty: 0.0,
31 | });
32 |
33 | const updateConfig = (key: keyof ConfigState, value: any) => {
34 | setConfig((prev) => ({ ...prev, [key]: value }));
35 |
36 | // @ts-ignore
37 | chrome.storage.sync.set({ [key]: value });
38 | };
39 |
40 | useEffect(() => {
41 | // Load all available models from GPT-3 API
42 | if (loaded) {
43 | fetch("https://api.openai.com/v1/models", {
44 | headers: {
45 | Authorization: `Bearer ${config.apiKey}`,
46 | },
47 | })
48 | .then((res) => res.json())
49 | .then((res) => {
50 | if (res.error) {
51 | setLoggedIn(false);
52 | return;
53 | }
54 | const model_ids = res.data.map((a: any) => a.id);
55 | const sorted = model_ids.sort();
56 | setModels(sorted);
57 | setLoggedIn(true);
58 | });
59 | }
60 | }, [config.apiKey, loaded]);
61 |
62 | useEffect(() => {
63 | // Load config from chrome storage
64 | // @ts-ignore
65 | chrome.storage.sync.get(
66 | [
67 | "apiKey",
68 | "model",
69 | "temperature",
70 | "maxTokens",
71 | "topP",
72 | "frequencyPenalty",
73 | "presencePenalty",
74 | ],
75 | (res: ConfigState) => {
76 | setConfig({
77 | apiKey: res.apiKey || "",
78 | model: res.model || "text-davinci-002",
79 | temperature: res.temperature || 0.7,
80 | maxTokens: res.maxTokens || 256,
81 | topP: res.topP || 1,
82 | frequencyPenalty: res.frequencyPenalty || 0.0,
83 | presencePenalty: res.presencePenalty || 0.0,
84 | });
85 | setLoaded(true);
86 | }
87 | );
88 | }, []);
89 |
90 | return (
91 |
92 |
GPT3-Email Config
93 | updateConfig("apiKey", n)}
96 | value={config.apiKey}
97 | isPassword={true}
98 | />
99 | {loggedIn && (
100 | <>
101 |
149 | );
150 | }
151 |
152 | export default App;
153 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import "styles/input.css";
2 |
3 | type PropTypes = {
4 | text: string;
5 | onChange: (value: string) => void;
6 | value: string;
7 | isPassword?: boolean;
8 | };
9 |
10 | const Input = ({ text, onChange, value, isPassword }: PropTypes) => {
11 | return (
12 |
13 |
14 | onChange(e.target.value)}
19 | />
20 |
21 | );
22 | };
23 |
24 | export default Input;
25 |
--------------------------------------------------------------------------------
/src/components/Select.tsx:
--------------------------------------------------------------------------------
1 | import "styles/select.css";
2 |
3 | type PropTypes = {
4 | text: string;
5 | onChange: (value: string) => void;
6 | value: string;
7 | options: string[];
8 | };
9 |
10 | const Input = ({ text, onChange, value, options }: PropTypes) => {
11 | return (
12 |
13 |
14 |
23 |
24 | );
25 | };
26 |
27 | export default Input;
28 |
--------------------------------------------------------------------------------
/src/components/Slider.tsx:
--------------------------------------------------------------------------------
1 | import "styles/slider.css";
2 |
3 | type PropTypes = {
4 | text: string;
5 | onChange: (value: number) => void;
6 | min: number;
7 | max: number;
8 | value: number;
9 | step?: number;
10 | };
11 |
12 | const Slider = ({ text, onChange, min, max, value, step }: PropTypes) => {
13 | return (
14 |
36 | );
37 | };
38 |
39 | export default Slider;
40 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App";
3 |
4 | const root = ReactDOM.createRoot(
5 | document.getElementById("root") as HTMLElement
6 | );
7 | root.render();
8 |
--------------------------------------------------------------------------------
/src/styles/input.css:
--------------------------------------------------------------------------------
1 | .input-label {
2 | font-size: 14px;
3 | width: 100%;
4 | font-family: Helvetica, sans-serif;
5 | color: #353740;
6 | }
7 |
8 | .input-input {
9 | padding: 7px 14px;
10 | border: 1px solid #d9d9e3;
11 | border-radius: 2px;
12 | transition: border-color 100ms;
13 | box-sizing: border-box;
14 | width: 100%;
15 | }
16 |
17 | .input-input:focus {
18 | padding: 6px 13px;
19 | border-color: #10a37f;
20 | border-width: 2px;
21 | outline: none;
22 | }
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | width: 300px;
5 | }
6 |
7 | #main {
8 | width: 100%;
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | padding: 40px;
13 | gap: 20px;
14 | box-sizing: border-box;
15 | }
16 |
17 | .setting {
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | flex-direction: column;
22 | width: 100%;
23 | gap: 10px;
24 | }
25 |
26 | h1 {
27 | font-size: 22px;
28 | font-family: Helvetica, sans-serif;
29 | color: #10a37f;
30 | font-weight: bold;
31 | width: 100%;
32 | text-align: center;
33 | margin: 0;
34 | padding: 0;
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/select.css:
--------------------------------------------------------------------------------
1 | .select-label {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | font-size: 14px;
6 | width: 100%;
7 | font-family: Helvetica, sans-serif;
8 | color: #353740;
9 | }
10 |
11 | .select-select {
12 | padding: 7px 14px;
13 | border: 1px solid #d9d9e3;
14 | border-radius: 2px;
15 | transition: border-color 100ms;
16 | box-sizing: border-box;
17 | width: 100%;
18 | }
19 |
20 | .select-select:focus {
21 | padding: 6px 13px;
22 | border-color: #10a37f;
23 | border-width: 2px;
24 | outline: none;
25 | }
--------------------------------------------------------------------------------
/src/styles/slider.css:
--------------------------------------------------------------------------------
1 | .slider-label {
2 | display: flex;
3 | justify-content: space-between;
4 | align-items: center;
5 | font-size: 14px;
6 | width: 100%;
7 | font-family: Helvetica, sans-serif;
8 | color: #353740;
9 | }
10 |
11 | .slider-input {
12 | padding: 3px 5px;
13 | border: 1px solid transparent;
14 | border-radius: 2px;
15 | transition: border-color 100ms;
16 | box-sizing: border-box;
17 | width: 46px;
18 | text-align: right;
19 | }
20 |
21 | .slider-input:hover {
22 | border-color:#d9d9e3;
23 | }
24 |
25 | .slider-input:focus {
26 | padding: 2px 4px;
27 | border-color: #10a37f;
28 | border-width: 2px;
29 | outline: none;
30 | }
31 |
32 | .slider-range {
33 | -webkit-appearance: none;
34 | appearance: none;
35 | width: 100%;
36 | height: 4px;
37 | background: #c5c5d2;
38 | outline: none;
39 | opacity: 0.7;
40 | -webkit-transition: 0.2s;
41 | transition: opacity 0.2s;
42 | border-radius: 100px;
43 | }
44 |
45 | .slider-range:hover {
46 | opacity: 1;
47 | }
48 |
49 | .slider-range::-moz-range-thumb {
50 | width: 14px;
51 | height: 14px;
52 | background: white;
53 | border: 2px solid #888;
54 | cursor: pointer;
55 | border-radius: 100px;
56 | transition: height 100ms, width 100ms;
57 | }
58 |
59 | .slider-range::-webkit-slider-thumb {
60 | -webkit-appearance: none;
61 | appearance: none;width: 14px;
62 | height: 14px;
63 | background: white;
64 | border: 2px solid #888;
65 | cursor: pointer;
66 | border-radius: 100px;
67 | transition: height 100ms, width 100ms;
68 | }
69 |
70 | .slider-range::-moz-range-thumb:hover {
71 | border-color: #10a37f;
72 | height: 18px;
73 | width: 18px;
74 | }
75 |
76 | .slider-range::-webkit-slider-thumb:hover {
77 | border-color: #10a37f;
78 | height: 18px;
79 | width: 18px;
80 | }
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "baseUrl": "src",
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------