mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +03:00
add file annotations, sync and delete assistant
This commit is contained in:
@@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body)
|
||||
|
||||
const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body)
|
||||
|
||||
const deleteAssistant = (id) => client.delete(`/assistants/${id}`)
|
||||
const deleteAssistant = (id, isDeleteBoth) =>
|
||||
isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`)
|
||||
|
||||
export default {
|
||||
getAllAssistants,
|
||||
|
||||
@@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import axios from 'axios'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
@@ -28,7 +29,7 @@ import DatePicker from 'react-datepicker'
|
||||
import robotPNG from 'assets/images/robot.png'
|
||||
import userPNG from 'assets/images/account.png'
|
||||
import msgEmptySVG from 'assets/images/message_empty.svg'
|
||||
import { IconFileExport, IconEraser, IconX } from '@tabler/icons'
|
||||
import { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons'
|
||||
|
||||
// Project import
|
||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||
@@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm'
|
||||
// Utils
|
||||
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||
|
||||
@@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
}
|
||||
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
||||
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
||||
obj[chatPK] = {
|
||||
@@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
}
|
||||
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||
if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools)
|
||||
if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||
|
||||
loadedMessages.push(obj)
|
||||
}
|
||||
@@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
window.open(data, '_blank')
|
||||
}
|
||||
|
||||
const downloadFile = async (fileAnnotation) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseURL}/api/v1/openai-assistants-file`,
|
||||
{ fileName: fileAnnotation.fileName },
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = fileAnnotation.fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onSourceDialogClick = (data, title) => {
|
||||
setSourceDialogProps({ data, title })
|
||||
setSourceDialogOpen(true)
|
||||
@@ -648,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
{message.fileAnnotations && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||
return (
|
||||
<Button
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
textTransform: 'none',
|
||||
mb: 1,
|
||||
mr: 1
|
||||
}}
|
||||
key={index}
|
||||
variant='outlined'
|
||||
onClick={() => downloadFile(fileAnnotation)}
|
||||
endIcon={
|
||||
<IconDownload color={theme.palette.primary.main} />
|
||||
}
|
||||
>
|
||||
{fileAnnotation.fileName}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.sourceDocuments && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{removeDuplicateURL(message).map((source, index) => {
|
||||
|
||||
@@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte
|
||||
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
|
||||
import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
|
||||
import { File } from 'ui-component/file/File'
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
import DeleteConfirmDialog from './DeleteConfirmDialog'
|
||||
|
||||
// Icons
|
||||
import { IconX } from '@tabler/icons'
|
||||
@@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons'
|
||||
import assistantsApi from 'api/assistants'
|
||||
|
||||
// Hooks
|
||||
import useConfirm from 'hooks/useConfirm'
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// utils
|
||||
@@ -71,14 +70,8 @@ const assistantAvailableModels = [
|
||||
|
||||
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
|
||||
useNotifier()
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
@@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const [assistantFiles, setAssistantFiles] = useState([])
|
||||
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [deleteDialogProps, setDeleteDialogProps] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
@@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (getAssistantObjApi.data) {
|
||||
setOpenAIAssistantId(getAssistantObjApi.data.id)
|
||||
setAssistantName(getAssistantObjApi.data.name)
|
||||
setAssistantDesc(getAssistantObjApi.data.description)
|
||||
setAssistantModel(getAssistantObjApi.data.model)
|
||||
setAssistantInstructions(getAssistantObjApi.data.instructions)
|
||||
setAssistantFiles(getAssistantObjApi.data.files ?? [])
|
||||
|
||||
let tools = []
|
||||
if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) {
|
||||
for (const tool of getAssistantObjApi.data.tools) {
|
||||
tools.push(tool.type)
|
||||
}
|
||||
}
|
||||
setAssistantTools(tools)
|
||||
syncData(getAssistantObjApi.data)
|
||||
}
|
||||
}, [getAssistantObjApi.data])
|
||||
|
||||
@@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialogProps])
|
||||
|
||||
const syncData = (data) => {
|
||||
setOpenAIAssistantId(data.id)
|
||||
setAssistantName(data.name)
|
||||
setAssistantDesc(data.description)
|
||||
setAssistantModel(data.model)
|
||||
setAssistantInstructions(data.instructions)
|
||||
setAssistantFiles(data.files ?? [])
|
||||
|
||||
let tools = []
|
||||
if (data.tools && data.tools.length) {
|
||||
for (const tool of data.tools) {
|
||||
tools.push(tool.type)
|
||||
}
|
||||
}
|
||||
setAssistantTools(tools)
|
||||
}
|
||||
|
||||
const addNewAssistant = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAssistant = async () => {
|
||||
const confirmPayload = {
|
||||
title: `Delete Assistant`,
|
||||
description: `Delete Assistant ${assistantName}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const delResp = await assistantsApi.deleteAssistant(assistantId)
|
||||
if (delResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Assistant deleted',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
const onSyncClick = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential)
|
||||
if (getResp.data) {
|
||||
syncData(getResp.data)
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete Assistant: ${errorData}`,
|
||||
message: 'Assistant successfully synced!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
@@ -351,8 +326,71 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to sync Assistant: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteClick = () => {
|
||||
setDeleteDialogProps({
|
||||
title: `Delete Assistant`,
|
||||
description: `Delete Assistant ${assistantName}?`,
|
||||
cancelButtonName: 'Cancel'
|
||||
})
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const deleteAssistant = async (isDeleteBoth) => {
|
||||
setDeleteDialogOpen(false)
|
||||
try {
|
||||
const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth)
|
||||
if (delResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Assistant deleted',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onConfirm()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete Assistant: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => deleteAssistant()}>
|
||||
<StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}>
|
||||
Sync
|
||||
</StyledButton>
|
||||
)}
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<StyledButton color='error' variant='contained' onClick={() => onDeleteClick()}>
|
||||
Delete
|
||||
</StyledButton>
|
||||
)}
|
||||
@@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
{dialogProps.confirmButtonName}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
<DeleteConfirmDialog
|
||||
show={deleteDialogOpen}
|
||||
dialogProps={deleteDialogProps}
|
||||
onCancel={() => setDeleteDialogOpen(false)}
|
||||
onDelete={() => deleteAssistant()}
|
||||
onDeleteBoth={() => deleteAssistant(true)}
|
||||
/>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
|
||||
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>{dialogProps.description}</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 20 }}>
|
||||
<StyledButton sx={{ mb: 1 }} color='orange' variant='contained' onClick={onDelete}>
|
||||
Delete only from Flowise
|
||||
</StyledButton>
|
||||
<StyledButton sx={{ mb: 1 }} color='error' variant='contained' onClick={onDeleteBoth}>
|
||||
Delete from both OpenAI and Flowise
|
||||
</StyledButton>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
DeleteConfirmDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onDeleteBoth: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default DeleteConfirmDialog
|
||||
@@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import axios from 'axios'
|
||||
|
||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material'
|
||||
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconSend } from '@tabler/icons'
|
||||
import { IconSend, IconDownload } from '@tabler/icons'
|
||||
|
||||
// project import
|
||||
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||
@@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
|
||||
setMessages((prevMessages) => [
|
||||
...prevMessages,
|
||||
{ message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' }
|
||||
{
|
||||
message: text,
|
||||
sourceDocuments: data?.sourceDocuments,
|
||||
usedTools: data?.usedTools,
|
||||
fileAnnotations: data?.fileAnnotations,
|
||||
type: 'apiMessage'
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
@@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (fileAnnotation) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseURL}/api/v1/openai-assistants-file`,
|
||||
{ fileName: fileAnnotation.fileName },
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
const blob = new Blob([response.data], { type: response.headers['content-type'] })
|
||||
const downloadUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = fileAnnotation.fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get chatmessages successful
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data?.length) {
|
||||
@@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
}
|
||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
|
||||
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
|
||||
return obj
|
||||
})
|
||||
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
|
||||
@@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
{message.message}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
{message.fileAnnotations && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||
return (
|
||||
<Button
|
||||
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
|
||||
key={index}
|
||||
variant='outlined'
|
||||
onClick={() => downloadFile(fileAnnotation)}
|
||||
endIcon={<IconDownload color={theme.palette.primary.main} />}
|
||||
>
|
||||
{fileAnnotation.fileName}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{message.sourceDocuments && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{removeDuplicateURL(message).map((source, index) => {
|
||||
|
||||
Reference in New Issue
Block a user