Skip to content

Commit ef52439

Browse files
Anush008diivi
andauthored
feat: AI PR review menu (#174)
* 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]>
1 parent ab8fb16 commit ef52439

File tree

17 files changed

+357
-207
lines changed

17 files changed

+357
-207
lines changed

npm-shrinkwrap.json

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const OPEN_SAUCED_USER_INSIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/u
1717
export const OPEN_SAUCED_AI_PR_DESCRIPTION_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/description/generate`;
1818
export const OPEN_SAUCED_USER_HIGHLIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/user/highlights`;
1919
export const OPEN_SAUCED_AI_CODE_REFACTOR_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/suggestion/generate`;
20+
export const OPEN_SAUCED_AI_CODE_EXPLANATION_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/explanation/generate`;
21+
export const OPEN_SAUCED_AI_CODE_TEST_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/prs/test/generate`;
2022
export const OPEN_SAUCED_HIGHLIGHTS_ENDPOINT = `${OPEN_SAUCED_API_ENDPOINT}/highlights/list`;
2123

2224
// GitHub constants/selectors

src/content-scripts/components/AICodeRefactor/ChangeSuggestorButton.ts

Lines changed: 0 additions & 88 deletions
This file was deleted.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { createHtmlElement } from "../../../utils/createHtmlElement";
2+
import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg";
3+
import { generateCodeExplanation, generateCodeSuggestion, generateCodeTest } from "../../../utils/ai-utils/openai";
4+
import {
5+
AICodeReviewMenu,
6+
AICodeReviewMenuItem,
7+
} from "./AICodeReviewMenu";
8+
9+
10+
export const AICodeReviewButton = (commentNode: HTMLElement) => {
11+
const changeSuggestorButton = createHtmlElement("a", {
12+
innerHTML: `<span id="ai-change-gen" class="toolbar-item btn-octicon">
13+
<img class="octicon octicon-heading" height="16px" width="16px" id="ai-description-button-logo" src=${chrome.runtime.getURL(
14+
openSaucedLogoIcon,
15+
)}>
16+
</span>`,
17+
onclick: (event: MouseEvent) => {
18+
event.stopPropagation();
19+
menu.classList.toggle("hidden");
20+
},
21+
id: "os-ai-change-gen",
22+
});
23+
24+
const refactorCode = AICodeReviewMenuItem(
25+
"Refactor Code",
26+
"Generate a code refactor",
27+
generateCodeSuggestion,
28+
commentNode,
29+
);
30+
const testCode = AICodeReviewMenuItem(
31+
"Test Code",
32+
"Generate a test for the code",
33+
generateCodeTest,
34+
commentNode,
35+
);
36+
const explainCode = AICodeReviewMenuItem(
37+
"Explain Code",
38+
"Generate an explanation for the code",
39+
generateCodeExplanation,
40+
commentNode,
41+
);
42+
43+
const menu = AICodeReviewMenu([refactorCode, testCode, explainCode]);
44+
45+
changeSuggestorButton.append(menu);
46+
return changeSuggestorButton;
47+
};
48+
49+
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
SUPABASE_LOGIN_URL,
3+
GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR,
4+
} from "../../../constants";
5+
import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert";
6+
import {
7+
DescriptionConfig,
8+
getAIDescriptionConfig,
9+
} from "../../../utils/ai-utils/descriptionconfig";
10+
import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication";
11+
import { createHtmlElement } from "../../../utils/createHtmlElement";
12+
import { isOutOfContextBounds } from "../../../utils/fetchGithubAPIData";
13+
14+
type SuggestionGenerator = (
15+
token: string,
16+
code: string,
17+
config: DescriptionConfig
18+
) => Promise<string | undefined>;
19+
20+
export const AICodeReviewMenu = (items: HTMLLIElement[]) => {
21+
const menu = createHtmlElement("div", {
22+
className: "SelectMenu js-slash-command-menu hidden mt-6",
23+
innerHTML: `<div class="SelectMenu-modal no-underline">
24+
<header class="SelectMenu-header">
25+
<div class="flex-1">
26+
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" class="octicon octicon-code-square">
27+
<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>
28+
</svg>
29+
<span class="color-fg-muted text-small pl-1">OpenSauced.ai</span>
30+
</div>
31+
<div class="Label Label--success">AI</div>
32+
<a class="ml-1 color-fg-muted d-block" target="_blank" href="https://github.com/orgs/open-sauced/discussions">
33+
Give feedback
34+
</a>
35+
</header>
36+
<div class="SelectMenu-list js-command-list-container" style="max-height: 270px;" id="combobox-5123">
37+
<ul role="listbox" class="SelectMenu-list js-slash-command-menu-items">
38+
</ul>
39+
</div>
40+
</div>`,
41+
});
42+
43+
menu.querySelector("ul")?.append(...items);
44+
45+
document.addEventListener("click", event => {
46+
if (event.target instanceof HTMLElement) {
47+
menu.classList.add("hidden");
48+
}
49+
});
50+
return menu;
51+
};
52+
53+
export const AICodeReviewMenuItem = (title: string, description: string, suggestionGenerator: SuggestionGenerator, commentNode: HTMLElement) => {
54+
const menuItem = createHtmlElement("li", {
55+
className: "SelectMenu-item d-block slash-command-menu-item",
56+
role: "option",
57+
onclick: () => {
58+
void handleSubmit(suggestionGenerator, commentNode);
59+
},
60+
innerHTML: `<h5>${title}</h5>
61+
<span class="command-description">${description}</span>`,
62+
});
63+
64+
return menuItem;
65+
};
66+
67+
const handleSubmit = async (
68+
suggestionGenerator: SuggestionGenerator,
69+
commentNode: HTMLElement,
70+
) => {
71+
const logo = commentNode.querySelector("#ai-description-button-logo");
72+
const button = commentNode.querySelector("#os-ai-change-gen");
73+
74+
try {
75+
if (!(await isLoggedIn())) {
76+
return window.open(SUPABASE_LOGIN_URL, "_blank");
77+
}
78+
79+
if (!logo || !button) {
80+
return;
81+
}
82+
83+
const descriptionConfig = await getAIDescriptionConfig();
84+
85+
if (!descriptionConfig) {
86+
return;
87+
}
88+
89+
logo.classList.toggle("animate-spin");
90+
button.classList.toggle("pointer-events-none");
91+
92+
const selectedLines = document.querySelectorAll(
93+
".code-review.selected-line",
94+
);
95+
let selectedCode = Array.from(selectedLines)
96+
.map(line => line.textContent)
97+
.join("\n");
98+
99+
// find input with name="position" and get its value
100+
if (!selectedCode) {
101+
const positionElement = commentNode.querySelector(
102+
"input[name=position]",
103+
)!;
104+
const position = positionElement.getAttribute("value")!;
105+
106+
const codeDiv = document.querySelector(`[data-line-number="${position}"]`)
107+
?.nextSibling?.nextSibling as HTMLElement;
108+
109+
selectedCode =
110+
codeDiv.getElementsByClassName("blob-code-inner")[0].textContent!;
111+
}
112+
if (
113+
isOutOfContextBounds(
114+
[selectedCode, [] ],
115+
descriptionConfig.config.maxInputLength,
116+
)
117+
) {
118+
logo.classList.toggle("animate-spin");
119+
return alert(
120+
`Max input length exceeded. Try reducing the number of selected lines to refactor.`,
121+
);
122+
}
123+
const token = await getAuthToken();
124+
const suggestionStream = await suggestionGenerator(
125+
token,
126+
selectedCode,
127+
descriptionConfig,
128+
);
129+
130+
logo.classList.toggle("animate-spin");
131+
button.classList.toggle("pointer-events-none");
132+
if (!suggestionStream) {
133+
return console.error("No description was generated!");
134+
}
135+
const textArea = commentNode.querySelector(
136+
GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR,
137+
)!;
138+
139+
insertTextAtCursor(textArea as HTMLTextAreaElement, suggestionStream);
140+
} catch (error: unknown) {
141+
logo?.classList.toggle("animate-spin");
142+
button?.classList.toggle("pointer-events-none");
143+
144+
if (error instanceof Error) {
145+
console.error("Description generation error:", error.message);
146+
}
147+
}
148+
};

src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { createHtmlElement } from "../../../utils/createHtmlElement";
22
import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg";
33
import { getPullRequestAPIURL } from "../../../utils/urlMatchers";
44
import { getDescriptionContext, isOutOfContextBounds } from "../../../utils/fetchGithubAPIData";
5-
import { generateDescription } from "../../../utils/aiprdescription/openai";
5+
import { generateDescription } from "../../../utils/ai-utils/openai";
66
import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, SUPABASE_LOGIN_URL } from "../../../constants";
7-
import { insertTextAtCursor } from "../../../utils/aiprdescription/cursorPositionInsert";
8-
import { getAIDescriptionConfig } from "../../../utils/aiprdescription/descriptionconfig";
7+
import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert";
8+
import { getAIDescriptionConfig } from "../../../utils/ai-utils/descriptionconfig";
99
import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication";
1010

1111
export const DescriptionGeneratorButton = () => {

src/hooks/useRefs.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)