From 73f7046316ac8cf6645515332b0c3f3b8aed7c95 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Wed, 6 Dec 2023 12:31:33 +0530 Subject: [PATCH] GPT Vision: Initial implementation of the OpenAI Vision API --- .../chains/VisionChain/OpenAIVisionChain.ts | 24 +- .../nodes/chains/VisionChain/VLLMChain.ts | 1 + .../nodes/chains/VisionChain/chain.svg | 6 + packages/server/src/Interface.ts | 2 + .../src/database/entities/ChatMessage.ts | 3 + ...01788586491-AddFileUploadsToChatMessage.ts | 12 + .../src/database/migrations/mysql/index.ts | 4 +- ...01788586491-AddFileUploadsToChatMessage.ts | 11 + .../src/database/migrations/postgres/index.ts | 4 +- ...01788586491-AddFileUploadsToChatMessage.ts | 20 ++ .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 44 ++- packages/ui/src/api/chatflows.js | 4 +- .../ui/src/views/chatmessage/ChatMessage.css | 29 ++ .../ui/src/views/chatmessage/ChatMessage.js | 301 +++++++++++++++++- 15 files changed, 447 insertions(+), 22 deletions(-) create mode 100644 packages/components/nodes/chains/VisionChain/chain.svg create mode 100644 packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts diff --git a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts index f2260a76..7745f05d 100644 --- a/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts +++ b/packages/components/nodes/chains/VisionChain/OpenAIVisionChain.ts @@ -12,6 +12,7 @@ class OpenAIVisionChain_Chains implements INode { version: number type: string icon: string + badge: string category: string baseClasses: string[] description: string @@ -21,10 +22,11 @@ class OpenAIVisionChain_Chains implements INode { constructor() { this.label = 'Open AI Vision Chain' this.name = 'openAIVisionChain' - this.version = 3.0 + this.version = 1.0 this.type = 'OpenAIVisionChain' this.icon = 'chain.svg' this.category = 'Chains' + this.badge = 'EXPERIMENTAL' this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] this.inputs = [ @@ -63,6 +65,20 @@ class OpenAIVisionChain_Chains implements INode { type: 'string', placeholder: 'Name Your Chain', optional: true + }, + { + label: 'Accepted Upload Types', + name: 'allowedUploadTypes', + type: 'string', + default: 'image/gif;image/jpeg;image/png;image/webp', + hidden: true + }, + { + label: 'Maximum Upload Size (MB)', + name: 'maxUploadSize', + type: 'number', + default: '5', + hidden: true } ] this.outputs = [ @@ -93,7 +109,7 @@ class OpenAIVisionChain_Chains implements INode { openAIApiKey: openAIModel.openAIApiKey, imageResolution: imageResolution, verbose: process.env.DEBUG === 'true', - imageUrls: options.url, + imageUrls: options.uploads, openAIModel: openAIModel } if (output === this.name) { @@ -156,8 +172,8 @@ const runPrediction = async ( * TO: { "value": "hello i am ben\n\n\thow are you?" } */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) - if (options?.url) { - chain.imageUrls = options.url + if (options?.uploads) { + chain.imageUrls = options.uploads } if (promptValues && inputVariables.length > 0) { let seen: string[] = [] diff --git a/packages/components/nodes/chains/VisionChain/VLLMChain.ts b/packages/components/nodes/chains/VisionChain/VLLMChain.ts index 17260be2..59a2483a 100644 --- a/packages/components/nodes/chains/VisionChain/VLLMChain.ts +++ b/packages/components/nodes/chains/VisionChain/VLLMChain.ts @@ -79,6 +79,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput { messages: [] } if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens + else vRequest.max_tokens = 1024 const userRole: any = { role: 'user' } userRole.content = [] diff --git a/packages/components/nodes/chains/VisionChain/chain.svg b/packages/components/nodes/chains/VisionChain/chain.svg new file mode 100644 index 00000000..a5b32f90 --- /dev/null +++ b/packages/components/nodes/chains/VisionChain/chain.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index c562b4ee..30b4bd35 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -31,6 +31,7 @@ export interface IChatMessage { sourceDocuments?: string usedTools?: string fileAnnotations?: string + fileUploads?: string chatType: string chatId: string memoryType?: string @@ -167,6 +168,7 @@ export interface IncomingInput { socketIOClientId?: string chatId?: string stopNodeId?: string + uploads?: string } export interface IActiveChatflows { diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index 4054a26d..c803ce50 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -26,6 +26,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) fileAnnotations?: string + @Column({ nullable: true, type: 'text' }) + fileUploads?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..d896066b --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileUploads') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileUploads\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileUploads\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 8f9824a8..f5adff64 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -23,5 +24,6 @@ export const mysqlMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..6574ac81 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileUploads" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index d196fbc1..f80335a0 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -23,5 +24,6 @@ export const postgresMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts new file mode 100644 index 00000000..68e33220 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1701788586491-AddFileUploadsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "fileUploads" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "fileAnnotations", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "fileAnnotations", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index fdd83064..bae0cec8 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' +import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -23,5 +24,6 @@ export const sqliteMigrations = [ AddAssistantEntity1699325775451, AddUsedToolsToChatMessage1699481607341, AddCategoryToChatFlow1699900910291, - AddFileAnnotationsToChatMessage1700271021237 + AddFileAnnotationsToChatMessage1700271021237, + AddFileUploadsToChatMessage1701788586491 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9bc3eb3a..195eaf1d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -410,9 +410,7 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) - const obj = { - allowUploads: this.shouldAllowUploads(chatflow) - } + const obj = this.shouldAllowUploads(chatflow) return res.json(obj) }) @@ -1255,16 +1253,30 @@ export class App { } private uploadAllowedNodes = ['OpenAIVisionChain'] - private shouldAllowUploads(result: ChatFlow): boolean { + private shouldAllowUploads(result: ChatFlow): any { const flowObj = JSON.parse(result.flowData) let allowUploads = false + let allowedTypes: string[] = [] + let maxUploadSize: number = -1 flowObj.nodes.forEach((node: IReactFlowNode) => { if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) allowUploads = true + node.data.inputParams.map((param: any) => { + if (param.name === 'allowedUploadTypes') { + allowedTypes = param.default.split(';') + } + if (param.name === 'maxUploadSize') { + maxUploadSize = parseInt(param.default ? param.default : '0') + } + }) } }) - return allowUploads + return { + allowUploads, + allowedTypes, + maxUploadSize + } } /** @@ -1392,6 +1404,23 @@ export class App { if (!isKeyValidated) return res.status(401).send('Unauthorized') } + if (incomingInput.uploads) { + // @ts-ignore + ;(incomingInput.uploads as any[]).forEach((url: any) => { + if (url.type === 'file') { + const filename = url.name + const bf = url.data + const filePath = path.join(getUserHome(), '.flowise', 'gptvision', filename) + if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'gptvision'))) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + } + if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, bf) + fs.unlinkSync(filePath) + url.data = bf.toString('base64') + } + }) + } + let isStreamValid = false const files = (req.files as any[]) || [] @@ -1534,6 +1563,7 @@ export class App { let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + uploads: incomingInput.uploads, chatHistory: incomingInput.history, socketIO, socketIOClientId: incomingInput.socketIOClientId, @@ -1544,6 +1574,7 @@ export class App { chatId }) : await nodeInstance.run(nodeToExecuteData, incomingInput.question, { + uploads: incomingInput.uploads, chatHistory: incomingInput.history, logger, appDataSource: this.AppDataSource, @@ -1567,7 +1598,8 @@ export class App { chatId, memoryType, sessionId, - createdDate: userMessageDateTime + createdDate: userMessageDateTime, + fileUploads: incomingInput.uploads ? JSON.stringify(incomingInput.uploads) : '' } await this.addChatMessage(userMessage) diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index 8810b5a5..c02ca5cd 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -13,6 +13,7 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body) const deleteChatflow = (id) => client.delete(`/chatflows/${id}`) const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`) +const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`) export default { getAllChatflows, @@ -21,5 +22,6 @@ export default { createNewChatflow, updateChatflow, deleteChatflow, - getIsChatflowStreaming + getIsChatflowStreaming, + getAllowChatflowUploads } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 2298fee6..f1831d39 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -144,3 +144,32 @@ justify-content: center; align-items: center; } + +.file-drop-field { + position: relative; /* Needed to position the icon correctly */ + /* Other styling for the field */ +} + +.drop-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(137, 134, 134, 0.83); /* Semi-transparent white */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 10; /* Ensure it's above other content */ + border: 2px dashed #0094ff; /* Example style */ +} + +.preview-container { + +} + +.button { + flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */ + margin: 5px; /* Adjust as needed for spacing between buttons */ +} \ No newline at end of file diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index c610f944..0243f252 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' import PropTypes from 'prop-types' import socketIOClient from 'socket.io-client' @@ -9,9 +9,23 @@ import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' +import { + Box, + Button, + Card, + CardActions, + CardMedia, + Chip, + CircularProgress, + Divider, + Grid, + IconButton, + InputAdornment, + OutlinedInput, + Typography +} from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend, IconDownload } from '@tabler/icons' +import { IconDownload, IconSend, IconUpload } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -33,6 +47,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' +import DeleteIcon from '@mui/icons-material/Delete' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -58,6 +73,185 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) + const fileUploadRef = useRef(null) + const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) + const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) + const [previews, setPreviews] = useState([]) + const [isDragOver, setIsDragOver] = useState(false) + const handleDragOver = (e) => { + if (!isChatFlowAvailableForUploads) { + return + } + e.preventDefault() + } + const isFileAllowedForUpload = (file) => { + // check if file type is allowed + if (getAllowChatFlowUploads.data?.allowedTypes?.length > 0) { + const allowedFileTypes = getAllowChatFlowUploads.data?.allowedTypes + if (!allowedFileTypes.includes(file.type)) { + alert(`File ${file.name} is not allowed.\nAllowed file types are ${allowedFileTypes.join(', ')}.`) + return false + } + } + // check if file size is allowed + if (getAllowChatFlowUploads.data?.maxUploadSize > 0) { + const sizeInMB = file.size / 1024 / 1024 + if (sizeInMB > getAllowChatFlowUploads.data?.maxUploadSize) { + alert(`File ${file.name} is too large.\nMaximum allowed size is ${getAllowChatFlowUploads.data?.maxUploadSize} MB.`) + return false + } + } + return true + } + const handleDrop = async (e) => { + if (!isChatFlowAvailableForUploads) { + return + } + e.preventDefault() + setIsDragOver(false) + let files = [] + if (e.dataTransfer.files.length > 0) { + for (const file of e.dataTransfer.files) { + if (isFileAllowedForUpload(file) === false) { + return + } + const reader = new FileReader() + const { name } = file + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + resolve({ + data: result, + preview: URL.createObjectURL(file), + type: 'file', + name: name + }) + } + reader.readAsDataURL(file) + }) + ) + } + + const newFiles = await Promise.all(files) + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) + } + if (e.dataTransfer.items) { + const newUploads = [] + for (const item of e.dataTransfer.items) { + if (item.kind === 'string' && item.type.match('^text/uri-list')) { + item.getAsString((s) => { + let upload = { + data: s, + preview: s, + type: 'url', + name: s.substring(s.lastIndexOf('/') + 1) + } + setPreviews((prevPreviews) => [...prevPreviews, upload]) + }) + } else if (item.kind === 'string' && item.type.match('^text/html')) { + item.getAsString((s) => { + if (s.indexOf('href') === -1) return + //extract href + let start = s.substring(s.indexOf('href') + 6) + let hrefStr = start.substring(0, start.indexOf('"')) + + let upload = { + data: hrefStr, + preview: hrefStr, + type: 'url', + name: hrefStr.substring(hrefStr.lastIndexOf('/') + 1) + } + setPreviews((prevPreviews) => [...prevPreviews, upload]) + }) + } + } + } + } + const handleFileChange = async (event) => { + const fileObj = event.target.files && event.target.files[0] + if (!fileObj) { + return + } + let files = [] + for (const file of event.target.files) { + if (isFileAllowedForUpload(file) === false) { + return + } + const reader = new FileReader() + const { name } = file + files.push( + new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + resolve({ + data: result, + preview: URL.createObjectURL(file), + type: 'file', + name: name + }) + } + reader.readAsDataURL(file) + }) + ) + } + + const newFiles = await Promise.all(files) + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) + // 👇️ reset file input + event.target.value = null + } + + const handleDragEnter = (e) => { + if (isChatFlowAvailableForUploads) { + e.preventDefault() + setIsDragOver(true) + } + } + + const handleDragLeave = (e) => { + if (isChatFlowAvailableForUploads) { + e.preventDefault() + if (e.originalEvent?.pageX !== 0 || e.originalEvent?.pageY !== 0) { + return false + } + setIsDragOver(false) // Set the drag over state to false when the drag leaves + } + } + const handleDeletePreview = (itemToDelete) => { + if (itemToDelete.type === 'file') { + URL.revokeObjectURL(itemToDelete.preview) // Clean up for file + } + setPreviews(previews.filter((item) => item !== itemToDelete)) + } + const handleUploadClick = () => { + // 👇️ open file input box on click of another element + fileUploadRef.current.click() + } + + const previewStyle = { + width: '64px', + height: '64px', + objectFit: 'cover' // This makes the image cover the area, cropping it if necessary + } + const messageImageStyle = { + width: '128px', + height: '128px', + objectFit: 'cover' // This makes the image cover the area, cropping it if necessary + } + + const clearPreviews = () => { + // Revoke the data uris to avoid memory leaks + previews.forEach((file) => URL.revokeObjectURL(file.preview)) + setPreviews([]) + } + const onSourceDialogClick = (data, title) => { setSourceDialogProps({ data, title }) setSourceDialogOpen(true) @@ -113,7 +307,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } setLoading(true) - setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }]) + const urls = [] + previews.map((item) => { + urls.push({ + data: item.data, + type: item.type, + name: item.name + }) + }) + clearPreviews() + setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage', fileUploads: urls }]) // Send user question and history to API try { @@ -122,6 +325,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'), chatId } + if (urls) params.uploads = urls if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) @@ -209,6 +413,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) + if (message.fileUploads) obj.fileUploads = JSON.parse(message.fileUploads) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -227,6 +432,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getIsChatflowStreamingApi.data]) + // Get chatflow uploads capability + useEffect(() => { + if (getAllowChatFlowUploads.data) { + setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.allowUploads ?? false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllowChatFlowUploads.data]) + // Auto scroll chat to bottom useEffect(() => { scrollToBottom() @@ -245,6 +458,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (open && chatflowid) { getChatmessageApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid) + getAllowChatFlowUploads.request(chatflowid) scrollToBottom() socket = socketIOClient(baseURL) @@ -281,9 +495,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { }, [open, chatflowid]) return ( - <> -
-
+
+ {isDragOver && ( + + Drop here to upload + {getAllowChatFlowUploads.data?.allowedTypes?.join(', ')} + Max Allowed Size: {getAllowChatFlowUploads.data?.maxUploadSize} MB + + )} +
+
{messages && messages.map((message, index) => { return ( @@ -375,6 +602,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { })}
)} + {message.fileUploads && + message.fileUploads.map((item, index) => { + return ( + + + + ) + })} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { @@ -430,6 +671,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { onChange={onChange} multiline={true} maxRows={isDialog ? 7 : 2} + startAdornment={ + isChatFlowAvailableForUploads && ( + + + + + + ) + } endAdornment={ @@ -447,11 +704,39 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } /> + {isChatFlowAvailableForUploads && ( + + )}
+ {previews && previews.length > 0 && ( + + {previews.map((item, index) => ( + + + + +
) }