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: ``,
+ });
+
+ 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 = () => {
{
{
{
Description Language