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:
parent
10cde58363
commit
d84e043c93
7 changed files with 134 additions and 80 deletions
|
|
@ -2,17 +2,21 @@ import React, { useEffect, useRef, useState, ReactElement } from "react";
|
|||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Sparkle } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
import { PageDetailsHeader } from "components/headers/page-details";
|
||||
import { EmptyState } from "components/common";
|
||||
import { GptAssistantModal } from "components/core";
|
||||
// ui
|
||||
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
|
||||
import { Spinner } from "@plane/ui";
|
||||
|
|
@ -30,18 +34,23 @@ import { PAGE_DETAILS } from "constants/fetch-keys";
|
|||
const fileService = new FileService();
|
||||
const pageService = new PageService();
|
||||
|
||||
const PageDetailsPage: NextPageWithLayout = () => {
|
||||
const PageDetailsPage: NextPageWithLayout = observer(() => {
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
|
||||
const [gptModalOpen, setGptModal] = useState(false);
|
||||
// store
|
||||
const {
|
||||
appConfig: { envConfig },
|
||||
} = useMobxStore();
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, pageId } = router.query;
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { handleSubmit, reset, getValues, control } = useForm<IPage>({
|
||||
defaultValues: { name: "" },
|
||||
const { handleSubmit, reset, getValues, control, setValue, watch } = useForm<IPage>({
|
||||
defaultValues: { name: "", description_html: "<p></p>" },
|
||||
});
|
||||
|
||||
// =================== 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(() => {
|
||||
if (!pageDetails) return;
|
||||
|
||||
|
|
@ -227,45 +252,71 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
|||
}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
documentDetails={{
|
||||
title: pageDetails.name,
|
||||
created_by: pageDetails.created_by,
|
||||
created_on: pageDetails.created_at,
|
||||
last_updated_at: pageDetails.updated_at,
|
||||
last_updated_by: pageDetails.updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
value={!value || value === "" ? "<p></p>" : value}
|
||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
setIsSubmitting("submitting");
|
||||
debouncedFormSave();
|
||||
}}
|
||||
duplicationConfig={{ action: duplicate_page }}
|
||||
pageArchiveConfig={
|
||||
user && pageDetails.owned_by === user.id
|
||||
? {
|
||||
is_archived: pageDetails.archived_at ? true : false,
|
||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pageLockConfig={
|
||||
user && pageDetails.owned_by === user.id ? { is_locked: false, action: lockPage } : undefined
|
||||
}
|
||||
/>
|
||||
<div className="h-full w-full relative overflow-hidden">
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<DocumentEditorWithRef
|
||||
documentDetails={{
|
||||
title: pageDetails.name,
|
||||
created_by: pageDetails.created_by,
|
||||
created_on: pageDetails.created_at,
|
||||
last_updated_at: pageDetails.updated_at,
|
||||
last_updated_by: pageDetails.updated_by,
|
||||
}}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
ref={editorRef}
|
||||
debouncedUpdatesEnabled={false}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
value={!value || value === "" ? "<p></p>" : value}
|
||||
customClassName="tracking-tight self-center px-0 h-full w-full"
|
||||
onChange={(_description_json: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
setIsSubmitting("submitting");
|
||||
debouncedFormSave();
|
||||
}}
|
||||
duplicationConfig={{ action: duplicate_page }}
|
||||
pageArchiveConfig={
|
||||
user && pageDetails.owned_by === user.id
|
||||
? {
|
||||
is_archived: pageDetails.archived_at ? true : false,
|
||||
action: pageDetails.archived_at ? unArchivePage : archivePage,
|
||||
}
|
||||
: 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>
|
||||
|
|
@ -276,7 +327,7 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
|||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
PageDetailsPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue