Feature/Full File Uploads & Message Delete API (#3314)

* add functionality for full file uploads, add remove messages from view dialog and API

* add attachments swagger

* update question to include uploadedFilesContent

* make config dialog modal lg size
This commit is contained in:
Henry Heng
2024-10-23 11:00:46 +01:00
committed by GitHub
parent 116d02d0bc
commit 53e504c32f
31 changed files with 1012 additions and 193 deletions
@@ -11,6 +11,7 @@ import AnalyseFlow from '@/ui-component/extended/AnalyseFlow'
import StarterPrompts from '@/ui-component/extended/StarterPrompts'
import Leads from '@/ui-component/extended/Leads'
import FollowUpPrompts from '@/ui-component/extended/FollowUpPrompts'
import FileUpload from '@/ui-component/extended/FileUpload'
const CHATFLOW_CONFIGURATION_TABS = [
{
@@ -44,6 +45,10 @@ const CHATFLOW_CONFIGURATION_TABS = [
{
label: 'Leads',
id: 'leads'
},
{
label: 'File Upload',
id: 'fileUpload'
}
]
@@ -85,7 +90,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
onClose={onCancel}
open={show}
fullWidth
maxWidth={'md'}
maxWidth={'lg'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
@@ -127,6 +132,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
{item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null}
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
{item.id === 'fileUpload' ? <FileUpload dialogProps={dialogProps} /> : null}
</TabPanel>
))}
</DialogContent>
@@ -25,7 +25,10 @@ import {
Chip,
Card,
CardMedia,
CardContent
CardContent,
FormControlLabel,
Checkbox,
DialogActions
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import DatePicker from 'react-datepicker'
@@ -84,6 +87,52 @@ const messageImageStyle = {
objectFit: 'cover'
}
const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const [hardDelete, setHardDelete] = useState(false)
const onSubmit = () => {
onConfirm(hardDelete)
}
const component = show ? (
<Dialog
fullWidth
maxWidth='xs'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<span style={{ marginTop: '20px', marginBottom: '20px' }}>{dialogProps.description}</span>
<FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node'
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
<StyledButton variant='contained' onClick={onSubmit}>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ConfirmDeleteMessageDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
@@ -103,6 +152,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [selectedChatId, setSelectedChatId] = useState('')
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const [hardDeleteDialogOpen, setHardDeleteDialogOpen] = useState(false)
const [hardDeleteDialogProps, setHardDeleteDialogProps] = useState({})
const [chatTypeFilter, setChatTypeFilter] = useState([])
const [feedbackTypeFilter, setFeedbackTypeFilter] = useState([])
const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1))
@@ -175,6 +226,83 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
})
}
const onDeleteMessages = () => {
setHardDeleteDialogProps({
title: 'Delete Messages',
description: 'Are you sure you want to delete messages? This action cannot be undone.',
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
})
setHardDeleteDialogOpen(true)
}
const deleteMessages = async (hardDelete) => {
setHardDeleteDialogOpen(false)
const chatflowid = dialogProps.chatflow.id
try {
const obj = { chatflowid, isClearFromViewMessageDialog: true }
let _chatTypeFilter = chatTypeFilter
if (typeof chatTypeFilter === 'string') {
_chatTypeFilter = JSON.parse(chatTypeFilter)
}
if (_chatTypeFilter.length === 1) {
obj.chatType = _chatTypeFilter[0]
}
let _feedbackTypeFilter = feedbackTypeFilter
if (typeof feedbackTypeFilter === 'string') {
_feedbackTypeFilter = JSON.parse(feedbackTypeFilter)
}
if (_feedbackTypeFilter.length === 1) {
obj.feedbackType = _feedbackTypeFilter[0]
}
if (startDate) obj.startDate = startDate
if (endDate) obj.endDate = endDate
if (hardDelete) obj.hardDelete = true
await chatmessageApi.deleteChatmessage(chatflowid, obj)
enqueueSnackbar({
message: 'Succesfully deleted messages',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
getChatmessageApi.request(chatflowid, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate
})
getStatsApi.request(chatflowid, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate
})
} catch (error) {
console.error(error)
enqueueSnackbar({
message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
const exportMessages = async () => {
if (!storagePath && getStoragePathFromServer.data) {
storagePath = getStoragePathFromServer.data.storagePath
@@ -675,7 +803,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
onClose={onCancel}
open={show}
fullWidth
maxWidth={chatlogs && chatlogs.length == 0 ? 'md' : 'lg'}
maxWidth={'lg'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
@@ -781,6 +909,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
/>
</div>
<div style={{ flex: 1 }}></div>
{stats.totalMessages > 0 && (
<Button color='error' variant='outlined' onClick={() => onDeleteMessages()} startIcon={<IconEraser />}>
Delete Messages
</Button>
)}
</div>
<div
style={{
@@ -1375,6 +1508,12 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
)}
</div>
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
<ConfirmDeleteMessageDialog
show={hardDeleteDialogOpen}
dialogProps={hardDeleteDialogProps}
onCancel={() => setHardDeleteDialogOpen(false)}
onConfirm={(hardDelete) => deleteMessages(hardDelete)}
/>
</>
</DialogContent>
</Dialog>
@@ -0,0 +1,122 @@
import { useDispatch } from 'react-redux'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions'
// material-ui
import { Button, Box, Typography } from '@mui/material'
import { IconX } from '@tabler/icons-react'
// Project import
import { StyledButton } from '@/ui-component/button/StyledButton'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
import { SwitchInput } from '@/ui-component/switch/Switch'
// store
import useNotifier from '@/utils/useNotifier'
// API
import chatflowsApi from '@/api/chatflows'
const message = `Allow files to be uploaded from the chat. Uploaded files will be parsed as string and sent to LLM. If File Upload is enabled on Vector Store as well, this will override and takes precedence.`
const FileUpload = ({ dialogProps }) => {
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [fullFileUpload, setFullFileUpload] = useState(false)
const [chatbotConfig, setChatbotConfig] = useState({})
const handleChange = (value) => {
setFullFileUpload(value)
}
const onSave = async () => {
try {
const value = {
status: fullFileUpload
}
chatbotConfig.fullFileUpload = value
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
chatbotConfig: JSON.stringify(chatbotConfig)
})
if (saveResp.data) {
enqueueSnackbar({
message: 'File Upload Configuration Saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
enqueueSnackbar({
message: `Failed to save File Upload Configuration: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
useEffect(() => {
if (dialogProps.chatflow) {
if (dialogProps.chatflow.chatbotConfig) {
try {
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
setChatbotConfig(chatbotConfig || {})
if (chatbotConfig.fullFileUpload) {
setFullFileUpload(chatbotConfig.fullFileUpload.status)
}
} catch (e) {
setChatbotConfig({})
}
}
}
return () => {}
}, [dialogProps])
return (
<>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', mb: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Enable Full File Upload
<TooltipWithParser style={{ marginLeft: 10 }} title={message} />
</Typography>
</div>
<SwitchInput onChange={handleChange} value={fullFileUpload} />
</Box>
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={onSave}>
Save
</StyledButton>
</>
)
}
FileUpload.propTypes = {
dialogProps: PropTypes.object
}
export default FileUpload
@@ -5,6 +5,7 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
import robotPNG from '@/assets/images/robot.png'
import chatPNG from '@/assets/images/chathistory.png'
import diskPNG from '@/assets/images/floppy-disc.png'
import fileAttachmentPNG from '@/assets/images/fileAttachment.png'
import { baseURL } from '@/store/constant'
const sequentialStateMessagesSelection = [
@@ -119,6 +120,45 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
/>
</ListItem>
</ListItemButton>
<ListItemButton
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
mb: 1
}}
disabled={disabled}
onClick={() => onSelectOutputResponseClick(null, 'file_attachment')}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
}}
alt='fileAttachment'
src={fileAttachmentPNG}
/>
</div>
</ListItemAvatar>
<ListItemText
sx={{ ml: 1 }}
primary='file_attachment'
secondary={`Files uploaded from the chat when Full File Upload is enabled on the Configuration`}
/>
</ListItem>
</ListItemButton>
{availableNodesForVariable &&
availableNodesForVariable.length > 0 &&
availableNodesForVariable.map((node, index) => {