diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d1a81a86..5805cc40 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -6600,11 +6600,10 @@ } }, "node_modules/vite": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.15.tgz", - "integrity": "sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.14.27", "postcss": "^8.4.13", diff --git a/src/constants.ts b/src/constants.ts index 5f8aefa7..c493c81c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,6 +17,8 @@ export const OPEN_SAUCED_USER_INSIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/u export const OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/description/generate`; export const OPEN_SAUCED_USER_HIGHLIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/user/highlights`; export const OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/suggestion/generate`; +export const OPEN_SAUCED_AI_CODE_EXPLANATION_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/explanation/generate`; +export const OPEN_SAUCED_AI_CODE_TEST_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/test/generate`; export const OPEN_SAUCED_HIGHLIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/highlights/list`; // GitHub constants/selectors diff --git a/src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts b/src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts deleted file mode 100644 index fa6f1c58..00000000 --- a/src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { createHtmlElement } from "../../../utils/createHtmlElement"; -import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; -import { GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR, SUPABASE_LOGIN_URL } from "../../../constants"; -import { generateCodeSuggestion } from "../../../utils/aiprdescription/openai"; -import { isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; -import { insertTextAtCursor } from "../../../utils/aiprdescription/cursorPositionInsert"; -import { getAIDescriptionConfig } from "../../../utils/aiprdescription/descriptionconfig"; -import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; - -export const ChangeSuggestorButton = (commentNode: HTMLElement) => { - const changeSuggestorButton = createHtmlElement("a", { - innerHTML: ` - - - Get Refactor Suggestions`, - onclick: async () => handleSubmit(commentNode), - id: "os-ai-change-gen", - }); - - return changeSuggestorButton; -}; - -const handleSubmit = async (commentNode: HTMLElement) => { - const logo = document.getElementById("ai-description-button-logo"); - const button = document.getElementById("os-ai-change-gen"); - - try { - if (!(await isLoggedIn())) { - return window.open(SUPABASE_LOGIN_URL, "_blank"); - } - - if (!logo || !button) { - return; - } - - const descriptionConfig = await getAIDescriptionConfig(); - - if (!descriptionConfig) { - return; - } - - logo.classList.toggle("animate-spin"); - button.classList.toggle("pointer-events-none"); - - const selectedLines = document.querySelectorAll(".code-review.selected-line"); - let selectedCode = Array.from(selectedLines).map(line => line.textContent) - .join("\n"); - - // find input with name="position" and get its value - if (!selectedCode) { - const positionElement = (commentNode.querySelector("input[name=position]")!); - const position = positionElement.getAttribute("value")!; - - const codeDiv = document.querySelector(`[data-line-number="${position}"]`)?.nextSibling?.nextSibling as HTMLElement; - - selectedCode = codeDiv.getElementsByClassName("blob-code-inner")[0].textContent!; - } - if (isOutOfContextBounds([selectedCode, [] ], descriptionConfig.config.maxInputLength)) { - logo.classList.toggle("animate-spin"); - return alert(`Max input length exceeded. Try reducing the number of selected lines to refactor.`); - } - const token = await getAuthToken(); - const suggestionStream = await generateCodeSuggestion( - token, - descriptionConfig.config.language, - descriptionConfig.config.length, - descriptionConfig.config.temperature / 10, - selectedCode, - ); - - logo.classList.toggle("animate-spin"); - button.classList.toggle("pointer-events-none"); - if (!suggestionStream) { - return console.error("No description was generated!"); - } - const textArea = commentNode.querySelector(GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR)!; - - insertTextAtCursor(textArea as HTMLTextAreaElement, suggestionStream); - } catch (error: unknown) { - logo?.classList.toggle("animate-spin"); - button?.classList.toggle("pointer-events-none"); - - if (error instanceof Error) { - console.error("Description generation error:", error.message); - } - } -}; - diff --git a/src/content-scripts/components/AICodeReview/AICodeReviewButton.ts b/src/content-scripts/components/AICodeReview/AICodeReviewButton.ts new file mode 100644 index 00000000..273ed1f0 --- /dev/null +++ b/src/content-scripts/components/AICodeReview/AICodeReviewButton.ts @@ -0,0 +1,49 @@ +import { createHtmlElement } from "../../../utils/createHtmlElement"; +import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; +import { generateCodeExplanation, generateCodeSuggestion, generateCodeTest } from "../../../utils/ai-utils/openai"; +import { + AICodeReviewMenu, + AICodeReviewMenuItem, +} from "./AICodeReviewMenu"; + + +export const AICodeReviewButton = (commentNode: HTMLElement) => { + const changeSuggestorButton = createHtmlElement("a", { + innerHTML: ` + + `, + onclick: (event: MouseEvent) => { + event.stopPropagation(); + menu.classList.toggle("hidden"); + }, + id: "os-ai-change-gen", + }); + + const refactorCode = AICodeReviewMenuItem( + "Refactor Code", + "Generate a code refactor", + generateCodeSuggestion, + commentNode, + ); + const testCode = AICodeReviewMenuItem( + "Test Code", + "Generate a test for the code", + generateCodeTest, + commentNode, + ); + const explainCode = AICodeReviewMenuItem( + "Explain Code", + "Generate an explanation for the code", + generateCodeExplanation, + commentNode, + ); + + const menu = AICodeReviewMenu([refactorCode, testCode, explainCode]); + + changeSuggestorButton.append(menu); + return changeSuggestorButton; +}; + + diff --git a/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts new file mode 100644 index 00000000..c5d76ddf --- /dev/null +++ b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts @@ -0,0 +1,148 @@ +import { + SUPABASE_LOGIN_URL, + GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR, +} from "../../../constants"; +import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; +import { + DescriptionConfig, + getAIDescriptionConfig, +} from "../../../utils/ai-utils/descriptionconfig"; +import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; +import { createHtmlElement } from "../../../utils/createHtmlElement"; +import { isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; + +type SuggestionGenerator = ( + token: string, + code: string, + config: DescriptionConfig +) => Promise; + +export const AICodeReviewMenu = (items: HTMLLIElement[]) => { + const menu = createHtmlElement("div", { + className: "SelectMenu js-slash-command-menu hidden mt-6", + innerHTML: `
+
+
+ + OpenSauced.ai +
+
AI
+ + Give feedback + +
+
+
    +
