Skip to content

Commit

Permalink
feat: AI PR review menu (#174)
Browse files Browse the repository at this point in the history
* feat: code-test-refactor-explanation menu

* refactor: pr description config page

* chore: restructure AICodeReview

* fix: discussions URL

* fix: dependency vulnerability

* fix: multiple processing of PR page

* refactor: Move AI utilities to ai-utils dir

* chore: use new endpoints

* chore: Added optional trailing slash PR page regex

* Update src/content-scripts/components/AICodeReview/AICodeReviewButton.ts

Co-authored-by: Divyansh Singh <[email protected]>

---------

Co-authored-by: Divyansh Singh <[email protected]>
  • Loading branch information
Anush008 and diivi authored Jun 15, 2023
1 parent ab8fb16 commit ef52439
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 207 deletions.
7 changes: 3 additions & 4 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

49 changes: 49 additions & 0 deletions src/content-scripts/components/AICodeReview/AICodeReviewButton.ts
Original file line number Diff line number Diff line change
@@ -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: `<span id="ai-change-gen" class="toolbar-item btn-octicon">
<img class="octicon octicon-heading" height="16px" width="16px" id="ai-description-button-logo" src=${chrome.runtime.getURL(
openSaucedLogoIcon,
)}>
</span>`,
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;
};


148 changes: 148 additions & 0 deletions src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>;

export const AICodeReviewMenu = (items: HTMLLIElement[]) => {
const menu = createHtmlElement("div", {
className: "SelectMenu js-slash-command-menu hidden mt-6",
innerHTML: `<div class="SelectMenu-modal no-underline">
<header class="SelectMenu-header">
<div class="flex-1">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" class="octicon octicon-code-square">
<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"></path>
</svg>
<span class="color-fg-muted text-small pl-1">OpenSauced.ai</span>
</div>
<div class="Label Label--success">AI</div>
<a class="ml-1 color-fg-muted d-block" target="_blank" href="https://github.com/orgs/open-sauced/discussions">
Give feedback
</a>
</header>
<div class="SelectMenu-list js-command-list-container" style="max-height: 270px;" id="combobox-5123">
<ul role="listbox" class="SelectMenu-list js-slash-command-menu-items">
</ul>
</div>
</div>`,
});

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: `<h5>${title}</h5>
<span class="command-description">${description}</span>`,
});

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);
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
11 changes: 0 additions & 11 deletions src/hooks/useRefs.ts

This file was deleted.

Loading

0 comments on commit ef52439

Please sign in to comment.