GPT Vision: Initial implementation of the OpenAI Vision API

This commit is contained in:
vinodkiran
2023-12-06 12:31:33 +05:30
parent c96572e10f
commit 73f7046316
15 changed files with 447 additions and 22 deletions
@@ -12,6 +12,7 @@ class OpenAIVisionChain_Chains implements INode {
version: number version: number
type: string type: string
icon: string icon: string
badge: string
category: string category: string
baseClasses: string[] baseClasses: string[]
description: string description: string
@@ -21,10 +22,11 @@ class OpenAIVisionChain_Chains implements INode {
constructor() { constructor() {
this.label = 'Open AI Vision Chain' this.label = 'Open AI Vision Chain'
this.name = 'openAIVisionChain' this.name = 'openAIVisionChain'
this.version = 3.0 this.version = 1.0
this.type = 'OpenAIVisionChain' this.type = 'OpenAIVisionChain'
this.icon = 'chain.svg' this.icon = 'chain.svg'
this.category = 'Chains' this.category = 'Chains'
this.badge = 'EXPERIMENTAL'
this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .' this.description = 'Chain to run queries against OpenAI (GPT-4) Vision .'
this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)] this.baseClasses = [this.type, ...getBaseClasses(VLLMChain)]
this.inputs = [ this.inputs = [
@@ -63,6 +65,20 @@ class OpenAIVisionChain_Chains implements INode {
type: 'string', type: 'string',
placeholder: 'Name Your Chain', placeholder: 'Name Your Chain',
optional: true 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 = [ this.outputs = [
@@ -93,7 +109,7 @@ class OpenAIVisionChain_Chains implements INode {
openAIApiKey: openAIModel.openAIApiKey, openAIApiKey: openAIModel.openAIApiKey,
imageResolution: imageResolution, imageResolution: imageResolution,
verbose: process.env.DEBUG === 'true', verbose: process.env.DEBUG === 'true',
imageUrls: options.url, imageUrls: options.uploads,
openAIModel: openAIModel openAIModel: openAIModel
} }
if (output === this.name) { if (output === this.name) {
@@ -156,8 +172,8 @@ const runPrediction = async (
* TO: { "value": "hello i am ben\n\n\thow are you?" } * TO: { "value": "hello i am ben\n\n\thow are you?" }
*/ */
const promptValues = handleEscapeCharacters(promptValuesRaw, true) const promptValues = handleEscapeCharacters(promptValuesRaw, true)
if (options?.url) { if (options?.uploads) {
chain.imageUrls = options.url chain.imageUrls = options.uploads
} }
if (promptValues && inputVariables.length > 0) { if (promptValues && inputVariables.length > 0) {
let seen: string[] = [] let seen: string[] = []
@@ -79,6 +79,7 @@ export class VLLMChain extends BaseChain implements OpenAIVisionChainInput {
messages: [] messages: []
} }
if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens if (this.openAIModel.maxTokens) vRequest.max_tokens = this.openAIModel.maxTokens
else vRequest.max_tokens = 1024
const userRole: any = { role: 'user' } const userRole: any = { role: 'user' }
userRole.content = [] userRole.content = []
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

+2
View File
@@ -31,6 +31,7 @@ export interface IChatMessage {
sourceDocuments?: string sourceDocuments?: string
usedTools?: string usedTools?: string
fileAnnotations?: string fileAnnotations?: string
fileUploads?: string
chatType: string chatType: string
chatId: string chatId: string
memoryType?: string memoryType?: string
@@ -167,6 +168,7 @@ export interface IncomingInput {
socketIOClientId?: string socketIOClientId?: string
chatId?: string chatId?: string
stopNodeId?: string stopNodeId?: string
uploads?: string
} }
export interface IActiveChatflows { export interface IActiveChatflows {
@@ -26,6 +26,9 @@ export class ChatMessage implements IChatMessage {
@Column({ nullable: true, type: 'text' }) @Column({ nullable: true, type: 'text' })
fileAnnotations?: string fileAnnotations?: string
@Column({ nullable: true, type: 'text' })
fileUploads?: string
@Column() @Column()
chatType: string chatType: string
@@ -0,0 +1,12 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileUploads\`;`)
}
}
@@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
export const mysqlMigrations = [ export const mysqlMigrations = [
Init1693840429259, Init1693840429259,
@@ -23,5 +24,6 @@ export const mysqlMigrations = [
AddAssistantEntity1699325775451, AddAssistantEntity1699325775451,
AddUsedToolsToChatMessage1699481607341, AddUsedToolsToChatMessage1699481607341,
AddCategoryToChatFlow1699900910291, AddCategoryToChatFlow1699900910291,
AddFileAnnotationsToChatMessage1700271021237 AddFileAnnotationsToChatMessage1700271021237,
AddFileUploadsToChatMessage1701788586491
] ]
@@ -0,0 +1,11 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileUploads" TEXT;`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`)
}
}
@@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
export const postgresMigrations = [ export const postgresMigrations = [
Init1693891895163, Init1693891895163,
@@ -23,5 +24,6 @@ export const postgresMigrations = [
AddAssistantEntity1699325775451, AddAssistantEntity1699325775451,
AddUsedToolsToChatMessage1699481607341, AddUsedToolsToChatMessage1699481607341,
AddCategoryToChatFlow1699900910291, AddCategoryToChatFlow1699900910291,
AddFileAnnotationsToChatMessage1700271021237 AddFileAnnotationsToChatMessage1700271021237,
AddFileUploadsToChatMessage1701788586491
] ]
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddFileUploadsToChatMessage1701788586491 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`)
await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileUploads";`)
}
}
@@ -10,6 +10,7 @@ import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEnt
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
export const sqliteMigrations = [ export const sqliteMigrations = [
Init1693835579790, Init1693835579790,
@@ -23,5 +24,6 @@ export const sqliteMigrations = [
AddAssistantEntity1699325775451, AddAssistantEntity1699325775451,
AddUsedToolsToChatMessage1699481607341, AddUsedToolsToChatMessage1699481607341,
AddCategoryToChatFlow1699900910291, AddCategoryToChatFlow1699900910291,
AddFileAnnotationsToChatMessage1700271021237 AddFileAnnotationsToChatMessage1700271021237,
AddFileUploadsToChatMessage1701788586491
] ]
+38 -6
View File
@@ -410,9 +410,7 @@ export class App {
}) })
if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
const obj = { const obj = this.shouldAllowUploads(chatflow)
allowUploads: this.shouldAllowUploads(chatflow)
}
return res.json(obj) return res.json(obj)
}) })
@@ -1255,16 +1253,30 @@ export class App {
} }
private uploadAllowedNodes = ['OpenAIVisionChain'] private uploadAllowedNodes = ['OpenAIVisionChain']
private shouldAllowUploads(result: ChatFlow): boolean { private shouldAllowUploads(result: ChatFlow): any {
const flowObj = JSON.parse(result.flowData) const flowObj = JSON.parse(result.flowData)
let allowUploads = false let allowUploads = false
let allowedTypes: string[] = []
let maxUploadSize: number = -1
flowObj.nodes.forEach((node: IReactFlowNode) => { flowObj.nodes.forEach((node: IReactFlowNode) => {
if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) { if (this.uploadAllowedNodes.indexOf(node.data.type) > -1) {
logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`) logger.debug(`[server]: Found Eligible Node ${node.data.type}, Allowing Uploads.`)
allowUploads = true 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 (!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 let isStreamValid = false
const files = (req.files as any[]) || [] const files = (req.files as any[]) || []
@@ -1534,6 +1563,7 @@ export class App {
let result = isStreamValid let result = isStreamValid
? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
uploads: incomingInput.uploads,
chatHistory: incomingInput.history, chatHistory: incomingInput.history,
socketIO, socketIO,
socketIOClientId: incomingInput.socketIOClientId, socketIOClientId: incomingInput.socketIOClientId,
@@ -1544,6 +1574,7 @@ export class App {
chatId chatId
}) })
: await nodeInstance.run(nodeToExecuteData, incomingInput.question, { : await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
uploads: incomingInput.uploads,
chatHistory: incomingInput.history, chatHistory: incomingInput.history,
logger, logger,
appDataSource: this.AppDataSource, appDataSource: this.AppDataSource,
@@ -1567,7 +1598,8 @@ export class App {
chatId, chatId,
memoryType, memoryType,
sessionId, sessionId,
createdDate: userMessageDateTime createdDate: userMessageDateTime,
fileUploads: incomingInput.uploads ? JSON.stringify(incomingInput.uploads) : ''
} }
await this.addChatMessage(userMessage) await this.addChatMessage(userMessage)
+3 -1
View File
@@ -13,6 +13,7 @@ const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`) const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`) const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`)
export default { export default {
getAllChatflows, getAllChatflows,
@@ -21,5 +22,6 @@ export default {
createNewChatflow, createNewChatflow,
updateChatflow, updateChatflow,
deleteChatflow, deleteChatflow,
getIsChatflowStreaming getIsChatflowStreaming,
getAllowChatflowUploads
} }
@@ -144,3 +144,32 @@
justify-content: center; justify-content: center;
align-items: 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 */
}
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import socketIOClient from 'socket.io-client' import socketIOClient from 'socket.io-client'
@@ -9,9 +9,23 @@ import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math' import remarkMath from 'remark-math'
import axios from 'axios' 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 { useTheme } from '@mui/material/styles'
import { IconSend, IconDownload } from '@tabler/icons' import { IconDownload, IconSend, IconUpload } from '@tabler/icons'
// project import // project import
import { CodeBlock } from 'ui-component/markdown/CodeBlock' 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 robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png' import userPNG from 'assets/images/account.png'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper' import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper'
import DeleteIcon from '@mui/icons-material/Delete'
export const ChatMessage = ({ open, chatflowid, isDialog }) => { export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const theme = useTheme() const theme = useTheme()
@@ -58,6 +73,185 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) 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) => { const onSourceDialogClick = (data, title) => {
setSourceDialogProps({ data, title }) setSourceDialogProps({ data, title })
setSourceDialogOpen(true) setSourceDialogOpen(true)
@@ -113,7 +307,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
} }
setLoading(true) 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 // Send user question and history to API
try { try {
@@ -122,6 +325,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'), history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'),
chatId chatId
} }
if (urls) params.uploads = urls
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) 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.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools)
if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations)
if (message.fileUploads) obj.fileUploads = JSON.parse(message.fileUploads)
return obj return obj
}) })
setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
@@ -227,6 +432,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIsChatflowStreamingApi.data]) }, [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 // Auto scroll chat to bottom
useEffect(() => { useEffect(() => {
scrollToBottom() scrollToBottom()
@@ -245,6 +458,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (open && chatflowid) { if (open && chatflowid) {
getChatmessageApi.request(chatflowid) getChatmessageApi.request(chatflowid)
getIsChatflowStreamingApi.request(chatflowid) getIsChatflowStreamingApi.request(chatflowid)
getAllowChatFlowUploads.request(chatflowid)
scrollToBottom() scrollToBottom()
socket = socketIOClient(baseURL) socket = socketIOClient(baseURL)
@@ -281,9 +495,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
}, [open, chatflowid]) }, [open, chatflowid])
return ( return (
<> <div
<div className={isDialog ? 'cloud-dialog' : 'cloud'}> onDragOver={handleDragOver}
<div ref={ps} className='messagelist'> onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`file-drop-field`}
>
{isDragOver && (
<Box className='drop-overlay'>
<Typography variant='h2'>Drop here to upload</Typography>
<Typography variant='subtitle1'>{getAllowChatFlowUploads.data?.allowedTypes?.join(', ')}</Typography>
<Typography variant='subtitle1'>Max Allowed Size: {getAllowChatFlowUploads.data?.maxUploadSize} MB</Typography>
</Box>
)}
<div className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}>
<div ref={ps} className={'messagelist'}>
{messages && {messages &&
messages.map((message, index) => { messages.map((message, index) => {
return ( return (
@@ -375,6 +602,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})} })}
</div> </div>
)} )}
{message.fileUploads &&
message.fileUploads.map((item, index) => {
return (
<Card key={index} sx={{ maxWidth: 128, margin: 5 }}>
<CardMedia
component='img'
image={item.data}
sx={{ height: 64 }}
alt={'preview'}
style={messageImageStyle}
/>
</Card>
)
})}
{message.sourceDocuments && ( {message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}> <div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{removeDuplicateURL(message).map((source, index) => { {removeDuplicateURL(message).map((source, index) => {
@@ -430,6 +671,22 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
onChange={onChange} onChange={onChange}
multiline={true} multiline={true}
maxRows={isDialog ? 7 : 2} maxRows={isDialog ? 7 : 2}
startAdornment={
isChatFlowAvailableForUploads && (
<InputAdornment position='start' sx={{ padding: '15px' }}>
<IconButton
onClick={handleUploadClick}
type='button'
disabled={loading || !chatflowid}
edge='start'
>
<IconUpload
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
)
}
endAdornment={ endAdornment={
<InputAdornment position='end' sx={{ padding: '15px' }}> <InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'> <IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
@@ -447,11 +704,39 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
</InputAdornment> </InputAdornment>
} }
/> />
{isChatFlowAvailableForUploads && (
<input style={{ display: 'none' }} ref={fileUploadRef} type='file' onChange={handleFileChange} />
)}
</form> </form>
</div> </div>
</div> </div>
{previews && previews.length > 0 && (
<Grid className='preview-container' container spacing={2} sx={{ p: 1, mt: '5px', ml: '1px' }}>
{previews.map((item, index) => (
<Grid item xs={12} sm={6} md={3} key={index}>
<Card variant='outlined' sx={{ maxWidth: 64 }}>
<CardMedia
component='img'
image={item.preview}
sx={{ height: 64 }}
alt={`preview ${index}`}
style={previewStyle}
/>
<CardActions className='center' sx={{ padding: 0, margin: 0 }}>
<Button
startIcon={<DeleteIcon />}
onClick={() => handleDeletePreview(item)}
size='small'
variant='text'
/>
</CardActions>
</Card>
</Grid>
))}
</Grid>
)}
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} /> <SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
</> </div>
) )
} }