Skip to content

Commit

Permalink
feat: generate code suggestions via AI (#90)
Browse files Browse the repository at this point in the history
* feat: generate code suggestions via AI

* avoid multiple icons

* multiple reviews

* refactor a single line of code

* extensions suggestions

* dont blindly believe ai

* add delay to DOM manipulation

* improve type checks

* remove description tone from code suggestions

* update to opensauced api endpoint

* update stream selector

* fix: showing the icon (#104)

---------

Co-authored-by: Abdurrahman Rajab <[email protected]>
  • Loading branch information
diivi and a0m0rajab authored May 23, 2023
1 parent d83480c commit 14eede1
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 28 deletions.
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ export const OPEN_SAUCED_REPOS_ENDPOINT = "https://api.opensauced.pizza/v1/repos
export const OPEN_SAUCED_SESSION_ENDPOINT = "https://api.opensauced.pizza/v1/auth/session";
export const OPEN_SAUCED_USER_INSIGHTS_ENDPOINT = "https://api.opensauced.pizza/v1/user/insights";
export const OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT = "https://api.opensauced.pizza/v1/prs/description/generate";
export const OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT = "https://api.opensauced.pizza/v1/prs/suggestion/generate";

// GitHub constants/selectors
export const GITHUB_PROFILE_MENU_SELECTOR = ".p-nickname.vcard-username.d-block";
export const GITHUB_PROFILE_EDIT_MENU_SELECTOR = "button.js-profile-editable-edit-button";
export const GITHUB_PR_COMMENT_HEADER_SELECTOR = "timeline-comment-header clearfix d-flex";
export const GITHUB_NEW_PR_COMMENT_EDITOR_SELECTOR = "flex-nowrap d-none d-md-inline-block mr-md-0 mr-3";
export const GITHUB_PR_COMMENT_EDITOR_SELECTOR = "flex-nowrap d-inline-block mr-3";
export const GITHUB_REVIEW_SUGGESTION_SELECTOR = "js-suggestion-button-placeholder";
export const GITHUB_REPO_ACTIONS_SELECTOR = ".pagehead-actions";
export const GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR = "pull_request[body]";
export const GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR = "[name='comment[body]']";
export const GITHUB_PR_BASE_BRANCH_SELECTOR = "css-truncate css-truncate-target";

// External Links
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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: `<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>
<tool-tip for="ai-change-gen" class="sr-only" role="tooltip">Get Refactor Suggestions</tool-tip>`,
onclick: async () => handleSubmit(commentNode),
id: "os-ai-change-gen",
});

return changeSuggestorButton;
};

const handleSubmit = async (commentNode: HTMLElement) => {
try {
if (!(await isLoggedIn())) {
return window.open(SUPABASE_LOGIN_URL, "_blank");
}
const logo = document.getElementById("ai-description-button-logo");

if (!logo) {
return;
}

const descriptionConfig = await getAIDescriptionConfig();

if (!descriptionConfig) {
return;
}
if (!descriptionConfig.enabled) {
return alert("AI PR description is disabled!");
}

logo.classList.toggle("animate-spin");

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");
if (!suggestionStream) {
return console.error("No description was generated!");
}
const textArea = commentNode.querySelector(GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR)!;

void insertTextAtCursor(textArea as HTMLTextAreaElement, suggestionStream);
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Description generation error:", error.message);
}
}
};

Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ const handleSubmit = async () => {
}
logo.classList.toggle("animate-spin");
const [diff, commitMessages] = await getDescriptionContext(url, descriptionConfig.config.source);
if(!diff && !commitMessages) {

if (!diff && !commitMessages) {
logo.classList.toggle("animate-spin");
return alert(`No input context was generated.`)
return alert(`No input context was generated.`);
}
if (isOutOfContextBounds([diff, commitMessages], descriptionConfig.config.maxInputLength)) {
logo.classList.toggle("animate-spin");
Expand Down
6 changes: 5 additions & 1 deletion src/content-scripts/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isGithubPullRequestPage,
isGithubRepoPage,
isPullRequestCreatePage,
isPullRequestFilesChangedPage,
} from "../utils/urlMatchers";
import { isOpenSaucedUser } from "../utils/fetchOpenSaucedApiData";
import injectViewOnOpenSauced from "../utils/dom-utils/viewOnOpenSauced";
Expand All @@ -13,14 +14,17 @@ import injectAddPRToHighlightsButton from "../utils/dom-utils/addPRToHighlights"
import injectRepoVotingButtons from "../utils/dom-utils/repoVotingButtons";
import domUpdateWatch from "../utils/dom-utils/domUpdateWatcher";
import injectDescriptionGeneratorButton from "../utils/dom-utils/addDescriptionGenerator";
import prEditWatch from "../utils/dom-utils/prEditWatcher";
import injectChangeSuggestorButton from "../utils/dom-utils/changeSuggestorButton";
import prEditWatch, { prReviewWatch } from "../utils/dom-utils/prWatcher";

const processGithubPage = async () => {
if (prefersDarkMode(document.cookie)) {
document.documentElement.classList.add("dark");
}
if (isPullRequestCreatePage(window.location.href)) {
void injectDescriptionGeneratorButton();
} else if (isPullRequestFilesChangedPage(window.location.href)) {
prReviewWatch(injectChangeSuggestorButton, 500);
} else if (isGithubPullRequestPage(window.location.href)) {
prEditWatch(injectDescriptionGeneratorButton);
void injectAddPRToHighlightsButton();
Expand Down
46 changes: 41 additions & 5 deletions src/utils/aiprdescription/openai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT } from "../../constants";
import { OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT, OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT } from "../../constants";
import type { DescriptionTone } from "./descriptionconfig";

export const generateDescription = async (
Expand All @@ -10,32 +10,68 @@ export const generateDescription = async (
diff?: string,
commitMessages?: string[],
): Promise<string | undefined> => {

try {
const response = await fetch(OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
descriptionLength,
temperature,
tone,
language,
diff,
commitMessages
})
commitMessages,
}),
});

if (response.status === 201) {
const { description } = await response.json();

return description;
}
} catch (error: unknown) {
if (error instanceof Error) {
console.error("OpenAI error: ", error.message);
}
}
return undefined;
};

export const generateCodeSuggestion = async (
apiKey: string,
language: string,
descriptionLength: number,
temperature: number,
code: string,
): Promise<string | undefined> => {
try {
const response = await fetch(OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
descriptionLength,
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;
};

19 changes: 19 additions & 0 deletions src/utils/dom-utils/changeSuggestorButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ChangeSuggestorButton } from "../../content-scripts/components/AICodeRefactor/ChangeSuggestorButton";
import { GITHUB_REVIEW_SUGGESTION_SELECTOR } from "../../constants";
import { isPublicRepository } from "../fetchGithubAPIData";

const injectChangeSuggestorButton = async (commentNode: HTMLElement) => {
if (!(await isPublicRepository(window.location.href))) {
return;
}

const suggestChangesIcon = commentNode.getElementsByClassName(GITHUB_REVIEW_SUGGESTION_SELECTOR)[0];
const changeSuggestorButton = ChangeSuggestorButton(commentNode);

if (suggestChangesIcon.querySelector("#os-ai-change-gen")) {
return;
}
suggestChangesIcon.insertBefore(changeSuggestorButton, suggestChangesIcon.firstChild);
};

export default injectChangeSuggestorButton;
14 changes: 0 additions & 14 deletions src/utils/dom-utils/prEditWatcher.ts

This file was deleted.

33 changes: 33 additions & 0 deletions src/utils/dom-utils/prWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const prEditWatch = (callback: () => void, delayInMs = 0) => {
const observer = new MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
for (const mutation of mutationList) {
if (Array.from((mutation.target as HTMLElement).classList).includes("is-comment-editing")) {
setTimeout(callback, delayInMs);
observer.disconnect();
}
}
});

observer.observe(document.body, { attributes: true, subtree: true });
};

export const prReviewWatch = (callback: (node: HTMLElement) => void, delayInMs = 0) => {
const githubCommentSelector = "inline-comment-form-container";
const observer = new MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
mutationList.forEach(mutation => {
if (Array.from((mutation.target as HTMLElement).classList).includes(githubCommentSelector)) {
setTimeout(() => {
const commentNodes = document.getElementsByClassName(githubCommentSelector);

Array.from(commentNodes).forEach(node => {
callback(node as HTMLElement);
});
}, delayInMs);
}
});
});

observer.observe(document.body, { attributes: true, subtree: true, childList: true });
};

export default prEditWatch;
17 changes: 11 additions & 6 deletions src/utils/urlMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const isGithubProfilePage = (url: string) => {
return githubProfilePattern.test(url);
};


export const isGithubRepoPage = (url: string) => {
const githubRepoPattern = /github\.com\/[^/]+\/[^/]+$/;

Expand All @@ -47,17 +46,23 @@ export const isPullRequestCreatePage = (url: string) => {
return githubPullRequestPattern.test(url);
};

export const isPullRequestFilesChangedPage = (url: string) => {
const githubPullRequestFilesChangedPattern = /github\.com\/[\w.-]+\/[^/]+\/pull\/\d+\/files/;

return githubPullRequestFilesChangedPattern.test(url);
};

export const getPullRequestAPIURL = (url: string) => {
const apiURL = url.replace(/github\.com/, "api.github.com/repos");

if (isGithubPullRequestPage(url)) {
return apiURL.replace("pull", "pulls");
}

if (url.match(/compare\/.*\.\.\./)) {
return apiURL;
}
const baseBranch = document.getElementsByClassName(GITHUB_PR_BASE_BRANCH_SELECTOR)[1].textContent;
if (url.match(/compare\/.*\.\.\./)) {
return apiURL;
}
const baseBranch = document.getElementsByClassName(GITHUB_PR_BASE_BRANCH_SELECTOR)[1].textContent;

return apiURL.replace(/compare\//, `compare/${baseBranch}...`);
return apiURL.replace(/compare\//, `compare/${baseBranch}...`);
};

0 comments on commit 14eede1

Please sign in to comment.