plane/web/components/core/modals/gpt-assistant-modal.tsx
M. Palanikannan 0a8b99a074
feat: Editor Core Packaging and Restructuring (#2358)
* initialized tiptap component with common tailwind config

* added common tailwind config to web

* abstracted upload and delete functions

* removed tiptap pro extension

* fixed types

* removed old tailwind config and fixed plane package imports

* exported tiptap editor with and without ref

* updated package name to @plane/editor

* finally fixed import errors

* added turbo dependency for tiptap

* reverted back types and fixed tailwind

* migrated all components to use the common package

* removed old tiptap dependency

* improved dev experience to build the tiptap package before starting dev server

* resolved lock life and missing deps

* fixed dependency issue with react type resolution

* chore: updated pulls build CI for using turbo builds

* comment editor basic version added

* new structure of editor components

* refactored editor to not require workspace slug

* added seperation of extensions and props

* refactoring to LiteTextEditor and RichTextEditor

* fixed global css issue with highlight js

* refactoring tiptap to core/lite/rich text editor

* read only editor support added

* replaced all read-only instances

* trimming html at start and end of content added

* onSubmit on enterkey captured

* removed absolute imports from editor/core package

* removed absolute imports from lite-text-editor

* removed absolute imports from rich-text-editor

* fixed dependencies in editor package

* fixed tailwind config for editor

* Enter key behaviour added for Comments

* fixed modal form issue

* added comment editor with fixed menu

* added support for range commands

* modified turbo config for build pipeline of space and web projects

* fixed shift enter behavior for lists

* removed extra margin from access specifiers

* removed tiptap instance from web

* fixed bugs returning empty editor boxes

* fixed toggle Underline behvaiour

* updated bubble menu to use core package's utilities

* added editor/core readme and fixed imports

* fixed ts issues with link plugin

* added usage of common dependance for slash commands

* completed core package's documentation

* fixed tsconfig by removing path aliases

* Completed readme for rich-text-editor

* Added rich text editor documentation

* changed readme title of core package

---------

Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
2023-10-13 12:05:49 +05:30

220 lines
5.7 KiB
TypeScript

import React, { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { useForm } from "react-hook-form";
// services
import aiService from "services/ai.service";
import trackEventServices from "services/track-event.service";
// hooks
import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// components
import { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@plane/rich-text-editor";
// types
import { IIssue, IPageBlock } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
inset?: string;
content: string;
htmlContent?: string;
onResponse: (response: string) => void;
projectId: string;
block?: IPageBlock;
issue?: IIssue;
};
type FormData = {
prompt: string;
task: string;
};
export const GptAssistantModal: React.FC<Props> = ({
isOpen,
handleClose,
inset = "top-0 left-0",
content,
htmlContent,
onResponse,
projectId,
block,
issue,
}) => {
const [response, setResponse] = useState("");
const [invalidResponse, setInvalidResponse] = useState(false);
const router = useRouter();
const { workspaceSlug } = router.query;
const { user } = useUserAuth();
const editorRef = useRef<any>(null);
const { setToastAlert } = useToast();
const {
handleSubmit,
register,
reset,
setFocus,
formState: { isSubmitting },
} = useForm({
defaultValues: {
prompt: content,
task: "",
},
});
const onClose = () => {
handleClose();
setResponse("");
setInvalidResponse(false);
reset();
};
const handleResponse = async (formData: FormData) => {
if (!workspaceSlug || !projectId) return;
if (formData.task === "") {
setToastAlert({
type: "error",
title: "Error!",
message: "Please enter some task to get AI assistance.",
});
return;
}
await aiService
.createGptTask(
workspaceSlug as string,
projectId as string,
{
prompt: content && content !== "" ? content : htmlContent ?? "",
task: formData.task,
},
user
)
.then((res) => {
setResponse(res.response_html);
setFocus("task");
if (res.response === "") setInvalidResponse(true);
else setInvalidResponse(false);
})
.catch((err) => {
const error = err?.data?.error;
if (err.status === 429)
setToastAlert({
type: "error",
title: "Error!",
message:
error ||
"You have reached the maximum number of requests of 50 requests per month per user.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: error || "Some error occurred. Please try again.",
});
});
};
useEffect(() => {
if (isOpen) setFocus("task");
}, [isOpen, setFocus]);
useEffect(() => {
editorRef.current?.setEditorValue(htmlContent ?? `<p>${content}</p>`);
}, [htmlContent, editorRef, content]);
return (
<div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
}`}
>
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm">
Content:
<RichReadOnlyEditorWithRef
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
noBorder
borderOnFocus={false}
ref={editorRef}
/>
</div>
)}
{response !== "" && (
<div className="page-block-section text-sm">
Response:
<RichReadOnlyEditor
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
borderOnFocus={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();
if (block)
trackEventServices.trackUseGPTResponseEvent(
block,
"USE_GPT_RESPONSE_IN_PAGE_BLOCK",
user
);
else if (issue)
trackEventServices.trackUseGPTResponseEvent(
issue,
"USE_GPT_RESPONSE_IN_ISSUE",
user
);
}}
>
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>
);
};