mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 09:00:52 +03:00
GPT Vision: Initial implementation of the OpenAI Vision API
This commit is contained in:
@@ -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[] = []
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 |
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+12
@@ -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 { 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
|
||||
]
|
||||
|
||||
+11
@@ -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 { 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
|
||||
]
|
||||
|
||||
+20
@@ -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 { 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
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className={isDialog ? 'cloud-dialog' : 'cloud'}>
|
||||
<div ref={ps} className='messagelist'>
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
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.map((message, index) => {
|
||||
return (
|
||||
@@ -375,6 +602,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
})}
|
||||
</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 && (
|
||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||
{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 && (
|
||||
<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={
|
||||
<InputAdornment position='end' sx={{ padding: '15px' }}>
|
||||
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
|
||||
@@ -447,11 +704,39 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
{isChatFlowAvailableForUploads && (
|
||||
<input style={{ display: 'none' }} ref={fileUploadRef} type='file' onChange={handleFileChange} />
|
||||
)}
|
||||
</form>
|
||||
</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)} />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user