mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +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
|
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 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
+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 { 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
|
||||||
]
|
]
|
||||||
|
|||||||
+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 { 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
|
||||||
]
|
]
|
||||||
|
|||||||
+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 { 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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user