diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 45aa60c6..00639ffa 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -12,7 +12,6 @@ "@types/node-emoji": "^1.8.2", "gpt-tokenizer": "^1.0.5", "node-emoji": "^1.11.0", - "openai-streams": "^5.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-hot-toast": "^2.4.1", @@ -1934,14 +1933,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "engines": { - "node": ">=12" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.365", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.365.tgz", @@ -2647,14 +2638,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventsource-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", - "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", - "engines": { - "node": ">=14.18" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4039,19 +4022,6 @@ "wrappy": "1" } }, - "node_modules/openai-streams": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/openai-streams/-/openai-streams-5.3.0.tgz", - "integrity": "sha512-481B3eWLQ4P4E7jvF38vRzz2/aQz6CLHx2XSV7J+w83791fhiUxvTNGw8uh/M04N0X5AdCqPaEPqHc+sfxNTUQ==", - "dependencies": { - "dotenv": "^16.0.3", - "eventsource-parser": "^1.0.0", - "yield-stream": "^3.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -4681,14 +4651,6 @@ "node": ">=8" } }, - "node_modules/shim-streams": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/shim-streams/-/shim-streams-0.0.2.tgz", - "integrity": "sha512-9Otb+FCl13XxRp1nVddtsCbwvB7AEMTjzc3/fixowyzvSVoCzu/VEstblB2SdIDbd61u5D/zpS5u9fGzDdOoZg==", - "engines": { - "node": ">=14" - } - }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -5224,14 +5186,6 @@ } } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5329,18 +5283,6 @@ "node": ">= 6" } }, - "node_modules/yield-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/yield-stream/-/yield-stream-3.0.0.tgz", - "integrity": "sha512-I0fHp7xcR+ilGKgtLP+6d0SxTLfJcLDX/iiTEQZtg0U/cqWKvcHy4D9em6adhs3emSgik8YAZ2Jmv7gc1kyuhA==", - "dependencies": { - "shim-streams": "^0.0.2", - "web-streams-polyfill": "^3.2.1" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 29edbedd..57826479 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@types/node-emoji": "^1.8.2", "gpt-tokenizer": "^1.0.5", "node-emoji": "^1.11.0", - "openai-streams": "^5.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-hot-toast": "^2.4.1", diff --git a/src/constants.ts b/src/constants.ts index 8fa3bc0f..29d23428 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,13 +4,13 @@ export const SUPABASE_AUTH_COOKIE_NAME = "supabase-auth-token"; export const OPEN_SAUCED_AUTH_TOKEN_KEY = "os-access-token"; export const OPEN_SAUCED_INSIGHTS_DOMAIN = "insights.opensauced.pizza"; export const AI_PR_DESCRIPTION_CONFIG_KEY = "ai-pr-description-config"; -export const OPEN_AI_COMPLETION_MODEL_NAME = "gpt-3.5-turbo"; // API endpoints export const OPEN_SAUCED_USERS_ENDPOINT = "https://api.opensauced.pizza/v1/users"; export const OPEN_SAUCED_REPOS_ENDPOINT = "https://api.opensauced.pizza/v1/repos"; export const OPEN_SAUCED_SESSION_ENDPOINT = "https://api.opensauced.pizza/v1/auth/session"; export const OPEN_SAUCED_USER_INSIGHTS_ENDPOINT = "https://api.opensauced.pizza/v1/user/insights"; +export const OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT = "https://api.opensauced.pizza/v1/prs/description/generate"; // GitHub constants/selectors export const GITHUB_PROFILE_MENU_SELECTOR = ".p-nickname.vcard-username.d-block"; diff --git a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts index 0719e4f6..c67bb220 100644 --- a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts +++ b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts @@ -1,12 +1,12 @@ import { createHtmlElement } from "../../../utils/createHtmlElement"; import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; import { getPullRequestAPIURL } from "../../../utils/urlMatchers"; -import { getDescriptionContext, isContextWithinBounds } from "../../../utils/fetchGithubAPIData"; +import { getDescriptionContext, isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; import { generateDescription } from "../../../utils/aiprdescription/openai"; -import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, OPEN_AI_COMPLETION_MODEL_NAME, SUPABASE_LOGIN_URL } from "../../../constants"; -import { insertAtCursorFromStream } from "../../../utils/aiprdescription/cursorPositionInsert"; +import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, SUPABASE_LOGIN_URL } from "../../../constants"; +import { insertTextAtCursor } from "../../../utils/aiprdescription/cursorPositionInsert"; import { getAIDescriptionConfig } from "../../../utils/aiprdescription/descriptionconfig"; -import { isLoggedIn } from "../../../utils/checkAuthentication"; +import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; export const DescriptionGeneratorButton = () => { const descriptionGeneratorButton = createHtmlElement("a", { @@ -42,14 +42,17 @@ const handleSubmit = async () => { } logo.classList.toggle("animate-spin"); const [diff, commitMessages] = await getDescriptionContext(url, descriptionConfig.config.source); - - if (!isContextWithinBounds([diff, commitMessages], descriptionConfig.config.maxInputLength)) { + if(!diff && !commitMessages) { + logo.classList.toggle("animate-spin"); + return alert(`No input context was generated.`) + } + if (isOutOfContextBounds([diff, commitMessages], descriptionConfig.config.maxInputLength)) { logo.classList.toggle("animate-spin"); return alert(`Max input length exceeded. Try setting the description source to commit-messages.`); } + const token = await getAuthToken(); const descriptionStream = await generateDescription( -descriptionConfig.config.openai_api_key, - OPEN_AI_COMPLETION_MODEL_NAME, + token, descriptionConfig.config.language, descriptionConfig.config.length, descriptionConfig.config.temperature / 10, @@ -64,7 +67,7 @@ descriptionConfig.config.openai_api_key, } const textArea = document.getElementsByName(GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR)[0] as HTMLTextAreaElement; - void insertAtCursorFromStream(textArea, descriptionStream); + void insertTextAtCursor(textArea, descriptionStream); } catch (error: unknown) { if (error instanceof Error) { console.error("Description generation error:", error.message); diff --git a/src/pages/aiprdescription.tsx b/src/pages/aiprdescription.tsx index e43626d4..963db1d2 100644 --- a/src/pages/aiprdescription.tsx +++ b/src/pages/aiprdescription.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useReducer, useState } from "react"; -import { BiInfoCircle } from "react-icons/bi"; import { FaChevronLeft } from "react-icons/fa"; import OpenSaucedLogo from "../assets/opensauced-logo.svg"; import { RouteContext } from "../App"; @@ -13,7 +12,7 @@ import { DescriptionLanguage, setAIDescriptionConfig, getDefaultDescriptionConfig, - setDefaultDescriptionConfig, + toggleAIPRDescriptionEnabled, } from "../utils/aiprdescription/descriptionconfig"; import { useRefs } from "../hooks/useRefs"; import { configurationReducer } from "../utils/aiprdescription/configurationReducer"; @@ -35,7 +34,6 @@ const AIPRDescription = () => { dispatch({ type: "SET", value: configData }); setFormDisabled(!configData?.enabled); - console.log(config.config.openai_api_key); }; void descriptionConfig(); @@ -43,7 +41,6 @@ const AIPRDescription = () => { const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); - const openai_api_key = refs.openai_api_key!.getAttribute("value")!; const length = parseInt(refs.length?.getAttribute("value")!); const temperature = Number(Number(refs.temperature?.getAttribute("value")!)); const maxInputLength = parseInt(refs.maxInputLength?.getAttribute("value")!); @@ -53,7 +50,7 @@ const AIPRDescription = () => { void setAIDescriptionConfig({ enabled: true, - config: { openai_api_key, length, temperature, maxInputLength, language, source, tone }, + config: { length, temperature, maxInputLength, language, source, tone }, }); toast.success("Configuration updated!"); }; @@ -88,12 +85,12 @@ const AIPRDescription = () => { }`} onClick={() => { setFormDisabled(!formDisabled); - dispatch({ type: "CLEAR", value: null }); + dispatch({ type: "TOGGLE_ENABLED", value: config }); + void toggleAIPRDescriptionEnabled(); if (formDisabled) { - toast.success("AI PR Description enabled!"); -} else { + toast.success("AI PR Description enabled!"); + } else { toast.error("AI PR Description disabled!"); - setDefaultDescriptionConfig(); } }} > @@ -111,29 +108,6 @@ const AIPRDescription = () => { OpenSauced AI -

- OpenAI API Key - - window.open( - "https://platform.openai.com/account/api-keys", - "_blank", - )} - /> -

