forked from github/plane
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
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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">
|
||||||
|
@ -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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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"
|
||||||
|
@ -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 (
|
||||||
|
Loading…
Reference in New Issue
Block a user