+
+
`, + }); + + menu.querySelector("ul")?.append(...items); + + document.addEventListener("click", event => { + if (event.target instanceof HTMLElement) { + menu.classList.add("hidden"); + } + }); + return menu; +}; + +export const AICodeReviewMenuItem = (title: string, description: string, suggestionGenerator: SuggestionGenerator, commentNode: HTMLElement) => { + const menuItem = createHtmlElement("li", { + className: "SelectMenu-item d-block slash-command-menu-item", + role: "option", + onclick: () => { + void handleSubmit(suggestionGenerator, commentNode); + }, + innerHTML: `
${title}
+ ${description}`, + }); + + return menuItem; +}; + +const handleSubmit = async ( + suggestionGenerator: SuggestionGenerator, + commentNode: HTMLElement, +) => { + const logo = commentNode.querySelector("#ai-description-button-logo"); + const button = commentNode.querySelector("#os-ai-change-gen"); + + try { + if (!(await isLoggedIn())) { + return window.open(SUPABASE_LOGIN_URL, "_blank"); + } + + if (!logo || !button) { + return; + } + + const descriptionConfig = await getAIDescriptionConfig(); + + if (!descriptionConfig) { + return; + } + + logo.classList.toggle("animate-spin"); + button.classList.toggle("pointer-events-none"); + + const selectedLines = document.querySelectorAll( + ".code-review.selected-line", + ); + let selectedCode = Array.from(selectedLines) + .map(line => line.textContent) + .join("\n"); + + // find input with name="position" and get its value + if (!selectedCode) { + const positionElement = commentNode.querySelector( + "input[name=position]", + )!; + const position = positionElement.getAttribute("value")!; + + const codeDiv = document.querySelector(`[data-line-number="${position}"]`) + ?.nextSibling?.nextSibling as HTMLElement; + + selectedCode = + codeDiv.getElementsByClassName("blob-code-inner")[0].textContent!; + } + if ( + isOutOfContextBounds( + [selectedCode, [] ], + descriptionConfig.config.maxInputLength, + ) + ) { + logo.classList.toggle("animate-spin"); + return alert( + `Max input length exceeded. Try reducing the number of selected lines to refactor.`, + ); + } + const token = await getAuthToken(); + const suggestionStream = await suggestionGenerator( + token, + selectedCode, + descriptionConfig, + ); + + logo.classList.toggle("animate-spin"); + button.classList.toggle("pointer-events-none"); + if (!suggestionStream) { + return console.error("No description was generated!"); + } + const textArea = commentNode.querySelector( + GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR, + )!; + + insertTextAtCursor(textArea as HTMLTextAreaElement, suggestionStream); + } catch (error: unknown) { + logo?.classList.toggle("animate-spin"); + button?.classList.toggle("pointer-events-none"); + + if (error instanceof Error) { + console.error("Description generation error:", error.message); + } + } +}; diff --git a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts index 6c54d5a4..f5374fca 100644 --- a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts +++ b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts @@ -2,10 +2,10 @@ import { createHtmlElement } from "../../../utils/createHtmlElement"; import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; import { getPullRequestAPIURL } from "../../../utils/urlMatchers"; import { getDescriptionContext, isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; -import { generateDescription } from "../../../utils/aiprdescription/openai"; +import { generateDescription } from "../../../utils/ai-utils/openai"; 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 { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; +import { getAIDescriptionConfig } from "../../../utils/ai-utils/descriptionconfig"; import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; export const DescriptionGeneratorButton = () => { diff --git a/src/hooks/useRefs.ts b/src/hooks/useRefs.ts deleted file mode 100644 index f5a287c2..00000000 --- a/src/hooks/useRefs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useRef } from "react"; - -export const useRefs = () => { - const refs = useRef>({}); - - const setRefFromKey = (key: string) => (element: HTMLElement | null) => { - refs.current[key] = element; - }; - - return { refs: refs.current, setRefFromKey }; -}; diff --git a/src/popup/pages/aiprdescription.tsx b/src/popup/pages/aiprdescription.tsx index cf2e0dbc..5e82d402 100644 --- a/src/popup/pages/aiprdescription.tsx +++ b/src/popup/pages/aiprdescription.tsx @@ -10,14 +10,12 @@ import { DescriptionLanguage, setAIDescriptionConfig, getDefaultDescriptionConfig, -} from "../../utils/aiprdescription/descriptionconfig"; -import { useRefs } from "../../hooks/useRefs"; -import { configurationReducer } from "../../utils/aiprdescription/configurationReducer"; +} from "../../utils/ai-utils/descriptionconfig"; +import { configurationReducer } from "../../utils/ai-utils/configurationReducer"; import { goBack } from "react-chrome-extension-router"; const AIPRDescription = () => { const [config, dispatch] = useReducer(configurationReducer, getDefaultDescriptionConfig()); - const { refs, setRefFromKey } = useRefs(); const tones: DescriptionTone[] = ["exciting", "persuasive", "informative", "humorous", "formal"]; const sources: DescriptionSource[] = ["diff", "commitMessage", "both"]; @@ -36,14 +34,7 @@ const AIPRDescription = () => { const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); - const length = parseInt(refs.length?.getAttribute("value") ?? "0"); - const temperature = Number(Number(refs.temperature?.getAttribute("value") ?? "0")); - const maxInputLength = parseInt(refs.maxInputLength?.getAttribute("value") ?? "0"); - const language = (refs.language as HTMLSelectElement).value as DescriptionLanguage; - const source = (refs.source as HTMLSelectElement).value as DescriptionSource; - const tone = (refs.tone as HTMLSelectElement).value as DescriptionTone; - - void setAIDescriptionConfig({ config: { length, temperature, maxInputLength, language, source, tone } }); + void setAIDescriptionConfig(config); toast.success("Configuration updated!"); }; @@ -75,7 +66,6 @@ const AIPRDescription = () => {
@@ -94,7 +84,6 @@ const AIPRDescription = () => {

{

{

{

Description Language

{

Description Source