Merge pull request #149 from FlowiseAI/feature/Streaming

Feature/Streaming
This commit is contained in:
Ong Chung Yau
2023-05-26 09:01:40 +07:00
committed by GitHub
29 changed files with 902 additions and 301 deletions
+4 -1
View File
@@ -10,10 +10,13 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
createNewChatflow,
updateChatflow,
deleteChatflow
deleteChatflow,
getIsChatflowStreaming
}
@@ -1,6 +1,39 @@
export default function componentStyleOverrides(theme) {
const bgColor = theme.colors?.grey50
return {
MuiCssBaseline: {
styleOverrides: {
body: {
scrollbarWidth: 'thin',
scrollbarColor: theme?.customization?.isDarkMode
? `${theme.colors?.grey500} ${theme.colors?.darkPrimaryMain}`
: `${theme.colors?.grey300} ${theme.paper}`,
'&::-webkit-scrollbar, & *::-webkit-scrollbar': {
width: 12,
height: 12,
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
},
'&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb': {
borderRadius: 8,
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.grey500 : theme.colors?.grey300,
minHeight: 24,
border: `3px solid ${theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper}`
},
'&::-webkit-scrollbar-thumb:focus, & *::-webkit-scrollbar-thumb:focus': {
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
},
'&::-webkit-scrollbar-thumb:active, & *::-webkit-scrollbar-thumb:active': {
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
},
'&::-webkit-scrollbar-thumb:hover, & *::-webkit-scrollbar-thumb:hover': {
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
},
'&::-webkit-scrollbar-corner, & *::-webkit-scrollbar-corner': {
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
}
}
}
},
MuiButton: {
styleOverrides: {
root: {
+2 -1
View File
@@ -7,7 +7,8 @@ export default function themePalette(theme) {
return {
mode: theme?.customization?.navType,
common: {
black: theme.colors?.darkPaper
black: theme.colors?.darkPaper,
dark: theme.colors?.darkPrimaryMain
},
primary: {
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom'
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
import useConfirm from 'hooks/useConfirm'
import { StyledButton } from 'ui-component/button/StyledButton'
@@ -20,9 +20,7 @@ const ConfirmDialog = () => {
{confirmState.title}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: 'black' }} id='alert-dialog-description'>
{confirmState.description}
</DialogContentText>
<span>{confirmState.description}</span>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
@@ -0,0 +1,123 @@
import { IconClipboard, IconDownload } from '@tabler/icons'
import { memo, useState } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import PropTypes from 'prop-types'
import { Box, IconButton, Popover, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
const programmingLanguages = {
javascript: '.js',
python: '.py',
java: '.java',
c: '.c',
cpp: '.cpp',
'c++': '.cpp',
'c#': '.cs',
ruby: '.rb',
php: '.php',
swift: '.swift',
'objective-c': '.m',
kotlin: '.kt',
typescript: '.ts',
go: '.go',
perl: '.pl',
rust: '.rs',
scala: '.scala',
haskell: '.hs',
lua: '.lua',
shell: '.sh',
sql: '.sql',
html: '.html',
css: '.css'
}
export const CodeBlock = memo(({ language, chatflowid, isDialog, value }) => {
const theme = useTheme()
const [anchorEl, setAnchorEl] = useState(null)
const openPopOver = Boolean(anchorEl)
const handleClosePopOver = () => {
setAnchorEl(null)
}
const copyToClipboard = (event) => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return
}
navigator.clipboard.writeText(value)
setAnchorEl(event.currentTarget)
setTimeout(() => {
handleClosePopOver()
}, 1500)
}
const downloadAsFile = () => {
const fileExtension = programmingLanguages[language] || '.file'
const suggestedFileName = `file-${chatflowid}${fileExtension}`
const fileName = suggestedFileName
if (!fileName) {
// user pressed cancel on prompt
return
}
const blob = new Blob([value], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.download = fileName
link.href = url
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
return (
<div style={{ width: isDialog ? '' : 300 }}>
<Box sx={{ color: 'white', background: theme.palette?.common.dark, p: 1, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{language}
<div style={{ flex: 1 }}></div>
<IconButton size='small' title='Copy' color='success' onClick={copyToClipboard}>
<IconClipboard />
</IconButton>
<Popover
open={openPopOver}
anchorEl={anchorEl}
onClose={handleClosePopOver}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
>
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
Copied!
</Typography>
</Popover>
<IconButton size='small' title='Download' color='primary' onClick={downloadAsFile}>
<IconDownload />
</IconButton>
</div>
</Box>
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
{value}
</SyntaxHighlighter>
</div>
)
})
CodeBlock.displayName = 'CodeBlock'
CodeBlock.propTypes = {
language: PropTypes.string,
chatflowid: PropTypes.string,
isDialog: PropTypes.bool,
value: PropTypes.string
}
@@ -0,0 +1,4 @@
import { memo } from 'react'
import ReactMarkdown from 'react-markdown'
export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children)
+20
View File
@@ -314,3 +314,23 @@ export const rearrangeToolsOrdering = (newValues, sourceNodeId) => {
newValues.sort((a, b) => sortKey(a) - sortKey(b))
}
export const throttle = (func, limit) => {
let lastFunc
let lastRan
return (...args) => {
if (!lastRan) {
func(...args)
lastRan = Date.now()
} else {
clearTimeout(lastFunc)
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func(...args)
lastRan = Date.now()
}
}, limit - (Date.now() - lastRan))
}
}
}
+2 -2
View File
@@ -23,7 +23,7 @@ import ButtonEdge from './ButtonEdge'
import CanvasHeader from './CanvasHeader'
import AddNodes from './AddNodes'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { ChatMessage } from 'views/chatmessage/ChatMessage'
import { ChatPopUp } from 'views/chatmessage/ChatPopUp'
import { flowContext } from 'store/context/ReactFlowContext'
// API
@@ -514,7 +514,7 @@ const Canvas = () => {
/>
<Background color='#aaa' gap={16} />
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
<ChatMessage chatflowid={chatflowId} />
<ChatPopUp chatflowid={chatflowId} />
</ReactFlow>
</div>
</div>
@@ -0,0 +1,62 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { Dialog, DialogContent, DialogTitle, Button } from '@mui/material'
import { ChatMessage } from './ChatMessage'
import { StyledButton } from 'ui-component/button/StyledButton'
import { IconEraser } from '@tabler/icons'
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
const component = show ? (
<Dialog
open={show}
fullWidth
maxWidth='md'
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
sx={{ overflow: 'visible' }}
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }}></div>
{customization.isDarkMode && (
<StyledButton
variant='outlined'
color='error'
title='Clear Conversation'
onClick={onClear}
startIcon={<IconEraser />}
>
Clear Chat
</StyledButton>
)}
{!customization.isDarkMode && (
<Button variant='outlined' color='error' title='Clear Conversation' onClick={onClear} startIcon={<IconEraser />}>
Clear Chat
</Button>
)}
</div>
</DialogTitle>
<DialogContent>
<ChatMessage isDialog={true} open={dialogProps.open} chatflowid={dialogProps.chatflowid} />
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ChatExpandDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onClear: PropTypes.func,
onCancel: PropTypes.func
}
export default ChatExpandDialog
@@ -80,7 +80,7 @@
}
.markdownanswer code {
color: #15cb19;
color: #0ab126;
font-weight: 500;
white-space: pre-wrap !important;
}
@@ -92,6 +92,7 @@
.boticon,
.usericon {
margin-top: 1rem;
margin-right: 1rem;
border-radius: 1rem;
}
@@ -119,3 +120,12 @@
justify-content: center;
align-items: center;
}
.cloud-dialog {
width: 100%;
height: calc(100vh - 230px);
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
}
+186 -240
View File
@@ -1,53 +1,38 @@
import { useState, useRef, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ReactMarkdown from 'react-markdown'
import { useState, useRef, useEffect, useCallback } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import socketIOClient from 'socket.io-client'
import { cloneDeep } from 'lodash'
import rehypeMathjax from 'rehype-mathjax'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import {
ClickAwayListener,
Paper,
Popper,
CircularProgress,
OutlinedInput,
Divider,
InputAdornment,
IconButton,
Box,
Button
} from '@mui/material'
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons'
import { IconSend } 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 { CodeBlock } from 'ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import './ChatMessage.css'
// api
import chatmessageApi from 'api/chatmessage'
import chatflowsApi from 'api/chatflows'
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'
// Const
import { baseURL, maxScroll } from 'store/constant'
export const ChatMessage = ({ chatflowid }) => {
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
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([
@@ -56,72 +41,21 @@ export const ChatMessage = ({ chatflowid }) => {
type: 'apiMessage'
}
])
const [socketIOClientId, setSocketIOClientId] = useState('')
const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false)
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
} 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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const scrollToBottom = () => {
if (ps.current) {
ps.current.scrollTo({ top: maxScroll, behavior: 'smooth' })
ps.current.scrollTo({ top: maxScroll })
}
}
const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput])
const addChatMessage = async (message, type) => {
try {
const newChatMessageBody = {
@@ -135,6 +69,15 @@ export const ChatMessage = ({ chatflowid }) => {
}
}
const updateLastMessage = (text) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].message += text
return allMessages
})
}
// 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`, '')
@@ -143,7 +86,7 @@ export const ChatMessage = ({ chatflowid }) => {
setLoading(false)
setUserInput('')
setTimeout(() => {
inputRef.current.focus()
inputRef.current?.focus()
}, 100)
}
@@ -161,18 +104,22 @@ export const ChatMessage = ({ chatflowid }) => {
// Send user question and history to API
try {
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, {
const params = {
question: userInput,
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?')
})
}
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
if (response.data) {
const data = response.data
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
if (!isChatFlowAvailableToStream) setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
addChatMessage(data, 'apiMessage')
setLoading(false)
setUserInput('')
setTimeout(() => {
inputRef.current.focus()
inputRef.current?.focus()
scrollToBottom()
}, 100)
}
@@ -210,22 +157,47 @@ export const ChatMessage = ({ chatflowid }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatmessageApi.data])
// Get chatflow streaming capability
useEffect(() => {
if (getIsChatflowStreamingApi.data) {
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIsChatflowStreamingApi.data])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
}, [messages])
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
if (isDialog && inputRef) {
setTimeout(() => {
inputRef.current?.focus()
}, 100)
}
}, [isDialog, inputRef])
useEffect(() => {
let socket
if (open && chatflowid) {
getChatmessageApi.request(chatflowid)
getIsChatflowStreamingApi.request(chatflowid)
scrollToBottom()
}
prevOpen.current = open
socket = socketIOClient(baseURL)
socket.on('connect', () => {
setSocketIOClientId(socket.id)
})
socket.on('start', () => {
setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }])
})
socket.on('token', updateLastMessage)
}
return () => {
setUserInput('')
@@ -236,6 +208,10 @@ export const ChatMessage = ({ chatflowid }) => {
type: 'apiMessage'
}
])
if (socket) {
socket.disconnect()
setSocketIOClientId('')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -243,151 +219,121 @@ export const ChatMessage = ({ chatflowid }) => {
return (
<>
<StyledFab
sx={{ position: 'absolute', right: 20, top: 20 }}
ref={anchorRef}
size='small'
color='secondary'
aria-label='chat'
title='Chat'
onClick={handleToggle}
>
{open ? <IconX /> : <IconMessage />}
</StyledFab>
{open && (
<StyledFab
sx={{ position: 'absolute', right: 80, top: 20 }}
onClick={clearChat}
size='small'
color='error'
aria-label='clear'
title='Clear Chat History'
>
<IconEraser />
</StyledFab>
)}
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [40, 14]
<div className={isDialog ? 'cloud-dialog' : 'cloud'}>
<div ref={ps} className='messagelist'>
{messages &&
messages.map((message, index) => {
return (
// The latest message sent by the user will be animated while waiting for a response
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
className='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
className='usericon'
/>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
</div>
</Box>
)
})}
</div>
</div>
<Divider />
<div className='center'>
<div style={{ width: '100%' }}>
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
<OutlinedInput
inputRef={inputRef}
// eslint-disable-next-line
autoFocus
sx={{ width: '100%' }}
disabled={loading || !chatflowid}
onKeyDown={handleEnter}
id='userInput'
name='userInput'
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
value={userInput}
onChange={onChange}
endAdornment={
<InputAdornment position='end'>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
)}
</IconButton>
</InputAdornment>
}
}
]
}}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<div className='cloud'>
<div ref={ps} className='messagelist'>
{messages.map((message, index) => {
return (
// The latest message sent by the user will be animated while waiting for a response
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex', alignItems: 'center' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
className='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
className='usericon'
/>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<ReactMarkdown linkTarget={'_blank'}>{message.message}</ReactMarkdown>
</div>
</Box>
)
})}
</div>
</div>
<Divider />
<div className='center'>
<div style={{ width: '100%' }}>
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
<OutlinedInput
inputRef={inputRef}
// eslint-disable-next-line
autoFocus
sx={{ width: '100%' }}
disabled={loading || !chatflowid}
onKeyDown={handleEnter}
id='userInput'
name='userInput'
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
endAdornment={
<InputAdornment position='end'>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid
? '#9e9e9e'
: customization.isDarkMode
? 'white'
: '#1e88e5'
}
/>
)}
</IconButton>
</InputAdornment>
}
/>
</form>
</div>
</div>
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
/>
</form>
</div>
</div>
</>
)
}
ChatMessage.propTypes = { chatflowid: PropTypes.string }
ChatMessage.propTypes = {
open: PropTypes.bool,
chatflowid: PropTypes.string,
isDialog: PropTypes.bool
}
@@ -0,0 +1,208 @@
import { useState, useRef, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import { ClickAwayListener, Paper, Popper, Button } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconMessage, IconX, IconEraser, IconArrowsMaximize } 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 } from './ChatMessage'
import ChatExpandDialog from './ChatExpandDialog'
// api
import chatmessageApi from 'api/chatmessage'
// Hooks
import useConfirm from 'hooks/useConfirm'
import useNotifier from 'utils/useNotifier'
// Const
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
export const ChatPopUp = ({ chatflowid }) => {
const theme = useTheme()
const { confirm } = useConfirm()
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [open, setOpen] = useState(false)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const anchorRef = useRef(null)
const prevOpen = useRef(open)
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return
}
setOpen(false)
}
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen)
}
const expandChat = () => {
const props = {
open: true,
chatflowid: chatflowid
}
setExpandDialogProps(props)
setShowExpandDialog(true)
}
const resetChatDialog = () => {
const props = {
...expandDialogProps,
open: false
}
setExpandDialogProps(props)
setTimeout(() => {
const resetProps = {
...expandDialogProps,
open: true
}
setExpandDialogProps(resetProps)
}, 500)
}
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)
resetChatDialog()
enqueueSnackbar({
message: 'Succesfully cleared all chat history',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
} 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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
prevOpen.current = open
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, chatflowid])
return (
<>
<StyledFab
sx={{ position: 'absolute', right: 20, top: 20 }}
ref={anchorRef}
size='small'
color='secondary'
aria-label='chat'
title='Chat'
onClick={handleToggle}
>
{open ? <IconX /> : <IconMessage />}
</StyledFab>
{open && (
<StyledFab
sx={{ position: 'absolute', right: 80, top: 20 }}
onClick={clearChat}
size='small'
color='error'
aria-label='clear'
title='Clear Chat History'
>
<IconEraser />
</StyledFab>
)}
{open && (
<StyledFab
sx={{ position: 'absolute', right: 140, top: 20 }}
onClick={expandChat}
size='small'
color='primary'
aria-label='expand'
title='Expand Chat'
>
<IconArrowsMaximize />
</StyledFab>
)}
<Popper
placement='bottom-end'
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
popperOptions={{
modifiers: [
{
name: 'offset',
options: {
offset: [40, 14]
}
}
]
}}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<ChatMessage chatflowid={chatflowid} open={open} />
</MainCard>
</ClickAwayListener>
</Paper>
</Transitions>
)}
</Popper>
<ChatExpandDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
onClear={clearChat}
onCancel={() => setShowExpandDialog(false)}
></ChatExpandDialog>
</>
)
}
ChatPopUp.propTypes = { chatflowid: PropTypes.string }