diff --git a/src/frontend/src/components/chatComponent/chatTrigger/index.tsx b/src/frontend/src/components/chatComponent/chatTrigger/index.tsx index 6c4bbadde..2f08ae92a 100644 --- a/src/frontend/src/components/chatComponent/chatTrigger/index.tsx +++ b/src/frontend/src/components/chatComponent/chatTrigger/index.tsx @@ -5,7 +5,7 @@ import { PopUpContext } from "../../../contexts/popUpContext"; import { useContext } from "react"; import ChatModal from "../../../modals/chatModal"; -export default function ChatTrigger({open, setOpen}){ +export default function ChatTrigger({open, setOpen,flow}){ const {openPopUp} = useContext(PopUpContext) return( { setOpen(true); - openPopUp() + openPopUp() }} >
diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index 6393df55b..c18deb8de 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -259,7 +259,7 @@ export default function Chat({ flow, reactFlowInstance }: ChatType) {
- + ); } diff --git a/src/frontend/src/contexts/index.tsx b/src/frontend/src/contexts/index.tsx index 310606ea5..06c576b83 100644 --- a/src/frontend/src/contexts/index.tsx +++ b/src/frontend/src/contexts/index.tsx @@ -13,12 +13,10 @@ export default function ContextWrapper({ children }: { children: ReactNode }) { - - + - {children} + {children} - diff --git a/src/frontend/src/modals/chatModal/index.tsx b/src/frontend/src/modals/chatModal/index.tsx index 6c232939f..5d3c8df81 100644 --- a/src/frontend/src/modals/chatModal/index.tsx +++ b/src/frontend/src/modals/chatModal/index.tsx @@ -2,14 +2,177 @@ import { Dialog, Transition } from "@headlessui/react"; import { XMarkIcon, ClipboardDocumentListIcon, + LockClosedIcon, + PaperAirplaneIcon, } from "@heroicons/react/24/outline"; -import { Fragment, useContext, useRef, useState } from "react"; +import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { PopUpContext } from "../../contexts/popUpContext"; +import { NodeType } from "../../types/flow"; +import { TabsContext } from "../../contexts/tabsContext"; +import { alertContext } from "../../contexts/alertContext"; +import { classNames, snakeToNormalCase } from "../../utils"; +import { sendAll } from "../../controllers/API"; +import { typesContext } from "../../contexts/typesContext"; +import ChatMessage from "../../components/chatComponent/chatMessage"; +const _ = require("lodash"); + +export default function ChatModal({ flow }) { + const { updateFlow, lockChat, setLockChat, flows, tabIndex } = + useContext(TabsContext); + const [saveChat, setSaveChat] = useState(false); + const [chatValue, setChatValue] = useState(""); + const [chatHistory, setChatHistory] = useState(flow.chat); + const { reactFlowInstance } = useContext(typesContext); + const { setErrorData, setNoticeData } = useContext(alertContext); + const addChatHistory = ( + message: string, + isSend: boolean, + thought?: string + ) => { + let tabsChange = false; + setChatHistory((old) => { + let newChat = _.cloneDeep(old); + if (JSON.stringify(flow.chat) !== JSON.stringify(old)) { + tabsChange = true; + return old; + } + if (thought) { + newChat.push({ message, isSend, thought }); + } else { + newChat.push({ message, isSend }); + } + return newChat; + }); + if (tabsChange) { + if (thought) { + updateFlow({ + ..._.cloneDeep(flow), + chat: [...flow.chat, { isSend, message, thought }], + }); + } else { + updateFlow({ + ..._.cloneDeep(flow), + chat: [...flow.chat, { isSend, message }], + }); + } + } + setSaveChat((chat) => !chat); + }; + useEffect(() => { + updateFlow({ ..._.cloneDeep(flow), chat: chatHistory }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [saveChat]); + useEffect(() => { + setChatHistory(flow.chat); + }, [flow]); + useEffect(() => { + if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" }); + }, [chatHistory]); + + function validateNode(n: NodeType): Array { + if (!n.data?.node?.template || !Object.keys(n.data.node.template)) { + setNoticeData({ + title: + "We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!", + }); + return []; + } + + const { + type, + node: { template }, + } = n.data; + + return Object.keys(template).reduce( + (errors: Array, t) => + errors.concat( + template[t].required && + template[t].show && + (!template[t].value || template[t].value === "") && + !reactFlowInstance + .getEdges() + .some( + (e) => + e.targetHandle.split("|")[1] === t && + e.targetHandle.split("|")[2] === n.id + ) + ? [ + `${type} is missing ${ + template.display_name + ? template.display_name + : snakeToNormalCase(template[t].name) + }.`, + ] + : [] + ), + [] as string[] + ); + } + + function validateNodes() { + console.log(reactFlowInstance) + return reactFlowInstance + .getNodes() + .flatMap((n: NodeType) => validateNode(n)); + } + + const ref = useRef(null); + + function sendMessage() { + if (chatValue !== "") { + let nodeValidationErrors = validateNodes(); + if (nodeValidationErrors.length === 0) { + setLockChat(true); + let message = chatValue; + setChatValue(""); + addChatHistory(message, true); + + sendAll({ + ...reactFlowInstance.toObject(), + message, + chatHistory, + name: flow.name, + description: flow.description, + }) + .then((r) => { + addChatHistory(r.data.result, false, r.data.thought); + setLockChat(false); + }) + .catch((error) => { + setErrorData({ + title: error.message ?? "Unknown Error", + list: [error.response.data.detail], + }); + setLockChat(false); + let lastMessage; + setChatHistory((chatHistory) => { + let newChat = chatHistory; + + lastMessage = newChat.pop().message; + return newChat; + }); + setChatValue(lastMessage); + }); + } else { + setErrorData({ + title: "Oops! Looks like you missed some required information:", + list: nodeValidationErrors, + }); + } + } else { + setErrorData({ + title: "Error sending message", + list: ["The message cannot be empty."], + }); + } + } + function clearChat() { + setChatHistory([]); + updateFlow({ ..._.cloneDeep(flow), chat: [] }); + } -export default function ChatModal() { const [open, setOpen] = useState(true); const { closePopUp } = useContext(PopUpContext); - const ref = useRef(); function setModalOpen(x: boolean) { setOpen(x); if (x === false) { @@ -50,10 +213,50 @@ export default function ChatModal() { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
-
+
+ {chatHistory.map((c, i) => ( + + ))} +
+
+
+
+ { + if (event.key === "Enter" && !lockChat) { + sendMessage(); + } + }} + type="text" + disabled={lockChat} + value={lockChat ? "Thinking..." : chatValue} + onChange={(e) => { + setChatValue(e.target.value); + }} + className={classNames( + lockChat + ? "bg-gray-500 text-white" + : "dark:bg-gray-700", + "form-input block w-full rounded-md border-gray-300 dark:border-gray-600 dark:text-white pr-10 sm:text-sm" + )} + placeholder={"Send a message..."} + /> +
+ +
-
input area