├── .github └── workflows │ └── submit.yml ├── .gitignore ├── .prettierrc.cjs ├── README.md ├── assets ├── icon.png └── inter.woff2 ├── package.json ├── pnpm-lock.yaml ├── src ├── background.ts ├── background │ └── messages │ │ └── completion.ts ├── contents │ ├── font.css │ ├── plasmo-overlay.css │ ├── plasmo-overlay.tsx │ └── plasmo.ts ├── lib │ ├── linkedin.ts │ └── openai.ts ├── newtab.tsx ├── options.tsx ├── parseJSONSSE.ts ├── popup.tsx └── style.css └── tsconfig.json /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build and zip extension artifact 27 | run: pnpm package 28 | - name: Browser Platform Publish 29 | uses: PlasmoHQ/bpp@v3 30 | with: 31 | keys: ${{ secrets.SUBMIT_KEYS }} 32 | artifact: build/chrome-mv3-prod.zip 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | #cache 13 | .turbo 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | 25 | # local env files 26 | .env* 27 | 28 | out/ 29 | build/ 30 | dist/ 31 | 32 | # plasmo - https://www.plasmo.com 33 | .plasmo 34 | 35 | # bpp - http://bpp.browser.market/ 36 | keys.json 37 | 38 | # typescript 39 | .tsbuildinfo 40 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | module.exports = { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: [require.resolve("@plasmohq/prettier-plugin-sort-imports")], 14 | importOrder: ["^@plasmohq/(.*)$", "^~(.*)$", "^[./]"], 15 | importOrderSeparation: true, 16 | importOrderSortSpecifiers: true 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOTCHAS 2 | 3 | if you update the content script, you might need to refresh the extension in the chrome 'manage extensions' page 4 | 5 | # Environment 6 | 7 | add the following to `.env.local` 8 | 9 | ``` 10 | OPENAI_API_KEY= 11 | ``` 12 | 13 | This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo). 14 | 15 | ## Getting Started 16 | 17 | First, run the development server: 18 | 19 | ```bash 20 | pnpm dev 21 | # or 22 | npm run dev 23 | ``` 24 | 25 | Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`. 26 | 27 | You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser. 28 | 29 | For further guidance, [visit our Documentation](https://docs.plasmo.com/) 30 | 31 | ## Making production build 32 | 33 | Run the following: 34 | 35 | ```bash 36 | pnpm build 37 | # or 38 | npm run build 39 | ``` 40 | 41 | This should create a production bundle for your extension, ready to be zipped and published to the stores. 42 | 43 | ## Submit to the webstores 44 | 45 | The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission! 46 | 47 | ### Local Testing 48 | 49 | To test the Chrome extension locally: 50 | 51 | - Open chrome://extensions/ 52 | - Click "Load unpacked" 53 | - Navigate to this repo's copy 54 | - Click "Select" 55 | - Navigate to the Chrome Plugins 56 | - Open Gpt Plasmo starter 57 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/promptable/gpt3-chrome-starter/92d3c49b42538c4bc8d51fcbd846969f9169911b/assets/icon.png -------------------------------------------------------------------------------- /assets/inter.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/promptable/gpt3-chrome-starter/92d3c49b42538c4bc8d51fcbd846969f9169911b/assets/inter.woff2 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpt3-plasmo-starter", 3 | "displayName": "Gpt3 plasmo starter", 4 | "version": "0.0.0", 5 | "description": "A basic Plasmo extension.", 6 | "author": "bfortuner", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "package": "plasmo package" 11 | }, 12 | "dependencies": { 13 | "@plasmohq/messaging": "^0.0.2", 14 | "axios": "^1.2.2", 15 | "copy-to-clipboard": "^3.3.3", 16 | "plasmo": "0.62.2", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@plasmohq/prettier-plugin-sort-imports": "3.6.1", 22 | "@types/chrome": "0.0.208", 23 | "@types/node": "18.11.18", 24 | "@types/react": "18.0.26", 25 | "@types/react-dom": "18.0.10", 26 | "prettier": "2.8.2", 27 | "typescript": "4.9.4" 28 | }, 29 | "manifest": { 30 | "host_permissions": [ 31 | "https://*/*", 32 | "https://api.openai.com/v1/completions" 33 | ], 34 | "permissions": [ 35 | "api.openai.com", 36 | "activeTab", 37 | "tabs", 38 | "storage", 39 | "clipboardWrite" 40 | ], 41 | "browser_action": { 42 | "default_popup": "popup.html" 43 | }, 44 | "commands": { 45 | "_execute_browser_action": { 46 | "suggested_key": { 47 | "default": "Ctrl+Shift+E", 48 | "linux": "Ctrl+Shift+K", 49 | "windows": "Alt+Shift+P", 50 | "mac": "Alt+Shift+P" 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { parseJsonSSE } from "~parseJSONSSE" 2 | 3 | // First, check to see if an OpenAI API key exists and if it is valid 4 | chrome.runtime.onInstalled.addListener((reason) => { 5 | // Set default preferences 6 | chrome.storage.sync.set({ 7 | API_KEY: process.env.OPENAI_API_KEY 8 | }) 9 | }) 10 | 11 | chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { 12 | // First get the API key from storage 13 | console.log("Received message", message) 14 | if (message.type !== "basic_completion") { 15 | return 16 | } 17 | console.log("Received basic completion request") 18 | chrome.storage.sync.get(["API_KEY"], function (result) { 19 | const data = { 20 | model: "text-davinci-003", 21 | prompt: message["prompt"], 22 | max_tokens: 128, 23 | temperature: 0.7 24 | } 25 | 26 | fetch("https://api.openai.com/v1/completions", { 27 | method: "POST", 28 | headers: { 29 | "Content-Type": "application/json", 30 | Accept: "application/json", 31 | Authorization: "Bearer " + result.API_KEY 32 | }, 33 | body: JSON.stringify(data) 34 | }) 35 | .then((response) => response.json()) 36 | .then((data) => { 37 | sendResponse(data) 38 | }) 39 | .catch((error) => { 40 | console.error(error) 41 | sendResponse({ error: error }) 42 | }) 43 | }) 44 | return true 45 | }) 46 | 47 | chrome.runtime.onConnect.addListener(function (port) { 48 | // console.assert(port.name === "completion") 49 | port.onMessage.addListener(async (msg) => { 50 | if (msg.type !== "completion") { 51 | return 52 | } 53 | 54 | const payload = { 55 | prompt: msg.prompt, 56 | model: "text-davinci-003", 57 | max_tokens: 128, 58 | temperature: 0.7, 59 | stream: true 60 | } 61 | 62 | const res = await fetch("https://api.openai.com/v1/completions", { 63 | headers: { 64 | "Content-Type": "application/json", 65 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}` 66 | }, 67 | method: "POST", 68 | body: JSON.stringify(payload) 69 | }) 70 | 71 | parseJsonSSE({ 72 | data: res.body, 73 | onParse: (obj) => { 74 | port.postMessage({ 75 | //@ts-ignore 76 | completion: obj.choices?.[0].text 77 | }) 78 | }, 79 | onFinish: () => {} 80 | }) 81 | }) 82 | }) 83 | 84 | export {} 85 | -------------------------------------------------------------------------------- /src/background/messages/completion.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoMessaging } from "@plasmohq/messaging" 2 | 3 | import { runCompletion, streamCompletion } from "~lib/openai" 4 | 5 | export const handler: PlasmoMessaging.MessageHandler = async (req, res) => { 6 | const { prompt, config, options } = req.body 7 | 8 | let completion 9 | if (options.stream) { 10 | const onMessage = (completion) => { 11 | res.send({ 12 | event: "message", 13 | completion 14 | }) 15 | } 16 | const onError = (err: string) => {} 17 | const onClose = () => {} 18 | 19 | streamCompletion({ data: { prompt, config }, onMessage, onError, onClose }) 20 | } else { 21 | completion = await runCompletion({ 22 | prompt, 23 | config 24 | }) 25 | 26 | res.send({ 27 | event: "message", 28 | completion 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/contents/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "inter"; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url(data-base64:~assets/inter.woff2) format("woff2"); 7 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 8 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 9 | U+FEFF, U+FFFD; 10 | } -------------------------------------------------------------------------------- /src/contents/plasmo-overlay.css: -------------------------------------------------------------------------------- 1 | .hw-top { 2 | background: white; 3 | color: black; 4 | box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.4); 5 | } 6 | 7 | #plasmo-shadow-container { 8 | width: 200px; 9 | background-color: purple; 10 | } 11 | 12 | #plasmo-mount-container { 13 | border: 8px solid aqua; 14 | } 15 | -------------------------------------------------------------------------------- /src/contents/plasmo-overlay.tsx: -------------------------------------------------------------------------------- 1 | import copy from "copy-to-clipboard" 2 | import cssText from "data-text:~/contents/plasmo-overlay.css" 3 | import type { PlasmoContentScript, PlasmoWatchOverlayAnchor } from "plasmo" 4 | import { useCallback, useEffect, useRef, useState } from "react" 5 | 6 | import { runCompletion, streamCompletion } from "~lib/openai" 7 | 8 | export const config: PlasmoContentScript = { 9 | matches: ["https://*/*"] 10 | // font: ["font.css"] 11 | } 12 | 13 | export const getStyle = () => { 14 | const style = document.createElement("style") 15 | style.textContent = cssText 16 | return style 17 | } 18 | 19 | export const watchOverlayAnchor = (updatePosition) => { 20 | setInterval(() => { 21 | updatePosition() 22 | }, 100) 23 | } 24 | 25 | const PlasmoOverlay = () => { 26 | const [show, setShow] = useState(false) 27 | const [completion, setCompletion] = useState("") 28 | const [prompt, setPrompt] = useState("") 29 | const [stream, setStream] = useState(true) 30 | const [loading, setLoading] = useState(false) 31 | const wrapperRef = useRef(null) 32 | const [context, setContext] = useState("") 33 | 34 | const handleClickAway = () => { 35 | setShow(false) 36 | } 37 | 38 | const handlePromptChange = (e) => { 39 | setPrompt(e.target.value) 40 | } 41 | 42 | const handleCopy = () => { 43 | copy(completion) 44 | } 45 | 46 | const handleMessage = useCallback( 47 | (msg) => { 48 | handleScroll() 49 | setCompletion((p) => p + msg) 50 | }, 51 | [setCompletion] 52 | ) 53 | 54 | /** 55 | * Close the overlay when you click outside of it 56 | */ 57 | useEffect(() => { 58 | /** 59 | * Alert if clicked on outside of element 60 | */ 61 | const handleClickOutside = (event) => { 62 | // NOTE: this is because the event.target === shadow dom. 63 | if (event.target.tagName !== "PLASMO-CSUI") { 64 | handleClickAway() 65 | } 66 | } 67 | // Bind the event listener 68 | document.addEventListener("mousedown", handleClickOutside) 69 | return () => { 70 | // Unbind the event listener on clean up 71 | document.removeEventListener("mousedown", handleClickOutside) 72 | } 73 | }, []) 74 | 75 | /** 76 | * Keybindings 77 | */ 78 | useEffect(() => { 79 | const handleToggleOverlay = (e) => { 80 | if (e.key === "." && e.metaKey) { 81 | e.preventDefault() 82 | setContext(document.getSelection().toString()) 83 | setShow((p) => !p) 84 | } 85 | 86 | if (show && !loading && e.key === "c" && e.metaKey) { 87 | e.preventDefault() 88 | copy(completion) 89 | } 90 | 91 | if (e.key === "Escape") { 92 | e.preventDefault() 93 | setShow(false) 94 | } 95 | 96 | if (show && !loading && e.key === "Enter") { 97 | e.preventDefault() 98 | handleRun(prompt) 99 | } 100 | } 101 | 102 | addEventListener("keydown", handleToggleOverlay) 103 | 104 | return () => { 105 | removeEventListener("keydown", handleToggleOverlay) 106 | } 107 | }, [prompt, show, loading]) 108 | 109 | const handleRun = async (p: string) => { 110 | if (loading) { 111 | return 112 | } 113 | 114 | const reqPrompt = context.length ? context + "\n\n" + p : p 115 | 116 | setLoading(true) 117 | setCompletion("") 118 | if (!stream) { 119 | const res = await runCompletion({ 120 | prompt: reqPrompt, 121 | config: { 122 | model: "text-davinci-003" 123 | } 124 | }) 125 | setCompletion(res) 126 | setLoading(false) 127 | } else { 128 | streamCompletion({ 129 | data: { 130 | prompt: reqPrompt, 131 | config: { 132 | model: "text-davinci-003" 133 | } 134 | }, 135 | onMessage: handleMessage, 136 | onError: (e) => { 137 | console.error(e) 138 | setLoading(false) 139 | }, 140 | onClose: () => { 141 | setLoading(false) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | const ref = useRef(null) 148 | 149 | const handleScroll = useCallback(() => { 150 | if (ref.current) { 151 | ref.current.scroll({ 152 | behavior: "auto", 153 | top: ref.current.scrollHeight 154 | }) 155 | } 156 | }, []) 157 | 158 | return show ? ( 159 |
176 | 187 | {!!context.length && ( 188 | <> 189 |
196 | Your selected context: 197 |
198 |
204 |

209 | {context} 210 |

211 |
212 | 213 | )} 214 |
221 |
226 | Output 227 |
228 |
233 | 251 | 273 |
274 |
275 |
284 | {completion} 285 |
286 |
287 | 290 |
291 |
292 | ) : null 293 | } 294 | 295 | export default PlasmoOverlay 296 | -------------------------------------------------------------------------------- /src/contents/plasmo.ts: -------------------------------------------------------------------------------- 1 | import copy from "copy-to-clipboard" 2 | import type { PlasmoContentScript } from "plasmo" 3 | 4 | import { sendToBackground } from "@plasmohq/messaging" 5 | 6 | //@ts-ignore 7 | window.AbortController = AbortController 8 | 9 | //@ts-ignore 10 | window.fetch = fetch 11 | 12 | export const config: PlasmoContentScript = { 13 | matches: [""] 14 | } 15 | 16 | function insertTextAtCaret(text) { 17 | let range: Range 18 | let sel: Selection 19 | if (window.getSelection) { 20 | console.log("get select") 21 | sel = window.getSelection() 22 | if (sel.getRangeAt && sel.rangeCount) { 23 | console.log("get range at") 24 | range = sel.getRangeAt(0) 25 | const prevText = sel.anchorNode.textContent 26 | console.log("insert text at caret") 27 | range.insertNode(document.createTextNode(text)) 28 | } 29 | //@ts-ignore 30 | } else if (document.selection && document.selection.createRange) { 31 | console.log("create range") 32 | //@ts-ignore 33 | document.selection.createRange().text = text 34 | } 35 | } 36 | 37 | const handleCompletionMessage = (msg: any) => { 38 | console.log("Handling completion message") 39 | updateDOMWithCompletion(msg.completion) 40 | } 41 | 42 | var port = chrome.runtime.connect({ name: "completion" }) 43 | port.onMessage.addListener(handleCompletionMessage) 44 | 45 | const streamCompletion = (prompt: string) => { 46 | port.postMessage({ type: "completion", prompt }) 47 | } 48 | 49 | const showLoadingCursor = () => { 50 | const style = document.createElement("style") 51 | style.id = "cursor_wait" 52 | style.innerHTML = `* {cursor: wait;}` 53 | document.head.insertBefore(style, null) 54 | } 55 | 56 | const restoreCursor = () => { 57 | document.getElementById("cursor_wait").remove() 58 | } 59 | 60 | const wait = (timeout) => { 61 | return new Promise((res) => setTimeout(() => res(null), timeout)) 62 | } 63 | 64 | export const updateDOMWithCompletion = async (text) => { 65 | console.log("updating DOM with completion text: '", text, "'") 66 | 67 | let activeElement = document.activeElement 68 | //@ts-ignore 69 | // activeElement.select() 70 | // document.execCommand("paste") 71 | 72 | if ( 73 | activeElement instanceof HTMLInputElement || 74 | activeElement instanceof HTMLTextAreaElement 75 | ) { 76 | // activeElement.select() 77 | // Use the value property for input and textarea elements 78 | console.log("TextArea - Existing text: ", activeElement.value) 79 | // Insert after selection 80 | activeElement.value = 81 | activeElement.value.slice(0, activeElement.selectionEnd) + 82 | text + 83 | activeElement.value.slice( 84 | activeElement.selectionEnd, 85 | //@ts-ignore 86 | activeElement.length 87 | ) 88 | } else if (activeElement.hasAttribute("contenteditable")) { 89 | //@ts-ignore 90 | activeElement.focus() 91 | 92 | const existingText = 93 | document.getSelection().toString().trim() || 94 | activeElement.textContent.trim() 95 | console.log("ContentEditable - Existing text: ", existingText) 96 | 97 | await wait(1) 98 | 99 | // const displayText = existingText.concat(text) 100 | // console.log("ContentEditable - Display text: ", displayText) 101 | // try { 102 | // document.execCommand("selectAll") 103 | // } catch (e) {} 104 | // try { 105 | // document.execCommand("insertHTML", false, displayText) 106 | // } catch (e) {} 107 | 108 | try { 109 | document.execCommand("insertHTML", false, text) 110 | } catch (e) {} 111 | 112 | // CODE BELOW: 113 | // This works sometimes, but less reliably than "insertHTML" 114 | 115 | // document.execCommand('insertHTML', false, text); 116 | // // Special handling for contenteditable 117 | // const replyNode = document.createTextNode(text) 118 | // let selection = window.getSelection() 119 | 120 | // if (selection.rangeCount === 0) { 121 | // selection.addRange(document.createRange()) 122 | // //@ts-ignore 123 | // selection.getRangeAt(0).collapse(activeElement, 1) 124 | // } 125 | 126 | // const range = selection.getRangeAt(0) 127 | // range.collapse(false) 128 | 129 | // // Insert reply 130 | // range.insertNode(replyNode) 131 | 132 | // selection = document.getSelection() 133 | // // Move the cursor to the end 134 | // selection.collapse(replyNode, replyNode.length) 135 | } 136 | // restoreCursor() 137 | } 138 | 139 | const completeText = async (prompt, completionType) => { 140 | if (completionType === "completion") { 141 | streamCompletion(prompt) 142 | } else { 143 | showLoadingCursor() 144 | try { 145 | console.log("Running basic completion") 146 | chrome.runtime.sendMessage( 147 | { 148 | type: "basic_completion", 149 | prompt: prompt 150 | }, 151 | function (response) { 152 | if (response["error"]) { 153 | console.log(response["error"]) 154 | // mediumStatusUpdate("error"); 155 | restoreCursor() 156 | } else { 157 | console.log("Response received successfully") 158 | const completionText = response.choices[0].text 159 | console.log("Basic Completion text: ", completionText) 160 | updateDOMWithCompletion(completionText) 161 | restoreCursor() 162 | } 163 | } 164 | ) 165 | } catch {} 166 | } 167 | } 168 | 169 | document.addEventListener("keydown", async (event) => { 170 | // Check if the 'ctrl', 'shift' & '.' (Ctrl + >) keys were pressed to trigger the extension 171 | if ( 172 | event.ctrlKey && 173 | event.shiftKey && 174 | (event.key === "." || event.key === ">") 175 | ) { 176 | // if ( 177 | // (event.metaKey && event.key === "m") 178 | // ) { 179 | // Prevent the default action 180 | event.preventDefault() 181 | 182 | // First get the domain name of the current page 183 | const domain = window.location.hostname 184 | 185 | console.log("About to run a completion on website: ", domain) 186 | 187 | let activeElement = document.activeElement 188 | console.log("activeElement", activeElement) 189 | //@ts-ignore 190 | console.log("activeElement.value", activeElement.value) 191 | console.log( 192 | "document.getSelection().toString()", 193 | document.getSelection().toString() 194 | ) 195 | console.log("activeElement.textContent", activeElement.textContent) 196 | console.log("activeElement.innerHTML;", activeElement.innerHTML) 197 | let prompt 198 | // If there's an active text input 199 | if ( 200 | activeElement && 201 | //@ts-ignore 202 | (activeElement.isContentEditable || 203 | activeElement.nodeName.toUpperCase() === "TEXTAREA" || 204 | activeElement.nodeName.toUpperCase() === "INPUT") 205 | ) { 206 | // Use selected text or all text in the input 207 | prompt = 208 | document.getSelection().toString().trim() || 209 | //@ts-ignore 210 | activeElement.textContent.trim() || 211 | //@ts-ignore 212 | activeElement.value 213 | } else { 214 | // If no active text input use any selected text on page 215 | prompt = document.getSelection().toString().trim() 216 | } 217 | 218 | console.log("Prompt: ", prompt) 219 | 220 | if (!prompt) { 221 | alert("No text in active element.") 222 | return 223 | } 224 | 225 | //@ts-ignore 226 | const completionType = activeElement.isContentEditable 227 | ? "basic_completion" 228 | : "completion" 229 | console.log("CompletionType: ", completionType) 230 | completeText(prompt, completionType) 231 | } 232 | }) 233 | -------------------------------------------------------------------------------- /src/lib/linkedin.ts: -------------------------------------------------------------------------------- 1 | // export const extraction = () => { 2 | // window.onscroll = function () { 3 | // //@ts-ignore 4 | // document.getElementById("basicprofile").value = JSON.stringify(extract()) 5 | // } 6 | 7 | // //deploying listeners for `manual extraction` buttons feature 8 | // document 9 | // .getElementById("certification_extract_button") 10 | // .addEventListener("click", extractCert) 11 | // document 12 | // .getElementById("skills_extract_button") 13 | // .addEventListener("click", extractSkills) 14 | // document 15 | // .getElementById("experience_extract_button") 16 | // .addEventListener("click", extractExperience) 17 | // document 18 | // .getElementById("education_extract_button") 19 | // .addEventListener("click", extractEducation) 20 | // 4 21 | 22 | // //save data button 23 | // document 24 | // .getElementById("save_profile_data_button") 25 | // .addEventListener("click", saveProfileData) 26 | // } 27 | 28 | // function saveProfileData() { 29 | // let textBoxIds = [ 30 | // "basicprofile", 31 | // "educationtext", 32 | // "experiencetext", 33 | // "skillstext", 34 | // "certificationstext" 35 | // ] 36 | // let profileData = {} 37 | // for (let i = 0; i < textBoxIds.length; i++) { 38 | // let tempid = textBoxIds[i] 39 | // if (tempid.includes("text")) tempid = tempid.replace("text", "") 40 | 41 | // //@ts-ignore 42 | // if (document.getElementById(textBoxIds[i]).value) 43 | // profileData[tempid] = JSON.parse( 44 | // //@ts-ignore 45 | // document.getElementById(textBoxIds[i]).value 46 | // ) 47 | // else profileData[tempid] = "No data" 48 | // } 49 | 50 | // // download file code 51 | // let filename = prompt("Enter file Name:") 52 | // let data = new Blob([JSON.stringify(profileData)], { 53 | // type: "application/json" 54 | // }) 55 | // let a = document.createElement("a"), 56 | // url = URL.createObjectURL(data) 57 | // a.href = url 58 | // a.download = filename + ".txt" 59 | // document.body.appendChild(a) 60 | // a.click() 61 | // setTimeout(function () { 62 | // document.body.removeChild(a) 63 | // window.URL.revokeObjectURL(url) 64 | // }, 0) 65 | // } // save profile data ends here 66 | 67 | // function printName() { 68 | // let uname = 69 | // document?.querySelector("div.pv-text-details__left-panel > div > h1") || 70 | // document?.getElementsByClassName( 71 | // "artdeco-entity-lockup__title ember-view" 72 | // )[0] || 73 | // null 74 | 75 | // //@ts-ignore 76 | // uname = uname?.textContent || "" 77 | // uname = getCleanText(uname) 78 | // document.getElementById("slider").querySelector("#sheaderheader").innerHTML = 79 | // "

