fix: adding ai assistance to pages (#2905)

* fix: adding ai modal to pages

* fix: pages overflow

* chore: update pages UI

* fix: updating page description while using ai assistance

* fix: gpt assistant modal height and position

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
sriram veeraghanta 2023-11-27 20:39:18 +05:30
parent 10cde58363
commit d84e043c93
7 changed files with 134 additions and 80 deletions

View File

@ -46,7 +46,7 @@ export const EditorHeader = (props: IEditorHeader) => {
return ( return (
<div className="flex items-center border-b border-custom-border-200 py-2 px-5"> <div className="flex items-center border-b border-custom-border-200 py-2 px-5">
<div className="flex-shrink-0 w-56 lg:w-80"> <div className="flex-shrink-0 w-56 lg:w-72">
<SummaryPopover <SummaryPopover
editor={editor} editor={editor}
markings={markings} markings={markings}

View File

@ -7,7 +7,7 @@ export const HeadingComp = ({
}) => ( }) => (
<h3 <h3
onClick={onClick} onClick={onClick}
className="ml-4 mt-3 cursor-pointer text-sm font-bold font-medium leading-[125%] tracking-tight hover:text-custom-primary max-md:ml-2.5" className="ml-4 mt-3 cursor-pointer text-sm font-medium leading-[125%] tracking-tight hover:text-custom-primary max-md:ml-2.5"
role="button" role="button"
> >
{heading} {heading}

View File

@ -19,7 +19,7 @@ export const PageRenderer = (props: IPageRenderer) => {
return ( return (
<div className="w-full pl-7 pt-5 pb-64"> <div className="w-full pl-7 pt-5 pb-64">
<h1 className="text-4xl font-bold break-all pr-5 -mt-2"> <h1 className="text-4xl font-bold break-words pr-5 -mt-2">
{documentDetails.title} {documentDetails.title}
</h1> </h1>
<div className="flex flex-col h-full w-full pr-5"> <div className="flex flex-col h-full w-full pr-5">

View File

@ -15,7 +15,7 @@ export const SummarySideBar = ({
}: ISummarySideBarProps) => { }: ISummarySideBarProps) => {
return ( return (
<div <div
className={`h-full px-5 pt-5 transition-all duration-200 transform overflow-hidden ${ className={`h-full p-5 transition-all duration-200 transform overflow-hidden ${
sidePeekVisible ? "translate-x-0" : "-translate-x-full" sidePeekVisible ? "translate-x-0" : "-translate-x-full"
}`} }`}
> >

View File

@ -127,14 +127,14 @@ const DocumentEditor = ({
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
<div className="h-full w-full flex overflow-y-auto"> <div className="h-full w-full flex overflow-y-auto">
<div className="flex-shrink-0 h-full w-56 lg:w-80 sticky top-0"> <div className="flex-shrink-0 h-full w-56 lg:w-72 sticky top-0">
<SummarySideBar <SummarySideBar
editor={editor} editor={editor}
markings={markings} markings={markings}
sidePeekVisible={sidePeekVisible} sidePeekVisible={sidePeekVisible}
/> />
</div> </div>
<div className="h-full w-full"> <div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
<PageRenderer <PageRenderer
editor={editor} editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames} editorContentCustomClassNames={editorContentCustomClassNames}
@ -142,7 +142,7 @@ const DocumentEditor = ({
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
</div> </div>
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" /> <div className="hidden lg:block flex-shrink-0 w-56 lg:w-72" />
</div> </div>
</div> </div>
); );

View File

@ -117,38 +117,41 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
return ( return (
<div <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 ${ className={`absolute ${inset} z-20 w-full flex flex-col space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow overflow-hidden ${
isOpen ? "block" : "hidden" isOpen ? "block" : "hidden"
}`} }`}
> >
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && ( <div className="max-h-72 overflow-y-auto space-y-4 vertical-scroll-enable">
<div className="text-sm"> {((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
Content: <div className="text-sm">
<RichReadOnlyEditorWithRef Content:
value={htmlContent ?? `<p>${content}</p>`} <RichReadOnlyEditorWithRef
customClassName="-m-3" value={htmlContent ?? `<p>${content}</p>`}
noBorder customClassName="-m-3"
borderOnFocus={false} noBorder
ref={editorRef} borderOnFocus={false}
/> ref={editorRef}
</div> />
)} </div>
{response !== "" && ( )}
<div className="page-block-section text-sm"> {response !== "" && (
Response: <div className="page-block-section text-sm">
<RichReadOnlyEditor Response:
value={`<p>${response}</p>`} <RichReadOnlyEditor
customClassName="-mx-3 -my-3" value={`<p>${response}</p>`}
noBorder customClassName="-mx-3 -my-3"
borderOnFocus={false} noBorder
/> borderOnFocus={false}
</div> />
)} </div>
{invalidResponse && ( )}
<div className="text-sm text-red-500"> {invalidResponse && (
No response could be generated. This may be due to insufficient content or task information. Please try again. <div className="text-sm text-red-500">
</div> No response could be generated. This may be due to insufficient content or task information. Please try
)} again.
</div>
)}
</div>
<Controller <Controller
control={control} control={control}
name="task" name="task"

View File

@ -2,17 +2,21 @@ import React, { useEffect, useRef, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Sparkle } from "lucide-react";
import { observer } from "mobx-react-lite";
// services // services
import { PageService } from "services/page.service"; import { PageService } from "services/page.service";
import { FileService } from "services/file.service"; import { FileService } from "services/file.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { useMobxStore } from "lib/mobx/store-provider";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
import { PageDetailsHeader } from "components/headers/page-details"; import { PageDetailsHeader } from "components/headers/page-details";
import { EmptyState } from "components/common"; import { EmptyState } from "components/common";
import { GptAssistantModal } from "components/core";
// ui // ui
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
@ -30,18 +34,23 @@ import { PAGE_DETAILS } from "constants/fetch-keys";
const fileService = new FileService(); const fileService = new FileService();
const pageService = new PageService(); const pageService = new PageService();
const PageDetailsPage: NextPageWithLayout = () => { const PageDetailsPage: NextPageWithLayout = observer(() => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [gptModalOpen, setGptModal] = useState(false);
// store
const {
appConfig: { envConfig },
} = useMobxStore();
// router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query; const { workspaceSlug, projectId, pageId } = router.query;
const { user } = useUser(); const { user } = useUser();
const { handleSubmit, reset, getValues, control } = useForm<IPage>({ const { handleSubmit, reset, getValues, control, setValue, watch } = useForm<IPage>({
defaultValues: { name: "" }, defaultValues: { name: "", description_html: "<p></p>" },
}); });
// =================== Fetching Page Details ====================== // =================== Fetching Page Details ======================
@ -167,6 +176,22 @@ const PageDetailsPage: NextPageWithLayout = () => {
} }
}; };
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId || !pageId) return;
const newDescription = `${watch("description_html")}<p>${response}</p>`;
setValue("description_html", newDescription);
editorRef.current?.setEditorValue(newDescription);
pageService
.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), {
description_html: newDescription,
})
.then(() => {
mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription } as IPage), false);
});
};
useEffect(() => { useEffect(() => {
if (!pageDetails) return; if (!pageDetails) return;
@ -227,45 +252,71 @@ const PageDetailsPage: NextPageWithLayout = () => {
} }
/> />
) : ( ) : (
<Controller <div className="h-full w-full relative overflow-hidden">
name="description_html" <Controller
control={control} name="description_html"
render={({ field: { value, onChange } }) => ( control={control}
<DocumentEditorWithRef render={({ field: { value, onChange } }) => (
documentDetails={{ <DocumentEditorWithRef
title: pageDetails.name, documentDetails={{
created_by: pageDetails.created_by, title: pageDetails.name,
created_on: pageDetails.created_at, created_by: pageDetails.created_by,
last_updated_at: pageDetails.updated_at, created_on: pageDetails.created_at,
last_updated_by: pageDetails.updated_by, last_updated_at: pageDetails.updated_at,
}} last_updated_by: pageDetails.updated_by,
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)} }}
deleteFile={fileService.deleteImage} uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
ref={editorRef} deleteFile={fileService.deleteImage}
debouncedUpdatesEnabled={false} ref={editorRef}
setIsSubmitting={setIsSubmitting} debouncedUpdatesEnabled={false}
value={!value || value === "" ? "<p></p>" : value} setIsSubmitting={setIsSubmitting}
customClassName="tracking-tight self-center px-0 h-full w-full" value={!value || value === "" ? "<p></p>" : value}
onChange={(_description_json: Object, description_html: string) => { customClassName="tracking-tight self-center px-0 h-full w-full"
onChange(description_html); onChange={(_description_json: Object, description_html: string) => {
setIsSubmitting("submitting"); onChange(description_html);
debouncedFormSave(); setIsSubmitting("submitting");
}} debouncedFormSave();
duplicationConfig={{ action: duplicate_page }} }}
pageArchiveConfig={ duplicationConfig={{ action: duplicate_page }}
user && pageDetails.owned_by === user.id pageArchiveConfig={
? { user && pageDetails.owned_by === user.id
is_archived: pageDetails.archived_at ? true : false, ? {
action: pageDetails.archived_at ? unArchivePage : archivePage, is_archived: pageDetails.archived_at ? true : false,
} action: pageDetails.archived_at ? unArchivePage : archivePage,
: undefined }
} : undefined
pageLockConfig={ }
user && pageDetails.owned_by === user.id ? { is_locked: false, action: lockPage } : undefined pageLockConfig={
} user && pageDetails.owned_by === user.id ? { is_locked: false, action: lockPage } : undefined
/> }
/>
)}
/>
{projectId && envConfig?.has_openai_configured && (
<>
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 absolute top-3 right-[68px]"
onClick={() => setGptModal((prevData) => !prevData)}
>
<Sparkle className="h-4 w-4" />
AI
</button>
<GptAssistantModal
isOpen={gptModalOpen}
handleClose={() => {
setGptModal(false);
}}
inset="top-9 right-[68px] !w-1/2 !max-h-[50%]"
content=""
onResponse={(response) => {
handleAiAssistance(response);
}}
projectId={projectId.toString()}
/>
</>
)} )}
/> </div>
)} )}
</div> </div>
</div> </div>
@ -276,7 +327,7 @@ const PageDetailsPage: NextPageWithLayout = () => {
)} )}
</> </>
); );
}; });
PageDetailsPage.getLayout = function getLayout(page: ReactElement) { PageDetailsPage.getLayout = function getLayout(page: ReactElement) {
return ( return (