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 committed by GitHub
parent f361cd045e
commit 67de6d0729
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,10 +117,11 @@ 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"
}`} }`}
> >
<div className="max-h-72 overflow-y-auto space-y-4 vertical-scroll-enable">
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && ( {((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm"> <div className="text-sm">
Content: Content:
@ -146,9 +147,11 @@ export const GptAssistantModal: React.FC<Props> = (props) => {
)} )}
{invalidResponse && ( {invalidResponse && (
<div className="text-sm text-red-500"> <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. No response could be generated. This may be due to insufficient content or task information. Please try
again.
</div> </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,6 +252,7 @@ const PageDetailsPage: NextPageWithLayout = () => {
} }
/> />
) : ( ) : (
<div className="h-full w-full relative overflow-hidden">
<Controller <Controller
name="description_html" name="description_html"
control={control} control={control}
@ -266,6 +292,31 @@ const PageDetailsPage: NextPageWithLayout = () => {
/> />
)} )}
/> />
{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 (