- - dispatch({ type: "SET_OPENAI_API_KEY", value: e.currentTarget.value })} - /> -

diff --git a/src/utils/aiprdescription/configurationReducer.ts b/src/utils/aiprdescription/configurationReducer.ts index 6c612a15..9ec1361f 100644 --- a/src/utils/aiprdescription/configurationReducer.ts +++ b/src/utils/aiprdescription/configurationReducer.ts @@ -7,9 +7,6 @@ export const configurationReducer = (state: DescriptionConfig, action: { type: s case "SET": newState = action.value; break; - case "SET_OPENAI_API_KEY": - newState.config.openai_api_key = action.value; - break; case "SET_LENGTH": newState.config.length = action.value; break; @@ -28,6 +25,9 @@ export const configurationReducer = (state: DescriptionConfig, action: { type: s case "SET_TONE": newState.config.tone = action.value; break; + case "TOGGLE_ENABLED": + newState.enabled = !newState.enabled; + break; case "CLEAR": newState = getDefaultDescriptionConfig(); break; diff --git a/src/utils/aiprdescription/cursorPositionInsert.ts b/src/utils/aiprdescription/cursorPositionInsert.ts index 2e943350..5ff1bb61 100644 --- a/src/utils/aiprdescription/cursorPositionInsert.ts +++ b/src/utils/aiprdescription/cursorPositionInsert.ts @@ -1,4 +1,16 @@ // This function is used to insert text at the cursor position in the text area +export const insertTextAtCursor = async (textArea: HTMLTextAreaElement, text: string) => { + let length = 0; + const typewriter = setInterval(() => { + textArea.setRangeText(text[length++], textArea.selectionStart, textArea.selectionEnd, "end"); + if (length >= text.length) { + clearInterval(typewriter); + textArea.setRangeText("\n\n_Generated using [OpenSauced](https://opensauced.ai/)._", textArea.selectionStart, textArea.selectionEnd, "end"); + } + }, 10); + +}; + export const insertAtCursorFromStream = async (textArea: HTMLTextAreaElement, stream: ReadableStream) => { const reader = stream.getReader(); const decoder = new TextDecoder("utf-8"); @@ -15,5 +27,5 @@ export const insertAtCursorFromStream = async (textArea: HTMLTextAreaElement, st textArea.setRangeText(chunk, start, end, "end"); } } - textArea.setRangeText("\n\n_Description generated using [OpenSauced](https://opensauced.ai/_).", textArea.selectionStart, textArea.selectionEnd, "end"); + textArea.setRangeText("\n\n_Generated using [OpenSauced](https://opensauced.ai/)._", textArea.selectionStart, textArea.selectionEnd, "end"); }; diff --git a/src/utils/aiprdescription/descriptionconfig.ts b/src/utils/aiprdescription/descriptionconfig.ts index 22fb6acf..4500ad4f 100644 --- a/src/utils/aiprdescription/descriptionconfig.ts +++ b/src/utils/aiprdescription/descriptionconfig.ts @@ -17,7 +17,6 @@ export type DescriptionLanguage = export interface DescriptionConfig { enabled: boolean; config: { - openai_api_key: string; length: number; maxInputLength: number; temperature: number; @@ -43,20 +42,29 @@ export const setAIDescriptionConfig = async (data: DescriptionConfig): Promise ({ - enabled: false, - config: { - openai_api_key: "", - length: 500, - maxInputLength: 3900, - temperature: 7, - language: "english", - tone: "informative", - source: "diff", - }, - }); + enabled: true, + config: { + length: 500, + maxInputLength: 3900, + temperature: 7, + language: "english", + tone: "informative", + source: "diff", + }, +}); export const setDefaultDescriptionConfig = () => { const defaultConfig = getDefaultDescriptionConfig(); void setAIDescriptionConfig(defaultConfig); }; + +export const toggleAIPRDescriptionEnabled = async () => { + const config = await getAIDescriptionConfig(); + + if (typeof config?.enabled === "undefined") return; + config.enabled = !config.enabled; + await setAIDescriptionConfig(config); + + return; +} diff --git a/src/utils/aiprdescription/openai.ts b/src/utils/aiprdescription/openai.ts index 344cbe79..bd8d9a30 100644 --- a/src/utils/aiprdescription/openai.ts +++ b/src/utils/aiprdescription/openai.ts @@ -1,65 +1,41 @@ +import { OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT } from "../../constants"; import type { DescriptionTone } from "./descriptionconfig"; -import { OpenAI, CreateChatCompletionRequest } from "openai-streams"; - -const generatePrompt = ( - locale: string, - maxLength: number, - tone: DescriptionTone, -) => [ - `Generate an apt github PR description written in present tense and ${tone} tone for the given code diff/commit-messages with the specifications mentioned below`, - `Description language: ${locale}`, - `Description must be a maximum of ${maxLength} characters.`, - "Exclude anything unnecessary such as translation. Your entire response will be passed directly into a pull request description", -].join("\n"); - -const createChatCompletion = async ( - apiKey: string, - json: CreateChatCompletionRequest, -): Promise> => { - const stream = await OpenAI("chat", json, { - apiKey, - mode: "tokens", - }); - - return stream; -}; export const generateDescription = async ( apiKey: string, - model: "gpt-3.5-turbo" | "gpt-3.5-turbo-0301", - locale: string, - maxLength: number, + language: string, + descriptionLength: number, temperature: number, tone: DescriptionTone, diff?: string, commitMessages?: string[], -) => { - const content = `${diff ? `Diff: ${diff}\n` : ""}${commitMessages ? `\nCommit Messages: ${commitMessages.join(",")}` : ""}`; +): Promise => { try { - const completion = await createChatCompletion( - apiKey, - { - model, - messages: [ - { - role: "system", - content: generatePrompt(locale, maxLength, tone), - }, - { - role: "user", - content, - }, - ], - temperature, - n: 1, + const response = await fetch(OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${apiKey}` }, - ); + body: JSON.stringify({ + descriptionLength, + temperature, + tone, + language, + diff, + commitMessages + }) + }); + if (response.status === 201) { + const { description } = await response.json(); + return description; + } - return completion; } catch (error: unknown) { if (error instanceof Error) { - console.error("OpenAI error: ", error.message); -} + console.error("OpenAI error: ", error.message); + } } + return undefined; }; diff --git a/src/utils/checkAuthentication.ts b/src/utils/checkAuthentication.ts index 60c6b7ef..99954fc3 100644 --- a/src/utils/checkAuthentication.ts +++ b/src/utils/checkAuthentication.ts @@ -32,11 +32,8 @@ export const checkAuthentication = () => { ); }; -export const isLoggedIn = async () => +export const isLoggedIn = async (): Promise => Object.entries(await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY)).length !== 0; - // only a valid auth token can exist in the storage due to the check in line 23 - Object.entries(await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY)).length !== 0; - -export const getAuthToken = async () => (await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY))[OPEN_SAUCED_AUTH_TOKEN_KEY]; +export const getAuthToken = async (): Promise => (await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY))[OPEN_SAUCED_AUTH_TOKEN_KEY]; diff --git a/src/utils/fetchGithubAPIData.ts b/src/utils/fetchGithubAPIData.ts index e94572ae..8fcab7f4 100644 --- a/src/utils/fetchGithubAPIData.ts +++ b/src/utils/fetchGithubAPIData.ts @@ -47,7 +47,7 @@ export const getDescriptionContext = async (url: string, source: DescriptionSour return response; }; -export const isContextWithinBounds = (context: DescriptionContext, limit: number): boolean => { +export const isOutOfContextBounds = (context: DescriptionContext, limit: number): boolean => { let text = ""; if (context[0]) { @@ -57,7 +57,7 @@ export const isContextWithinBounds = (context: DescriptionContext, limit: number text += context[1].join(""); } - return Boolean(isWithinTokenLimit(text, limit)); + return isWithinTokenLimit(text, limit) === false; }; export const isPublicRepository = async (url: string): Promise => {