" + uname + "

" 80 | // } 81 | 82 | // //module for extracting the details 83 | // function extract() { 84 | // // retreiving profile Section data 85 | // const profileSection = document.querySelector(".pv-top-card") 86 | 87 | // const fullNameElement = profileSection?.querySelector("h1") 88 | // const fullName = fullNameElement?.textContent || null 89 | 90 | // const titleElement = profileSection?.querySelector(".text-body-medium") 91 | // let title = titleElement?.textContent || null 92 | 93 | // let tbs = profileSection?.querySelectorAll(".text-body-small") 94 | // const locationElement = tbs ? tbs[1] : null 95 | // let loc = locationElement?.textContent || null 96 | 97 | // const photoElement = 98 | // document.querySelector(".pv-top-card-profile-picture__image") || 99 | // profileSection?.querySelector(".profile-photo-edit__preview") 100 | // const photo = photoElement?.getAttribute("src") || null 101 | 102 | // const descriptionElement = document 103 | // .querySelector("div#about") 104 | // ?.parentElement.querySelector( 105 | // ".pv-shared-text-with-see-more > div > span.visually-hidden" 106 | // ) // Is outside "profileSection" 107 | // let description = descriptionElement?.textContent || null 108 | 109 | // //@ts-ignore 110 | // const url = window.location.url 111 | // let rawProfileData = { 112 | // fullName, 113 | // title, 114 | // loc, 115 | // photo, 116 | // description, 117 | // url 118 | // } 119 | 120 | // let profileData = { 121 | // fullName: getCleanText(rawProfileData.fullName), 122 | // title: getCleanText(rawProfileData.title), 123 | // location: getCleanText(rawProfileData.loc), 124 | // description: getCleanText(rawProfileData.description), 125 | // photo: rawProfileData.photo, 126 | // url: rawProfileData.url 127 | // } 128 | // ///extraction of profile data ends here/// 129 | 130 | // return profileData 131 | // } //Extract() functions ends here 132 | 133 | // // Save PDF document of a linkedinProfile 134 | // function savePDF() { 135 | // let spanList = document.getElementsByTagName("span") 136 | // let m = [] 137 | 138 | // for (let i = 0; i < spanList.length; i++) { 139 | // if (spanList[i].textContent == "Save to PDF") { 140 | // m.push(spanList[i]) 141 | // } 142 | // } 143 | 144 | // if (m.length < 1) { 145 | // alert("No option to download profile.") 146 | // } else { 147 | // m[0].click() 148 | // } 149 | // } 150 | 151 | // // Extract license and certifications 152 | // function extractCert() { 153 | // let anchor1 = document.getElementById("licenses_and_certifications") 154 | // let anchor2 = document.querySelector(".pvs-list") 155 | 156 | // let list = null 157 | // let certs = [] 158 | 159 | // if (anchor1) { 160 | // //@ts-ignore 161 | // anchor1 = anchor1.nextElementSibling.nextElementSibling 162 | // list = anchor1.querySelector("ul").children 163 | // } 164 | 165 | // if ( 166 | // anchor2 && 167 | // //@ts-ignore 168 | // document.getElementById("deepscan").checked && 169 | // location.href.includes("certifications") 170 | // ) { 171 | // list = anchor2.children 172 | // } 173 | 174 | // if (list) { 175 | // //if the anchor exists 176 | // for (let i = 0; i < list.length; i++) { 177 | // let elem = null 178 | // let firstdiv = null 179 | // let url = "" 180 | 181 | // //@ts-ignore 182 | // if (anchor1 && !document.getElementById("deepscan").checked) { 183 | // //alert("anchor1"); 184 | // elem = 185 | // list[ 186 | // i 187 | // ].firstElementChild.firstElementChild.nextElementSibling.querySelectorAll( 188 | // "div" 189 | // ) 190 | 191 | // if (elem[0].querySelector("a")) { 192 | // firstdiv = elem[0].querySelector("a").children 193 | // } else { 194 | // firstdiv = elem[1].children 195 | // } 196 | 197 | // url = elem[4]?.querySelector("a")?.href || "" 198 | // //if anchor1 199 | // } else if ( 200 | // anchor1 == null && 201 | // anchor2 && 202 | // //@ts-ignore 203 | // document.getElementById("deepscan").checked && 204 | // location.href.includes("certifications") 205 | // ) { 206 | // //alert("anchor2s"); 207 | // elem = 208 | // list[i].querySelector("div > div").firstElementChild 209 | // .nextElementSibling 210 | // firstdiv = elem.firstElementChild.firstElementChild.children 211 | 212 | // url = 213 | // elem.firstElementChild.nextElementSibling?.querySelector("a").href || 214 | // "" 215 | // } //if anchor2 216 | // else { 217 | // break 218 | // } 219 | 220 | // //let condn = (firstdiv.querySelector('a'))? 'a >' : ''; 221 | // let name = getCleanText( 222 | // firstdiv[0].querySelector("span > span")?.textContent || "" 223 | // ) 224 | // let issuedby = getCleanText( 225 | // firstdiv[1].querySelector("span > span")?.textContent || "" 226 | // ) 227 | // let issuedon = getCleanText( 228 | // firstdiv[2]?.querySelector("span > span")?.textContent || "" 229 | // ) 230 | // let expiration = issuedon ? issuedon.split("·")[1] : "" 231 | // let issuedon = issuedon 232 | // ? issuedon.split("·")[0]?.split("Issued ")[1] || "" 233 | // : "" 234 | 235 | // let temp = { 236 | // id: i, 237 | // title: name, 238 | // issuer: issuedby, 239 | // date: issuedon, 240 | // expiration: expiration, 241 | // link: url 242 | // } 243 | 244 | // certs.push(temp) 245 | // } //for loop to scrape through the list 246 | // } 247 | // let objtemp = { 248 | // name: "licenses", 249 | // data: certs 250 | // } 251 | 252 | // document.getElementById("certificationstext").value = JSON.stringify(objtemp) 253 | // } //license extraction ends here 254 | 255 | // // Extract Skills 256 | // function extractSkills() { 257 | // //defining anchors (roots from where scraping starts) 258 | // let anchor1 = document.getElementById("skills") 259 | // let anchor2 = document.querySelector(".pvs-list") 260 | 261 | // let list = null 262 | // let skills = [] 263 | 264 | // if (anchor1 && !document.getElementById("deepscan").checked) { 265 | // anchor1 = anchor1.nextElementSibling.nextElementSibling 266 | // list = anchor1.querySelector("ul").children 267 | // } 268 | 269 | // if ( 270 | // anchor2 && 271 | // document.getElementById("deepscan").checked && 272 | // location.href.includes("skills") 273 | // ) { 274 | // list = anchor2.children 275 | // } 276 | 277 | // if (list) { 278 | // //if the anchor exists 279 | // for (i = 0; i < list.length; i++) { 280 | // let elem = null 281 | // //let firstdiv = null; 282 | 283 | // if (anchor1 && !document.getElementById("deepscan").checked) { 284 | // //alert("anchor1"); 285 | // elem = 286 | // list[ 287 | // i 288 | // ].firstElementChild.firstElementChild.nextElementSibling.querySelectorAll( 289 | // "div" 290 | // ) 291 | 292 | // let index = 0 293 | // elem = getCleanText( 294 | // elem[index]?.querySelector("div > span > span").textContent || "" 295 | // ) 296 | // } // anchor1 ends here 297 | // else if ( 298 | // anchor1 == null && 299 | // anchor2 && 300 | // document.getElementById("deepscan").checked && 301 | // location.href.includes("skills") 302 | // ) { 303 | // elem = 304 | // list[i].querySelector("div > div").firstElementChild 305 | // .nextElementSibling 306 | // elem = elem.firstElementChild.firstElementChild.children 307 | 308 | // elem = getCleanText( 309 | // elem[0]?.querySelector("div > span > span").textContent || "" 310 | // ) 311 | // } //anchor2 ends here 312 | // else { 313 | // //exit 314 | // break 315 | // } 316 | 317 | // skills.push({ 318 | // id: i, 319 | // title: elem 320 | // }) 321 | // } //for loop 322 | // } //if `the list from anchor exists` condn ends here 323 | 324 | // let objtemp = { 325 | // name: "skills", 326 | // data: skills 327 | // } 328 | 329 | // document.getElementById("skillstext").value = JSON.stringify(objtemp) 330 | // } //Extraction of skills ends here 331 | 332 | // // Extract Experience ///// 333 | 334 | // function extractExperience() { 335 | // //defining anchors (roots from where scraping starts) 336 | // let anchor1 = document.getElementById("experience") 337 | // let anchor2 = document.querySelector(".pvs-list") 338 | 339 | // let list = null 340 | // let exp = {} 341 | // let roles = [] 342 | // let company = "" 343 | 344 | // if (anchor1 && !document.getElementById("deepscan").checked) { 345 | // anchor1 = anchor1.nextElementSibling.nextElementSibling 346 | // list = anchor1.querySelector("ul").children 347 | // } 348 | 349 | // if ( 350 | // anchor2 && 351 | // document.getElementById("deepscan").checked && 352 | // location.href.includes("experience") 353 | // ) { 354 | // list = anchor2.children 355 | // } 356 | 357 | // if (list) { 358 | // //if the anchor exists 359 | // for (i = 0; i < list.length; i++) { 360 | // if ( 361 | // document.getElementById("deepscan").checked && 362 | // !location.href.includes("experience") 363 | // ) 364 | // break 365 | // company = "" 366 | // roles = [] 367 | 368 | // let elem = list[i].querySelector("div > div").nextElementSibling //for anchor 1 369 | // if (elem.querySelector("div > a")) { 370 | // // condition for multiple roles in same company 371 | // company = 372 | // elem.querySelector("div > a > div > span > span")?.textContent || "" 373 | // company = getCleanText(company) 374 | 375 | // elem = elem.firstElementChild.nextElementSibling 376 | // let elems = elem.querySelector("ul").children 377 | 378 | // for (j = 0; j < elems.length; j++) { 379 | // // traversing roles list in a company 380 | 381 | // let keke = 382 | // elems[j].querySelector("div > div")?.nextElementSibling || null 383 | // keke = keke?.querySelector("div > a") || null 384 | 385 | // kchilds = keke.children 386 | // let rname = " ", 387 | // startDate = " ", 388 | // endDate = " ", 389 | // loc = " " 390 | // for (k = 0; k < kchilds.length; k++) { 391 | // //each role's details taken 392 | // if (k == 0) 393 | // //role name 394 | // rname = kchilds[k]?.querySelector("span > span").textContent || "" 395 | // if (k == 1) { 396 | // //role duration 397 | // let ta = kchilds[k] 398 | // .querySelector("span") 399 | // .textContent.split(/[-·]/) 400 | // startDate = ta[0] 401 | // endDate = ta[1] 402 | // } 403 | // if (k == 2) 404 | // //role location 405 | // loc = kchilds[k].querySelector("span")?.textContent || "" 406 | // } //kloop 407 | 408 | // roles.push({ 409 | // id: j, 410 | // title: getCleanText(rname), 411 | // startDate: getCleanText(startDate), 412 | // endDate: getCleanText(endDate), 413 | // location: getCleanText(loc) 414 | // }) 415 | // } // role traversal loop 416 | // } else { 417 | // //condition when single role in one company 418 | // elem = elem.querySelector("div > div > div > div") 419 | 420 | // echilds = elem.children 421 | // let rname = " ", 422 | // startDate = " ", 423 | // endDate = " ", 424 | // loc = " " 425 | // for (k = 0; k < echilds.length; k++) { 426 | // //each role's details taken 427 | // if (k == 0) 428 | // //role name 429 | // rname = echilds[k]?.querySelector("span > span").textContent || "" 430 | // if (k == 2) { 431 | // //role duration 432 | // let ta = echilds[k].querySelector("span").textContent.split(/[-·]/) 433 | // startDate = ta[0] 434 | // endDate = ta[1] 435 | // } 436 | // if (k == 3) 437 | // //role location 438 | // loc = echilds[k].querySelector("span")?.textContent || "" 439 | 440 | // if (k == 1) 441 | // //role company title 442 | // company = echilds[k].querySelector("span")?.textContent || "" 443 | // if (company) company = company.split(/[-·]/)[0] 444 | // } //kloop 445 | 446 | // roles.push({ 447 | // id: 0, 448 | // title: getCleanText(rname), 449 | // startDate: getCleanText(startDate), 450 | // endDate: getCleanText(endDate), 451 | // location: getCleanText(loc) 452 | // }) 453 | // } //single role else condn ends 454 | 455 | // exp[i] = { 456 | // company: company, 457 | // roles: roles 458 | // } 459 | // } //for loop over 'i' for each item in anchor list 460 | // } // if list anchor exists condition 461 | 462 | // document.getElementById("experiencetext").value = JSON.stringify(exp) 463 | // } //extract experience ends here 464 | 465 | // // Extract Experience // 466 | 467 | // function extractEducation() { 468 | // //defining anchors (roots from where scraping starts) 469 | // let anchor1 = document.getElementById("education") 470 | // let anchor2 = document.querySelector(".pvs-list") 471 | 472 | // let list = null 473 | 474 | // if (anchor1 && !document.getElementById("deepscan").checked) { 475 | // anchor1 = anchor1.nextElementSibling.nextElementSibling 476 | // list = anchor1.querySelector("ul").children 477 | // } 478 | 479 | // if ( 480 | // anchor2 && 481 | // document.getElementById("deepscan").checked && 482 | // location.href.includes("experience") 483 | // ) { 484 | // list = anchor2.children 485 | // } 486 | 487 | // if (list) { 488 | // //if the anchor exists 489 | // for (i = 0; i < list.length; i++) { 490 | // if ( 491 | // document.getElementById("deepscan").checked && 492 | // !location.href.includes("experience") 493 | // ) 494 | // break 495 | // } // for loops 496 | // } // if 497 | // } //extract education ends here 498 | 499 | // //////////// *---- UTILS -----* ////////////// 500 | // // Utility functions 501 | 502 | // function expandButtons() { 503 | // const expandButtonsSelectors = [ 504 | // ".pv-profile-section.pv-about-section .lt-line-clamp__more", // About 505 | // "#experience-section .pv-profile-section__see-more-inline.link", // Experience 506 | // ".pv-profile-section.education-section button.pv-profile-section__see-more-inline", // Education 507 | // '.pv-skill-categories-section [data-control-name="skill_details"]' // Skills 508 | // ] 509 | 510 | // const seeMoreButtonsSelectors = [ 511 | // '.pv-entity__description .lt-line-clamp__line.lt-line-clamp__line--last .lt-line-clamp__more[href="#"]', 512 | // '.lt-line-clamp__more[href="#"]:not(.lt-line-clamp__ellipsis--dummy)' 513 | // ] 514 | 515 | // for (const buttonSelector of expandButtonsSelectors) { 516 | // try { 517 | // if ($(buttonSelector) !== null) { 518 | // $(buttonSelector).click() 519 | // } 520 | // } catch (err) { 521 | // alert("Couldn't expand buttons") 522 | // } 523 | // } 524 | 525 | // for (const seeMoreButtonSelector of seeMoreButtonsSelectors) { 526 | // const buttons = $(seeMoreButtonSelector) 527 | 528 | // for (const button of buttons) { 529 | // if (button) { 530 | // try { 531 | // button.click() 532 | // } catch (err) { 533 | // alert("Error expanding see more buttons") 534 | // } 535 | // } 536 | // } 537 | // } 538 | // } 539 | 540 | // function getCleanText(text) { 541 | // const regexRemoveMultipleSpaces = / +/g 542 | // const regexRemoveLineBreaks = /(\r\n\t|\n|\r\t)/gm 543 | 544 | // if (!text) return null 545 | 546 | // const cleanText = text 547 | // .toString() 548 | // .replace(regexRemoveLineBreaks, "") 549 | // .replace(regexRemoveMultipleSpaces, " ") 550 | // .replace("...", "") 551 | // .replace("See more", "") 552 | // .replace("See less", "") 553 | // .trim() 554 | 555 | // return cleanText 556 | // } 557 | 558 | export {} 559 | -------------------------------------------------------------------------------- /src/lib/openai.ts: -------------------------------------------------------------------------------- 1 | import { parseJsonSSE } from "~parseJSONSSE" 2 | 3 | export const OPENAI_API_KEY = process.env.OPENAI_API_KEY 4 | 5 | export const runCompletion = async ({ 6 | prompt, 7 | config = { 8 | model: "text-davinci-003", 9 | max_tokens: 128, 10 | temperature: 0.7 11 | } 12 | }: { 13 | prompt: string 14 | config: any 15 | }) => { 16 | const data = { prompt, ...config, provider: undefined } 17 | try { 18 | const res = await fetch("https://api.openai.com/v1/completions", { 19 | method: "POST", 20 | body: JSON.stringify(data), 21 | headers: { 22 | "Content-Type": "application/json", 23 | Authorization: "Bearer " + OPENAI_API_KEY 24 | } 25 | }) 26 | 27 | const compRes = await res.json() 28 | 29 | return compRes.choices[0].text 30 | } catch (e) { 31 | console.error(e) 32 | } 33 | } 34 | 35 | export const streamCompletion = async (args: { 36 | data: { 37 | prompt: string 38 | config: any 39 | } 40 | onMessage: (completion: string) => void 41 | onError: (err: string) => void 42 | onClose: () => void 43 | }) => { 44 | const { data, onMessage, onClose } = args 45 | 46 | const payload = { 47 | prompt: data.prompt, 48 | model: "text-davinci-003", 49 | max_tokens: 128, 50 | temperature: 0.7, 51 | stream: true 52 | } 53 | 54 | console.log("STREAMING WITH PYLOADl", payload) 55 | 56 | const res = await fetch("https://api.openai.com/v1/completions", { 57 | headers: { 58 | "Content-Type": "application/json", 59 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}` 60 | }, 61 | method: "POST", 62 | body: JSON.stringify(payload) 63 | }) 64 | 65 | parseJsonSSE({ 66 | data: res.body, 67 | onParse: (obj) => { 68 | onMessage( 69 | //@ts-ignore 70 | obj.choices?.[0].text 71 | ) 72 | }, 73 | onFinish: onClose 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/newtab.tsx: -------------------------------------------------------------------------------- 1 | import "./style.css" 2 | import IndexPopup from "./popup"; 3 | 4 | // Temp new tab page. 5 | function IndexNewtab() { 6 | return 7 | } 8 | 9 | export default IndexNewtab 10 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | function IndexOptions() { 4 | const [data, setData] = useState("") 5 | 6 | return ( 7 |
13 |

14 | Welcome to your Plasmo Extension! 15 |

16 | setData(e.target.value)} value={data} /> 17 |
18 | ) 19 | } 20 | 21 | export default IndexOptions 22 | -------------------------------------------------------------------------------- /src/parseJSONSSE.ts: -------------------------------------------------------------------------------- 1 | export const parseJsonSSE = async ({ 2 | data, 3 | onParse, 4 | onFinish 5 | }: { 6 | data: ReadableStream 7 | onParse: (object: T) => void 8 | onFinish: () => void 9 | }) => { 10 | const reader = data.getReader() 11 | const decoder = new TextDecoder() 12 | 13 | let done = false 14 | let tempState = "" 15 | 16 | while (!done) { 17 | // eslint-disable-next-line no-await-in-loop 18 | const { value, done: doneReading } = await reader.read() 19 | done = doneReading 20 | const newValue = decoder.decode(value).split("\n\n").filter(Boolean) 21 | 22 | if (tempState) { 23 | newValue[0] = tempState + newValue[0] 24 | tempState = "" 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-loop-func 28 | newValue.forEach((newVal) => { 29 | try { 30 | const json = JSON.parse(newVal.replace("data: ", "")) as T 31 | 32 | onParse(json) 33 | } catch (error) { 34 | tempState = newVal 35 | } 36 | }) 37 | } 38 | 39 | onFinish() 40 | } 41 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | 3 | import { runCompletion } from "~lib/openai" 4 | 5 | function IndexPopup() { 6 | const [text, setText] = useState("") 7 | const [completion, setCompletion] = useState("") 8 | const handleClick = async () => { 9 | const res = await runCompletion({ 10 | prompt: text, 11 | config: { 12 | model: "text-davinci-003", 13 | max_tokens: 128, 14 | temperature: 0.7 15 | } 16 | }) 17 | 18 | setCompletion(text + res.choices[0].text) 19 | 20 | setText("") 21 | } 22 | 23 | return ( 24 |
31 |

32 | GPT Chrome Starter 33 |

34 | 35 |
{ 42 | if (e.code === "Enter") { 43 | handleClick(); 44 | } 45 | }} 46 | > 47 |

Input:

48 |