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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1012 additions and 193 deletions

View File

@ -1,5 +1,6 @@
tags:
- name: assistants
- name: attachments
- name: chatmessage
- name: chatflows
- name: document-store
@ -270,6 +271,61 @@ paths:
'500':
description: Internal error
/attachments/{chatflowId}/{chatId}:
post:
tags:
- attachments
security:
- bearerAuth: []
operationId: createAttachment
summary: Create attachments array
description: Return contents of the files in plain string format
parameters:
- in: path
name: chatflowId
required: true
schema:
type: string
description: Chatflow ID
- in: path
name: chatId
required: true
schema:
type: string
description: Chat ID
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
files:
type: array
items:
type: string
format: binary
description: Files to be uploaded
required:
- files
required: true
responses:
'200':
description: Attachments created successfully
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CreateAttachmentResponse'
'400':
description: Invalid input provided
'404':
description: Chatflow or ChatId not found
'422':
description: Validation error
'500':
description: Internal server error
/chatflows:
post:
tags:
@ -1825,7 +1881,8 @@ components:
properties:
type:
type: string
description: The type of file upload (e.g., 'file', 'audio', 'url')
enum: [audio, url, file, file:rag, file:full]
description: The type of file upload
example: file
name:
type: string
@ -2193,6 +2250,22 @@ components:
format: date-time
description: Date and time when the feedback was created
CreateAttachmentResponse:
type: object
properties:
name:
type: string
description: Name of the file
mimeType:
type: string
description: Mime type of the file
size:
type: string
description: Size of the file
content:
type: string
description: Content of the file in string format
securitySchemes:
bearerAuth:
type: http

View File

