Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AI PR review menu #174

Merged
merged 10 commits into from
Jun 15, 2023
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