Merge pull request #149 from FlowiseAI/feature/Streaming
Feature/Streaming
This commit is contained in:
commit
2968dccd83
|
|
@ -1,6 +1,6 @@
|
||||||
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { ConversationChain } from 'langchain/chains'
|
import { ConversationChain } from 'langchain/chains'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts'
|
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts'
|
||||||
import { BufferMemory, ChatMessageHistory } from 'langchain/memory'
|
import { BufferMemory, ChatMessageHistory } from 'langchain/memory'
|
||||||
import { BaseChatModel } from 'langchain/chat_models/base'
|
import { BaseChatModel } from 'langchain/chat_models/base'
|
||||||
|
|
@ -90,8 +90,14 @@ class ConversationChain_Chains implements INode {
|
||||||
chain.memory = memory
|
chain.memory = memory
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await chain.call({ input })
|
if (options.socketIO && options.socketIOClientId) {
|
||||||
return res?.response
|
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
|
||||||
|
const res = await chain.call({ input }, [handler])
|
||||||
|
return res?.response
|
||||||
|
} else {
|
||||||
|
const res = await chain.call({ input })
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { BaseLanguageModel } from 'langchain/base_language'
|
import { BaseLanguageModel } from 'langchain/base_language'
|
||||||
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { ConversationalRetrievalQAChain } from 'langchain/chains'
|
import { ConversationalRetrievalQAChain } from 'langchain/chains'
|
||||||
import { BaseRetriever } from 'langchain/schema'
|
import { BaseRetriever } from 'langchain/schema'
|
||||||
|
|
||||||
|
|
@ -74,6 +74,12 @@ class ConversationalRetrievalQAChain_Chains implements INode {
|
||||||
|
|
||||||
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
||||||
const chain = nodeData.instance as ConversationalRetrievalQAChain
|
const chain = nodeData.instance as ConversationalRetrievalQAChain
|
||||||
|
let model = nodeData.inputs?.model
|
||||||
|
|
||||||
|
// Temporary fix: https://github.com/hwchase17/langchainjs/issues/754
|
||||||
|
model.streaming = false
|
||||||
|
chain.questionGeneratorChain.llm = model
|
||||||
|
|
||||||
let chatHistory = ''
|
let chatHistory = ''
|
||||||
|
|
||||||
if (options && options.chatHistory) {
|
if (options && options.chatHistory) {
|
||||||
|
|
@ -90,9 +96,14 @@ class ConversationalRetrievalQAChain_Chains implements INode {
|
||||||
chat_history: chatHistory ? chatHistory : []
|
chat_history: chatHistory ? chatHistory : []
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await chain.call(obj)
|
if (options.socketIO && options.socketIOClientId) {
|
||||||
|
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
|
||||||
return res?.text
|
const res = await chain.call(obj, [handler])
|
||||||
|
return res?.text
|
||||||
|
} else {
|
||||||
|
const res = await chain.call(obj)
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { LLMChain } from 'langchain/chains'
|
import { LLMChain } from 'langchain/chains'
|
||||||
import { BaseLanguageModel } from 'langchain/base_language'
|
import { BaseLanguageModel } from 'langchain/base_language'
|
||||||
|
|
||||||
|
|
@ -76,12 +76,14 @@ class LLMChain_Chains implements INode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
||||||
const inputVariables = nodeData.instance.prompt.inputVariables as string[] // ["product"]
|
const inputVariables = nodeData.instance.prompt.inputVariables as string[] // ["product"]
|
||||||
const chain = nodeData.instance as LLMChain
|
const chain = nodeData.instance as LLMChain
|
||||||
const promptValues = nodeData.inputs?.prompt.promptValues as ICommonObject
|
const promptValues = nodeData.inputs?.prompt.promptValues as ICommonObject
|
||||||
|
|
||||||
const res = await runPrediction(inputVariables, chain, input, promptValues)
|
const res = options.socketIO
|
||||||
|
? await runPrediction(inputVariables, chain, input, promptValues, true, options.socketIO, options.socketIOClientId)
|
||||||
|
: await runPrediction(inputVariables, chain, input, promptValues)
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m')
|
console.log('\x1b[93m\x1b[1m\n*****FINAL RESULT*****\n\x1b[0m\x1b[0m')
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
@ -90,10 +92,24 @@ class LLMChain_Chains implements INode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runPrediction = async (inputVariables: string[], chain: LLMChain, input: string, promptValues: ICommonObject) => {
|
const runPrediction = async (
|
||||||
|
inputVariables: string[],
|
||||||
|
chain: LLMChain,
|
||||||
|
input: string,
|
||||||
|
promptValues: ICommonObject,
|
||||||
|
isStreaming?: boolean,
|
||||||
|
socketIO?: any,
|
||||||
|
socketIOClientId = ''
|
||||||
|
) => {
|
||||||
if (inputVariables.length === 1) {
|
if (inputVariables.length === 1) {
|
||||||
const res = await chain.run(input)
|
if (isStreaming) {
|
||||||
return res
|
const handler = new CustomChainHandler(socketIO, socketIOClientId)
|
||||||
|
const res = await chain.run(input, [handler])
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
const res = await chain.run(input)
|
||||||
|
return res
|
||||||
|
}
|
||||||
} else if (inputVariables.length > 1) {
|
} else if (inputVariables.length > 1) {
|
||||||
let seen: string[] = []
|
let seen: string[] = []
|
||||||
|
|
||||||
|
|
@ -109,8 +125,14 @@ const runPrediction = async (inputVariables: string[], chain: LLMChain, input: s
|
||||||
const options = {
|
const options = {
|
||||||
...promptValues
|
...promptValues
|
||||||
}
|
}
|
||||||
const res = await chain.call(options)
|
if (isStreaming) {
|
||||||
return res?.text
|
const handler = new CustomChainHandler(socketIO, socketIOClientId)
|
||||||
|
const res = await chain.call(options, [handler])
|
||||||
|
return res?.text
|
||||||
|
} else {
|
||||||
|
const res = await chain.call(options)
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
} else if (seen.length === 1) {
|
} else if (seen.length === 1) {
|
||||||
// If one inputVariable is not specify, use input (user's question) as value
|
// If one inputVariable is not specify, use input (user's question) as value
|
||||||
const lastValue = seen.pop()
|
const lastValue = seen.pop()
|
||||||
|
|
@ -119,14 +141,26 @@ const runPrediction = async (inputVariables: string[], chain: LLMChain, input: s
|
||||||
...promptValues,
|
...promptValues,
|
||||||
[lastValue]: input
|
[lastValue]: input
|
||||||
}
|
}
|
||||||
const res = await chain.call(options)
|
if (isStreaming) {
|
||||||
return res?.text
|
const handler = new CustomChainHandler(socketIO, socketIOClientId)
|
||||||
|
const res = await chain.call(options, [handler])
|
||||||
|
return res?.text
|
||||||
|
} else {
|
||||||
|
const res = await chain.call(options)
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Please provide Prompt Values for: ${seen.join(', ')}`)
|
throw new Error(`Please provide Prompt Values for: ${seen.join(', ')}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const res = await chain.run(input)
|
if (isStreaming) {
|
||||||
return res
|
const handler = new CustomChainHandler(socketIO, socketIOClientId)
|
||||||
|
const res = await chain.run(input, [handler])
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
const res = await chain.run(input)
|
||||||
|
return res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { RetrievalQAChain } from 'langchain/chains'
|
import { RetrievalQAChain } from 'langchain/chains'
|
||||||
import { BaseRetriever } from 'langchain/schema'
|
import { BaseRetriever } from 'langchain/schema'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { BaseLanguageModel } from 'langchain/base_language'
|
import { BaseLanguageModel } from 'langchain/base_language'
|
||||||
|
|
||||||
class RetrievalQAChain_Chains implements INode {
|
class RetrievalQAChain_Chains implements INode {
|
||||||
|
|
@ -44,13 +44,20 @@ class RetrievalQAChain_Chains implements INode {
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
||||||
const chain = nodeData.instance as RetrievalQAChain
|
const chain = nodeData.instance as RetrievalQAChain
|
||||||
const obj = {
|
const obj = {
|
||||||
query: input
|
query: input
|
||||||
}
|
}
|
||||||
const res = await chain.call(obj)
|
|
||||||
return res?.text
|
if (options.socketIO && options.socketIOClientId) {
|
||||||
|
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
|
||||||
|
const res = await chain.call(obj, [handler])
|
||||||
|
return res?.text
|
||||||
|
} else {
|
||||||
|
const res = await chain.call(obj)
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { SqlDatabaseChain, SqlDatabaseChainInput } from 'langchain/chains'
|
import { SqlDatabaseChain, SqlDatabaseChainInput } from 'langchain/chains'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { DataSource } from 'typeorm'
|
import { DataSource } from 'typeorm'
|
||||||
import { SqlDatabase } from 'langchain/sql_db'
|
import { SqlDatabase } from 'langchain/sql_db'
|
||||||
import { BaseLanguageModel } from 'langchain/base_language'
|
import { BaseLanguageModel } from 'langchain/base_language'
|
||||||
|
|
@ -59,14 +59,20 @@ class SqlDatabaseChain_Chains implements INode {
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
||||||
const databaseType = nodeData.inputs?.database as 'sqlite'
|
const databaseType = nodeData.inputs?.database as 'sqlite'
|
||||||
const model = nodeData.inputs?.model as BaseLanguageModel
|
const model = nodeData.inputs?.model as BaseLanguageModel
|
||||||
const dbFilePath = nodeData.inputs?.dbFilePath
|
const dbFilePath = nodeData.inputs?.dbFilePath
|
||||||
|
|
||||||
const chain = await getSQLDBChain(databaseType, dbFilePath, model)
|
const chain = await getSQLDBChain(databaseType, dbFilePath, model)
|
||||||
const res = await chain.run(input)
|
if (options.socketIO && options.socketIOClientId) {
|
||||||
return res
|
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
|
||||||
|
const res = await chain.run(input, [handler])
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
const res = await chain.run(input)
|
||||||
|
return res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||||
import { getBaseClasses } from '../../../src/utils'
|
import { CustomChainHandler, getBaseClasses } from '../../../src/utils'
|
||||||
import { VectorDBQAChain } from 'langchain/chains'
|
import { VectorDBQAChain } from 'langchain/chains'
|
||||||
import { BaseLanguageModel } from 'langchain/base_language'
|
import { BaseLanguageModel } from 'langchain/base_language'
|
||||||
import { VectorStore } from 'langchain/vectorstores'
|
import { VectorStore } from 'langchain/vectorstores'
|
||||||
|
|
@ -44,13 +44,20 @@ class VectorDBQAChain_Chains implements INode {
|
||||||
return chain
|
return chain
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
|
||||||
const chain = nodeData.instance as VectorDBQAChain
|
const chain = nodeData.instance as VectorDBQAChain
|
||||||
const obj = {
|
const obj = {
|
||||||
query: input
|
query: input
|
||||||
}
|
}
|
||||||
const res = await chain.call(obj)
|
|
||||||
return res?.text
|
if (options.socketIO && options.socketIOClientId) {
|
||||||
|
const handler = new CustomChainHandler(options.socketIO, options.socketIOClientId)
|
||||||
|
const res = await chain.call(obj, [handler])
|
||||||
|
return res?.text
|
||||||
|
} else {
|
||||||
|
const res = await chain.call(obj)
|
||||||
|
return res?.text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ class AzureChatOpenAI_ChatModels implements INode {
|
||||||
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
|
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
|
||||||
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
||||||
const timeout = nodeData.inputs?.timeout as string
|
const timeout = nodeData.inputs?.timeout as string
|
||||||
|
const streaming = nodeData.inputs?.streaming as boolean
|
||||||
|
|
||||||
const obj: Partial<AzureOpenAIInput> & Partial<OpenAIBaseInput> = {
|
const obj: Partial<AzureOpenAIInput> & Partial<OpenAIBaseInput> = {
|
||||||
temperature: parseInt(temperature, 10),
|
temperature: parseInt(temperature, 10),
|
||||||
|
|
@ -128,7 +129,8 @@ class AzureChatOpenAI_ChatModels implements INode {
|
||||||
azureOpenAIApiKey,
|
azureOpenAIApiKey,
|
||||||
azureOpenAIApiInstanceName,
|
azureOpenAIApiInstanceName,
|
||||||
azureOpenAIApiDeploymentName,
|
azureOpenAIApiDeploymentName,
|
||||||
azureOpenAIApiVersion
|
azureOpenAIApiVersion,
|
||||||
|
streaming: streaming ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
||||||
|
|
|
||||||
|
|
@ -117,11 +117,13 @@ class ChatAnthropic_ChatModels implements INode {
|
||||||
const maxTokensToSample = nodeData.inputs?.maxTokensToSample as string
|
const maxTokensToSample = nodeData.inputs?.maxTokensToSample as string
|
||||||
const topP = nodeData.inputs?.topP as string
|
const topP = nodeData.inputs?.topP as string
|
||||||
const topK = nodeData.inputs?.topK as string
|
const topK = nodeData.inputs?.topK as string
|
||||||
|
const streaming = nodeData.inputs?.streaming as boolean
|
||||||
|
|
||||||
const obj: Partial<AnthropicInput> & { anthropicApiKey?: string } = {
|
const obj: Partial<AnthropicInput> & { anthropicApiKey?: string } = {
|
||||||
temperature: parseInt(temperature, 10),
|
temperature: parseInt(temperature, 10),
|
||||||
modelName,
|
modelName,
|
||||||
anthropicApiKey
|
anthropicApiKey,
|
||||||
|
streaming: streaming ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTokensToSample) obj.maxTokensToSample = parseInt(maxTokensToSample, 10)
|
if (maxTokensToSample) obj.maxTokensToSample = parseInt(maxTokensToSample, 10)
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,13 @@ class ChatOpenAI_ChatModels implements INode {
|
||||||
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
|
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
|
||||||
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
||||||
const timeout = nodeData.inputs?.timeout as string
|
const timeout = nodeData.inputs?.timeout as string
|
||||||
|
const streaming = nodeData.inputs?.streaming as boolean
|
||||||
|
|
||||||
const obj: Partial<OpenAIChatInput> & { openAIApiKey?: string } = {
|
const obj: Partial<OpenAIChatInput> & { openAIApiKey?: string } = {
|
||||||
temperature: parseInt(temperature, 10),
|
temperature: parseInt(temperature, 10),
|
||||||
modelName,
|
modelName,
|
||||||
openAIApiKey
|
openAIApiKey,
|
||||||
|
streaming: streaming ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ class AzureOpenAI_LLMs implements INode {
|
||||||
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
const presencePenalty = nodeData.inputs?.presencePenalty as string
|
||||||
const timeout = nodeData.inputs?.timeout as string
|
const timeout = nodeData.inputs?.timeout as string
|
||||||
const bestOf = nodeData.inputs?.bestOf as string
|
const bestOf = nodeData.inputs?.bestOf as string
|
||||||
|
const streaming = nodeData.inputs?.streaming as boolean
|
||||||
|
|
||||||
const obj: Partial<AzureOpenAIInput> & Partial<OpenAIInput> = {
|
const obj: Partial<AzureOpenAIInput> & Partial<OpenAIInput> = {
|
||||||
temperature: parseInt(temperature, 10),
|
temperature: parseInt(temperature, 10),
|
||||||
|
|
@ -183,7 +184,8 @@ class AzureOpenAI_LLMs implements INode {
|
||||||
azureOpenAIApiKey,
|
azureOpenAIApiKey,
|
||||||
azureOpenAIApiInstanceName,
|
azureOpenAIApiInstanceName,
|
||||||
azureOpenAIApiDeploymentName,
|
azureOpenAIApiDeploymentName,
|
||||||
azureOpenAIApiVersion
|
azureOpenAIApiVersion,
|
||||||
|
streaming: streaming ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
||||||
|
|
|
||||||
|
|
@ -121,11 +121,13 @@ class OpenAI_LLMs implements INode {
|
||||||
const timeout = nodeData.inputs?.timeout as string
|
const timeout = nodeData.inputs?.timeout as string
|
||||||
const batchSize = nodeData.inputs?.batchSize as string
|
const batchSize = nodeData.inputs?.batchSize as string
|
||||||
const bestOf = nodeData.inputs?.bestOf as string
|
const bestOf = nodeData.inputs?.bestOf as string
|
||||||
|
const streaming = nodeData.inputs?.streaming as boolean
|
||||||
|
|
||||||
const obj: Partial<OpenAIInput> & { openAIApiKey?: string } = {
|
const obj: Partial<OpenAIInput> & { openAIApiKey?: string } = {
|
||||||
temperature: parseInt(temperature, 10),
|
temperature: parseInt(temperature, 10),
|
||||||
modelName,
|
modelName,
|
||||||
openAIApiKey
|
openAIApiKey,
|
||||||
|
streaming: streaming ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import axios from 'axios'
|
||||||
import { load } from 'cheerio'
|
import { load } from 'cheerio'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import { BaseCallbackHandler } from 'langchain/callbacks'
|
||||||
|
import { Server } from 'socket.io'
|
||||||
|
|
||||||
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
|
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
|
||||||
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
|
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
|
||||||
|
|
@ -152,6 +154,12 @@ export const getInputVariables = (paramValue: string): string[] => {
|
||||||
return inputVariables
|
return inputVariables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crawl all available urls given a domain url and limit
|
||||||
|
* @param {string} url
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
export const getAvailableURLs = async (url: string, limit: number) => {
|
export const getAvailableURLs = async (url: string, limit: number) => {
|
||||||
try {
|
try {
|
||||||
const availableUrls: string[] = []
|
const availableUrls: string[] = []
|
||||||
|
|
@ -190,3 +198,31 @@ export const getAvailableURLs = async (url: string, limit: number) => {
|
||||||
throw new Error(`getAvailableURLs: ${err?.message}`)
|
throw new Error(`getAvailableURLs: ${err?.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom chain handler class
|
||||||
|
*/
|
||||||
|
export class CustomChainHandler extends BaseCallbackHandler {
|
||||||
|
name = 'custom_chain_handler'
|
||||||
|
isLLMStarted = false
|
||||||
|
socketIO: Server
|
||||||
|
socketIOClientId = ''
|
||||||
|
|
||||||
|
constructor(socketIO: Server, socketIOClientId: string) {
|
||||||
|
super()
|
||||||
|
this.socketIO = socketIO
|
||||||
|
this.socketIOClientId = socketIOClientId
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLLMNewToken(token: string) {
|
||||||
|
if (!this.isLLMStarted) {
|
||||||
|
this.isLLMStarted = true
|
||||||
|
this.socketIO.to(this.socketIOClientId).emit('start', token)
|
||||||
|
}
|
||||||
|
this.socketIO.to(this.socketIOClientId).emit('token', token)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLLMEnd() {
|
||||||
|
this.socketIO.to(this.socketIOClientId).emit('end')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.34",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"socket.io": "^4.6.1",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"typeorm": "^0.3.6"
|
"typeorm": "^0.3.6"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ export interface IncomingInput {
|
||||||
question: string
|
question: string
|
||||||
history: IMessage[]
|
history: IMessage[]
|
||||||
overrideConfig?: ICommonObject
|
overrideConfig?: ICommonObject
|
||||||
|
socketIOClientId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IActiveChatflows {
|
export interface IActiveChatflows {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import cors from 'cors'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import basicAuth from 'express-basic-auth'
|
import basicAuth from 'express-basic-auth'
|
||||||
|
import { Server } from 'socket.io'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IChatFlow,
|
IChatFlow,
|
||||||
|
|
@ -32,7 +33,8 @@ import {
|
||||||
mapMimeTypeToInputField,
|
mapMimeTypeToInputField,
|
||||||
findAvailableConfigs,
|
findAvailableConfigs,
|
||||||
isSameOverrideConfig,
|
isSameOverrideConfig,
|
||||||
replaceAllAPIKeys
|
replaceAllAPIKeys,
|
||||||
|
isFlowValidForStream
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { getDataSource } from './DataSource'
|
import { getDataSource } from './DataSource'
|
||||||
|
|
@ -73,7 +75,7 @@ export class App {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async config() {
|
async config(socketIO?: Server) {
|
||||||
// Limit is needed to allow sending/receiving base64 encoded string
|
// Limit is needed to allow sending/receiving base64 encoded string
|
||||||
this.app.use(express.json({ limit: '50mb' }))
|
this.app.use(express.json({ limit: '50mb' }))
|
||||||
this.app.use(express.urlencoded({ limit: '50mb', extended: true }))
|
this.app.use(express.urlencoded({ limit: '50mb', extended: true }))
|
||||||
|
|
@ -200,6 +202,30 @@ export class App {
|
||||||
return res.json(results)
|
return res.json(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Check if chatflow valid for streaming
|
||||||
|
this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => {
|
||||||
|
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||||
|
id: req.params.id
|
||||||
|
})
|
||||||
|
if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
|
||||||
|
|
||||||
|
/*** Get Ending Node with Directed Graph ***/
|
||||||
|
const flowData = chatflow.flowData
|
||||||
|
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||||
|
const nodes = parsedFlowData.nodes
|
||||||
|
const edges = parsedFlowData.edges
|
||||||
|
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
||||||
|
const endingNodeId = getEndingNode(nodeDependencies, graph)
|
||||||
|
if (!endingNodeId) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||||
|
const endingNodeData = nodes.find((nd) => nd.id === endingNodeId)?.data
|
||||||
|
if (!endingNodeData) return res.status(500).send(`Ending node must be either a Chain or Agent`)
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
isStreaming: isFlowValidForStream(nodes, endingNodeData)
|
||||||
|
}
|
||||||
|
return res.json(obj)
|
||||||
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// ChatMessage
|
// ChatMessage
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
@ -303,12 +329,12 @@ export class App {
|
||||||
|
|
||||||
// Send input message and get prediction result (External)
|
// Send input message and get prediction result (External)
|
||||||
this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => {
|
this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => {
|
||||||
await this.processPrediction(req, res)
|
await this.processPrediction(req, res, socketIO)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send input message and get prediction result (Internal)
|
// Send input message and get prediction result (Internal)
|
||||||
this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
|
this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
|
||||||
await this.processPrediction(req, res, true)
|
await this.processPrediction(req, res, socketIO, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
@ -464,9 +490,10 @@ export class App {
|
||||||
* Process Prediction
|
* Process Prediction
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
|
* @param {Server} socketIO
|
||||||
* @param {boolean} isInternal
|
* @param {boolean} isInternal
|
||||||
*/
|
*/
|
||||||
async processPrediction(req: Request, res: Response, isInternal = false) {
|
async processPrediction(req: Request, res: Response, socketIO?: Server, isInternal = false) {
|
||||||
try {
|
try {
|
||||||
const chatflowid = req.params.id
|
const chatflowid = req.params.id
|
||||||
let incomingInput: IncomingInput = req.body
|
let incomingInput: IncomingInput = req.body
|
||||||
|
|
@ -482,6 +509,8 @@ export class App {
|
||||||
await this.validateKey(req, res, chatflow)
|
await this.validateKey(req, res, chatflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isStreamValid = false
|
||||||
|
|
||||||
const files = (req.files as any[]) || []
|
const files = (req.files as any[]) || []
|
||||||
|
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
|
|
@ -542,15 +571,16 @@ export class App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
/*** Get chatflows and prepare data ***/
|
||||||
|
const flowData = chatflow.flowData
|
||||||
|
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
||||||
|
const nodes = parsedFlowData.nodes
|
||||||
|
const edges = parsedFlowData.edges
|
||||||
|
|
||||||
if (isRebuildNeeded()) {
|
if (isRebuildNeeded()) {
|
||||||
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
|
||||||
|
isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData)
|
||||||
} else {
|
} else {
|
||||||
/*** Get chatflows and prepare data ***/
|
|
||||||
const flowData = chatflow.flowData
|
|
||||||
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
|
|
||||||
const nodes = parsedFlowData.nodes
|
|
||||||
const edges = parsedFlowData.edges
|
|
||||||
|
|
||||||
/*** Get Ending Node with Directed Graph ***/
|
/*** Get Ending Node with Directed Graph ***/
|
||||||
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
const { graph, nodeDependencies } = constructGraphs(nodes, edges)
|
||||||
const directedGraph = graph
|
const directedGraph = graph
|
||||||
|
|
@ -572,6 +602,8 @@ export class App {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isStreamValid = isFlowValidForStream(nodes, endingNodeData)
|
||||||
|
|
||||||
/*** Get Starting Nodes with Non-Directed Graph ***/
|
/*** Get Starting Nodes with Non-Directed Graph ***/
|
||||||
const constructedObj = constructGraphs(nodes, edges, true)
|
const constructedObj = constructGraphs(nodes, edges, true)
|
||||||
const nonDirectedGraph = constructedObj.graph
|
const nonDirectedGraph = constructedObj.graph
|
||||||
|
|
@ -602,7 +634,13 @@ export class App {
|
||||||
const nodeModule = await import(nodeInstanceFilePath)
|
const nodeModule = await import(nodeInstanceFilePath)
|
||||||
const nodeInstance = new nodeModule.nodeClass()
|
const nodeInstance = new nodeModule.nodeClass()
|
||||||
|
|
||||||
const result = await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
const result = isStreamValid
|
||||||
|
? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
|
||||||
|
chatHistory: incomingInput.history,
|
||||||
|
socketIO,
|
||||||
|
socketIOClientId: incomingInput.socketIOClientId
|
||||||
|
})
|
||||||
|
: await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history })
|
||||||
|
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
}
|
}
|
||||||
|
|
@ -629,8 +667,14 @@ export async function start(): Promise<void> {
|
||||||
const port = parseInt(process.env.PORT || '', 10) || 3000
|
const port = parseInt(process.env.PORT || '', 10) || 3000
|
||||||
const server = http.createServer(serverApp.app)
|
const server = http.createServer(serverApp.app)
|
||||||
|
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
await serverApp.initDatabase()
|
await serverApp.initDatabase()
|
||||||
await serverApp.config()
|
await serverApp.config(io)
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.info(`⚡️[server]: Flowise Server is listening at ${port}`)
|
console.info(`⚡️[server]: Flowise Server is listening at ${port}`)
|
||||||
|
|
|
||||||
|
|
@ -610,3 +610,28 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => {
|
||||||
|
|
||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if flow valid for stream
|
||||||
|
* @param {IReactFlowNode[]} reactFlowNodes
|
||||||
|
* @param {INodeData} endingNodeData
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNodeData: INodeData) => {
|
||||||
|
const streamAvailableLLMs = {
|
||||||
|
'Chat Models': ['azureChatOpenAI', 'chatOpenAI', 'chatAnthropic'],
|
||||||
|
LLMs: ['azureOpenAI', 'openAI']
|
||||||
|
}
|
||||||
|
|
||||||
|
let isChatOrLLMsExist = false
|
||||||
|
for (const flowNode of reactFlowNodes) {
|
||||||
|
const data = flowNode.data
|
||||||
|
if (data.category === 'Chat Models' || data.category === 'LLMs') {
|
||||||
|
isChatOrLLMsExist = true
|
||||||
|
const validLLMs = streamAvailableLLMs[data.category]
|
||||||
|
if (!validLLMs.includes(data.name)) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isChatOrLLMsExist && endingNodeData.category === 'Chains'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,13 @@
|
||||||
"react-router": "~6.3.0",
|
"react-router": "~6.3.0",
|
||||||
"react-router-dom": "~6.3.0",
|
"react-router-dom": "~6.3.0",
|
||||||
"react-simple-code-editor": "^0.11.2",
|
"react-simple-code-editor": "^0.11.2",
|
||||||
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"reactflow": "^11.5.6",
|
"reactflow": "^11.5.6",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
|
"rehype-mathjax": "^4.0.2",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"socket.io-client": "^4.6.1",
|
||||||
"yup": "^0.32.9"
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ 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}`)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAllChatflows,
|
getAllChatflows,
|
||||||
getSpecificChatflow,
|
getSpecificChatflow,
|
||||||
createNewChatflow,
|
createNewChatflow,
|
||||||
updateChatflow,
|
updateChatflow,
|
||||||
deleteChatflow
|
deleteChatflow,
|
||||||
|
getIsChatflowStreaming
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,39 @@
|
||||||
export default function componentStyleOverrides(theme) {
|
export default function componentStyleOverrides(theme) {
|
||||||
const bgColor = theme.colors?.grey50
|
const bgColor = theme.colors?.grey50
|
||||||
return {
|
return {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
body: {
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: theme?.customization?.isDarkMode
|
||||||
|
? `${theme.colors?.grey500} ${theme.colors?.darkPrimaryMain}`
|
||||||
|
: `${theme.colors?.grey300} ${theme.paper}`,
|
||||||
|
'&::-webkit-scrollbar, & *::-webkit-scrollbar': {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb': {
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.grey500 : theme.colors?.grey300,
|
||||||
|
minHeight: 24,
|
||||||
|
border: `3px solid ${theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper}`
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:focus, & *::-webkit-scrollbar-thumb:focus': {
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:active, & *::-webkit-scrollbar-thumb:active': {
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:hover, & *::-webkit-scrollbar-thumb:hover': {
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.grey500
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-corner, & *::-webkit-scrollbar-corner': {
|
||||||
|
backgroundColor: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryMain : theme.paper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiButton: {
|
MuiButton: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ export default function themePalette(theme) {
|
||||||
return {
|
return {
|
||||||
mode: theme?.customization?.navType,
|
mode: theme?.customization?.navType,
|
||||||
common: {
|
common: {
|
||||||
black: theme.colors?.darkPaper
|
black: theme.colors?.darkPaper,
|
||||||
|
dark: theme.colors?.darkPrimaryMain
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
|
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
|
||||||
import useConfirm from 'hooks/useConfirm'
|
import useConfirm from 'hooks/useConfirm'
|
||||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
|
||||||
|
|
@ -20,9 +20,7 @@ const ConfirmDialog = () => {
|
||||||
{confirmState.title}
|
{confirmState.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ color: 'black' }} id='alert-dialog-description'>
|
<span>{confirmState.description}</span>
|
||||||
{confirmState.description}
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
|
<Button onClick={onCancel}>{confirmState.cancelButtonName}</Button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { IconClipboard, IconDownload } from '@tabler/icons'
|
||||||
|
import { memo, useState } from 'react'
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
|
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Box, IconButton, Popover, Typography } from '@mui/material'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
const programmingLanguages = {
|
||||||
|
javascript: '.js',
|
||||||
|
python: '.py',
|
||||||
|
java: '.java',
|
||||||
|
c: '.c',
|
||||||
|
cpp: '.cpp',
|
||||||
|
'c++': '.cpp',
|
||||||
|
'c#': '.cs',
|
||||||
|
ruby: '.rb',
|
||||||
|
php: '.php',
|
||||||
|
swift: '.swift',
|
||||||
|
'objective-c': '.m',
|
||||||
|
kotlin: '.kt',
|
||||||
|
typescript: '.ts',
|
||||||
|
go: '.go',
|
||||||
|
perl: '.pl',
|
||||||
|
rust: '.rs',
|
||||||
|
scala: '.scala',
|
||||||
|
haskell: '.hs',
|
||||||
|
lua: '.lua',
|
||||||
|
shell: '.sh',
|
||||||
|
sql: '.sql',
|
||||||
|
html: '.html',
|
||||||
|
css: '.css'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeBlock = memo(({ language, chatflowid, isDialog, value }) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
|
const openPopOver = Boolean(anchorEl)
|
||||||
|
|
||||||
|
const handleClosePopOver = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = (event) => {
|
||||||
|
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClosePopOver()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadAsFile = () => {
|
||||||
|
const fileExtension = programmingLanguages[language] || '.file'
|
||||||
|
const suggestedFileName = `file-${chatflowid}${fileExtension}`
|
||||||
|
const fileName = suggestedFileName
|
||||||
|
|
||||||
|
if (!fileName) {
|
||||||
|
// user pressed cancel on prompt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([value], { type: 'text/plain' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.download = fileName
|
||||||
|
link.href = url
|
||||||
|
link.style.display = 'none'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: isDialog ? '' : 300 }}>
|
||||||
|
<Box sx={{ color: 'white', background: theme.palette?.common.dark, p: 1, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
{language}
|
||||||
|
<div style={{ flex: 1 }}></div>
|
||||||
|
<IconButton size='small' title='Copy' color='success' onClick={copyToClipboard}>
|
||||||
|
<IconClipboard />
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
open={openPopOver}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClosePopOver}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' sx={{ pl: 1, pr: 1, color: 'white', background: theme.palette.success.dark }}>
|
||||||
|
Copied!
|
||||||
|
</Typography>
|
||||||
|
</Popover>
|
||||||
|
<IconButton size='small' title='Download' color='primary' onClick={downloadAsFile}>
|
||||||
|
<IconDownload />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0 }}>
|
||||||
|
{value}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
CodeBlock.displayName = 'CodeBlock'
|
||||||
|
|
||||||
|
CodeBlock.propTypes = {
|
||||||
|
language: PropTypes.string,
|
||||||
|
chatflowid: PropTypes.string,
|
||||||
|
isDialog: PropTypes.bool,
|
||||||
|
value: PropTypes.string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { memo } from 'react'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
|
||||||
|
export const MemoizedReactMarkdown = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children)
|
||||||
|
|
@ -314,3 +314,23 @@ export const rearrangeToolsOrdering = (newValues, sourceNodeId) => {
|
||||||
|
|
||||||
newValues.sort((a, b) => sortKey(a) - sortKey(b))
|
newValues.sort((a, b) => sortKey(a) - sortKey(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const throttle = (func, limit) => {
|
||||||
|
let lastFunc
|
||||||
|
let lastRan
|
||||||
|
|
||||||
|
return (...args) => {
|
||||||
|
if (!lastRan) {
|
||||||
|
func(...args)
|
||||||
|
lastRan = Date.now()
|
||||||
|
} else {
|
||||||
|
clearTimeout(lastFunc)
|
||||||
|
lastFunc = setTimeout(() => {
|
||||||
|
if (Date.now() - lastRan >= limit) {
|
||||||
|
func(...args)
|
||||||
|
lastRan = Date.now()
|
||||||
|
}
|
||||||
|
}, limit - (Date.now() - lastRan))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import ButtonEdge from './ButtonEdge'
|
||||||
import CanvasHeader from './CanvasHeader'
|
import CanvasHeader from './CanvasHeader'
|
||||||
import AddNodes from './AddNodes'
|
import AddNodes from './AddNodes'
|
||||||
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
|
||||||
import { ChatMessage } from 'views/chatmessage/ChatMessage'
|
import { ChatPopUp } from 'views/chatmessage/ChatPopUp'
|
||||||
import { flowContext } from 'store/context/ReactFlowContext'
|
import { flowContext } from 'store/context/ReactFlowContext'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
|
@ -514,7 +514,7 @@ const Canvas = () => {
|
||||||
/>
|
/>
|
||||||
<Background color='#aaa' gap={16} />
|
<Background color='#aaa' gap={16} />
|
||||||
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
|
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
|
||||||
<ChatMessage chatflowid={chatflowId} />
|
<ChatPopUp chatflowid={chatflowId} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import { Dialog, DialogContent, DialogTitle, Button } from '@mui/material'
|
||||||
|
import { ChatMessage } from './ChatMessage'
|
||||||
|
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||||
|
import { IconEraser } from '@tabler/icons'
|
||||||
|
|
||||||
|
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
|
||||||
|
const portalElement = document.getElementById('portal')
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
|
const component = show ? (
|
||||||
|
<Dialog
|
||||||
|
open={show}
|
||||||
|
fullWidth
|
||||||
|
maxWidth='md'
|
||||||
|
onClose={onCancel}
|
||||||
|
aria-labelledby='alert-dialog-title'
|
||||||
|
aria-describedby='alert-dialog-description'
|
||||||
|
sx={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
{dialogProps.title}
|
||||||
|
<div style={{ flex: 1 }}></div>
|
||||||
|
{customization.isDarkMode && (
|
||||||
|
<StyledButton
|
||||||
|
variant='outlined'
|
||||||
|
color='error'
|
||||||
|
title='Clear Conversation'
|
||||||
|
onClick={onClear}
|
||||||
|
startIcon={<IconEraser />}
|
||||||
|
>
|
||||||
|
Clear Chat
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
{!customization.isDarkMode && (
|
||||||
|
<Button variant='outlined' color='error' title='Clear Conversation' onClick={onClear} startIcon={<IconEraser />}>
|
||||||
|
Clear Chat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<ChatMessage isDialog={true} open={dialogProps.open} chatflowid={dialogProps.chatflowid} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatExpandDialog.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
dialogProps: PropTypes.object,
|
||||||
|
onClear: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatExpandDialog
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownanswer code {
|
.markdownanswer code {
|
||||||
color: #15cb19;
|
color: #0ab126;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
|
|
||||||
.boticon,
|
.boticon,
|
||||||
.usericon {
|
.usericon {
|
||||||
|
margin-top: 1rem;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -119,3 +120,12 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cloud-dialog {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 230px);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,38 @@
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import ReactMarkdown from 'react-markdown'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
import socketIOClient from 'socket.io-client'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import rehypeMathjax from 'rehype-mathjax'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
|
||||||
import {
|
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box } from '@mui/material'
|
||||||
ClickAwayListener,
|
|
||||||
Paper,
|
|
||||||
Popper,
|
|
||||||
CircularProgress,
|
|
||||||
OutlinedInput,
|
|
||||||
Divider,
|
|
||||||
InputAdornment,
|
|
||||||
IconButton,
|
|
||||||
Box,
|
|
||||||
Button
|
|
||||||
} from '@mui/material'
|
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
import { IconMessage, IconX, IconSend, IconEraser } from '@tabler/icons'
|
import { IconSend } from '@tabler/icons'
|
||||||
|
|
||||||
// project import
|
// project import
|
||||||
import { StyledFab } from 'ui-component/button/StyledFab'
|
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||||
import MainCard from 'ui-component/cards/MainCard'
|
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||||
import Transitions from 'ui-component/extended/Transitions'
|
|
||||||
import './ChatMessage.css'
|
import './ChatMessage.css'
|
||||||
|
|
||||||
// api
|
// api
|
||||||
import chatmessageApi from 'api/chatmessage'
|
import chatmessageApi from 'api/chatmessage'
|
||||||
|
import chatflowsApi from 'api/chatflows'
|
||||||
import predictionApi from 'api/prediction'
|
import predictionApi from 'api/prediction'
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import useApi from 'hooks/useApi'
|
import useApi from 'hooks/useApi'
|
||||||
import useConfirm from 'hooks/useConfirm'
|
|
||||||
import useNotifier from 'utils/useNotifier'
|
|
||||||
|
|
||||||
import { maxScroll } from 'store/constant'
|
// Const
|
||||||
|
import { baseURL, maxScroll } from 'store/constant'
|
||||||
|
|
||||||
export const ChatMessage = ({ chatflowid }) => {
|
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const customization = useSelector((state) => state.customization)
|
const customization = useSelector((state) => state.customization)
|
||||||
const { confirm } = useConfirm()
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
const ps = useRef()
|
const ps = useRef()
|
||||||
|
|
||||||
useNotifier()
|
|
||||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
|
||||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [userInput, setUserInput] = useState('')
|
const [userInput, setUserInput] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [messages, setMessages] = useState([
|
const [messages, setMessages] = useState([
|
||||||
|
|
@ -56,72 +41,21 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
type: 'apiMessage'
|
type: 'apiMessage'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
const [socketIOClientId, setSocketIOClientId] = useState('')
|
||||||
|
const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false)
|
||||||
|
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const anchorRef = useRef(null)
|
|
||||||
const prevOpen = useRef(open)
|
|
||||||
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
|
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
|
||||||
|
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
|
||||||
const handleClose = (event) => {
|
|
||||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setOpen((prevOpen) => !prevOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearChat = async () => {
|
|
||||||
const confirmPayload = {
|
|
||||||
title: `Clear Chat History`,
|
|
||||||
description: `Are you sure you want to clear all chat history?`,
|
|
||||||
confirmButtonName: 'Clear',
|
|
||||||
cancelButtonName: 'Cancel'
|
|
||||||
}
|
|
||||||
const isConfirmed = await confirm(confirmPayload)
|
|
||||||
|
|
||||||
if (isConfirmed) {
|
|
||||||
try {
|
|
||||||
await chatmessageApi.deleteChatmessage(chatflowid)
|
|
||||||
enqueueSnackbar({
|
|
||||||
message: 'Succesfully cleared all chat history',
|
|
||||||
options: {
|
|
||||||
key: new Date().getTime() + Math.random(),
|
|
||||||
variant: 'success',
|
|
||||||
action: (key) => (
|
|
||||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
|
||||||
<IconX />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
|
||||||
enqueueSnackbar({
|
|
||||||
message: errorData,
|
|
||||||
options: {
|
|
||||||
key: new Date().getTime() + Math.random(),
|
|
||||||
variant: 'error',
|
|
||||||
persist: true,
|
|
||||||
action: (key) => (
|
|
||||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
|
||||||
<IconX />
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (ps.current) {
|
if (ps.current) {
|
||||||
ps.current.scrollTo({ top: maxScroll, behavior: 'smooth' })
|
ps.current.scrollTo({ top: maxScroll })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput])
|
||||||
|
|
||||||
const addChatMessage = async (message, type) => {
|
const addChatMessage = async (message, type) => {
|
||||||
try {
|
try {
|
||||||
const newChatMessageBody = {
|
const newChatMessageBody = {
|
||||||
|
|
@ -135,6 +69,15 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateLastMessage = (text) => {
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
let allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||||
|
allMessages[allMessages.length - 1].message += text
|
||||||
|
return allMessages
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
|
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
|
||||||
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
|
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
|
||||||
|
|
@ -143,7 +86,7 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setUserInput('')
|
setUserInput('')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current.focus()
|
inputRef.current?.focus()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,18 +104,22 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
|
|
||||||
// Send user question and history to API
|
// Send user question and history to API
|
||||||
try {
|
try {
|
||||||
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, {
|
const params = {
|
||||||
question: userInput,
|
question: userInput,
|
||||||
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?')
|
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?')
|
||||||
})
|
}
|
||||||
|
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
|
||||||
|
|
||||||
|
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
|
if (!isChatFlowAvailableToStream) setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
|
||||||
addChatMessage(data, 'apiMessage')
|
addChatMessage(data, 'apiMessage')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setUserInput('')
|
setUserInput('')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current.focus()
|
inputRef.current?.focus()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
@ -210,22 +157,47 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [getChatmessageApi.data])
|
}, [getChatmessageApi.data])
|
||||||
|
|
||||||
|
// Get chatflow streaming capability
|
||||||
|
useEffect(() => {
|
||||||
|
if (getIsChatflowStreamingApi.data) {
|
||||||
|
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [getIsChatflowStreamingApi.data])
|
||||||
|
|
||||||
// Auto scroll chat to bottom
|
// Auto scroll chat to bottom
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevOpen.current === true && open === false) {
|
if (isDialog && inputRef) {
|
||||||
anchorRef.current.focus()
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
}, [isDialog, inputRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let socket
|
||||||
if (open && chatflowid) {
|
if (open && chatflowid) {
|
||||||
getChatmessageApi.request(chatflowid)
|
getChatmessageApi.request(chatflowid)
|
||||||
|
getIsChatflowStreamingApi.request(chatflowid)
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
|
||||||
|
|
||||||
prevOpen.current = open
|
socket = socketIOClient(baseURL)
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setSocketIOClientId(socket.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('start', () => {
|
||||||
|
setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('token', updateLastMessage)
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setUserInput('')
|
setUserInput('')
|
||||||
|
|
@ -236,6 +208,10 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
type: 'apiMessage'
|
type: 'apiMessage'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect()
|
||||||
|
setSocketIOClientId('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -243,151 +219,121 @@ export const ChatMessage = ({ chatflowid }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledFab
|
<div className={isDialog ? 'cloud-dialog' : 'cloud'}>
|
||||||
sx={{ position: 'absolute', right: 20, top: 20 }}
|
<div ref={ps} className='messagelist'>
|
||||||
ref={anchorRef}
|
{messages &&
|
||||||
size='small'
|
messages.map((message, index) => {
|
||||||
color='secondary'
|
return (
|
||||||
aria-label='chat'
|
// The latest message sent by the user will be animated while waiting for a response
|
||||||
title='Chat'
|
<Box
|
||||||
onClick={handleToggle}
|
sx={{
|
||||||
>
|
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
|
||||||
{open ? <IconX /> : <IconMessage />}
|
}}
|
||||||
</StyledFab>
|
key={index}
|
||||||
{open && (
|
style={{ display: 'flex' }}
|
||||||
<StyledFab
|
className={
|
||||||
sx={{ position: 'absolute', right: 80, top: 20 }}
|
message.type === 'userMessage' && loading && index === messages.length - 1
|
||||||
onClick={clearChat}
|
? customization.isDarkMode
|
||||||
size='small'
|
? 'usermessagewaiting-dark'
|
||||||
color='error'
|
: 'usermessagewaiting-light'
|
||||||
aria-label='clear'
|
: message.type === 'usermessagewaiting'
|
||||||
title='Clear Chat History'
|
? 'apimessage'
|
||||||
>
|
: 'usermessage'
|
||||||
<IconEraser />
|
}
|
||||||
</StyledFab>
|
>
|
||||||
)}
|
{/* Display the correct icon depending on the message type */}
|
||||||
<Popper
|
{message.type === 'apiMessage' ? (
|
||||||
placement='bottom-end'
|
<img
|
||||||
open={open}
|
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
|
||||||
anchorEl={anchorRef.current}
|
alt='AI'
|
||||||
role={undefined}
|
width='30'
|
||||||
transition
|
height='30'
|
||||||
disablePortal
|
className='boticon'
|
||||||
popperOptions={{
|
/>
|
||||||
modifiers: [
|
) : (
|
||||||
{
|
<img
|
||||||
name: 'offset',
|
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
|
||||||
options: {
|
alt='Me'
|
||||||
offset: [40, 14]
|
width='30'
|
||||||
|
height='30'
|
||||||
|
className='usericon'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='markdownanswer'>
|
||||||
|
{/* Messages are being rendered in Markdown format */}
|
||||||
|
<MemoizedReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
|
rehypePlugins={[rehypeMathjax]}
|
||||||
|
components={{
|
||||||
|
code({ inline, className, children, ...props }) {
|
||||||
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
|
return !inline ? (
|
||||||
|
<CodeBlock
|
||||||
|
key={Math.random()}
|
||||||
|
chatflowid={chatflowid}
|
||||||
|
isDialog={isDialog}
|
||||||
|
language={(match && match[1]) || ''}
|
||||||
|
value={String(children).replace(/\n$/, '')}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.message}
|
||||||
|
</MemoizedReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div className='center'>
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
|
||||||
|
<OutlinedInput
|
||||||
|
inputRef={inputRef}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
autoFocus
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
disabled={loading || !chatflowid}
|
||||||
|
onKeyDown={handleEnter}
|
||||||
|
id='userInput'
|
||||||
|
name='userInput'
|
||||||
|
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
|
||||||
|
value={userInput}
|
||||||
|
onChange={onChange}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position='end'>
|
||||||
|
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
|
||||||
|
{loading ? (
|
||||||
|
<div>
|
||||||
|
<CircularProgress color='inherit' size={20} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Send icon SVG in input field
|
||||||
|
<IconSend
|
||||||
|
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
]
|
</form>
|
||||||
}}
|
</div>
|
||||||
sx={{ zIndex: 1000 }}
|
</div>
|
||||||
>
|
|
||||||
{({ TransitionProps }) => (
|
|
||||||
<Transitions in={open} {...TransitionProps}>
|
|
||||||
<Paper>
|
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
|
||||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
|
||||||
<div className='cloud'>
|
|
||||||
<div ref={ps} className='messagelist'>
|
|
||||||
{messages.map((message, index) => {
|
|
||||||
return (
|
|
||||||
// The latest message sent by the user will be animated while waiting for a response
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
|
|
||||||
}}
|
|
||||||
key={index}
|
|
||||||
style={{ display: 'flex', alignItems: 'center' }}
|
|
||||||
className={
|
|
||||||
message.type === 'userMessage' && loading && index === messages.length - 1
|
|
||||||
? customization.isDarkMode
|
|
||||||
? 'usermessagewaiting-dark'
|
|
||||||
: 'usermessagewaiting-light'
|
|
||||||
: message.type === 'usermessagewaiting'
|
|
||||||
? 'apimessage'
|
|
||||||
: 'usermessage'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Display the correct icon depending on the message type */}
|
|
||||||
{message.type === 'apiMessage' ? (
|
|
||||||
<img
|
|
||||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
|
|
||||||
alt='AI'
|
|
||||||
width='30'
|
|
||||||
height='30'
|
|
||||||
className='boticon'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
|
|
||||||
alt='Me'
|
|
||||||
width='30'
|
|
||||||
height='30'
|
|
||||||
className='usericon'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className='markdownanswer'>
|
|
||||||
{/* Messages are being rendered in Markdown format */}
|
|
||||||
<ReactMarkdown linkTarget={'_blank'}>{message.message}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
<div className='center'>
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
|
|
||||||
<OutlinedInput
|
|
||||||
inputRef={inputRef}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
autoFocus
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
disabled={loading || !chatflowid}
|
|
||||||
onKeyDown={handleEnter}
|
|
||||||
id='userInput'
|
|
||||||
name='userInput'
|
|
||||||
placeholder={loading ? 'Waiting for response...' : 'Type your question...'}
|
|
||||||
value={userInput}
|
|
||||||
onChange={(e) => setUserInput(e.target.value)}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
|
|
||||||
{loading ? (
|
|
||||||
<div>
|
|
||||||
<CircularProgress color='inherit' size={20} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Send icon SVG in input field
|
|
||||||
<IconSend
|
|
||||||
color={
|
|
||||||
loading || !chatflowid
|
|
||||||
? '#9e9e9e'
|
|
||||||
: customization.isDarkMode
|
|
||||||
? 'white'
|
|
||||||
: '#1e88e5'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MainCard>
|
|
||||||
</ClickAwayListener>
|
|
||||||
</Paper>
|
|
||||||
</Transitions>
|
|
||||||
)}
|
|
||||||
</Popper>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMessage.propTypes = { chatflowid: PropTypes.string }
|
ChatMessage.propTypes = {
|
||||||
|
open: PropTypes.bool,
|
||||||
|
chatflowid: PropTypes.string,
|
||||||
|
isDialog: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import { ClickAwayListener, Paper, Popper, Button } from '@mui/material'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import { IconMessage, IconX, IconEraser, IconArrowsMaximize } from '@tabler/icons'
|
||||||
|
|
||||||
|
// project import
|
||||||
|
import { StyledFab } from 'ui-component/button/StyledFab'
|
||||||
|
import MainCard from 'ui-component/cards/MainCard'
|
||||||
|
import Transitions from 'ui-component/extended/Transitions'
|
||||||
|
import { ChatMessage } from './ChatMessage'
|
||||||
|
import ChatExpandDialog from './ChatExpandDialog'
|
||||||
|
|
||||||
|
// api
|
||||||
|
import chatmessageApi from 'api/chatmessage'
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import useConfirm from 'hooks/useConfirm'
|
||||||
|
import useNotifier from 'utils/useNotifier'
|
||||||
|
|
||||||
|
// Const
|
||||||
|
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
|
||||||
|
|
||||||
|
export const ChatPopUp = ({ chatflowid }) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const { confirm } = useConfirm()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
useNotifier()
|
||||||
|
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||||
|
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [showExpandDialog, setShowExpandDialog] = useState(false)
|
||||||
|
const [expandDialogProps, setExpandDialogProps] = useState({})
|
||||||
|
|
||||||
|
const anchorRef = useRef(null)
|
||||||
|
const prevOpen = useRef(open)
|
||||||
|
|
||||||
|
const handleClose = (event) => {
|
||||||
|
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandChat = () => {
|
||||||
|
const props = {
|
||||||
|
open: true,
|
||||||
|
chatflowid: chatflowid
|
||||||
|
}
|
||||||
|
setExpandDialogProps(props)
|
||||||
|
setShowExpandDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetChatDialog = () => {
|
||||||
|
const props = {
|
||||||
|
...expandDialogProps,
|
||||||
|
open: false
|
||||||
|
}
|
||||||
|
setExpandDialogProps(props)
|
||||||
|
setTimeout(() => {
|
||||||
|
const resetProps = {
|
||||||
|
...expandDialogProps,
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
setExpandDialogProps(resetProps)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearChat = async () => {
|
||||||
|
const confirmPayload = {
|
||||||
|
title: `Clear Chat History`,
|
||||||
|
description: `Are you sure you want to clear all chat history?`,
|
||||||
|
confirmButtonName: 'Clear',
|
||||||
|
cancelButtonName: 'Cancel'
|
||||||
|
}
|
||||||
|
const isConfirmed = await confirm(confirmPayload)
|
||||||
|
|
||||||
|
if (isConfirmed) {
|
||||||
|
try {
|
||||||
|
await chatmessageApi.deleteChatmessage(chatflowid)
|
||||||
|
resetChatDialog()
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'Succesfully cleared all chat history',
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'success',
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: errorData,
|
||||||
|
options: {
|
||||||
|
key: new Date().getTime() + Math.random(),
|
||||||
|
variant: 'error',
|
||||||
|
persist: true,
|
||||||
|
action: (key) => (
|
||||||
|
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||||
|
<IconX />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevOpen.current === true && open === false) {
|
||||||
|
anchorRef.current.focus()
|
||||||
|
}
|
||||||
|
prevOpen.current = open
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, chatflowid])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledFab
|
||||||
|
sx={{ position: 'absolute', right: 20, top: 20 }}
|
||||||
|
ref={anchorRef}
|
||||||
|
size='small'
|
||||||
|
color='secondary'
|
||||||
|
aria-label='chat'
|
||||||
|
title='Chat'
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
{open ? <IconX /> : <IconMessage />}
|
||||||
|
</StyledFab>
|
||||||
|
{open && (
|
||||||
|
<StyledFab
|
||||||
|
sx={{ position: 'absolute', right: 80, top: 20 }}
|
||||||
|
onClick={clearChat}
|
||||||
|
size='small'
|
||||||
|
color='error'
|
||||||
|
aria-label='clear'
|
||||||
|
title='Clear Chat History'
|
||||||
|
>
|
||||||
|
<IconEraser />
|
||||||
|
</StyledFab>
|
||||||
|
)}
|
||||||
|
{open && (
|
||||||
|
<StyledFab
|
||||||
|
sx={{ position: 'absolute', right: 140, top: 20 }}
|
||||||
|
onClick={expandChat}
|
||||||
|
size='small'
|
||||||
|
color='primary'
|
||||||
|
aria-label='expand'
|
||||||
|
title='Expand Chat'
|
||||||
|
>
|
||||||
|
<IconArrowsMaximize />
|
||||||
|
</StyledFab>
|
||||||
|
)}
|
||||||
|
<Popper
|
||||||
|
placement='bottom-end'
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
popperOptions={{
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [40, 14]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
sx={{ zIndex: 1000 }}
|
||||||
|
>
|
||||||
|
{({ TransitionProps }) => (
|
||||||
|
<Transitions in={open} {...TransitionProps}>
|
||||||
|
<Paper>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||||
|
<ChatMessage chatflowid={chatflowid} open={open} />
|
||||||
|
</MainCard>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Transitions>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
<ChatExpandDialog
|
||||||
|
show={showExpandDialog}
|
||||||
|
dialogProps={expandDialogProps}
|
||||||
|
onClear={clearChat}
|
||||||
|
onCancel={() => setShowExpandDialog(false)}
|
||||||
|
></ChatExpandDialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatPopUp.propTypes = { chatflowid: PropTypes.string }
|
||||||
Loading…
Reference in New Issue