chore: ai for issue description (#575)

* feat: block sync

* chore: ai assistant for issue description
This commit is contained in:
Aaryan Khandelwal 2023-03-29 16:30:40 +05:30 committed by GitHub
parent 2f69761130
commit 96910e1897
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 114 deletions

View File

@ -19,6 +19,7 @@ type Props = {
content: string; content: string;
htmlContent?: string; htmlContent?: string;
onResponse: (response: string) => void; onResponse: (response: string) => void;
projectId: string;
}; };
type FormData = { type FormData = {
@ -37,12 +38,13 @@ export const GptAssistantModal: React.FC<Props> = ({
content, content,
htmlContent, htmlContent,
onResponse, onResponse,
projectId,
}) => { }) => {
const [response, setResponse] = useState(""); const [response, setResponse] = useState("");
const [invalidResponse, setInvalidResponse] = useState(false); const [invalidResponse, setInvalidResponse] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -80,7 +82,7 @@ export const GptAssistantModal: React.FC<Props> = ({
await aiService await aiService
.createGptTask(workspaceSlug as string, projectId as string, { .createGptTask(workspaceSlug as string, projectId as string, {
prompt: content && content !== "" ? content : "", prompt: content && content !== "" ? content : htmlContent ?? "",
task: formData.task, task: formData.task,
}) })
.then((res) => { .then((res) => {
@ -98,75 +100,77 @@ export const GptAssistantModal: React.FC<Props> = ({
return ( return (
<div <div
className={`absolute ${inset} z-20 w-full rounded-[10px] border bg-white p-4 shadow ${ className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border bg-white p-4 shadow ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
> >
<form onSubmit={handleSubmit(handleResponse)} className="space-y-4"> {((content && content !== "") || htmlContent) && (
{content && content !== "" && ( <div className="text-sm page-block-section">
<div className="text-sm"> Content:
Content: <RemirrorRichTextEditor
<RemirrorRichTextEditor value={htmlContent ?? <p>{content}</p>}
value={htmlContent ?? <p>{content}</p>} customClassName="-mx-3 -my-3"
customClassName="-mx-3 -my-3" noBorder
noBorder borderOnFocus={false}
borderOnFocus={false} editable={false}
editable={false} />
/>
</div>
)}
{response !== "" && (
<div className="text-sm">
Response:
<RemirrorRichTextEditor
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
borderOnFocus={false}
editable={false}
/>
</div>
)}
{invalidResponse && (
<div className="text-sm text-red-500">
No response could be generated. This may be due to insufficient content or task
information. Please try again.
</div>
)}
<Input
type="text"
name="task"
register={register}
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
{response !== "" && (
<PrimaryButton
onClick={() => {
onResponse(response);
onClose();
}}
>
Use this response
</PrimaryButton>
)}
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Close</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div> </div>
</form> )}
{response !== "" && (
<div className="text-sm page-block-section">
Response:
<RemirrorRichTextEditor
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
borderOnFocus={false}
editable={false}
/>
</div>
)}
{invalidResponse && (
<div className="text-sm text-red-500">
No response could be generated. This may be due to insufficient content or task
information. Please try again.
</div>
)}
<Input
type="text"
name="task"
register={register}
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
{response !== "" && (
<PrimaryButton
onClick={() => {
onResponse(response);
onClose();
}}
>
Use this response
</PrimaryButton>
)}
<div className="flex items-center gap-2">
<SecondaryButton onClick={onClose}>Close</SecondaryButton>
<PrimaryButton
type="button"
onClick={handleSubmit(handleResponse)}
loading={isSubmitting}
>
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>
</div> </div>
); );
}; };

View File

@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components // components
import { Loader, TextArea } from "components/ui"; import { Loader, TextArea } from "components/ui";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
@ -42,6 +42,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
watch, watch,
setValue, setValue,
reset, reset,
register,
control,
formState: { errors }, formState: { errors },
} = useForm<IIssue>({ } = useForm<IIssue>({
defaultValues: { defaultValues: {
@ -64,19 +66,18 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
[handleFormSubmit] [handleFormSubmit]
); );
useEffect(() => { // useEffect(() => {
const alertUser = (e: BeforeUnloadEvent) => { // const alertUser = (e: BeforeUnloadEvent) => {
console.log("beforeunload"); // e.preventDefault();
e.preventDefault(); // e.returnValue = "";
e.returnValue = ""; // return "Are you sure you want to leave?";
return "Are you sure you want to leave?"; // };
};
window.addEventListener("beforeunload", alertUser); // window.addEventListener("beforeunload", alertUser);
return () => { // return () => {
window.removeEventListener("beforeunload", alertUser); // window.removeEventListener("beforeunload", alertUser);
}; // };
}, [isSubmitting]); // }, [isSubmitting]);
// reset form values // reset form values
useEffect(() => { useEffect(() => {
@ -94,7 +95,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
id="name" id="name"
name="name" name="name"
placeholder="Enter issue name" placeholder="Enter issue name"
value={watch("name")} register={register}
onFocus={() => setCharacterLimit(true)} onFocus={() => setCharacterLimit(true)}
onBlur={() => { onBlur={() => {
setCharacterLimit(false); setCharacterLimit(false);
@ -108,9 +109,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
setIsSubmitting(false); setIsSubmitting(false);
}); });
}} }}
onChange={(e) => {
setValue("name", e.target.value);
}}
required={true} required={true}
className="min-h-10 block w-full resize-none className="min-h-10 block w-full resize-none
overflow-hidden rounded border-none bg-transparent overflow-hidden rounded border-none bg-transparent
@ -131,26 +129,34 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
)} )}
</div> </div>
<span>{errors.name ? errors.name.message : null}</span> <span>{errors.name ? errors.name.message : null}</span>
<RemirrorRichTextEditor <Controller
value={ name="description"
watch("description") && watch("description") !== "" control={control}
? watch("description") render={({ field: { value } }) => (
: watch("description_html") <RemirrorRichTextEditor
} value={
placeholder="Describe the issue..." !value ||
onBlur={() => { value === "" ||
setIsSubmitting(true); (typeof value === "object" && Object.keys(value).length === 0)
handleSubmit(handleDescriptionFormSubmit)() ? watch("description_html")
.then(() => { : value
setIsSubmitting(false); }
}) onJSONChange={(jsonValue) => setValue("description", jsonValue)}
.catch(() => { onHTMLChange={(htmlValue) => setValue("description_html", htmlValue)}
setIsSubmitting(false); onBlur={() => {
}); setIsSubmitting(true);
}} handleSubmit(handleDescriptionFormSubmit)()
onJSONChange={(json) => setValue("description", json)} .then(() => {
onHTMLChange={(html) => setValue("description_html", html)} setIsSubmitting(false);
editable={!isNotAllowed} })
.catch(() => {
setIsSubmitting(false);
});
}}
placeholder="Describe the issue..."
editable={!isNotAllowed}
/>
)}
/> />
<div <div
className={`absolute -bottom-8 right-0 text-sm text-gray-500 ${ className={`absolute -bottom-8 right-0 text-sm text-gray-500 ${

View File

@ -7,6 +7,7 @@ import { useRouter } from "next/router";
// react-hook-form // react-hook-form
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// components // components
import { GptAssistantModal } from "components/core";
import { import {
IssueAssigneeSelect, IssueAssigneeSelect,
IssueLabelSelect, IssueLabelSelect,
@ -22,7 +23,7 @@ import { CreateLabelModal } from "components/labels";
// ui // ui
import { CustomMenu, Input, Loader, PrimaryButton, SecondaryButton } from "components/ui"; import { CustomMenu, Input, Loader, PrimaryButton, SecondaryButton } from "components/ui";
// icons // icons
import { XMarkIcon } from "@heroicons/react/24/outline"; import { SparklesIcon, XMarkIcon } from "@heroicons/react/24/outline";
// helpers // helpers
import { cosineSimilarity } from "helpers/string.helper"; import { cosineSimilarity } from "helpers/string.helper";
// types // types
@ -31,7 +32,7 @@ import type { IIssue } from "types";
const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), {
ssr: false, ssr: false,
loading: () => ( loading: () => (
<Loader> <Loader className="mt-4">
<Loader.Item height="12rem" width="100%" /> <Loader.Item height="12rem" width="100%" />
</Loader> </Loader>
), ),
@ -81,6 +82,8 @@ export const IssueForm: FC<IssueFormProps> = ({
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
const [gptAssistantModal, setGptAssistantModal] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -115,6 +118,13 @@ export const IssueForm: FC<IssueFormProps> = ({
}); });
}; };
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId) return;
setValue("description", {});
setValue("description_html", `${watch("description_html")}<p>${response}</p>`);
};
useEffect(() => { useEffect(() => {
setFocus("name"); setFocus("name");
@ -233,7 +243,17 @@ export const IssueForm: FC<IssueFormProps> = ({
</div> </div>
)} )}
</div> </div>
<div> <div className="relative">
<div className="flex justify-end -mb-2 mr-2">
<button
type="button"
className="-mr-2 flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-gray-100"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
>
<SparklesIcon className="h-4 w-4" />
AI
</button>
</div>
<Controller <Controller
name="description" name="description"
control={control} control={control}
@ -250,6 +270,15 @@ export const IssueForm: FC<IssueFormProps> = ({
/> />
)} )}
/> />
<GptAssistantModal
isOpen={gptAssistantModal}
handleClose={() => setGptAssistantModal(false)}
inset="top-2 left-0"
content=""
htmlContent={watch("description_html")}
onResponse={handleAiAssistance}
projectId={projectId}
/>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Controller <Controller

View File

@ -333,6 +333,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails }) => {
content={block.description_stripped} content={block.description_stripped}
htmlContent={block.description_html} htmlContent={block.description_html}
onResponse={handleAiAssistance} onResponse={handleAiAssistance}
projectId={projectId as string}
/> />
</div> </div>
</div> </div>

View File

@ -50,8 +50,6 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
: null : null
); );
console.log(views)
return ( return (
<AppLayout <AppLayout
meta={{ meta={{
@ -88,11 +86,7 @@ const ProjectViews: NextPage<UserAuth> = (props) => {
<h3 className="text-3xl font-semibold text-black">Views</h3> <h3 className="text-3xl font-semibold text-black">Views</h3>
<div className="rounded-[10px] border"> <div className="rounded-[10px] border">
{views.map((view) => ( {views.map((view) => (
<SingleViewItem <SingleViewItem key={view.id} view={view} setSelectedView={setSelectedView} />
key={view.id}
view={view}
setSelectedView={setSelectedView}
/>
))} ))}
</div> </div>
</div> </div>