From de3922a3f6718d2e8a55870b8cc682e6999d5c46 Mon Sep 17 00:00:00 2001
From: Anush008
Date: Sun, 11 Jun 2023 13:11:53 +0530
Subject: [PATCH 01/10] feat: code-test-refactor-explanation menu
---
src/constants.ts | 2 +
.../AICodeRefactor/ChangeSuggestorButton.ts | 111 +++++--------
.../AICodeReviewMenu/AICodeReviewMenu.ts | 148 ++++++++++++++++++
src/utils/aiprdescription/openai.ts | 91 ++++++++++-
4 files changed, 271 insertions(+), 81 deletions(-)
create mode 100644 src/content-scripts/components/AICodeReviewMenu/AICodeReviewMenu.ts
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
index fa6f1c58..277fb549 100644
--- a/src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts
+++ b/src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts
@@ -1,88 +1,49 @@
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";
+import { generateCodeExplanation, generateCodeSuggestion, generateCodeTest } from "../../../utils/aiprdescription/openai";
+import {
+ AICodeReviewMenu,
+ AICodeReviewMenuItem,
+} from "../AICodeReviewMenu/AICodeReviewMenu";
+
export const ChangeSuggestorButton = (commentNode: HTMLElement) => {
const changeSuggestorButton = createHtmlElement("a", {
innerHTML: `
-
-
- Get Refactor Suggestions`,
- onclick: async () => handleSubmit(commentNode),
+
+ `,
+ 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;
};
-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/AICodeReviewMenu/AICodeReviewMenu.ts b/src/content-scripts/components/AICodeReviewMenu/AICodeReviewMenu.ts
new file mode 100644
index 00000000..8ef9fb2c
--- /dev/null
+++ b/src/content-scripts/components/AICodeReviewMenu/AICodeReviewMenu.ts
@@ -0,0 +1,148 @@
+import {
+ SUPABASE_LOGIN_URL,
+ GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR,
+} from "../../../constants";
+import { insertTextAtCursor } from "../../../utils/aiprdescription/cursorPositionInsert";
+import {
+ DescriptionConfig,
+ getAIDescriptionConfig,
+} from "../../../utils/aiprdescription/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/utils/aiprdescription/openai.ts b/src/utils/aiprdescription/openai.ts
index dbed8db9..391885df 100644
--- a/src/utils/aiprdescription/openai.ts
+++ b/src/utils/aiprdescription/openai.ts
@@ -1,5 +1,5 @@
-import { OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT, OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT } from "../../constants";
-import type { DescriptionTone } from "./descriptionconfig";
+import { OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT, OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT, OPEN_SAUCED_AI_CODE_TEST_ENDPOINT, OPEN_SAUCED_AI_CODE_EXPLANATION_ENDPOINT } from "../../constants";
+import type { DescriptionConfig, DescriptionTone } from "./descriptionconfig";
export const generateDescription = async (
apiKey: string,
@@ -42,10 +42,8 @@ export const generateDescription = async (
export const generateCodeSuggestion = async (
apiKey: string,
- language: string,
- descriptionLength: number,
- temperature: number,
code: string,
+ { config: { length, temperature, language } }: DescriptionConfig,
): Promise => {
try {
const response = await fetch(OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT, {
@@ -55,7 +53,7 @@ export const generateCodeSuggestion = async (
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
- descriptionLength,
+ descriptionLength: length,
temperature,
language,
code,
@@ -75,3 +73,84 @@ export const generateCodeSuggestion = async (
return undefined;
};
+export const generateCodeTest = async (
+ apiKey: string,
+ code: string,
+ { config: { length, temperature, language } }: DescriptionConfig,
+): Promise =>
+
+/*
+ * try {
+ * const response = await fetch(OPEN_SAUCED_AI_CODE_TEST_ENDPOINT, {
+ * method: "POST",
+ * headers: {
+ * "Content-Type": "application/json",
+ * Authorization: `Bearer ${apiKey}`,
+ * },
+ * body: JSON.stringify({
+ * descriptionLength: length,
+ * temperature,
+ * language,
+ * code,
+ * }),
+ * });
+ */
+
+/*
+ * if (response.status === 201) {
+ * const { suggestion } = await response.json();
+ */
+
+/*
+ * return suggestion;
+ * }
+ * } catch (error: unknown) {
+ * if (error instanceof Error) {
+ * console.error("OpenAI error: ", error.message);
+ * }
+ * }
+ * return undefined;
+ */
+
+ "Some generic test code";
+export const generateCodeExplanation = async (
+ apiKey: string,
+ code: string,
+ { descriptionLength, temperature, language }: DescriptionConfig,
+): Promise =>
+
+/*
+ * try {
+ * const response = await fetch(OPEN_SAUCED_AI_CODE_EXPLANATION_ENDPOINT, {
+ * method: "POST",
+ * headers: {
+ * "Content-Type": "application/json",
+ * Authorization: `Bearer ${apiKey}`,
+ * },
+ * body: JSON.stringify({
+ * descriptionLength: length,
+ * temperature,
+ * language,
+ * code,
+ * }),
+ * });
+ */
+
+/*
+ * if (response.status === 201) {
+ * const { suggestion } = await response.json();
+ */
+
+/*
+ * return suggestion;
+ * }
+ * } catch (error: unknown) {
+ * if (error instanceof Error) {
+ * console.error("OpenAI error: ", error.message);
+ * }
+ * }
+ * return undefined;
+ */
+
+ "Some generic explanation";
+
From 27616662de9d062f36a980059de04b73e0839322 Mon Sep 17 00:00:00 2001
From: Anush008
Date: Sun, 11 Jun 2023 13:23:14 +0530
Subject: [PATCH 02/10] refactor: pr description config page
---
src/hooks/useRefs.ts | 11 -----------
src/popup/pages/aiprdescription.tsx | 21 ++-------------------
2 files changed, 2 insertions(+), 30 deletions(-)
delete mode 100644 src/hooks/useRefs.ts
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 1ebedba4..caffa903 100644
--- a/src/popup/pages/aiprdescription.tsx
+++ b/src/popup/pages/aiprdescription.tsx
@@ -1,7 +1,6 @@
-import React, { useEffect, useReducer, useState } from "react";
+import React, { useEffect, useReducer } from "react";
import { FaChevronLeft } from "react-icons/fa";
import OpenSaucedLogo from "../../assets/opensauced-logo.svg";
-import { ImSwitch } from "react-icons/im";
import toast, { Toaster } from "react-hot-toast";
import {
@@ -12,13 +11,11 @@ import {
setAIDescriptionConfig,
getDefaultDescriptionConfig,
} from "../../utils/aiprdescription/descriptionconfig";
-import { useRefs } from "../../hooks/useRefs";
import { configurationReducer } from "../../utils/aiprdescription/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"];
@@ -37,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!");
};
@@ -76,7 +66,6 @@ const AIPRDescription = () => {
{
{
{
Description Language