mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +03:00
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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user