import React, { useEffect, useState, useRef, Fragment } from "react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // services import { AIService } from "services/ai.service"; // hooks import useToast from "hooks/use-toast"; import { usePopper } from "react-popper"; // ui import { Button, Input } from "@plane/ui"; // components import { RichReadOnlyEditorWithRef } from "@plane/rich-text-editor"; import { Popover, Transition } from "@headlessui/react"; // types import { Placement } from "@popperjs/core"; type Props = { isOpen: boolean; projectId: string; handleClose: () => void; onResponse: (response: any) => void; onError?: (error: any) => void; placement?: Placement; prompt?: string; button: JSX.Element; className?: string; }; type FormData = { prompt: string; task: string; }; const aiService = new AIService(); export const GptAssistantPopover: React.FC = (props) => { const { isOpen, projectId, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props; // states const [response, setResponse] = useState(""); const [invalidResponse, setInvalidResponse] = useState(false); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const editorRef = useRef(null); const responseRef = useRef(null); // router const router = useRouter(); const { workspaceSlug } = router.query; // toast alert const { setToastAlert } = useToast(); // popper const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "auto", }); // form const { handleSubmit, control, reset, setFocus, formState: { isSubmitting }, } = useForm({ defaultValues: { prompt: prompt || "", task: "", }, }); const onClose = () => { handleClose(); setResponse(""); setInvalidResponse(false); reset(); }; const handleServiceError = (err: any) => { const error = err?.data?.error; const errorMessage = err?.status === 429 ? error || "You have reached the maximum number of requests of 50 requests per month per user." : error || "Some error occurred. Please try again."; setToastAlert({ type: "error", title: "Error!", message: errorMessage, }); if (onError) onError(err); }; const callAIService = async (formData: FormData) => { try { const res = await aiService.createGptTask(workspaceSlug as string, projectId, { prompt: prompt || "", task: formData.task, }); setResponse(res.response_html); setFocus("task"); setInvalidResponse(res.response === ""); } catch (err) { handleServiceError(err); } }; const handleInvalidTask = () => { setToastAlert({ type: "error", title: "Error!", message: "Please enter some task to get AI assistance.", }); }; const handleAIResponse = async (formData: FormData) => { if (!workspaceSlug || !projectId) return; if (formData.task === "") { handleInvalidTask(); return; } await callAIService(formData); }; useEffect(() => { if (isOpen) setFocus("task"); }, [isOpen, setFocus]); useEffect(() => { editorRef.current?.setEditorValue(prompt || ""); }, [editorRef, prompt]); useEffect(() => { responseRef.current?.setEditorValue(`

${response}

`); }, [response, responseRef]); useEffect(() => { const handleEnterKeyPress = (event: KeyboardEvent) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSubmit(handleAIResponse)(); } }; const handleEscapeKeyPress = (event: KeyboardEvent) => { if (event.key === "Escape") { onClose(); } }; if (isOpen) { window.addEventListener("keydown", handleEnterKeyPress); window.addEventListener("keydown", handleEscapeKeyPress); } return () => { window.removeEventListener("keydown", handleEnterKeyPress); window.removeEventListener("keydown", handleEscapeKeyPress); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, handleSubmit, onClose]); const responseActionButton = response !== "" && ( ); const generateResponseButtonText = isSubmitting ? "Generating response..." : response === "" ? "Generate response" : "Generate again"; return (
{prompt && (
Content:
)} {response !== "" && (
Response: ${response}

`} customClassName={response ? "-mx-3 -my-3" : ""} noBorder borderOnFocus={false} ref={responseRef} />
)} {invalidResponse && (
No response could be generated. This may be due to insufficient content or task information. Please try again.
)}
( )} />
{responseActionButton}
); };