diff --git a/package.json b/package.json index 8969044..37b817a 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "release": "HUSKY_SKIP_HOOKS=1 dotenv release-it" }, "dependencies": { + "eventsource-parser": "^1.0.0", "hast-util-to-html": "^8.0.4", "highlight.js": "^11.7.0", "lowlight": "^2.8.1", diff --git a/src/pages/Background/index.ts b/src/pages/Background/index.ts index 7f0b29f..e0be24e 100644 --- a/src/pages/Background/index.ts +++ b/src/pages/Background/index.ts @@ -1,5 +1,5 @@ import { nonNullable } from '../../utils'; -import { Actions, type Action, type OpenaiResponse } from '../../types'; +import { Actions, type Action } from '../../types'; let isDark: boolean; @@ -44,41 +44,75 @@ function setCSSTheme(isDark: boolean, tabId: number | undefined) { } } -chrome.runtime.onMessage.addListener( - (message: Action, sender, sendResponse) => { - switch (message.action) { - case Actions.fetchOpenAi: - //INFO: cant use async/await here because onMessage doesnt work properly with `await` - fetch('https://openai-api.hadnet.workers.dev/?action=explain-code', { - method: 'POST', - body: JSON.stringify({ code: message.payload?.code }), - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, +chrome.runtime.onMessage.addListener((message: Action, sender) => { + switch (message.action) { + case Actions.fetchOpenAi: { + //INFO: cant use async/await here because onMessage doesnt work properly with `await` + fetch('https://openai-api.hadnet.workers.dev/?action=explain-code', { + method: 'POST', + body: JSON.stringify({ code: message.payload?.code }), + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then((response) => { + let content = ''; + + if (!response.ok) { + throw new Error(response.statusText); + } + + // This data is a ReadableStream + const data = response.body; + if (!data) { + return; + } + + (async () => { + const reader = data.getReader(); + const _decoder = new TextDecoder(); + let done = false; + + while (!done) { + const { value, done: doneReading } = await reader.read(); + done = doneReading; + const chunkValue = _decoder.decode(value); + content += chunkValue; + chrome.tabs.query( + { active: true, currentWindow: true }, + function (tabs) { + if (!tabs[0].id) return; + chrome.tabs.sendMessage(tabs[0].id, { + action: Actions.stream, + payload: content, + }); + } + ); + } + })(); }) - .then((response) => response.json()) - .then((data: OpenaiResponse) => { - sendResponse(data); - }) - .catch((e) => console.log('Error', e)); - break; - case Actions.copyToClipboard: - chrome.notifications.create( - { - type: 'basic', - title: 'Copied!', - message: 'The code snippet was copied to your clipboard', - iconUrl: 'icon-128.png', - }, - () => console.log('notification created') - ); - break; - default: - if (sender.url?.includes('https://twitter.com')) { - isDark = message.payload?.isDark; - setCSSTheme(isDark, sender.tab?.id); - } + .catch((e) => { + if (e instanceof Error) + throw new Error(`Error in fetching API: ${e.message}`); + }); + break; } - return true; + case Actions.copyToClipboard: + chrome.notifications.create( + { + type: 'basic', + title: 'Copied!', + message: 'The code snippet was copied to your clipboard', + iconUrl: 'icon-128.png', + }, + () => console.log('A notification was created') + ); + break; + default: + if (sender.url?.includes('https://twitter.com')) { + isDark = message.payload?.isDark; + setCSSTheme(isDark, sender.tab?.id); + } } -); + return true; +}); diff --git a/src/pages/Content/index.ts b/src/pages/Content/index.ts index 2e868c5..8e36476 100644 --- a/src/pages/Content/index.ts +++ b/src/pages/Content/index.ts @@ -2,10 +2,10 @@ import hljs from 'highlight.js'; import { marked } from 'marked'; import { nonNullable } from '../../utils'; import { IconNames, getIcon, type Color } from './modules/svg-icons'; -import { Actions, type Action, type OpenaiResponse } from '../../types'; -// import { toHtml } from 'hast-util-to-html'; +import { Actions, type Action } from '../../types'; -// console.log('Content script works!'); +let shimmer: HTMLDivElement; +let loading = false; function convertToMarkdownAndHighlight(t: Element, color: Color) { if (t.textContent) { @@ -79,7 +79,6 @@ function convertToMarkdownAndHighlight(t: Element, color: Color) { cardDOM.appendChild(langIconBtn); if (language.match(/tsx?$/)) { - let loading = false; const helpIconBtn = document.createElement('a'); helpIconBtn.style.position = 'absolute'; helpIconBtn.style.cursor = 'pointer'; @@ -89,7 +88,7 @@ function convertToMarkdownAndHighlight(t: Element, color: Color) { helpIconBtn.innerHTML = getIcon('openai', isDark ? '#d3d3d3' : '#545063'); helpIconBtn.addEventListener('click', () => { - const shimmer = document.createElement('div'); + shimmer = document.createElement('div'); shimmer.style.display = 'flex'; shimmer.style.flexDirection = 'column'; shimmer.style.background = isDark ? '#1c1a23' : '#f1eae7'; @@ -102,27 +101,10 @@ function convertToMarkdownAndHighlight(t: Element, color: Color) { cardDOM.parentElement?.appendChild(shimmer); if (!loading) { loading = true; - chrome.runtime.sendMessage( - { - action: Actions.fetchOpenAi, - payload: { code: codeString }, - }, - (data) => { - const setContent = (content: string, icon: IconNames = 'info') => - `
${getIcon( - icon, - isDark ? '#a8edff' : '#232323' - )}
${content}
`.trim(); - if ('error' in data) { - shimmer.innerHTML = setContent(data.error, 'warning'); - } else { - shimmer.style.padding = '20px'; - shimmer.style.lineHeight = '150%'; - shimmer.innerHTML = setContent(data.choices[0].text); - } - loading = false; - } - ); + chrome.runtime.sendMessage({ + action: Actions.fetchOpenAi, + payload: { code: codeString }, + }); } }); cardDOM.appendChild(helpIconBtn); @@ -159,6 +141,26 @@ chrome.runtime.sendMessage({ payload: { isDark }, }); +chrome.runtime.onMessage.addListener((message: Action) => { + switch (message.action) { + case Actions.stream: { + const data = message.payload; + loading = false; + + const setContent = (content: string, icon: IconNames = 'info') => + `
${getIcon( + icon, + isDark ? '#a8edff' : '#232323' + )}
${content}
`.trim(); + + shimmer.style.padding = '20px'; + shimmer.style.lineHeight = '150%'; + shimmer.innerHTML = setContent(data ?? 'No response'); + break; + } + } +}); + theme.addEventListener('change', (event) => { event.preventDefault(); const theme = window.matchMedia('(prefers-color-scheme: dark)'); diff --git a/src/static/manifest.json b/src/static/manifest.json index 6cf4879..c9e31b7 100755 --- a/src/static/manifest.json +++ b/src/static/manifest.json @@ -20,7 +20,8 @@ { "matches": [ "https://twitter.com/*", - "https://openai-api.hadnet.workers.dev/*" + "https://openai-api.hadnet.workers.dev/*", + "http://127.0.0.1:8787/*" ], "js": ["contentScript.js"] } diff --git a/src/types/index.ts b/src/types/index.ts index dec4056..347d29f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,9 +24,10 @@ export const enum Actions { copyToClipboard = 'copyToClipboard', fetchOpenAi = 'fetchOpenAi', changeTheme = 'changeTheme', + stream = 'streamText', } -export type Action = { +export type Action = { action: Actions; - payload?: any; + payload?: T; }; diff --git a/yarn.lock b/yarn.lock index 3c3c8a3..f7a6602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1337,6 +1337,11 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@microsoft/fetch-event-source@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" + integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" @@ -4190,6 +4195,11 @@ events@^3.2.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource-parser@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-1.0.0.tgz#6332e37fd5512e3c8d9df05773b2bf9e152ccc04" + integrity sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g== + execa@7.1.1, execa@^7.0.0: version "7.1.1" resolved "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz"