diff --git a/apps/app/components/core/gpt-assistant-modal.tsx b/apps/app/components/core/gpt-assistant-modal.tsx new file mode 100644 index 000000000..0afe481a7 --- /dev/null +++ b/apps/app/components/core/gpt-assistant-modal.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + +// react-hook-form +import { useForm } from "react-hook-form"; +// services +import aiService from "services/ai.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Input, PrimaryButton, SecondaryButton } from "components/ui"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + inset?: string; + content: string; + onResponse: (response: string) => void; +}; + +type FormData = { + prompt: string; + task: string; +}; + +export const GptAssistantModal: React.FC = ({ + isOpen, + handleClose, + inset = "top-0 left-0", + content, + onResponse, +}) => { + const [response, setResponse] = useState(""); + const [invalidResponse, setInvalidResponse] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + 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 (!content || content === "") { + setToastAlert({ + type: "error", + title: "Error!", + message: "Please enter some description to get AI assistance.", + }); + 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, + task: formData.task, + }) + .then((res) => { + setResponse(res.response); + setFocus("task"); + + if (res.response === "") setInvalidResponse(true); + else setInvalidResponse(false); + }); + }; + + useEffect(() => { + if (isOpen) setFocus("task"); + }, [isOpen, setFocus]); + + return ( +
+
+
+ Content:

{content}

+
+ {response !== "" && ( +
+ Response:

{response}

+
+ )} + {invalidResponse && ( +
+ No response could be generated. This may be due to insufficient content or task + information. Please try again. +
+ )} + +
+ {response !== "" && ( + { + onResponse(response); + onClose(); + }} + > + Use this response + + )} +
+ Close + + {isSubmitting + ? "Generating response..." + : response === "" + ? "Generate response" + : "Generate again"} + +
+
+
+
+ ); +}; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 68b7af198..d25df79d2 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -3,6 +3,7 @@ export * from "./list-view"; export * from "./sidebar"; export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; +export * from "./gpt-assistant-modal"; export * from "./image-upload-modal"; export * from "./issues-view-filter"; export * from "./issues-view"; diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 873d88014..f4212d751 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -9,8 +9,12 @@ import { mutate } from "swr"; import { Controller, useForm } from "react-hook-form"; // services import pagesService from "services/pages.service"; +import aiService from "services/ai.service"; // hooks import useToast from "hooks/use-toast"; +// components +import { CreateUpdateIssueModal } from "components/issues"; +import { GptAssistantModal } from "components/core"; // ui import { CustomMenu, Loader, TextArea } from "components/ui"; // icons @@ -18,13 +22,13 @@ import { WaterDropIcon } from "components/icons"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import { IPageBlock } from "types"; +import { IPageBlock, IProject } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; -import { CreateUpdateIssueModal } from "components/issues"; type Props = { block: IPageBlock; + projectDetails: IProject | undefined; }; const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { @@ -36,9 +40,11 @@ const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor ), }); -export const SinglePageBlock: React.FC = ({ block }) => { +export const SinglePageBlock: React.FC = ({ block, projectDetails }) => { const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + const [gptAssistantModal, setGptAssistantModal] = useState(false); + const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; @@ -68,17 +74,15 @@ export const SinglePageBlock: React.FC = ({ block }) => { false ); - await pagesService.patchPageBlock( - workspaceSlug as string, - projectId as string, - pageId as string, - block.id, - { + await pagesService + .patchPageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id, { name: formData.name, description: formData.description, description_html: formData.description_html, - } - ); + }) + .then(() => { + mutate(PAGE_BLOCKS_LIST(pageId as string)); + }); }; const pushBlockIntoIssues = async () => { @@ -142,6 +146,28 @@ export const SinglePageBlock: React.FC = ({ block }) => { }); }; + const handleAiAssistance = async (response: string) => { + if (!workspaceSlug || !projectId) return; + + setValue("description", {}); + setValue("description_html", `

${response}

`); + handleSubmit(updatePageBlock)() + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Block description updated successfully.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Block description could not be updated. Please try again.", + }); + }); + }; + const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; @@ -187,23 +213,32 @@ export const SinglePageBlock: React.FC = ({ block }) => { role="textbox" disabled={block.issue ? true : false} /> - } noBorder noChevron> - {block.issue ? ( - Copy issue link - ) : ( - <> - - Push into issues - - - Edit and push into issues - - - )} - Delete block - +
+ + } noBorder noChevron> + {block.issue ? ( + Copy issue link + ) : ( + <> + + Push into issues + + + Edit and push into issues + + + )} + Delete block + +
-
+
= ({ block }) => { placeholder="Description..." editable={block.issue ? false : true} customClassName="text-gray-500" + // gptOption noBorder /> )} /> + setGptAssistantModal(false)} + inset="top-2 left-0" + content={block.description_stripped} + onResponse={handleAiAssistance} + />
); diff --git a/apps/app/components/pages/single-page-list-item.tsx b/apps/app/components/pages/single-page-list-item.tsx index b95eeaa49..27cb02de5 100644 --- a/apps/app/components/pages/single-page-list-item.tsx +++ b/apps/app/components/pages/single-page-list-item.tsx @@ -12,6 +12,7 @@ import { truncateText } from "helpers/string.helper"; import { renderShortDate, renderShortTime } from "helpers/date-time.helper"; // types import { IPage } from "types"; +import { PencilScribbleIcon } from "components/icons"; type TSingleStatProps = { page: IPage; @@ -36,6 +37,7 @@ export const SinglePageListItem: React.FC = ({
+

{truncateText(page.name, 75)}

diff --git a/apps/app/components/rich-text-editor/index.tsx b/apps/app/components/rich-text-editor/index.tsx index f4f988ecd..66bbac8d8 100644 --- a/apps/app/components/rich-text-editor/index.tsx +++ b/apps/app/components/rich-text-editor/index.tsx @@ -48,6 +48,7 @@ export interface IRemirrorRichTextEditor { showToolbar?: boolean; editable?: boolean; customClassName?: string; + gptOption?: boolean; noBorder?: boolean; } @@ -66,6 +67,7 @@ const RemirrorRichTextEditor: FC = (props) => { showToolbar = true, editable = true, customClassName, + gptOption = false, noBorder = false, } = props; @@ -215,7 +217,7 @@ const RemirrorRichTextEditor: FC = (props) => { renderOutsideEditor > - + )} diff --git a/apps/app/components/rich-text-editor/toolbar/float-tool-tip.tsx b/apps/app/components/rich-text-editor/toolbar/float-tool-tip.tsx index 8146159a3..87f2427d3 100644 --- a/apps/app/components/rich-text-editor/toolbar/float-tool-tip.tsx +++ b/apps/app/components/rich-text-editor/toolbar/float-tool-tip.tsx @@ -8,39 +8,61 @@ import { ToggleBulletListButton, ToggleCodeButton, ToggleHeadingButton, + useActive, } from "@remirror/react"; +import { EditorState } from "remirror"; -export const CustomFloatingToolbar: React.FC = () => ( -
-
- - - +type Props = { + gptOption?: boolean; + editorState: Readonly; +}; + +export const CustomFloatingToolbar: React.FC = ({ gptOption, editorState }) => { + const active = useActive(); + + return ( +
+
+ + + +
+
+ + + + +
+
+ + +
+ {gptOption && ( +
+ +
+ )} +
+ +
-
- - - - -
-
- - -
-
- -
-
-); + ); +}; diff --git a/apps/app/components/ui/buttons/index.ts b/apps/app/components/ui/buttons/index.ts index 9edd83eed..0ce5c556a 100644 --- a/apps/app/components/ui/buttons/index.ts +++ b/apps/app/components/ui/buttons/index.ts @@ -1,4 +1,3 @@ export * from "./danger-button"; -export * from "./no-border-button"; export * from "./primary-button"; export * from "./secondary-button"; diff --git a/apps/app/components/ui/buttons/no-border-button.tsx b/apps/app/components/ui/buttons/no-border-button.tsx deleted file mode 100644 index 2d329a063..000000000 --- a/apps/app/components/ui/buttons/no-border-button.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// types -import { ButtonProps } from "./type"; - -export const NoBorderButton: React.FC = ({ - children, - className = "", - onClick, - type = "button", - disabled = false, - loading = false, - size = "sm", - outline = false, -}) => ( - -); diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx index 7e426cb6c..702f1b7d9 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/pages/[pageId].tsx @@ -323,9 +323,6 @@ const SinglePage: NextPage = () => { Share -
{({ open }) => ( @@ -407,7 +404,11 @@ const SinglePage: NextPage = () => { <>
{pageBlocks.map((block) => ( - + ))}
diff --git a/apps/app/services/ai.service.ts b/apps/app/services/ai.service.ts new file mode 100644 index 000000000..03d3ce10b --- /dev/null +++ b/apps/app/services/ai.service.ts @@ -0,0 +1,24 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class AiServices extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async createGptTask( + workspaceSlug: string, + projectId: string, + data: { prompt: string; task: string } + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/ai-assistant/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} + +export default new AiServices(); diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index d723ab053..711082727 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -75,6 +75,7 @@ export interface IIssue { cycle_detail: ICycle | null; description: any; description_html: any; + description_stripped: any; id: string; issue_cycle: IIssueCycle | null; issue_link: { diff --git a/apps/app/types/pages.d.ts b/apps/app/types/pages.d.ts index e1f1e6cc5..fc782f4d4 100644 --- a/apps/app/types/pages.d.ts +++ b/apps/app/types/pages.d.ts @@ -33,6 +33,7 @@ export interface IPageBlock { created_by: string; description: any; description_html: any; + description_stripped: any; id: string; issue: string | null; issue_detail: string | null;