From ceec9d4ecc96e425dc3a81db303ee72c6bfde507 Mon Sep 17 00:00:00 2001 From: Sunday Ogbonna Date: Mon, 17 Jul 2023 14:55:27 +0100 Subject: [PATCH 01/18] chore: initial pr commit --- components/atoms/TextInput/text-input.tsx | 17 ++-- components/atoms/Tooltip/tooltip.tsx | 10 +-- .../HighlightInput/highlight-input-form.tsx | 88 +++++++++++-------- lib/utils/github.ts | 18 +++- 4 files changed, 79 insertions(+), 54 deletions(-) diff --git a/components/atoms/TextInput/text-input.tsx b/components/atoms/TextInput/text-input.tsx index f8bcaf7434..9b20ca3b8f 100644 --- a/components/atoms/TextInput/text-input.tsx +++ b/components/atoms/TextInput/text-input.tsx @@ -7,10 +7,10 @@ interface TextInputProps extends React.InputHTMLAttributes { state?: "default" | "valid" | "invalid"; borderless?: boolean; descriptionText?: string; - classNames?: string; errorMsg?: string; fieldRef?: React.RefObject; handleChange?: (value: string) => void; + placeholderClassNames?: string; } const TextInput = ({ @@ -21,12 +21,13 @@ const TextInput = ({ id, value, descriptionText, - classNames, + className, fieldRef, disabled = false, borderless = false, handleChange, errorMsg = "", + placeholderClassNames, ...props }: TextInputProps) => { const inputRef = useRef(null); @@ -52,7 +53,7 @@ const TextInput = ({ borderless && "!border-none", state === "invalid" ? "focus-within:border-light-red-10" : "focus-within:border-light-orange-9 ", disabled && "bg-light-slate-3 text-light-slate-6", - classNames + className )} > {state === "valid" ? ( - + ) : !!value ? ( { - const { Portal, Root, Content, Trigger } = TooltipPrimitive; + const { Portal, Root, Content, Trigger, Arrow } = TooltipPrimitive; return ( @@ -19,13 +20,10 @@ const Tooltip = ({ children, content, className, direction }: TooltipProps): JSX -
+
{content}
+ diff --git a/components/molecules/HighlightInput/highlight-input-form.tsx b/components/molecules/HighlightInput/highlight-input-form.tsx index eda8ec18f7..b81090d8d6 100644 --- a/components/molecules/HighlightInput/highlight-input-form.tsx +++ b/components/molecules/HighlightInput/highlight-input-form.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; -import { BsCalendar2Event } from "react-icons/bs"; +import { FiCalendar } from "react-icons/fi"; import { format } from "date-fns"; +import { HiOutlineSparkles } from "react-icons/hi"; import Button from "components/atoms/Button/button"; import { Textarea } from "components/atoms/Textarea/text-area"; import Tooltip from "components/atoms/Tooltip/tooltip"; @@ -11,6 +12,7 @@ import { createHighlights } from "lib/hooks/createHighlights"; import { generateApiPrUrl } from "lib/utils/github"; import { fetchGithubPRInfo } from "lib/hooks/fetchGithubPRInfo"; import { useToast } from "lib/hooks/useToast"; +import TextInput from "components/atoms/TextInput/text-input"; import { Calendar } from "../Calendar/calendar"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../Collapsible/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "../Popover/popover"; @@ -115,9 +117,7 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E onChange={(e) => setTitle(e.target.value)} className="flex-1 focus:outline-none" type="text" - placeholder={ - isDivFocused ? "Add title (optional)" : "Click here to highlight your merged PRs and provide a link!" - } + placeholder={isDivFocused ? "Add title (optional)" : "Post a highlight to show your work!"} />
@@ -128,45 +128,57 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E }`} defaultRow={4} value={bodyText} - placeholder={`Share your thoughts and link to it. - -https://github.com/open-sauced/insights/pull/913`} + placeholder={` Tell us about your highlight and add a link + `} onChangeText={(value) => { handleTextAreaInputChange(value); setCharCount(value.length); }} /> - -
- - - - - - - - - - - -

- - {!validCharLimit() - ? `-${charCount - pullrequestLink.length - charLimit}` - : charCount - pullrequestLink.length} - - / {charLimit} -

+

+ + {!validCharLimit() + ? `-${charCount - pullrequestLink.length - charLimit}` + : charCount - pullrequestLink.length} + + / {charLimit} +

+ +
+
+ + + + + + + + + + + + + + setPullRequestLink(e.target.value)} + placeholder="Paste your PR URL and get it auto-summarized!" + /> +
diff --git a/lib/utils/github.ts b/lib/utils/github.ts index 62cd7c6cfe..2d7dc77dfd 100644 --- a/lib/utils/github.ts +++ b/lib/utils/github.ts @@ -26,13 +26,13 @@ const generateApiPrUrl = ( if (githubUrl.hostname !== "github.com") { return { isValidUrl: false, - apiPaths: { orgName: null, repoName: null, issueId: null } + apiPaths: { orgName: null, repoName: null, issueId: null }, }; } return { isValidUrl: true, - apiPaths: { orgName, repoName, issueId } + apiPaths: { orgName, repoName, issueId }, }; } catch (err) { return { isValidUrl: false, apiPaths: { orgName: null, repoName: null, issueId: null } }; @@ -55,4 +55,16 @@ const generateGhOgImage = (githubUrl: string): { isValid: boolean; url: string } } }; -export { getAvatarById, getAvatarByUsername, getProfileLink, getRepoIssuesLink, generateApiPrUrl, generateGhOgImage }; +const isValidPullRequestUrl = (url: string): boolean => { + return url.match(/((https?:\/\/)?(www\.)?github\.com\/[^\/]+\/[^\/]+\/pull\/[0-9]+)/) ? true : false; +}; + +export { + getAvatarById, + getAvatarByUsername, + getProfileLink, + getRepoIssuesLink, + generateApiPrUrl, + generateGhOgImage, + isValidPullRequestUrl, +}; From 527077bc2cc7b77fefcc9cc35d1cd572a6207ccb Mon Sep 17 00:00:00 2001 From: Sunday Ogbonna Date: Mon, 17 Jul 2023 15:09:57 +0100 Subject: [PATCH 02/18] misc: update text input props --- components/molecules/HighlightInput/highlight-input-form.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/molecules/HighlightInput/highlight-input-form.tsx b/components/molecules/HighlightInput/highlight-input-form.tsx index b81090d8d6..a91fc02188 100644 --- a/components/molecules/HighlightInput/highlight-input-form.tsx +++ b/components/molecules/HighlightInput/highlight-input-form.tsx @@ -172,8 +172,7 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E setPullRequestLink(e.target.value)} placeholder="Paste your PR URL and get it auto-summarized!" From c34442763e8d34c5f28710957c8c1d18954e85d0 Mon Sep 17 00:00:00 2001 From: Sunday Ogbonna Date: Mon, 17 Jul 2023 15:17:23 +0100 Subject: [PATCH 03/18] fix: build error --- .../UserSettingsPage/user-settings-page.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/components/organisms/UserSettingsPage/user-settings-page.tsx b/components/organisms/UserSettingsPage/user-settings-page.tsx index 31e43cfd5e..1ca3b37f29 100644 --- a/components/organisms/UserSettingsPage/user-settings-page.tsx +++ b/components/organisms/UserSettingsPage/user-settings-page.tsx @@ -114,7 +114,7 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => { }; const handleUpdateEmailPreference = async () => { - setUpdating(prev => ({ ...prev, emailPreferences: true })); + setUpdating((prev) => ({ ...prev, emailPreferences: true })); const data = await updateEmailPreferences({ ...emailPreference }); if (data) { @@ -123,11 +123,11 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => { toast({ description: "An error occured!", variant: "danger" }); } - setUpdating(prev => ({...prev, emailPreferences: false})); + setUpdating((prev) => ({ ...prev, emailPreferences: false })); }; const handleUpdateInterest = async () => { - setUpdating(prev => ({ ...prev, interests: true })); + setUpdating((prev) => ({ ...prev, interests: true })); const data = await updateUser({ data: { interests: selectedInterest }, @@ -146,7 +146,7 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => { const handleUpdateProfile = async (e: React.FormEvent) => { e.preventDefault(); - setUpdating(prev => ({...prev, profile: true})); + setUpdating((prev) => ({ ...prev, profile: true })); const payload: UpdateUserPayload = { name: formRef.current!.nameInput.value, email, @@ -191,14 +191,14 @@ const UserSettingsPage = ({ user }: userSettingsPageProps) => {
{ > Date: Tue, 18 Jul 2023 01:56:18 +0100 Subject: [PATCH 04/18] feat: add autogeneration setup for highlights --- .../HighlightInput/highlight-input-form.tsx | 64 +++++++++++++++---- lib/utils/generate-pr-highlight-summary.ts | 36 +++++++++++ lib/utils/github.ts | 22 +++++++ 3 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 lib/utils/generate-pr-highlight-summary.ts diff --git a/components/molecules/HighlightInput/highlight-input-form.tsx b/components/molecules/HighlightInput/highlight-input-form.tsx index a91fc02188..7f5b455901 100644 --- a/components/molecules/HighlightInput/highlight-input-form.tsx +++ b/components/molecules/HighlightInput/highlight-input-form.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useRef, useState } from "react"; import { FiCalendar } from "react-icons/fi"; import { format } from "date-fns"; @@ -9,10 +9,11 @@ import { Textarea } from "components/atoms/Textarea/text-area"; import Tooltip from "components/atoms/Tooltip/tooltip"; import { createHighlights } from "lib/hooks/createHighlights"; -import { generateApiPrUrl } from "lib/utils/github"; +import { generateApiPrUrl, getPullRequestCommitMessageFromUrl, isValidPullRequestUrl } from "lib/utils/github"; import { fetchGithubPRInfo } from "lib/hooks/fetchGithubPRInfo"; import { useToast } from "lib/hooks/useToast"; import TextInput from "components/atoms/TextInput/text-input"; +import { generatePrHighlightSummaryByCommitMsg } from "lib/utils/generate-pr-highlight-summary"; import { Calendar } from "../Calendar/calendar"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../Collapsible/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "../Popover/popover"; @@ -29,6 +30,7 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E const [title, setTitle] = useState(""); const [charCount, setCharCount] = useState(0); const [pullrequestLink, setPullRequestLink] = useState(""); + const textAreaRef = useRef(null); const charLimit = 500; @@ -40,22 +42,52 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E return charCount - pullrequestLink.length <= charLimit; }; - useEffect(() => { - const pullLink = bodyText.match(/((https?:\/\/)?(www\.)?github\.com\/[^\/]+\/[^\/]+\/pull\/[0-9]+)/); - const link = - pullLink && new URL(pullLink.includes("https://") ? (pullLink as unknown as string) : `https://${pullLink}`); + // useEffect(() => { + // const pullLink = bodyText.match(/((https?:\/\/)?(www\.)?github\.com\/[^\/]+\/[^\/]+\/pull\/[0-9]+)/); + // const link = + // pullLink && new URL(pullLink.includes("https://") ? (pullLink as unknown as string) : `https://${pullLink}`); - if (pullLink && pullLink.length > 0 && link?.hostname === "github.com" && link?.pathname.includes("pull")) { - setPullRequestLink(pullLink[0]); - } else { - setPullRequestLink(""); - } - }, [bodyText, pullrequestLink]); + // if (pullLink && pullLink.length > 0 && link?.hostname === "github.com" && link?.pathname.includes("pull")) { + // setPullRequestLink(pullLink[0]); + // } else { + // // setPullRequestLink(""); + // } + // }, [bodyText, pullrequestLink]); + + // getPullRequestCommitMessageFromUrl("https://github.com/open-sauced/insights/pull/1380").then((commits) => { + // console.log(commits); + // }); const handleTextAreaInputChange = (value: string) => { setBodyText(value); }; + const handleGenerateHighlightSummary = async () => { + if (!pullrequestLink || !isValidPullRequestUrl(pullrequestLink)) { + return; + } + + const commitMessages = await getPullRequestCommitMessageFromUrl(pullrequestLink); + const summary = await generatePrHighlightSummaryByCommitMsg(commitMessages); + + if (summary) { + // if (!textAreaRef.current?.focus) { + // textAreaRef.current?.focus(); + // } + // let length = 0; + // const typewriter = setInterval(() => { + // textAreaRef.current?.setRangeText( + // summary[length++], + // textAreaRef.current?.selectionStart, + // textAreaRef.current?.selectionEnd, + // "end" + // ); + // if (length === summary.length) clearInterval(typewriter); + // }, 10); + setBodyText(summary); + console.log(summary); + } + }; // Handle submit highlights const handlePostHighlight = async (e: React.FormEvent) => { e.preventDefault(); @@ -134,6 +166,7 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E handleTextAreaInputChange(value); setCharCount(value.length); }} + ref={textAreaRef} />

@@ -167,14 +200,17 @@ const HighlightInputForm = ({ refreshCallback }: HighlightInputFormProps): JSX.E - setPullRequestLink(e.target.value)} + handleChange={(value) => setPullRequestLink(value)} placeholder="Paste your PR URL and get it auto-summarized!" /> diff --git a/lib/utils/generate-pr-highlight-summary.ts b/lib/utils/generate-pr-highlight-summary.ts new file mode 100644 index 0000000000..7531566c27 --- /dev/null +++ b/lib/utils/generate-pr-highlight-summary.ts @@ -0,0 +1,36 @@ +import { supabase } from "./supabase"; + +const baseUrl = process.env.NEXT_PUBLIC_API_URL; +export const generatePrHighlightSummaryByCommitMsg = async (commitMessages: string[]) => { + const sessionResponse = await supabase.auth.getSession(); + const sessionToken = sessionResponse?.data.session?.access_token; + const payload = { + descriptionLength: 250, + commitMessages, + language: "english", + diff: "commitMessage", + tone: "formal", + temperature: 7, + }; + + try { + const res = await fetch(`${baseUrl}/prs/description/generate`, { + method: "POST", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${sessionToken}`, + }, + body: JSON.stringify(payload), + }); + + if (res.ok) { + const data = await res.json(); + return data.description as string; + } + + console.log(res); + } catch (err) { + console.log(err); + return null; + } +}; diff --git a/lib/utils/github.ts b/lib/utils/github.ts index 2d7dc77dfd..f870fae753 100644 --- a/lib/utils/github.ts +++ b/lib/utils/github.ts @@ -3,6 +3,12 @@ * @todo Use `getAvatarById` instead of `getAvatarByUsername` whenever possible * @see {@link https://github.com/open-sauced/insights/issues/746} */ +export interface Commit { + commit: { + message: string; + }; +} + const getAvatarByUsername = (username: string | null, size = 460) => `https://www.github.com/${username ?? "github"}.png?size=${size}`; @@ -55,6 +61,21 @@ const generateGhOgImage = (githubUrl: string): { isValid: boolean; url: string } } }; +const getPullRequestCommitMessageFromUrl = async (url: string): Promise => { + const [, , , owner, repoName, , pullRequestNumber] = url.split("/"); + + const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/pulls/${pullRequestNumber}/commits`; + + const response = await fetch(apiUrl); + const data = await response.json(); + + if (Array.isArray(data?.commits)) { + return (data.commits as Commit[]).map((commit: Commit): string => commit.commit.message); + } + + return (data as Commit[]).map((commit: Commit): string => commit.commit.message); +}; + const isValidPullRequestUrl = (url: string): boolean => { return url.match(/((https?:\/\/)?(www\.)?github\.com\/[^\/]+\/[^\/]+\/pull\/[0-9]+)/) ? true : false; }; @@ -67,4 +88,5 @@ export { generateApiPrUrl, generateGhOgImage, isValidPullRequestUrl, + getPullRequestCommitMessageFromUrl, }; From 3826e858642e149efefc388d4ea4a86469c5acea Mon Sep 17 00:00:00 2001 From: Sunday Ogbonna Date: Tue, 18 Jul 2023 14:02:17 +0100 Subject: [PATCH 05/18] feat: wrap up implementation for suggestion --- components/atoms/Textarea/text-area.tsx | 78 ++++++++++++------- .../HighlightInput/highlight-input-form.tsx | 60 +++++--------- 2 files changed, 71 insertions(+), 67 deletions(-) diff --git a/components/atoms/Textarea/text-area.tsx b/components/atoms/Textarea/text-area.tsx index 3a09dccce3..feb10ed864 100644 --- a/components/atoms/Textarea/text-area.tsx +++ b/components/atoms/Textarea/text-area.tsx @@ -1,38 +1,64 @@ -import React, { ChangeEvent, useRef } from "react"; +import React, { ChangeEvent, useEffect, useRef } from "react"; import clsx from "clsx"; interface TextareaProps extends React.TextareaHTMLAttributes { defaultRow?: number; onChangeText?: (value: string) => void; + typewrite?: boolean; + textContent?: string; } -const Textarea = React.forwardRef(({ className, onChangeText, ...props }, ref) => { - const textareaRef = useRef(null); - const autoGrowTextarea = () => { - const textarea = textareaRef.current; - if (textarea) { - textarea.style.height = "auto"; - textarea.style.height = `${textarea.scrollHeight}px`; - } - }; +const Textarea = React.forwardRef( + ({ className, textContent, typewrite, onChangeText, ...props }, ref) => { + const textareaRef = useRef(null); + const autoGrowTextarea = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + } + }; - const handleInputChange = (event: ChangeEvent) => { - onChangeText?.(event.target.value); - autoGrowTextarea(); - }; + const typeWrite = (text: string) => { + const textarea = textareaRef.current; + if (textarea) { + textarea.focus(); + textarea.value = ""; + textarea.selectionStart = textarea.selectionEnd = textarea.value.length; + const interval = setInterval(() => { + if (textarea.value.length === text.length) { + clearInterval(interval); + } else { + textarea.value = text.slice(0, textarea.value.length + 1); + } + }, 10); + } + }; - return ( -