Skip to content

Commit

Permalink
feat: OpenSauced AI PR description (#79)
Browse files Browse the repository at this point in the history
* WIP just starting

* not sure why it is not appending new element

* feat: AI highlight button

* feat: cursor position text insert, pr info

* feat: AI description config window

* feat: pr actions dropdown

* chore: Create PR description button

* feat: OpenAI Completion

* feat: type response from stream

* feat: Config window new values, save button

* chore: Default description config setters

* chore: form config submit

* refactor: formatting, linting fixes

* chore: description config type update, dropdown toggle

* chore: disable state handle

* chore: updated title

* fix: missing base branch, spinner

* chore: request errors to console

* feat: conditional diff, commitmsg fetching

* feat: Input context length check, redirect when logged out

* feat: highlights dropdown collapse

* feat: Edit PR button injection, tokenizer length check

* feat: Private repository check

* feat: "Description generated using OpenSauced" append

---------

Co-authored-by: Brian 'bdougie' Douglas <[email protected]>
  • Loading branch information
Anush008 and bdougie authored May 18, 2023
1 parent f90f275 commit 0e38777
Show file tree
Hide file tree
Showing 22 changed files with 873 additions and 31 deletions.
96 changes: 95 additions & 1 deletion npm-shrinkwrap.json

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

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
"dependencies": {
"@types/chrome": "^0.0.231",
"@types/node-emoji": "^1.8.2",
"gpt-tokenizer": "^1.0.5",
"node-emoji": "^1.11.0",
"openai-streams": "^5.3.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0"
},
"devDependencies": {
"@crxjs/vite-plugin": "^1.0.14",
"@types/node": "^20.1.4",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Home from "./pages/home";
import Loading from "./pages/loading";
import { Profile } from "./pages/profile";
import { useAuth } from "./hooks/useAuth";
import AIPRDescription from "./pages/aiprdescription";

export const RouteContext = createContext<{ page: { name: string, props?: any }, setCurrentPage:(page: RouteKeys, props?: any) => void }>({ page: { name: "loading" }, setCurrentPage: () => {} });

Expand All @@ -13,6 +14,7 @@ const routes = {
home: <Home />,
loading: <Loading />,
profile: <Profile />,
aiprdescription: <AIPRDescription />,
};

type RouteKeys = keyof typeof routes;
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export const SUPABASE_LOGIN_URL = "https://ibcwmlhcimymasokhgvn.supabase.co/auth
export const SUPABASE_AUTH_COOKIE_NAME = "supabase-auth-token";
export const OPEN_SAUCED_AUTH_TOKEN_KEY = "os-access-token";
export const OPEN_SAUCED_INSIGHTS_DOMAIN = "insights.opensauced.pizza";
export const AI_PR_DESCRIPTION_CONFIG_KEY = "ai-pr-description-config";
export const OPEN_AI_COMPLETION_MODEL_NAME = "gpt-3.5-turbo";

// API endpoints
export const OPEN_SAUCED_USERS_ENDPOINT = "https://api.opensauced.pizza/v1/users";
Expand All @@ -16,4 +18,8 @@ export const GITHUB_PROFILE_EDIT_MENU_SELECTOR = "button.js-profile-editable-edi
export const GITHUB_PR_AUTHOR_USERNAME_SELECTOR = "author Link--primary text-bold css-overflow-wrap-anywhere";
export const GITHUB_LOGGED_IN_USER_USERNAME_SELECTOR = "meta[name=\"user-login\"]";
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_REPO_ACTIONS_SELECTOR = ".pagehead-actions";
export const GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR = "pull_request[body]";
export const GITHUB_PR_BASE_BRANCH_SELECTOR = "css-truncate css-truncate-target";
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,38 @@ import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg";

export const AddPRToHighlightsButton = () => {
const addPRToHighlightsButton = createHtmlElement("a", {
href: `https://insights.opensauced.pizza/feed?url=${encodeURIComponent(window.location.href)}`,
target: "_blank",
rel: "noopener noreferrer",
innerHTML: `<span aria-label="Add PR to OpenSauced highlights." data-view-component="true" class="tooltipped tooltipped-n">
<img data-view-component="true" class="mr-1 mt-1" height="16px" width="16px" src=${chrome.runtime.getURL(openSaucedLogoIcon)}>
</span>`,
className: "relative cursor-pointer",
innerHTML: `<img class="mr-1 mt-1" height="16px" width="16px" src=${chrome.runtime.getURL(
openSaucedLogoIcon,
)}>
<details-menu id="details-menu-os" class="dropdown-menu hidden dropdown-menu-sw color-fg-default w-48 mt-2">
<a href="https://insights.opensauced.pizza/feed?prurl=${encodeURIComponent(window.location.href)}" class="dropdown-item" target="_blank">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-plus">
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path>
</svg> Add PR to Highlights
</a>
</details-menu>`,
onclick: () => {
const menu = document.getElementById("details-menu-os");

if (!menu) {
return;
}
menu.classList.toggle("hidden");
},
});

addPRToHighlightsButton.addEventListener("click", e => {
e.stopPropagation();
});

document.addEventListener("click", () => {
const menu = document.getElementById("details-menu-os");

if (!menu) {
return;
}
menu.classList.add("hidden");
});

return addPRToHighlightsButton;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createHtmlElement } from "../../../utils/createHtmlElement";
import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg";
import { getPullRequestAPIURL } from "../../../utils/urlMatchers";
import { getDescriptionContext, isContextWithinBounds } from "../../../utils/fetchGithubAPIData";
import { generateDescription } from "../../../utils/aiprdescription/openai";
import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, OPEN_AI_COMPLETION_MODEL_NAME, SUPABASE_LOGIN_URL } from "../../../constants";
import { insertAtCursorFromStream } from "../../../utils/aiprdescription/cursorPositionInsert";
import { getAIDescriptionConfig } from "../../../utils/aiprdescription/descriptionconfig";
import { isLoggedIn } from "../../../utils/checkAuthentication";

export const DescriptionGeneratorButton = () => {
const descriptionGeneratorButton = createHtmlElement("a", {
innerHTML: `<span id="ai-description-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-description-gen">Generate PR description</tool-tip>`,
onclick: handleSubmit,

});

return descriptionGeneratorButton;
};

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

if (!logo) {
return;
}
const url = getPullRequestAPIURL(window.location.href);
const descriptionConfig = await getAIDescriptionConfig();

if (!descriptionConfig) {
return;
}
if (!descriptionConfig.enabled) {
return alert("AI PR description is disabled!");
}
logo.classList.toggle("animate-spin");
const [diff, commitMessages] = await getDescriptionContext(url, descriptionConfig.config.source);

if (!isContextWithinBounds([diff, commitMessages], descriptionConfig.config.maxInputLength)) {
logo.classList.toggle("animate-spin");
return alert(`Max input length exceeded. Try setting the description source to commit-messages.`);
}
const descriptionStream = await generateDescription(
descriptionConfig.config.openai_api_key,
OPEN_AI_COMPLETION_MODEL_NAME,
descriptionConfig.config.language,
descriptionConfig.config.length,
descriptionConfig.config.temperature / 10,
descriptionConfig.config.tone,
diff,
commitMessages,
);

logo.classList.toggle("animate-spin");
if (!descriptionStream) {
return console.error("No description was generated!");
}
const textArea = document.getElementsByName(GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR)[0] as HTMLTextAreaElement;

void insertAtCursorFromStream(textArea, descriptionStream);
} catch (error: unknown) {
if (error instanceof Error) {
console.error("Description generation error:", error.message);
}
}
};
13 changes: 9 additions & 4 deletions src/content-scripts/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
isGithubProfilePage,
isGithubPullRequestPage,
isGithubRepoPage,
isPullRequestCreatePage,
} from "../utils/urlMatchers";
import { isOpenSaucedUser } from "../utils/fetchOpenSaucedApiData";
import injectViewOnOpenSauced from "../utils/dom-utils/viewOnOpenSauced";
Expand All @@ -11,14 +12,18 @@ import { prefersDarkMode } from "../utils/colorPreference";
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";

const processGithubPage = async () => {
if (prefersDarkMode(document.cookie)) {
document.documentElement.classList.add("dark");
}

if (isGithubPullRequestPage(window.location.href)) {
await injectAddPRToHighlightsButton();
if (isPullRequestCreatePage(window.location.href)) {
void injectDescriptionGeneratorButton();
} else if (isGithubPullRequestPage(window.location.href)) {
prEditWatch(injectDescriptionGeneratorButton);
void injectAddPRToHighlightsButton();
} else if (isGithubProfilePage(window.location.href)) {
const username = getGithubUsername(window.location.href);

Expand All @@ -37,7 +42,7 @@ const processGithubPage = async () => {
await injectRepoVotingButtons(ownerName, repoName);
}

domUpdateWatch(processGithubPage, 25);
domUpdateWatch(processGithubPage, 50);
};

void processGithubPage();
Loading

0 comments on commit 0e38777

Please sign in to comment.