@ -121,11 +121,22 @@ class File_DocumentLoaders implements INode {
}
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
fileBlobs.push({ blob, ext: file.split('.').pop() || '' })
// specific to createAttachment to get files from chatId
const retrieveAttachmentChatId = options.retrieveAttachmentChatId
if (retrieveAttachmentChatId) {
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid, options.chatId)
const blob = new Blob([fileData])
fileBlobs.push({ blob, ext: file.split('.').pop() || '' })
}
} else {
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
fileBlobs.push({ blob, ext: file.split('.').pop() || '' })
}
}
} else {
if (totalFiles.startsWith('[') && totalFiles.endsWith(']')) {
@ -288,7 +299,12 @@ class MultiFileLoader extends BaseDocumentLoader {
const loader = loaderFactory(fileBlob.blob)
documents.push(...(await loader.load()))
} else {
throw new Error(`Error loading file`)
const loader = new TextLoader(fileBlob.blob)
try {
documents.push(...(await loader.load()))
} catch (error) {
throw new Error(`Error loading file`)
}
}
}

View File

@ -68,9 +68,9 @@ const howToUseCode = `
"sourceDocuments": [
{
"pageContent": "This is the page content",
"metadata": "{foo: var}",
"metadata": "{foo: var}"
}
],
]
}
\`\`\`
@ -102,10 +102,10 @@ const howToUse = `
|-----------|-----------|
| user | john doe |
2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure:
2. If you want to use the Agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure:
\`\`\`json
{
"output": "Hello! How can I assist you today?",
"content": "Hello! How can I assist you today?",
"usedTools": [
{
"tool": "tool-name",
@ -116,9 +116,9 @@ const howToUse = `
"sourceDocuments": [
{
"pageContent": "This is the page content",
"metadata": "{foo: var}",
"metadata": "{foo: var}"
}
],
]
}
\`\`\`
@ -195,7 +195,7 @@ class Agent_SeqAgents implements INode {
constructor() {
this.label = 'Agent'
this.name = 'seqAgent'
this.version = 3.0
this.version = 3.1
this.type = 'Agent'
this.icon = 'seqAgent.png'
this.category = 'Sequential Agents'

View File

@ -88,7 +88,7 @@ const howToUse = `
|-----------|-----------|
| user | john doe |
2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure:
2. If you want to use the LLM Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure:
\`\`\`json
{
"content": 'Hello! How can I assist you today?',

View File

@ -48,9 +48,9 @@ const howToUseCode = `
"sourceDocuments": [
{
"pageContent": "This is the page content",
"metadata": "{foo: var}",
"metadata": "{foo: var}"
}
],
]
}
]
\`\`\`
@ -64,7 +64,7 @@ const howToUseCode = `
*/
return {
"sources": $flow.output[0].sourceDocuments
"sources": $flow.output[0].toolOutput
}
\`\`\`
@ -89,17 +89,19 @@ const howToUse = `
|-----------|-----------|
| user | john doe |
2. If you want to use the agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array):
2. If you want to use the Tool Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array):
\`\`\`json
[
{
"content": "Hello! How can I assist you today?",
"tool": "tool's name",
"toolInput": {},
"toolOutput": "tool's output content",
"sourceDocuments": [
{
"pageContent": "This is the page content",
"metadata": "{foo: var}",
"metadata": "{foo: var}"
}
],
]
}
]
\`\`\`
@ -107,7 +109,7 @@ const howToUse = `
For example:
| Key | Value |
|--------------|-------------------------------------------|
| sources | \`$flow.output[0].sourceDocuments\` |
| sources | \`$flow.output[0].toolOutput\` |
3. You can get default flow config, including the current "state":
- \`$flow.sessionId\`
@ -152,7 +154,7 @@ class ToolNode_SeqAgents implements INode {
constructor() {
this.label = 'Tool Node'
this.name = 'seqToolNode'
this.version = 2.0
this.version = 2.1
this.type = 'ToolNode'
this.icon = 'toolNode.svg'
this.category = 'Sequential Agents'

View File

@ -5,7 +5,7 @@ import * as path from 'path'
import { JSDOM } from 'jsdom'
import { z } from 'zod'
import { DataSource } from 'typeorm'
import { ICommonObject, IDatabaseEntity, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface'
import { ICommonObject, IDatabaseEntity, IDocument, IMessage, INodeData, IVariable, MessageContentImageUrl } from './Interface'
import { AES, enc } from 'crypto-js'
import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages'
import { getFileFromStorage } from './storageUtils'
@ -609,10 +609,11 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
if (message.role === 'apiMessage' || message.type === 'apiMessage') {
chatHistory.push(new AIMessage(message.content || ''))
} else if (message.role === 'userMessage' || message.role === 'userMessage') {
// check for image uploads
// check for image/files uploads
if (message.fileUploads) {
// example: [{"type":"stored-file","name":"0_DiXc4ZklSTo3M8J4.jpg","mime":"image/jpeg"}]
try {
let messageWithFileUploads = ''
const uploads = JSON.parse(message.fileUploads)
const imageContents: MessageContentImageUrl[] = []
for (const upload of uploads) {
@ -634,14 +635,32 @@ export const mapChatMessageToBaseMessage = async (chatmessages: any[] = []): Pro
url: upload.data
}
})
} else if (upload.type === 'stored-file:full') {
const fileLoaderNodeModule = await import('../nodes/documentloaders/File/File')
// @ts-ignore
const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass()
const options = {
retrieveAttachmentChatId: true,
chatflowid: message.chatflowid,
chatId: message.chatId
}
const nodeData = {
inputs: {
txtFile: `FILE-STORAGE::${JSON.stringify([upload.name])}`
}
}
const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options)
const pageContents = documents.map((doc) => doc.pageContent).join('\n')
messageWithFileUploads += `<doc name='${upload.name}'>${pageContents}</doc>\n\n`
}
}
const messageContent = messageWithFileUploads ? `${messageWithFileUploads}\n\n${message.content}` : message.content
chatHistory.push(
new HumanMessage({
content: [
{
type: 'text',
text: message.content
text: messageContent
},
...imageContents
]

View File

@ -4,7 +4,7 @@ export type MessageType = 'apiMessage' | 'userMessage'
export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT'
export enum chatType {
export enum ChatType {
INTERNAL = 'INTERNAL',
EXTERNAL = 'EXTERNAL'
}

View File

@ -0,0 +1,15 @@
import { Request, Response, NextFunction } from 'express'
import attachmentsService from '../../services/attachments'
const createAttachment = async (req: Request, res: Response, next: NextFunction) => {
try {
const apiResponse = await attachmentsService.createAttachment(req)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
createAttachment
}

View File

@ -1,13 +1,36 @@
import { Request, Response, NextFunction } from 'express'
import { ChatMessageRatingType, chatType, IReactFlowObject } from '../../Interface'
import { ChatMessageRatingType, ChatType, IReactFlowObject } from '../../Interface'
import chatflowsService from '../../services/chatflows'
import chatMessagesService from '../../services/chat-messages'
import { clearSessionMemory } from '../../utils'
import { aMonthAgo, clearSessionMemory, setDateToStartOrEndOfDay } from '../../utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { FindOptionsWhere } from 'typeorm'
import { Between, FindOptionsWhere } from 'typeorm'
import { ChatMessage } from '../../database/entities/ChatMessage'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { utilGetChatMessage } from '../../utils/getChatMessage'
const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => {
try {
let feedbackTypeFilters
const feedbackTypeFilterArray = JSON.parse(JSON.stringify(_feedbackTypeFilters))
if (
feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) &&
feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)
) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN]
} else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP]
} else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN]
} else {
feedbackTypeFilters = undefined
}
return feedbackTypeFilters
} catch (e) {
return _feedbackTypeFilters
}
}
const createChatMessage = async (req: Request, res: Response, next: NextFunction) => {
try {
@ -26,16 +49,16 @@ const createChatMessage = async (req: Request, res: Response, next: NextFunction
const getAllChatMessages = async (req: Request, res: Response, next: NextFunction) => {
try {
let chatTypeFilter = req.query?.chatType as chatType | undefined
let chatTypeFilter = req.query?.chatType as ChatType | undefined
if (chatTypeFilter) {
try {
const chatTypeFilterArray = JSON.parse(chatTypeFilter)
if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) {
chatTypeFilter = undefined
} else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
chatTypeFilter = chatType.EXTERNAL
} else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
chatTypeFilter = chatType.INTERNAL
} else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) {
chatTypeFilter = ChatType.EXTERNAL
} else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) {
chatTypeFilter = ChatType.INTERNAL
}
} catch (e) {
return res.status(500).send(e)
@ -51,23 +74,7 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio
const feedback = req.query?.feedback as boolean | undefined
let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (feedbackTypeFilters) {
try {
const feedbackTypeFilterArray = JSON.parse(JSON.stringify(feedbackTypeFilters))
if (
feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP) &&
feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)
) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP, ChatMessageRatingType.THUMBS_DOWN]
} else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_UP)) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_UP]
} else if (feedbackTypeFilterArray.includes(ChatMessageRatingType.THUMBS_DOWN)) {
feedbackTypeFilters = [ChatMessageRatingType.THUMBS_DOWN]
} else {
feedbackTypeFilters = undefined
}
} catch (e) {
return res.status(500).send(e)
}
feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters)
}
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
@ -105,9 +112,13 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex
const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined
const feedback = req.query?.feedback as boolean | undefined
let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (feedbackTypeFilters) {
feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters)
}
const apiResponse = await chatMessagesService.getAllInternalChatMessages(
req.params.id,
chatType.INTERNAL,
ChatType.INTERNAL,
sortOrder,
chatId,
memoryType,
@ -115,7 +126,8 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex
startDate,
endDate,
messageId,
feedback
feedback,
feedbackTypeFilters
)
return res.json(parseAPIResponse(apiResponse))
} catch (error) {
@ -123,7 +135,6 @@ const getAllInternalChatMessages = async (req: Request, res: Response, next: Nex
}
}
//Delete all chatmessages from chatId
const removeAllChatMessages = async (req: Request, res: Response, next: NextFunction) => {
try {
const appServer = getRunningExpressApp()
@ -138,35 +149,102 @@ const removeAllChatMessages = async (req: Request, res: Response, next: NextFunc
if (!chatflow) {
return res.status(404).send(`Chatflow ${req.params.id} not found`)
}
const chatId = req.query?.chatId as string
const memoryType = req.query?.memoryType as string | undefined
const sessionId = req.query?.sessionId as string | undefined
const chatType = req.query?.chatType as string | undefined
const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const nodes = parsedFlowData.nodes
try {
await clearSessionMemory(
nodes,
appServer.nodesPool.componentNodes,
chatId,
appServer.AppDataSource,
sessionId,
memoryType,
isClearFromViewMessageDialog
)
} catch (e) {
return res.status(500).send('Error clearing chat messages')
const chatId = req.query?.chatId as string
const memoryType = req.query?.memoryType as string | undefined
const sessionId = req.query?.sessionId as string | undefined
const _chatType = req.query?.chatType as string | undefined
const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined
const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined
let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (feedbackTypeFilters) {
feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters)
}
const deleteOptions: FindOptionsWhere<ChatMessage> = { chatflowid }
if (chatId) deleteOptions.chatId = chatId
if (memoryType) deleteOptions.memoryType = memoryType
if (sessionId) deleteOptions.sessionId = sessionId
if (chatType) deleteOptions.chatType = chatType
const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions)
return res.json(apiResponse)
if (!chatId) {
const isFeedback = feedbackTypeFilters?.length ? true : false
const hardDelete = req.query?.hardDelete as boolean | undefined
const messages = await utilGetChatMessage(
chatflowid,
_chatType as ChatType | undefined,
undefined,
undefined,
undefined,
undefined,
startDate,
endDate,
undefined,
isFeedback,
feedbackTypeFilters
)
const messageIds = messages.map((message) => message.id)
// Categorize by chatId_memoryType_sessionId
const chatIdMap = new Map<string, ChatMessage[]>()
messages.forEach((message) => {
const chatId = message.chatId
const memoryType = message.memoryType
const sessionId = message.sessionId
const composite_key = `${chatId}_${memoryType}_${sessionId}`
if (!chatIdMap.has(composite_key)) {
chatIdMap.set(composite_key, [])
}
chatIdMap.get(composite_key)?.push(message)
})
// If hardDelete is ON, we clearSessionMemory from third party integrations
if (hardDelete) {
for (const [composite_key] of chatIdMap) {
const [chatId, memoryType, sessionId] = composite_key.split('_')
try {
await clearSessionMemory(
nodes,
appServer.nodesPool.componentNodes,
chatId,
appServer.AppDataSource,
sessionId,
memoryType,
isClearFromViewMessageDialog
)
} catch (e) {
console.error('Error clearing chat messages')
}
}
}
const apiResponse = await chatMessagesService.removeChatMessagesByMessageIds(chatflowid, chatIdMap, messageIds)
return res.json(apiResponse)
} else {
try {
await clearSessionMemory(
nodes,
appServer.nodesPool.componentNodes,
chatId,
appServer.AppDataSource,
sessionId,
memoryType,
isClearFromViewMessageDialog
)
} catch (e) {
return res.status(500).send('Error clearing chat messages')
}
const deleteOptions: FindOptionsWhere<ChatMessage> = { chatflowid }
if (chatId) deleteOptions.chatId = chatId
if (memoryType) deleteOptions.memoryType = memoryType
if (sessionId) deleteOptions.sessionId = sessionId
if (_chatType) deleteOptions.chatType = _chatType
if (startDate && endDate) {
const fromDate = setDateToStartOrEndOfDay(startDate, 'start')
const toDate = setDateToStartOrEndOfDay(endDate, 'end')
deleteOptions.createdDate = Between(fromDate ?? aMonthAgo(), toDate ?? new Date())
}
const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions)
return res.json(apiResponse)
}
} catch (error) {
next(error)
}

View File

@ -1,7 +1,7 @@
import { StatusCodes } from 'http-status-codes'
import { Request, Response, NextFunction } from 'express'
import statsService from '../../services/stats'
import { ChatMessageRatingType, chatType } from '../../Interface'
import { ChatMessageRatingType, ChatType } from '../../Interface'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
@ -11,19 +11,19 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction)
throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: statsController.getChatflowStats - id not provided!`)
}
const chatflowid = req.params.id
let chatTypeFilter = req.query?.chatType as chatType | undefined
let chatTypeFilter = req.query?.chatType as ChatType | undefined
const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined
let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined
if (chatTypeFilter) {
try {
const chatTypeFilterArray = JSON.parse(chatTypeFilter)
if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
if (chatTypeFilterArray.includes(ChatType.EXTERNAL) && chatTypeFilterArray.includes(ChatType.INTERNAL)) {
chatTypeFilter = undefined
} else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
chatTypeFilter = chatType.EXTERNAL
} else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
chatTypeFilter = chatType.INTERNAL
} else if (chatTypeFilterArray.includes(ChatType.EXTERNAL)) {
chatTypeFilter = ChatType.EXTERNAL
} else if (chatTypeFilterArray.includes(ChatType.INTERNAL)) {
chatTypeFilter = ChatType.INTERNAL
}
} catch (e) {
throw new InternalFlowiseError(

View File

@ -136,7 +136,8 @@ export class App {
'/api/v1/get-upload-file',
'/api/v1/ip',
'/api/v1/ping',
'/api/v1/version'
'/api/v1/version',
'/api/v1/attachments'
]
const URL_CASE_INSENSITIVE_REGEX: RegExp = /\/api\/v1\//i
const URL_CASE_SENSITIVE_REGEX: RegExp = /\/api\/v1\//

View File

@ -0,0 +1,13 @@
import express from 'express'
import multer from 'multer'
import path from 'path'
import attachmentsController from '../../controllers/attachments'
const router = express.Router()
const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` })
// CREATE
router.post('/:chatflowId/:chatId', upload.array('files'), attachmentsController.createAttachment)
export default router

View File

@ -1,6 +1,7 @@
import express from 'express'
import apikeyRouter from './apikey'
import assistantsRouter from './assistants'
import attachmentsRouter from './attachments'
import chatMessageRouter from './chat-messages'
import chatflowsRouter from './chatflows'
import chatflowsStreamingRouter from './chatflows-streaming'
@ -47,6 +48,7 @@ const router = express.Router()
router.use('/ping', pingRouter)
router.use('/apikey', apikeyRouter)
router.use('/assistants', assistantsRouter)
router.use('/attachments', attachmentsRouter)
router.use('/chatflows', chatflowsRouter)
router.use('/chatflows-streaming', chatflowsStreamingRouter)
router.use('/chatmessage', chatMessageRouter)

View File

@ -0,0 +1,20 @@
import { Request } from 'express'
import { StatusCodes } from 'http-status-codes'
import { createFileAttachment } from '../../utils/createAttachment'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
const createAttachment = async (req: Request) => {
try {
return await createFileAttachment(req)
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: attachmentService.createAttachment - ${getErrorMessage(error)}`
)
}
}
export default {
createAttachment
}

