import { useState, useRef, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import ReactMarkdown from 'react-markdown' import PropTypes from 'prop-types' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' import { ClickAwayListener, Paper, Popper, CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons' // project import import { StyledFab } from 'ui-component/button/StyledFab' import MainCard from 'ui-component/cards/MainCard' import Transitions from 'ui-component/extended/Transitions' import './ChatMessage.css' // api import chatmessageApi from 'api/chatmessage' import predictionApi from 'api/prediction' // Hooks import useApi from 'hooks/useApi' import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' import { maxScroll } from 'store/constant' export const ChatMessage = ({ chatflowid }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) const { confirm } = useConfirm() const dispatch = useDispatch() const ps = useRef() useNotifier() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const [open, setOpen] = useState(false) const [userInput, setUserInput] = useState('') const [loading, setLoading] = useState(false) const [messages, setMessages] = useState([ { message: 'Hi there! How can I help?', type: 'apiMessage' } ]) const inputRef = useRef(null) const anchorRef = useRef(null) const prevOpen = useRef(open) const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow) const handleClose = (event) => { if (anchorRef.current && anchorRef.current.contains(event.target)) { return } setOpen(false) } const handleToggle = () => { setOpen((prevOpen) => !prevOpen) } const clearChat = async () => { const confirmPayload = { title: `Clear Chat History`, description: `Are you sure you want to clear all chat history?`, confirmButtonName: 'Clear', cancelButtonName: 'Cancel' } const isConfirmed = await confirm(confirmPayload) if (isConfirmed) { try { await chatmessageApi.deleteChatmessage(chatflowid) enqueueSnackbar({ message: 'Succesfully cleared all chat history', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( ) } }) } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ message: errorData, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } } } const scrollToBottom = () => { if (ps.current) { ps.current.scrollTo({ top: maxScroll, behavior: 'smooth' }) } } const addChatMessage = async (message, type) => { try { const newChatMessageBody = { role: type, content: message, chatflowid: chatflowid } await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody) } catch (error) { console.error(error) } } // Handle errors const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '') setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]) addChatMessage(message, 'apiMessage') setLoading(false) setUserInput('') setTimeout(() => { inputRef.current.focus() }, 100) } // Handle form submission const handleSubmit = async (e) => { e.preventDefault() if (userInput.trim() === '') { return } setLoading(true) setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }]) addChatMessage(userInput, 'userMessage') // Send user question and history to API try { const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, { question: userInput, history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?') }) if (response.data) { const data = response.data setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }]) addChatMessage(data, 'apiMessage') setLoading(false) setUserInput('') setTimeout(() => { inputRef.current.focus() scrollToBottom() }, 100) } } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` handleError(errorData) return } } // Prevent blank submissions and allow for multiline input const handleEnter = (e) => { if (e.key === 'Enter' && userInput) { if (!e.shiftKey && userInput) { handleSubmit(e) } } else if (e.key === 'Enter') { e.preventDefault() } } // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data) { const loadedMessages = [] for (const message of getChatmessageApi.data) { loadedMessages.push({ message: message.content, type: message.role }) } setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getChatmessageApi.data]) // Auto scroll chat to bottom useEffect(() => { scrollToBottom() }, [messages]) useEffect(() => { if (prevOpen.current === true && open === false) { anchorRef.current.focus() } if (open && chatflowid) { getChatmessageApi.request(chatflowid) scrollToBottom() } prevOpen.current = open return () => { setUserInput('') setLoading(false) setMessages([ { message: 'Hi there! How can I help?', type: 'apiMessage' } ]) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, chatflowid]) return ( <> {open ? : } {open && ( )} {({ TransitionProps }) => (
{messages.map((message, index) => { return ( // The latest message sent by the user will be animated while waiting for a response {/* Display the correct icon depending on the message type */} {message.type === 'apiMessage' ? ( AI ) : ( Me )}
{/* Messages are being rendered in Markdown format */} {message.message}
) })}
setUserInput(e.target.value)} endAdornment={ {loading ? (
) : ( // Send icon SVG in input field )}
} />
)}
) } ChatMessage.propTypes = { chatflowid: PropTypes.string }