View File

@ -1,6 +1,6 @@
import { DeleteResult, FindOptionsWhere } from 'typeorm'
import { StatusCodes } from 'http-status-codes'
import { ChatMessageRatingType, chatType, IChatMessage } from '../../Interface'
import { ChatMessageRatingType, ChatType, IChatMessage } from '../../Interface'
import { utilGetChatMessage } from '../../utils/getChatMessage'
import { utilAddChatMessage } from '../../utils/addChatMesage'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
@ -27,7 +27,7 @@ const createChatMessage = async (chatMessage: Partial<IChatMessage>) => {
// Get all chatmessages from chatflowid
const getAllChatMessages = async (
chatflowId: string,
chatTypeFilter: chatType | undefined,
chatTypeFilter: ChatType | undefined,
sortOrder: string = 'ASC',
chatId?: string,
memoryType?: string,
@ -64,7 +64,7 @@ const getAllChatMessages = async (
// Get internal chatmessages from chatflowid
const getAllInternalChatMessages = async (
chatflowId: string,
chatTypeFilter: chatType | undefined,
chatTypeFilter: ChatType | undefined,
sortOrder: string = 'ASC',
chatId?: string,
memoryType?: string,
@ -128,6 +128,35 @@ const removeAllChatMessages = async (
}
}
const removeChatMessagesByMessageIds = async (
chatflowid: string,
chatIdMap: Map<string, ChatMessage[]>,
messageIds: string[]
): Promise<DeleteResult> => {
try {
const appServer = getRunningExpressApp()
for (const [composite_key] of chatIdMap) {
const [chatId] = composite_key.split('_')
// Remove all related feedback records
const feedbackDeleteOptions: FindOptionsWhere<ChatMessageFeedback> = { chatId }
await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions)
// Delete all uploads corresponding to this chatflow/chatId
await removeFilesFromStorage(chatflowid, chatId)
}
const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(messageIds)
return dbResponse
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: chatMessagesService.removeAllChatMessages - ${getErrorMessage(error)}`
)
}
}
const abortChatMessage = async (chatId: string, chatflowid: string) => {
try {
const appServer = getRunningExpressApp()
@ -155,5 +184,6 @@ export default {
getAllChatMessages,
getAllInternalChatMessages,
removeAllChatMessages,
removeChatMessagesByMessageIds,
abortChatMessage
}

View File

@ -9,7 +9,7 @@ import {
removeSpecificFileFromStorage
} from 'flowise-components'
import {
chatType,
ChatType,
DocumentStoreStatus,
IDocumentStoreFileChunkPagedResponse,
IDocumentStoreLoader,
@ -995,7 +995,7 @@ const _insertIntoVectorStoreWorkerThread = async (data: ICommonObject) => {
data: {
version: await getAppVersion(),
chatlowId: chatflowid,
type: chatType.INTERNAL,
type: ChatType.INTERNAL,
flowGraph: omit(indexResult['result'], ['totalKeys', 'addedDocs'])
}
})

View File

@ -1,5 +1,5 @@
import { StatusCodes } from 'http-status-codes'
import { ChatMessageRatingType, chatType } from '../../Interface'
import { ChatMessageRatingType, ChatType } from '../../Interface'
import { ChatMessage } from '../../database/entities/ChatMessage'
import { utilGetChatMessage } from '../../utils/getChatMessage'
import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback'
@ -9,7 +9,7 @@ import { getErrorMessage } from '../../errors/utils'
// get stats for showing in chatflow
const getChatflowStats = async (
chatflowid: string,
chatTypeFilter: chatType | undefined,
chatTypeFilter: ChatType | undefined,
startDate?: string,
endDate?: string,
messageId?: string,

View File

@ -63,7 +63,8 @@ export const buildAgentGraph = async (
isInternal: boolean,
baseURL?: string,
sseStreamer?: IServerSideEventStreamer,
shouldStreamResponse?: boolean
shouldStreamResponse?: boolean,
uploadedFilesContent?: string
): Promise<any> => {
try {
const appServer = getRunningExpressApp()
@ -129,7 +130,8 @@ export const buildAgentGraph = async (
cachePool: appServer.cachePool,
isUpsert: false,
uploads: incomingInput.uploads,
baseURL
baseURL,
uploadedFilesContent
})
const options = {
@ -188,7 +190,8 @@ export const buildAgentGraph = async (
chatHistory,
incomingInput?.overrideConfig,
sessionId || chatId,
seqAgentNodes.some((node) => node.data.inputs?.summarization)
seqAgentNodes.some((node) => node.data.inputs?.summarization),
uploadedFilesContent
)
} else {
isSequential = true
@ -204,7 +207,8 @@ export const buildAgentGraph = async (
chatHistory,
incomingInput?.overrideConfig,
sessionId || chatId,
incomingInput.action
incomingInput.action,
uploadedFilesContent
)
}
@ -348,7 +352,6 @@ export const buildAgentGraph = async (
if (isSequential && !finalResult && agentReasoning.length) {
const lastMessages = agentReasoning[agentReasoning.length - 1].messages
const lastAgentReasoningMessage = lastMessages[lastMessages.length - 1]
// If last message is an AI Message with tool calls, that means the last node was interrupted
if (lastMessageRaw.tool_calls && lastMessageRaw.tool_calls.length > 0) {
// The last node that got interrupted
@ -456,6 +459,7 @@ export const buildAgentGraph = async (
* @param {ICommonObject} overrideConfig
* @param {string} threadId
* @param {boolean} summarization
* @param {string} uploadedFilesContent,
*/
const compileMultiAgentsGraph = async (
chatflow: IChatFlow,
@ -470,7 +474,8 @@ const compileMultiAgentsGraph = async (
chatHistory: IMessage[] = [],
overrideConfig?: ICommonObject,
threadId?: string,
summarization?: boolean
summarization?: boolean,
uploadedFilesContent?: string
) => {
const appServer = getRunningExpressApp()
const channels: ITeamState = {
@ -502,7 +507,15 @@ const compileMultiAgentsGraph = async (
let flowNodeData = cloneDeep(workerNode.data)
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig)
flowNodeData = await resolveVariables(
appServer.AppDataSource,
flowNodeData,
reactflowNodes,
question,
chatHistory,
overrideConfig,
uploadedFilesContent
)
try {
const workerResult: IMultiAgentNode = await newNodeInstance.init(flowNodeData, question, options)
@ -533,7 +546,15 @@ const compileMultiAgentsGraph = async (
let flowNodeData = cloneDeep(supervisorNode.data)
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig)
flowNodeData = await resolveVariables(
appServer.AppDataSource,
flowNodeData,
reactflowNodes,
question,
chatHistory,
overrideConfig,
uploadedFilesContent
)
if (flowNodeData.inputs) flowNodeData.inputs.workerNodes = supervisorWorkers[supervisor]
@ -603,9 +624,10 @@ const compileMultiAgentsGraph = async (
}
// Return stream result as we should only have 1 supervisor
const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question
return await graph.stream(
{
messages: [...prependMessages, new HumanMessage({ content: question })]
messages: [...prependMessages, new HumanMessage({ content: finalQuestion })]
},
{ recursionLimit: supervisorResult?.recursionLimit ?? 100, callbacks: [loggerHandler, ...callbacks], configurable: config }
)
@ -641,7 +663,8 @@ const compileSeqAgentsGraph = async (
chatHistory: IMessage[] = [],
overrideConfig?: ICommonObject,
threadId?: string,
action?: IAction
action?: IAction,
uploadedFilesContent?: string
) => {
const appServer = getRunningExpressApp()
@ -693,7 +716,15 @@ const compileSeqAgentsGraph = async (
flowNodeData = cloneDeep(node.data)
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
flowNodeData = await resolveVariables(appServer.AppDataSource, flowNodeData, reactflowNodes, question, chatHistory, overrideConfig)
flowNodeData = await resolveVariables(
appServer.AppDataSource,
flowNodeData,
reactflowNodes,
question,
chatHistory,
overrideConfig,
uploadedFilesContent
)
const seqAgentNode: ISeqAgentNode = await newNodeInstance.init(flowNodeData, question, options)
return seqAgentNode
@ -997,8 +1028,9 @@ const compileSeqAgentsGraph = async (
}
}
const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question
let humanMsg: { messages: HumanMessage[] | ToolMessage[] } | null = {
messages: [...prependMessages, new HumanMessage({ content: question })]
messages: [...prependMessages, new HumanMessage({ content: finalQuestion })]
}
if (action && action.mapping && question === action.mapping.approve) {

View File

@ -19,7 +19,7 @@ import {
IReactFlowObject,
IReactFlowNode,
IDepthQueue,
chatType,
ChatType,
IChatMessage,
IChatFlow,
IReactFlowEdge
@ -88,12 +88,14 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
}
let fileUploads: IFileUpload[] = []
let uploadedFilesContent = ''
if (incomingInput.uploads) {
fileUploads = incomingInput.uploads
for (let i = 0; i < fileUploads.length; i += 1) {
const upload = fileUploads[i]
if ((upload.type === 'file' || upload.type === 'audio') && upload.data) {
// if upload in an image, a rag file, or audio
if ((upload.type === 'file' || upload.type === 'file:rag' || upload.type === 'audio') && upload.data) {
const filename = upload.name
const splitDataURI = upload.data.split(',')
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@ -139,6 +141,13 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
}
}
}
if (upload.type === 'file:full' && upload.data) {
upload.type = 'stored-file:full'
// Omit upload.data since we don't store the content in database
uploadedFilesContent += `<doc name='${upload.name}'>${upload.data}</doc>\n\n`
fileUploads[i] = omit(upload, ['data'])
}
}
}
@ -229,7 +238,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
edges,
baseURL,
appServer.sseStreamer,
true
true,
uploadedFilesContent
)
}
@ -345,6 +355,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
apiMessageId,
componentNodes: appServer.nodesPool.componentNodes,
question: incomingInput.question,
uploadedFilesContent,
chatHistory,
chatId,
sessionId: sessionId ?? '',
@ -384,7 +395,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
reactFlowNodes,
incomingInput.question,
chatHistory,
flowData
flowData,
uploadedFilesContent
)
nodeToExecuteData = reactFlowNodeData
@ -398,6 +410,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
const nodeInstance = new nodeModule.nodeClass({ sessionId })
isStreamValid = (req.body.streaming === 'true' || req.body.streaming === true) && isStreamValid
const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${incomingInput.question}` : incomingInput.question
const runParams = {
chatId,
@ -411,7 +424,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
prependMessages
}
let result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
let result = await nodeInstance.run(nodeToExecuteData, finalQuestion, {
...runParams,
...(isStreamValid && { sseStreamer: appServer.sseStreamer, shouldStreamResponse: true })
})
@ -427,7 +440,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
role: 'userMessage',
content: incomingInput.question,
chatflowid,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
chatId,
memoryType,
sessionId,
@ -447,7 +460,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
role: 'apiMessage',
content: resultText,
chatflowid,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
chatId,
memoryType,
sessionId
@ -476,7 +489,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals
version: await getAppVersion(),
chatflowId: chatflowid,
chatId,
type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
flowGraph: getTelemetryFlowObj(nodes, edges)
})
@ -517,7 +530,8 @@ const utilBuildAgentResponse = async (
edges: IReactFlowEdge[],
baseURL?: string,
sseStreamer?: IServerSideEventStreamer,
shouldStreamResponse?: boolean
shouldStreamResponse?: boolean,
uploadedFilesContent?: string
) => {
try {
const appServer = getRunningExpressApp()
@ -530,7 +544,8 @@ const utilBuildAgentResponse = async (
isInternal,
baseURL,
sseStreamer,
shouldStreamResponse
shouldStreamResponse,
uploadedFilesContent
)
if (streamResults) {
const { finalResult, finalAction, sourceDocuments, artifacts, usedTools, agentReasoning } = streamResults
@ -538,7 +553,7 @@ const utilBuildAgentResponse = async (
role: 'userMessage',
content: incomingInput.question,
chatflowid: agentflow.id,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
chatId,
memoryType,
sessionId,
@ -553,7 +568,7 @@ const utilBuildAgentResponse = async (
role: 'apiMessage',
content: finalResult,
chatflowid: agentflow.id,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatType: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
chatId,
memoryType,
sessionId
@ -581,7 +596,7 @@ const utilBuildAgentResponse = async (
version: await getAppVersion(),
agentflowId: agentflow.id,
chatId,
type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
flowGraph: getTelemetryFlowObj(nodes, edges)
})

View File

@ -0,0 +1,84 @@
import { Request } from 'express'
import * as path from 'path'
import * as fs from 'fs'
import { addArrayFilesToStorage, IDocument, mapExtToInputField, mapMimeTypeToInputField } from 'flowise-components'
import { getRunningExpressApp } from './getRunningExpressApp'
import { getErrorMessage } from '../errors/utils'
/**
* Create attachment
* @param {Request} req
*/
export const createFileAttachment = async (req: Request) => {
const appServer = getRunningExpressApp()
const chatflowid = req.params.chatflowId
if (!chatflowid) {
throw new Error(
'Params chatflowId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId'
)
}
const chatId = req.params.chatId
if (!chatId) {
throw new Error(
'Params chatId is required! Please provide chatflowId and chatId in the URL: /api/v1/attachments/:chatflowId/:chatId'
)
}
// Find FileLoader node
const fileLoaderComponent = appServer.nodesPool.componentNodes['fileLoader']
const fileLoaderNodeInstanceFilePath = fileLoaderComponent.filePath as string
const fileLoaderNodeModule = await import(fileLoaderNodeInstanceFilePath)
const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass()
const options = {
retrieveAttachmentChatId: true,
chatflowid,
chatId
}
const files = (req.files as Express.Multer.File[]) || []
const fileAttachments = []
if (files.length) {
for (const file of files) {
const fileBuffer = fs.readFileSync(file.path)
const fileNames: string[] = []
const storagePath = await addArrayFilesToStorage(file.mimetype, fileBuffer, file.originalname, fileNames, chatflowid, chatId)
const fileInputFieldFromMimeType = mapMimeTypeToInputField(file.mimetype)
const fileExtension = path.extname(file.originalname)
const fileInputFieldFromExt = mapExtToInputField(fileExtension)
let fileInputField = 'txtFile'
if (fileInputFieldFromExt !== 'txtFile') {
fileInputField = fileInputFieldFromExt
} else if (fileInputFieldFromMimeType !== 'txtFile') {
fileInputField = fileInputFieldFromExt
}
fs.unlinkSync(file.path)
try {
const nodeData = {
inputs: {
[fileInputField]: storagePath
}
}
const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options)
const pageContents = documents.map((doc) => doc.pageContent).join('\n')
fileAttachments.push({
name: file.originalname,
mimeType: file.mimetype,
size: file.size,
content: pageContents
})
} catch (error) {
throw new Error(`Failed operation: createFileAttachment - ${getErrorMessage(error)}`)
}
}
}
return fileAttachments
}

View File

@ -1,13 +1,14 @@
import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm'
import { ChatMessageRatingType, chatType } from '../Interface'
import { ChatMessageRatingType, ChatType } from '../Interface'
import { ChatMessage } from '../database/entities/ChatMessage'
import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { aMonthAgo, setDateToStartOrEndOfDay } from '.'
/**
* Method that get chat messages.
* @param {string} chatflowid
* @param {chatType} chatType
* @param {ChatType} chatType
* @param {string} sortOrder
* @param {string} chatId
* @param {string} memoryType
@ -19,7 +20,7 @@ import { getRunningExpressApp } from '../utils/getRunningExpressApp'
*/
export const utilGetChatMessage = async (
chatflowid: string,
chatType: chatType | undefined,
chatType: ChatType | undefined,
sortOrder: string = 'ASC',
chatId?: string,
memoryType?: string,
@ -31,20 +32,6 @@ export const utilGetChatMessage = async (
feedbackTypes?: ChatMessageRatingType[]
): Promise<ChatMessage[]> => {
const appServer = getRunningExpressApp()
const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
const date = new Date(dateTimeStr)
if (isNaN(date.getTime())) {
return undefined
}
setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999)
return date
}
const aMonthAgo = () => {
const date = new Date()
date.setMonth(new Date().getMonth() - 1)
return date
}
let fromDate
if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start')

View File

@ -8,7 +8,7 @@ import { InternalFlowiseError } from '../errors/internalFlowiseError'
type IUploadConfig = {
isSpeechToTextEnabled: boolean
isImageUploadAllowed: boolean
isFileUploadAllowed: boolean
isRAGFileUploadAllowed: boolean
imgUploadSizeAndTypes: IUploadFileSizeAndTypes[]
fileUploadSizeAndTypes: IUploadFileSizeAndTypes[]
}
@ -32,7 +32,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
let isSpeechToTextEnabled = false
let isImageUploadAllowed = false
let isFileUploadAllowed = false
let isRAGFileUploadAllowed = false
/*
* Check for STT
@ -51,7 +51,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
}
/*
* Condition for isFileUploadAllowed
* Condition for isRAGFileUploadAllowed
* 1.) vector store with fileUpload = true && connected to a document loader with fileType
*/
const fileUploadSizeAndTypes: IUploadFileSizeAndTypes[] = []
@ -70,7 +70,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
fileTypes: fileType.split(', '),
maxUploadSize: 500
})
isFileUploadAllowed = true
isRAGFileUploadAllowed = true
}
}
break
@ -114,7 +114,7 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadC
return {
isSpeechToTextEnabled,
isImageUploadAllowed,
isFileUploadAllowed,
isRAGFileUploadAllowed,
imgUploadSizeAndTypes,
fileUploadSizeAndTypes
}

View File

@ -48,6 +48,7 @@ import { InternalFlowiseError } from '../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
const QUESTION_VAR_PREFIX = 'question'
const FILE_ATTACHMENT_PREFIX = 'file_attachment'
const CHAT_HISTORY_VAR_PREFIX = 'chat_history'
const REDACTED_CREDENTIAL_VALUE = '_FLOWISE_BLANK_07167752-1a71-43b1-bf8f-4f32252165db'
@ -438,6 +439,7 @@ type BuildFlowParams = {
stopNodeId?: string
uploads?: IFileUpload[]
baseURL?: string
uploadedFilesContent?: string
}
/**
@ -452,6 +454,7 @@ export const buildFlow = async ({
depthQueue,
componentNodes,
question,
uploadedFilesContent,
chatHistory,
apiMessageId,
chatId,
@ -516,7 +519,8 @@ export const buildFlow = async ({
flowNodes,
question,
chatHistory,
flowData
flowData,
uploadedFilesContent
)
if (isUpsert && stopNodeId && nodeId === stopNodeId) {
@ -546,7 +550,8 @@ export const buildFlow = async ({
initializedNodes.add(nodeId)
} else {
logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
let outputResult = await newNodeInstance.init(reactFlowNodeData, question, {
const finalQuestion = uploadedFilesContent ? `${uploadedFilesContent}\n\n${question}` : question
let outputResult = await newNodeInstance.init(reactFlowNodeData, finalQuestion, {
chatId,
sessionId,
chatflowid,
@ -770,7 +775,8 @@ export const getVariableValue = async (
question: string,
chatHistory: IMessage[],
isAcceptVariable = false,
flowData?: ICommonObject
flowData?: ICommonObject,
uploadedFilesContent?: string
) => {
const isObject = typeof paramValue === 'object'
const initialValue = (isObject ? JSON.stringify(paramValue) : paramValue) ?? ''
@ -803,6 +809,10 @@ export const getVariableValue = async (
variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(question, false)
}
if (isAcceptVariable && variableFullPath === FILE_ATTACHMENT_PREFIX) {
variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(uploadedFilesContent, false)
}
if (isAcceptVariable && variableFullPath === CHAT_HISTORY_VAR_PREFIX) {
variableDict[`{{${variableFullPath}}}`] = handleEscapeCharacters(convertChatHistoryToText(chatHistory), false)
}
@ -916,7 +926,8 @@ export const resolveVariables = async (
reactFlowNodes: IReactFlowNode[],
question: string,
chatHistory: IMessage[],
flowData?: ICommonObject
flowData?: ICommonObject,
uploadedFilesContent?: string
): Promise<INodeData> => {
let flowNodeData = cloneDeep(reactFlowNodeData)
const types = 'inputs'
@ -934,7 +945,8 @@ export const resolveVariables = async (
question,
chatHistory,
undefined,
flowData
flowData,
uploadedFilesContent
)
resolvedInstances.push(resolvedInstance)
}
@ -948,7 +960,8 @@ export const resolveVariables = async (
question,
chatHistory,
isAcceptVariable,
flowData
flowData,
uploadedFilesContent
)
paramsObj[key] = resolvedInstance
}
@ -1572,3 +1585,18 @@ export const convertToValidFilename = (word: string) => {
.replace(' ', '')
.toLowerCase()
}
export const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
const date = new Date(dateTimeStr)
if (isNaN(date.getTime())) {
return undefined
}
setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999)
return date
}
export const aMonthAgo = () => {
const date = new Date()
date.setMonth(new Date().getMonth() - 1)
return date
}

View File

@ -16,7 +16,7 @@ import {
getStartingNodes
} from '../utils'
import { validateChatflowAPIKey } from './validateKey'
import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface'
import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType } from '../Interface'
import { ChatFlow } from '../database/entities/ChatFlow'
import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { UpsertHistory } from '../database/entities/UpsertHistory'
@ -195,7 +195,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) =>
data: {
version: await getAppVersion(),
chatlowId: chatflowid,
type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
type: isInternal ? ChatType.INTERNAL : ChatType.EXTERNAL,
flowGraph: getTelemetryFlowObj(nodes, edges),
stopNodeId
}

View File

@ -0,0 +1,10 @@
import client from './client'
const createAttachment = (chatflowid, chatid, formData) =>
client.post(`/attachments/${chatflowid}/${chatid}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
export default {
createAttachment
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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) => {

View File

@ -67,6 +67,7 @@ import chatmessageApi from '@/api/chatmessage'
import chatflowsApi from '@/api/chatflows'
import predictionApi from '@/api/prediction'
import vectorstoreApi from '@/api/vectorstore'
import attachmentsApi from '@/api/attachments'
import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
import leadsApi from '@/api/lead'
@ -88,7 +89,7 @@ const messageImageStyle = {
objectFit: 'cover'
}
const CardWithDeleteOverlay = ({ item, customization, onDelete }) => {
const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => {
const [isHovered, setIsHovered] = useState(false)
const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'
@ -125,8 +126,9 @@ const CardWithDeleteOverlay = ({ item, customization, onDelete }) => {
{item.name}
</span>
</Card>
{isHovered && (
{isHovered && !disabled && (
<Button
disabled={disabled}
onClick={() => onDelete(item)}
startIcon={<IconTrash color='white' size={22} />}
title='Remove attachment'
@ -150,6 +152,7 @@ const CardWithDeleteOverlay = ({ item, customization, onDelete }) => {
CardWithDeleteOverlay.propTypes = {
item: PropTypes.object,
customization: PropTypes.object,
disabled: PropTypes.bool,
onDelete: PropTypes.func
}
@ -191,6 +194,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
const [starterPrompts, setStarterPrompts] = useState([])
// full file upload
const [fullFileUpload, setFullFileUpload] = useState(false)
// feedback
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
const [feedbackId, setFeedbackId] = useState('')
@ -213,6 +219,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
const fileUploadRef = useRef(null)
const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false)
const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false)
const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false)
const [isDragActive, setIsDragActive] = useState(false)
// recording
@ -235,7 +242,10 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}
})
}
if (constraints.isFileUploadAllowed) {
if (fullFileUpload) {
return true
} else if (constraints.isRAGFileUploadAllowed) {
const fileExt = file.name.split('.').pop()
if (fileExt) {
constraints.fileUploadSizeAndTypes.map((allowed) => {
@ -271,7 +281,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
const { name } = file
// Only add files
if (!imageUploadAllowedTypes.includes(file.type)) {
uploadedFiles.push(file)
uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' })
}
files.push(
new Promise((resolve) => {
@ -350,7 +360,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}
// Only add files
if (!imageUploadAllowedTypes.includes(file.type)) {
uploadedFiles.push(file)
uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' })
}
const reader = new FileReader()
const { name } = file
@ -683,6 +693,66 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}
}
const handleFileUploads = async (uploads) => {
if (!uploadedFiles.length) return uploads
if (fullFileUpload) {
const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full')
if (filesWithFullUploadType.length > 0) {
const formData = new FormData()
for (const file of filesWithFullUploadType) {
formData.append('files', file.file)
}
formData.append('chatId', chatId)
const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData)
const data = response.data
for (const extractedFileData of data) {
const content = extractedFileData.content
const fileName = extractedFileData.name
// find matching name in previews and replace data with content
const uploadIndex = uploads.findIndex((upload) => upload.name === fileName)
if (uploadIndex !== -1) {
uploads[uploadIndex] = {
...uploads[uploadIndex],
data: content,
name: fileName,
type: 'file:full'
}
}
}
}
} else if (isChatFlowAvailableForRAGFileUploads) {
const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag')
if (filesWithRAGUploadType.length > 0) {
const formData = new FormData()
for (const file of filesWithRAGUploadType) {
formData.append('files', file.file)
}
formData.append('chatId', chatId)
await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData)
// delay for vector store to be updated
const delay = (delayInms) => {
return new Promise((resolve) => setTimeout(resolve, delayInms))
}
await delay(2500) //TODO: check if embeddings can be retrieved using file name as metadata filter
uploads = uploads.map((upload) => {
return {
...upload,
type: 'file:rag'
}
})
}
}
return uploads
}
// Handle form submission
const handleSubmit = async (e, selectedInput, action) => {
if (e) e.preventDefault()
@ -699,7 +769,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
setLoading(true)
const uploads = previews.map((item) => {
let uploads = previews.map((item) => {
return {
data: item.data,
type: item.type,
@ -707,6 +777,14 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
mime: item.mime
}
})
try {
uploads = await handleFileUploads(uploads)
} catch (error) {
handleError('Unable to upload documents')
return
}
clearPreviews()
setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }])
@ -718,28 +796,8 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}
if (uploads && uploads.length > 0) params.uploads = uploads
if (leadEmail) params.leadEmail = leadEmail
if (action) params.action = action
if (uploadedFiles.length > 0) {
const formData = new FormData()
for (const file of uploadedFiles) {
formData.append('files', file)
}
formData.append('chatId', chatId)
const response = await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData)
if (!response.data) {
setMessages((prevMessages) => [...prevMessages, { message: 'Unable to upload documents', type: 'apiMessage' }])
} else {
// delay for vector store to be updated
const delay = (delayInms) => {
return new Promise((resolve) => setTimeout(resolve, delayInms))
}
await delay(2500) //TODO: check if embeddings can be retrieved using file name as metadata filter
}
}
if (isChatFlowAvailableToStream) {
fetchResponseFromEventStream(chatflowid, params)
} else {
@ -905,6 +963,11 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
return ''
}
const getFileUploadAllowedTypes = () => {
if (fullFileUpload) return '*'
return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'
}
const downloadFile = async (fileAnnotation) => {
try {
const response = await axios.post(
@ -993,7 +1056,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
useEffect(() => {
if (getAllowChatFlowUploads.data) {
setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false)
setIsChatFlowAvailableForFileUploads(getAllowChatFlowUploads.data?.isFileUploadAllowed ?? false)
setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false)
setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false)
setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(','))
setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(','))
@ -1035,11 +1098,25 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (config.followUpPrompts) {
setFollowUpPromptsStatus(config.followUpPrompts.status)
}
if (config.fullFileUpload) {
setFullFileUpload(config.fullFileUpload.status)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatflowConfig.data])
useEffect(() => {
if (fullFileUpload) {
setIsChatFlowAvailableForFileUploads(true)
} else if (isChatFlowAvailableForRAGFileUploads) {
setIsChatFlowAvailableForFileUploads(true)
} else {
setIsChatFlowAvailableForFileUploads(false)
}
}, [isChatFlowAvailableForRAGFileUploads, fullFileUpload])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
@ -1238,6 +1315,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
marginRight: '10px',
flex: '0 0 auto'
}}
disabled={getInputDisabled()}
onClick={() => handleDeletePreview(item)}
>
<ImageSrc style={{ backgroundImage: `url(${item.data})` }} />
@ -1263,13 +1341,20 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
variant='outlined'
>
<CardMedia component='audio' sx={{ color: 'transparent' }} controls src={item.data} />
<IconButton onClick={() => handleDeletePreview(item)} size='small'>
<IconButton disabled={getInputDisabled()} onClick={() => handleDeletePreview(item)} size='small'>
<IconTrash size={20} color='white' />
</IconButton>
</Card>
)
} else {
return <CardWithDeleteOverlay item={item} customization={customization} onDelete={() => handleDeletePreview(item)} />
return (
<CardWithDeleteOverlay
disabled={getInputDisabled()}
item={item}
customization={customization}
onDelete={() => handleDeletePreview(item)}
/>
)
}
}
@ -1415,11 +1500,14 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
onDrop={handleDrop}
/>
)}
{isDragActive && (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isFileUploadAllowed) && (
<Box className='drop-overlay'>
<Typography variant='h2'>Drop here to upload</Typography>
{[...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes].map(
(allowed) => {
{isDragActive &&
(getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && (
<Box className='drop-overlay'>
<Typography variant='h2'>Drop here to upload</Typography>
{[
...getAllowChatFlowUploads.data.imgUploadSizeAndTypes,
...getAllowChatFlowUploads.data.fileUploadSizeAndTypes
].map((allowed) => {
return (
<>
<Typography variant='subtitle1'>{allowed.fileTypes?.join(', ')}</Typography>
@ -1428,10 +1516,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
)}
</>
)
}
)}
</Box>
)}
})}
</Box>
)}
<div ref={ps} className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}>
<div id='messagelist' className={'messagelist'}>
{messages &&
@ -2256,7 +2343,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
ref={fileUploadRef}
type='file'
onChange={handleFileChange}
accept={fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'}
accept={getFileUploadAllowedTypes()}
/>
)}
</form>