Merge branch 'main' into FEATURE/Vision

# Conflicts:
#	packages/server/src/index.ts
#	packages/ui/src/views/chatmessage/ChatMessage.js
This commit is contained in:
vinodkiran
2023-12-15 13:26:49 +05:30
28 changed files with 1734 additions and 45 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "flowise",
"version": "1.4.3",
"version": "1.4.5",
"private": true,
"homepage": "https://flowiseai.com",
"workspaces": [
@@ -8,6 +8,7 @@ import * as path from 'node:path'
import fetch from 'node-fetch'
import { flatten, uniqWith, isEqual } from 'lodash'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { AnalyticHandler } from '../../../src/handler'
class OpenAIAssistant_Agents implements INode {
label: string
@@ -149,6 +150,11 @@ class OpenAIAssistant_Agents implements INode {
const openai = new OpenAI({ apiKey: openAIApiKey })
// Start analytics
const analyticHandlers = new AnalyticHandler(nodeData, options)
await analyticHandlers.init()
const parentIds = await analyticHandlers.onChainStart('OpenAIAssistant', input)
try {
const assistantDetails = JSON.parse(assistant.details)
const openAIAssistantId = assistantDetails.id
@@ -171,7 +177,8 @@ class OpenAIAssistant_Agents implements INode {
}
const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
chatId: options.chatId
chatId: options.chatId,
chatflowid: options.chatflowid
})
let threadId = ''
@@ -185,7 +192,7 @@ class OpenAIAssistant_Agents implements INode {
threadId = thread.id
}
// List all runs
// List all runs, in case existing thread is still running
if (!isNewThread) {
const promise = (threadId: string) => {
return new Promise<void>((resolve) => {
@@ -221,6 +228,7 @@ class OpenAIAssistant_Agents implements INode {
})
// Run assistant thread
const llmIds = await analyticHandlers.onLLMStart('ChatOpenAI', input, parentIds)
const runThread = await openai.beta.threads.runs.create(threadId, {
assistant_id: retrievedAssistant.id
})
@@ -253,7 +261,15 @@ class OpenAIAssistant_Agents implements INode {
for (let i = 0; i < actions.length; i += 1) {
const tool = tools.find((tool: any) => tool.name === actions[i].tool)
if (!tool) continue
// Start tool analytics
const toolIds = await analyticHandlers.onToolStart(tool.name, actions[i].toolInput, parentIds)
const toolOutput = await tool.call(actions[i].toolInput)
// End tool analytics
await analyticHandlers.onToolEnd(toolIds, toolOutput)
submitToolOutputs.push({
tool_call_id: actions[i].toolCallId,
output: toolOutput
@@ -302,7 +318,9 @@ class OpenAIAssistant_Agents implements INode {
runThreadId = newRunThread.id
state = await promise(threadId, newRunThread.id)
} else {
throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`)
const errMsg = `Error processing thread: ${state}, Thread ID: ${threadId}`
await analyticHandlers.onChainError(parentIds, errMsg)
throw new Error(errMsg)
}
}
@@ -387,11 +405,18 @@ class OpenAIAssistant_Agents implements INode {
const bitmap = fsDefault.readFileSync(filePath)
const base64String = Buffer.from(bitmap).toString('base64')
// TODO: Use a file path and retrieve image on the fly. Storing as base64 to localStorage and database will easily hit limits
const imgHTML = `<img src="data:image/png;base64,${base64String}" width="100%" height="max-content" alt="${fileObj.filename}" /><br/>`
returnVal += imgHTML
}
}
const imageRegex = /<img[^>]*\/>/g
let llmOutput = returnVal.replace(imageRegex, '')
llmOutput = llmOutput.replace('<br/>', '')
await analyticHandlers.onLLMEnd(llmIds, llmOutput)
await analyticHandlers.onChainEnd(parentIds, messageData, true)
return {
text: returnVal,
usedTools,
@@ -399,6 +424,7 @@ class OpenAIAssistant_Agents implements INode {
assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData }
}
} catch (error) {
await analyticHandlers.onChainError(parentIds, error, true)
throw new Error(error)
}
}
+1 -1
View File
@@ -89,7 +89,7 @@ class RedisCache implements INode {
redisClient.update = async (prompt: string, llmKey: string, value: Generation[]) => {
for (let i = 0; i < value.length; i += 1) {
const key = getCacheKey(prompt, llmKey, String(i))
if (ttl !== undefined) {
if (ttl) {
await client.set(key, JSON.stringify(serializeGeneration(value[i])), 'EX', parseInt(ttl, 10))
} else {
await client.set(key, JSON.stringify(serializeGeneration(value[i])))
@@ -162,8 +162,11 @@ class S3_DocumentLoaders implements INode {
accessKeyId?: string
secretAccessKey?: string
} = {
accessKeyId,
secretAccessKey
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
loader.load = async () => {
@@ -18,7 +18,7 @@ class HydeRetriever_Retrievers implements INode {
constructor() {
this.label = 'Hyde Retriever'
this.name = 'HydeRetriever'
this.version = 1.0
this.version = 2.0
this.type = 'HydeRetriever'
this.icon = 'hyderetriever.svg'
this.category = 'Retrievers'
@@ -36,41 +36,66 @@ class HydeRetriever_Retrievers implements INode {
type: 'VectorStore'
},
{
label: 'Prompt Key',
label: 'Select Defined Prompt',
name: 'promptKey',
description: 'Select a pre-defined prompt',
type: 'options',
options: [
{
label: 'websearch',
name: 'websearch'
name: 'websearch',
description: `Please write a passage to answer the question
Question: {question}
Passage:`
},
{
label: 'scifact',
name: 'scifact'
name: 'scifact',
description: `Please write a scientific paper passage to support/refute the claim
Claim: {question}
Passage:`
},
{
label: 'arguana',
name: 'arguana'
name: 'arguana',
description: `Please write a counter argument for the passage
Passage: {question}
Counter Argument:`
},
{
label: 'trec-covid',
name: 'trec-covid'
name: 'trec-covid',
description: `Please write a scientific paper passage to answer the question
Question: {question}
Passage:`
},
{
label: 'fiqa',
name: 'fiqa'
name: 'fiqa',
description: `Please write a financial article passage to answer the question
Question: {question}
Passage:`
},
{
label: 'dbpedia-entity',
name: 'dbpedia-entity'
name: 'dbpedia-entity',
description: `Please write a passage to answer the question.
Question: {question}
Passage:`
},
{
label: 'trec-news',
name: 'trec-news'
name: 'trec-news',
description: `Please write a news passage about the topic.
Topic: {question}
Passage:`
},
{
label: 'mr-tydi',
name: 'mr-tydi'
name: 'mr-tydi',
description: `Please write a passage in Swahili/Korean/Japanese/Bengali to answer the question in detail.
Question: {question}
Passage:`
}
],
default: 'websearch'
@@ -78,7 +103,7 @@ class HydeRetriever_Retrievers implements INode {
{
label: 'Custom Prompt',
name: 'customPrompt',
description: 'If custom prompt is used, this will override Prompt Key',
description: 'If custom prompt is used, this will override Defined Prompt',
placeholder: 'Please write a passage to answer the question\nQuestion: {question}\nPassage:',
type: 'string',
rows: 4,
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "flowise-components",
"version": "1.4.5",
"version": "1.4.6",
"description": "Flowiseai Components",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",
@@ -36,7 +36,7 @@
"@upstash/redis": "^1.22.1",
"@zilliz/milvus2-sdk-node": "^2.2.24",
"apify-client": "^2.7.1",
"axios": "^0.27.2",
"axios": "1.6.2",
"cheerio": "^1.0.0-rc.12",
"chromadb": "^1.5.11",
"cohere-ai": "^6.2.0",
@@ -51,8 +51,9 @@
"husky": "^8.0.3",
"ioredis": "^5.3.2",
"langchain": "^0.0.196",
"langfuse": "^1.2.0",
"langfuse-langchain": "^1.0.31",
"langsmith": "^0.0.32",
"langsmith": "^0.0.49",
"linkifyjs": "^4.1.1",
"llmonitor": "^0.5.5",
"mammoth": "^1.5.1",
+489
View File
@@ -8,6 +8,10 @@ import { LLMonitorHandler } from 'langchain/callbacks/handlers/llmonitor'
import { getCredentialData, getCredentialParam } from './utils'
import { ICommonObject, INodeData } from './Interface'
import CallbackHandler from 'langfuse-langchain'
import { RunTree, RunTreeConfig, Client as LangsmithClient } from 'langsmith'
import { Langfuse, LangfuseTraceClient, LangfuseSpanClient, LangfuseGenerationClient } from 'langfuse'
import monitor from 'llmonitor'
import { v4 as uuidv4 } from 'uuid'
interface AgentRun extends Run {
actions: AgentAction[]
@@ -273,3 +277,488 @@ export const additionalCallbacks = async (nodeData: INodeData, options: ICommonO
throw new Error(e)
}
}
export class AnalyticHandler {
nodeData: INodeData
options: ICommonObject = {}
handlers: ICommonObject = {}
constructor(nodeData: INodeData, options: ICommonObject) {
this.options = options
this.nodeData = nodeData
this.init()
}
async init() {
try {
if (!this.options.analytic) return
const analytic = JSON.parse(this.options.analytic)
for (const provider in analytic) {
const providerStatus = analytic[provider].status as boolean
if (providerStatus) {
const credentialId = analytic[provider].credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', this.options)
if (provider === 'langSmith') {
const langSmithProject = analytic[provider].projectName as string
const langSmithApiKey = getCredentialParam('langSmithApiKey', credentialData, this.nodeData)
const langSmithEndpoint = getCredentialParam('langSmithEndpoint', credentialData, this.nodeData)
const client = new LangsmithClient({
apiUrl: langSmithEndpoint ?? 'https://api.smith.langchain.com',
apiKey: langSmithApiKey
})
this.handlers['langSmith'] = { client, langSmithProject }
} else if (provider === 'langFuse') {
const release = analytic[provider].release as string
const langFuseSecretKey = getCredentialParam('langFuseSecretKey', credentialData, this.nodeData)
const langFusePublicKey = getCredentialParam('langFusePublicKey', credentialData, this.nodeData)
const langFuseEndpoint = getCredentialParam('langFuseEndpoint', credentialData, this.nodeData)
const langfuse = new Langfuse({
secretKey: langFuseSecretKey,
publicKey: langFusePublicKey,
baseUrl: langFuseEndpoint ?? 'https://cloud.langfuse.com',
release
})
this.handlers['langFuse'] = { client: langfuse }
} else if (provider === 'llmonitor') {
const llmonitorAppId = getCredentialParam('llmonitorAppId', credentialData, this.nodeData)
const llmonitorEndpoint = getCredentialParam('llmonitorEndpoint', credentialData, this.nodeData)
monitor.init({
appId: llmonitorAppId,
apiUrl: llmonitorEndpoint
})
this.handlers['llmonitor'] = { client: monitor }
}
}
}
} catch (e) {
throw new Error(e)
}
}
async onChainStart(name: string, input: string, parentIds?: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
langFuse: {},
llmonitor: {}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
if (!parentIds || !Object.keys(parentIds).length) {
const parentRunConfig: RunTreeConfig = {
name,
run_type: 'chain',
inputs: {
text: input
},
serialized: {},
project_name: this.handlers['langSmith'].langSmithProject,
client: this.handlers['langSmith'].client
}
const parentRun = new RunTree(parentRunConfig)
await parentRun.postRun()
this.handlers['langSmith'].chainRun = { [parentRun.id]: parentRun }
returnIds['langSmith'].chainRun = parentRun.id
} else {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
if (parentRun) {
const childChainRun = await parentRun.createChild({
name,
run_type: 'chain',
inputs: {
text: input
}
})
await childChainRun.postRun()
this.handlers['langSmith'].chainRun = { [childChainRun.id]: childChainRun }
returnIds['langSmith'].chainRun = childChainRun.id
}
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
let langfuseTraceClient: LangfuseTraceClient
if (!parentIds || !Object.keys(parentIds).length) {
const langfuse: Langfuse = this.handlers['langFuse'].client
langfuseTraceClient = langfuse.trace({
name,
userId: this.options.chatId,
metadata: { tags: ['openai-assistant'] }
})
} else {
langfuseTraceClient = this.handlers['langFuse'].trace[parentIds['langFuse']]
}
if (langfuseTraceClient) {
const span = langfuseTraceClient.span({
name,
input: {
text: input
}
})
this.handlers['langFuse'].trace = { [langfuseTraceClient.id]: langfuseTraceClient }
this.handlers['langFuse'].span = { [span.id]: span }
returnIds['langFuse'].trace = langfuseTraceClient.id
returnIds['langFuse'].span = span.id
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const monitor = this.handlers['llmonitor'].client
if (monitor) {
const runId = uuidv4()
await monitor.trackEvent('chain', 'start', {
runId,
name,
userId: this.options.chatId,
input
})
this.handlers['llmonitor'].chainEvent = { [runId]: runId }
returnIds['llmonitor'].chainEvent = runId
}
}
return returnIds
}
async onChainEnd(returnIds: ICommonObject, output: string | object, shutdown = false) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun]
if (chainRun) {
await chainRun.end({
outputs: {
output
}
})
await chainRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span]
if (span) {
span.end({
output
})
if (shutdown) {
const langfuse: Langfuse = this.handlers['langFuse'].client
await langfuse.shutdownAsync()
}
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const chainEventId = returnIds['llmonitor'].chainEvent
const monitor = this.handlers['llmonitor'].client
if (monitor && chainEventId) {
await monitor.trackEvent('chain', 'end', {
runId: chainEventId,
output
})
}
}
}
async onChainError(returnIds: ICommonObject, error: string | object, shutdown = false) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const chainRun: RunTree | undefined = this.handlers['langSmith'].chainRun[returnIds['langSmith'].chainRun]
if (chainRun) {
await chainRun.end({
error: {
error
}
})
await chainRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const span: LangfuseSpanClient | undefined = this.handlers['langFuse'].span[returnIds['langFuse'].span]
if (span) {
span.end({
output: {
error
}
})
if (shutdown) {
const langfuse: Langfuse = this.handlers['langFuse'].client
await langfuse.shutdownAsync()
}
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const chainEventId = returnIds['llmonitor'].chainEvent
const monitor = this.handlers['llmonitor'].client
if (monitor && chainEventId) {
await monitor.trackEvent('chain', 'end', {
runId: chainEventId,
output: error
})
}
}
}
async onLLMStart(name: string, input: string, parentIds: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
langFuse: {},
llmonitor: {}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
if (parentRun) {
const childLLMRun = await parentRun.createChild({
name,
run_type: 'llm',
inputs: {
prompts: [input]
}
})
await childLLMRun.postRun()
this.handlers['langSmith'].llmRun = { [childLLMRun.id]: childLLMRun }
returnIds['langSmith'].llmRun = childLLMRun.id
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace]
if (trace) {
const generation = trace.generation({
name,
prompt: input
})
this.handlers['langFuse'].generation = { [generation.id]: generation }
returnIds['langFuse'].generation = generation.id
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const monitor = this.handlers['llmonitor'].client
const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent]
if (monitor && chainEventId) {
const runId = uuidv4()
await monitor.trackEvent('llm', 'start', {
runId,
parentRunId: chainEventId,
name,
userId: this.options.chatId,
input
})
this.handlers['llmonitor'].llmEvent = { [runId]: runId }
returnIds['llmonitor'].llmEvent = runId
}
}
return returnIds
}
async onLLMEnd(returnIds: ICommonObject, output: string) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun]
if (llmRun) {
await llmRun.end({
outputs: {
generations: [output]
}
})
await llmRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation]
if (generation) {
generation.end({
completion: output
})
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent]
const monitor = this.handlers['llmonitor'].client
if (monitor && llmEventId) {
await monitor.trackEvent('llm', 'end', {
runId: llmEventId,
output
})
}
}
}
async onLLMError(returnIds: ICommonObject, error: string | object) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const llmRun: RunTree | undefined = this.handlers['langSmith'].llmRun[returnIds['langSmith'].llmRun]
if (llmRun) {
await llmRun.end({
error: {
error
}
})
await llmRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const generation: LangfuseGenerationClient | undefined = this.handlers['langFuse'].generation[returnIds['langFuse'].generation]
if (generation) {
generation.end({
completion: error
})
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const llmEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].llmEvent]
const monitor = this.handlers['llmonitor'].client
if (monitor && llmEventId) {
await monitor.trackEvent('llm', 'end', {
runId: llmEventId,
output: error
})
}
}
}
async onToolStart(name: string, input: string | object, parentIds: ICommonObject) {
const returnIds: ICommonObject = {
langSmith: {},
langFuse: {},
llmonitor: {}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const parentRun: RunTree | undefined = this.handlers['langSmith'].chainRun[parentIds['langSmith'].chainRun]
if (parentRun) {
const childToolRun = await parentRun.createChild({
name,
run_type: 'tool',
inputs: {
input
}
})
await childToolRun.postRun()
this.handlers['langSmith'].toolRun = { [childToolRun.id]: childToolRun }
returnIds['langSmith'].toolRun = childToolRun.id
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const trace: LangfuseTraceClient | undefined = this.handlers['langFuse'].trace[parentIds['langFuse'].trace]
if (trace) {
const toolSpan = trace.span({
name,
input
})
this.handlers['langFuse'].toolSpan = { [toolSpan.id]: toolSpan }
returnIds['langFuse'].toolSpan = toolSpan.id
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const monitor = this.handlers['llmonitor'].client
const chainEventId: string = this.handlers['llmonitor'].chainEvent[parentIds['llmonitor'].chainEvent]
if (monitor && chainEventId) {
const runId = uuidv4()
await monitor.trackEvent('tool', 'start', {
runId,
parentRunId: chainEventId,
name,
userId: this.options.chatId,
input
})
this.handlers['llmonitor'].toolEvent = { [runId]: runId }
returnIds['llmonitor'].toolEvent = runId
}
}
return returnIds
}
async onToolEnd(returnIds: ICommonObject, output: string | object) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun]
if (toolRun) {
await toolRun.end({
outputs: {
output
}
})
await toolRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan]
if (toolSpan) {
toolSpan.end({
output
})
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const toolEventId: string = this.handlers['llmonitor'].toolEvent[returnIds['llmonitor'].toolEvent]
const monitor = this.handlers['llmonitor'].client
if (monitor && toolEventId) {
await monitor.trackEvent('tool', 'end', {
runId: toolEventId,
output
})
}
}
}
async onToolError(returnIds: ICommonObject, error: string | object) {
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langSmith')) {
const toolRun: RunTree | undefined = this.handlers['langSmith'].toolRun[returnIds['langSmith'].toolRun]
if (toolRun) {
await toolRun.end({
error: {
error
}
})
await toolRun.patchRun()
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'langFuse')) {
const toolSpan: LangfuseSpanClient | undefined = this.handlers['langFuse'].toolSpan[returnIds['langFuse'].toolSpan]
if (toolSpan) {
toolSpan.end({
output: error
})
}
}
if (Object.prototype.hasOwnProperty.call(this.handlers, 'llmonitor')) {
const toolEventId: string = this.handlers['llmonitor'].llmEvent[returnIds['llmonitor'].toolEvent]
const monitor = this.handlers['llmonitor'].client
if (monitor && toolEventId) {
await monitor.trackEvent('tool', 'end', {
runId: toolEventId,
output: error
})
}
}
}
}
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "flowise",
"version": "1.4.3",
"version": "1.4.5",
"description": "Flowiseai Server",
"main": "dist/index",
"types": "dist/index.d.ts",
@@ -47,7 +47,7 @@
"dependencies": {
"@oclif/core": "^1.13.10",
"async-mutex": "^0.4.0",
"axios": "^0.27.2",
"axios": "1.6.2",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.0",
@@ -61,6 +61,7 @@
"mysql": "^2.18.1",
"pg": "^8.11.1",
"reflect-metadata": "^0.1.13",
"sanitize-html": "^2.11.0",
"socket.io": "^4.6.1",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.6",
@@ -71,6 +72,7 @@
"@types/cors": "^2.8.12",
"@types/crypto-js": "^4.1.1",
"@types/multer": "^1.4.7",
"@types/sanitize-html": "^2.9.5",
"concurrently": "^7.1.0",
"nodemon": "^2.0.15",
"oclif": "^3",
+1 -1
View File
@@ -16,7 +16,7 @@ export class ChatflowPool {
* @param {IReactFlowNode[]} startingNodes
* @param {ICommonObject} overrideConfig
*/
add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) {
add(chatflowid: string, endingNodeData: INodeData | undefined, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) {
this.activeChatflows[chatflowid] = {
startingNodes,
endingNodeData,
+1 -1
View File
@@ -174,7 +174,7 @@ export interface IncomingInput {
export interface IActiveChatflows {
[key: string]: {
startingNodes: IReactFlowNode[]
endingNodeData: INodeData
endingNodeData?: INodeData
inSync: boolean
overrideConfig?: ICommonObject
}
+80 -7
View File
@@ -58,6 +58,10 @@ import { CachePool } from './CachePool'
import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components'
import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit'
import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey'
import { sanitizeMiddleware } from './utils/XSS'
import axios from 'axios'
import { Client } from 'langchainhub'
import { parsePrompt } from './utils/hub'
export class App {
app: express.Application
@@ -115,9 +119,15 @@ export class App {
// Allow access from *
this.app.use(cors())
// Switch off the default 'X-Powered-By: Express' header
this.app.disable('x-powered-by')
// Add the expressRequestLogger middleware to log all requests
this.app.use(expressRequestLogger)
// Add the sanitizeMiddleware to guard against XSS
this.app.use(sanitizeMiddleware)
if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) {
const username = process.env.FLOWISE_USERNAME
const password = process.env.FLOWISE_PASSWORD
@@ -128,6 +138,7 @@ export class App {
'/api/v1/verify/apikey/',
'/api/v1/chatflows/apikey/',
'/api/v1/public-chatflows',
'/api/v1/public-chatbotConfig',
'/api/v1/prediction/',
'/api/v1/vector/upsert/',
'/api/v1/node-icon/',
@@ -190,7 +201,7 @@ export class App {
// Get component credential via name
this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => {
if (!req.params.name.includes('&')) {
if (!req.params.name.includes('&amp;')) {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) {
return res.json(this.nodesPool.componentCredentials[req.params.name])
} else {
@@ -198,7 +209,7 @@ export class App {
}
} else {
const returnResponse = []
for (const name of req.params.name.split('&')) {
for (const name of req.params.name.split('&amp;')) {
if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) {
returnResponse.push(this.nodesPool.componentCredentials[name])
} else {
@@ -318,6 +329,23 @@ export class App {
return res.status(404).send(`Chatflow ${req.params.id} not found`)
})
// Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat)
// Safe as public endpoint as chatbotConfig doesn't contain sensitive credential
this.app.get('/api/v1/public-chatbotConfig/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (chatflow && chatflow.chatbotConfig) {
try {
const parsedConfig = JSON.parse(chatflow.chatbotConfig)
return res.json(parsedConfig)
} catch (e) {
return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`)
}
}
return res.status(404).send(`Chatbot Config for Chatflow ${req.params.id} not found`)
})
// Save chatflow
this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => {
const body = req.body
@@ -980,6 +1008,12 @@ export class App {
// Download file from assistant
this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => {
const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
//raise error if file path is not absolute
if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
//raise error if file path contains '..'
if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
//only return from the .flowise openai-assistant folder
if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`)
res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath))
streamFileToUser(res, filePath)
})
@@ -1064,6 +1098,35 @@ export class App {
await this.buildChatflow(req, res, undefined, true, true)
})
// ----------------------------------------
// Prompt from Hub
// ----------------------------------------
this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => {
try {
let hub = new Client()
const prompt = await hub.pull(req.body.promptName)
const templates = parsePrompt(prompt)
return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates })
} catch (e: any) {
return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message })
}
})
this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => {
try {
const tags = req.body.tags ? `tags=${req.body.tags}` : ''
// Default to 100, TODO: add pagination and use offset & limit
const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false`
axios.get(url).then((response) => {
if (response.data.repos) {
return res.json({ status: 'OK', repos: response.data.repos })
}
})
} catch (e: any) {
return res.json({ status: 'ERROR', repos: [] })
}
})
// ----------------------------------------
// Prediction
// ----------------------------------------
@@ -1419,16 +1482,19 @@ export class App {
const nodes = parsedFlowData.nodes
const edges = parsedFlowData.edges
/* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation) when all these conditions met:
/* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met:
* - Node Data already exists in pool
* - Still in sync (i.e the flow has not been modified since)
* - Existing overrideConfig and new overrideConfig are the same
* - Flow doesn't start with/contain nodes that depend on incomingInput.question
* - Its not an Upsert request
* TODO: convert overrideConfig to hash when we no longer store base64 string but filepath
***/
const isFlowReusable = () => {
return (
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
this.chatflowPool.activeChatflows[chatflowid].inSync &&
this.chatflowPool.activeChatflows[chatflowid].endingNodeData &&
isSameOverrideConfig(
isInternal,
this.chatflowPool.activeChatflows[chatflowid].overrideConfig,
@@ -1440,7 +1506,7 @@ export class App {
}
if (isFlowReusable()) {
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData
isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData)
logger.debug(
`[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})`
@@ -1493,6 +1559,7 @@ export class App {
const constructedObj = constructGraphs(nodes, edges, true)
const nonDirectedGraph = constructedObj.graph
const { startingNodeIds, depthQueue } = getStartingNodes(nonDirectedGraph, endingNodeId)
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
logger.debug(`[server]: Start building chatflow ${chatflowid}`)
/*** BFS to traverse from Starting Nodes to Ending Node ***/
@@ -1512,13 +1579,18 @@ export class App {
isUpsert,
incomingInput.stopNodeId
)
if (isUpsert) return res.status(201).send('Successfully Upserted')
if (isUpsert) {
this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig)
return res.status(201).send('Successfully Upserted')
}
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
if (incomingInput.overrideConfig)
if (incomingInput.overrideConfig) {
nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig)
}
const reactFlowNodeData: INodeData = resolveVariables(
nodeToExecute.data,
reactFlowNodes,
@@ -1527,7 +1599,6 @@ export class App {
)
nodeToExecuteData = reactFlowNodeData
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig)
}
@@ -1551,6 +1622,7 @@ export class App {
let result = isStreamValid
? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
uploads: incomingInput.uploads,
chatflowid,
chatHistory,
socketIO,
socketIOClientId: incomingInput.socketIOClientId,
@@ -1561,6 +1633,7 @@ export class App {
chatId
})
: await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
chatflowid,
uploads: incomingInput.uploads,
chatHistory,
logger,
+20
View File
@@ -0,0 +1,20 @@
import { Request, Response, NextFunction } from 'express'
import sanitizeHtml from 'sanitize-html'
export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void {
// decoding is necessary as the url is encoded by the browser
const decodedURI = decodeURI(req.url)
req.url = sanitizeHtml(decodedURI)
for (let p in req.query) {
if (Array.isArray(req.query[p])) {
const sanitizedQ = []
for (const q of req.query[p] as string[]) {
sanitizedQ.push(sanitizeHtml(q))
}
req.query[p] = sanitizedQ
} else {
req.query[p] = sanitizeHtml(req.query[p] as string)
}
}
next()
}
+36
View File
@@ -0,0 +1,36 @@
export function parsePrompt(prompt: string): any[] {
const promptObj = JSON.parse(prompt)
let response = []
if (promptObj.kwargs.messages) {
promptObj.kwargs.messages.forEach((message: any) => {
let messageType = message.id.includes('SystemMessagePromptTemplate')
? 'systemMessagePrompt'
: message.id.includes('HumanMessagePromptTemplate')
? 'humanMessagePrompt'
: message.id.includes('AIMessagePromptTemplate')
? 'aiMessagePrompt'
: 'template'
let messageTypeDisplay = message.id.includes('SystemMessagePromptTemplate')
? 'System Message'
: message.id.includes('HumanMessagePromptTemplate')
? 'Human Message'
: message.id.includes('AIMessagePromptTemplate')
? 'AI Message'
: 'Message'
let template = message.kwargs.prompt.kwargs.template
response.push({
type: messageType,
typeDisplay: messageTypeDisplay,
template: template
})
})
} else if (promptObj.kwargs.template) {
let template = promptObj.kwargs.template
response.push({
type: 'template',
typeDisplay: 'Prompt',
template: template
})
}
return response
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "flowise-ui",
"version": "1.4.1",
"version": "1.4.3",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://flowiseai.com",
"author": {
+9
View File
@@ -0,0 +1,9 @@
import client from './client'
const getAvailablePrompts = (body) => client.post(`/prompts-list`, body)
const getPrompt = (body) => client.post(`/load-prompt`, body)
export default {
getAvailablePrompts,
getPrompt
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

+9 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons'
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage }
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
@@ -11,6 +11,13 @@ const settings = {
title: '',
type: 'group',
children: [
{
id: 'conversationStarters',
title: 'Starter Prompts',
type: 'item',
url: '',
icon: icons.IconPictureInPictureOff
},
{
id: 'viewMessages',
title: 'View Messages',
@@ -11,6 +11,7 @@ import FileCopyIcon from '@mui/icons-material/FileCopy'
import FileDownloadIcon from '@mui/icons-material/Downloading'
import FileDeleteIcon from '@mui/icons-material/Delete'
import FileCategoryIcon from '@mui/icons-material/Category'
import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt'
import Button from '@mui/material/Button'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import { IconX } from '@tabler/icons'
@@ -27,6 +28,7 @@ import TagDialog from '../dialog/TagDialog'
import { generateExportFlowData } from '../../utils/genericHelper'
import useNotifier from '../../utils/useNotifier'
import StarterPromptsDialog from '../dialog/StarterPromptsDialog'
const StyledMenu = styled((props) => (
<Menu
@@ -78,6 +80,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
const [categoryDialogProps, setCategoryDialogProps] = useState({})
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
@@ -92,6 +96,20 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
setFlowDialogOpen(true)
}
const handleFlowStarterPrompts = () => {
setAnchorEl(null)
setConversationStartersDialogProps({
title: 'Starter Prompts - ' + chatflow.name,
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
}
const saveFlowStarterPrompts = async () => {
setConversationStartersDialogOpen(false)
await updateFlowsApi.request()
}
const saveFlowRename = async (chatflowName) => {
const updateBody = {
name: chatflowName,
@@ -253,6 +271,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
Export
</MenuItem>
<Divider sx={{ my: 0.5 }} />
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
<PictureInPictureAltIcon />
Starter Prompts
</MenuItem>
<MenuItem onClick={handleFlowCategory} disableRipple>
<FileCategoryIcon />
Update Category
@@ -279,6 +301,12 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
onClose={() => setCategoryDialogOpen(false)}
onSubmit={saveFlowCategory}
/>
<StarterPromptsDialog
show={conversationStartersDialogOpen}
dialogProps={conversationStartersDialogProps}
onConfirm={saveFlowStarterPrompts}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
</div>
)
}
@@ -0,0 +1,14 @@
.button-container {
position: absolute;
bottom: 0;
z-index: 1000;
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */
scrollbar-width: none; /* For Firefox */
}
.button {
flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */
margin: 5px; /* Adjust as needed for spacing between buttons */
}
@@ -0,0 +1,22 @@
import Box from '@mui/material/Box'
import PropTypes from 'prop-types'
import { Chip } from '@mui/material'
import './StarterPromptsCard.css'
const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => {
return (
<Box className={'button-container'} sx={{ maxWidth: isGrid ? 'inherit' : '400px', m: 1 }}>
{starterPrompts.map((sp, index) => (
<Chip label={sp.prompt} className={'button'} key={index} onClick={(e) => onPromptClick(sp.prompt, e)} />
))}
</Box>
)
}
StarterPromptsCard.propTypes = {
isGrid: PropTypes.bool,
starterPrompts: PropTypes.arrayOf(PropTypes.string),
onPromptClick: PropTypes.func
}
export default StarterPromptsCard
@@ -0,0 +1,587 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
// MUI
import {
Box,
Button,
Card,
CardContent,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Chip,
Grid,
InputLabel,
List,
ListItemButton,
ListItemText,
OutlinedInput,
Select,
Typography,
Stack,
IconButton,
FormControl,
Checkbox,
MenuItem
} from '@mui/material'
import MuiAccordion from '@mui/material/Accordion'
import MuiAccordionSummary from '@mui/material/AccordionSummary'
import MuiAccordionDetails from '@mui/material/AccordionDetails'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'
import ClearIcon from '@mui/icons-material/Clear'
import { styled } from '@mui/material/styles'
//Project Import
import { StyledButton } from 'ui-component/button/StyledButton'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import promptEmptySVG from 'assets/images/prompt_empty.svg'
import useApi from 'hooks/useApi'
import promptApi from 'api/prompt'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const NewLineToBr = ({ children = '' }) => {
return children.split('\n').reduce(function (arr, line) {
return arr.concat(line, <br />)
}, [])
}
const Accordion = styled((props) => <MuiAccordion disableGutters elevation={0} square {...props} />)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
'&:not(:last-child)': {
borderBottom: 0
},
'&:before': {
display: 'none'
}
}))
const AccordionSummary = styled((props) => (
<MuiAccordionSummary expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />} {...props} />
))(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)',
flexDirection: 'row-reverse',
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(180deg)'
},
'& .MuiAccordionSummary-content': {
marginLeft: theme.spacing(1)
}
}))
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: '1px solid rgba(0, 0, 0, .125)'
}))
const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts)
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [show, dispatch])
useEffect(() => {
if (promptType) {
getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [promptType])
useEffect(() => {
if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) {
setAvailablePrompNameList(getAvailablePromptsApi.data.repos)
if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAvailablePromptsApi.data])
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}
const models = [
{ id: 101, name: 'anthropic:claude-instant-1' },
{ id: 102, name: 'anthropic:claude-instant-1.2' },
{ id: 103, name: 'anthropic:claude-2' },
{ id: 104, name: 'google:palm-2-chat-bison' },
{ id: 105, name: 'google:palm-2-codechat-bison' },
{ id: 106, name: 'google:palm-2-text-bison' },
{ id: 107, name: 'meta:llama-2-13b-chat' },
{ id: 108, name: 'meta:llama-2-70b-chat' },
{ id: 109, name: 'openai:gpt-3.5-turbo' },
{ id: 110, name: 'openai:gpt-4' },
{ id: 111, name: 'openai:text-davinci-003' }
]
const [modelName, setModelName] = useState([])
const usecases = [
{ id: 201, name: 'Agents' },
{ id: 202, name: 'Agent Stimulation' },
{ id: 203, name: 'Autonomous agents' },
{ id: 204, name: 'Classification' },
{ id: 205, name: 'Chatbots' },
{ id: 206, name: 'Code understanding' },
{ id: 207, name: 'Code writing' },
{ id: 208, name: 'Evaluation' },
{ id: 209, name: 'Extraction' },
{ id: 210, name: 'Interacting with APIs' },
{ id: 211, name: 'Multi-modal' },
{ id: 212, name: 'QA over documents' },
{ id: 213, name: 'Self-checking' },
{ id: 214, name: 'SQL' },
{ id: 215, name: 'Summarization' },
{ id: 216, name: 'Tagging' }
]
const [usecase, setUsecase] = useState([])
const languages = [
{ id: 301, name: 'Chinese' },
{ id: 302, name: 'English' },
{ id: 303, name: 'French' },
{ id: 304, name: 'German' },
{ id: 305, name: 'Russian' },
{ id: 306, name: 'Spanish' }
]
const [language, setLanguage] = useState([])
const [availablePrompNameList, setAvailablePrompNameList] = useState([])
const [selectedPrompt, setSelectedPrompt] = useState({})
const [accordionExpanded, setAccordionExpanded] = useState(['prompt'])
const handleAccordionChange = (accordionName) => (event, isExpanded) => {
const accordians = [...accordionExpanded]
if (!isExpanded) setAccordionExpanded(accordians.filter((accr) => accr !== accordionName))
else {
accordians.push(accordionName)
setAccordionExpanded(accordians)
}
}
const handleListItemClick = async (index, overridePromptNameList = []) => {
const prompt = overridePromptNameList.length ? overridePromptNameList[index] : availablePrompNameList[index]
if (!prompt.detailed) {
const createResp = await promptApi.getPrompt({
promptName: prompt.full_name
})
if (createResp.data) {
prompt.detailed = createResp.data.templates
}
}
setSelectedPrompt(prompt)
}
const fetchPrompts = async () => {
let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&'
modelName.forEach((item) => {
tags += `tags=${item.name}&`
})
usecase.forEach((item) => {
tags += `tags=${item.name}&`
})
language.forEach((item) => {
tags += `tags=${item.name}&`
})
getAvailablePromptsApi.request({ tags: tags })
}
const removeDuplicates = (value) => {
let duplicateRemoved = []
value.forEach((item) => {
if (value.filter((o) => o.id === item.id).length === 1) {
duplicateRemoved.push(item)
}
})
return duplicateRemoved
}
const handleModelChange = (event) => {
const {
target: { value }
} = event
setModelName(removeDuplicates(value))
}
const handleUsecaseChange = (event) => {
const {
target: { value }
} = event
setUsecase(removeDuplicates(value))
}
const handleLanguageChange = (event) => {
const {
target: { value }
} = event
setLanguage(removeDuplicates(value))
}
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth={'lg'}
aria-labelledby='prompt-dialog-title'
aria-describedby='prompt-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='prompt-dialog-title'>
Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'})
</DialogTitle>
<DialogContent dividers sx={{ p: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', p: 2, pt: 1, alignItems: 'center' }}>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='model-checkbox-label'>
Model
</InputLabel>
<Select
id='model-checkbox'
labelId='model-checkbox-label'
multiple
size='small'
value={modelName}
onChange={handleModelChange}
input={<OutlinedInput label='Model' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
modelName.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setModelName([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{models.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={modelName.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='usecase-checkbox-label'>
Usecase
</InputLabel>
<Select
autoWidth={false}
labelId='usecase-checkbox-label'
id='usecase-checkbox'
multiple
size='small'
value={usecase}
onChange={handleUsecaseChange}
input={<OutlinedInput label='Usecase' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
usecase.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setUsecase([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{usecases.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={usecase.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ mr: 1, width: '30%' }}>
<InputLabel size='small' id='language-checkbox-label'>
Language
</InputLabel>
<Select
labelId='language-checkbox-label'
id='language-checkbox'
multiple
size='small'
value={language}
onChange={handleLanguageChange}
input={<OutlinedInput label='language' />}
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
endAdornment={
language.length ? (
<IconButton sx={{ mr: 2 }} onClick={() => setLanguage([])}>
<ClearIcon style={{ width: 20, height: 20 }} />
</IconButton>
) : (
false
)
}
sx={{
'.MuiSvgIcon-root ': {
fill: customization.isDarkMode ? 'white !important' : ''
}
}}
MenuProps={MenuProps}
>
{languages.map((variant) => (
<MenuItem key={variant.id} value={variant}>
<Checkbox id={variant.id} checked={language.findIndex((item) => item.id === variant.id) >= 0} />
<ListItemText primary={variant.name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ width: '10%' }}>
<Button disableElevation variant='outlined' onClick={fetchPrompts}>
Search
</Button>
</FormControl>
</Box>
{availablePrompNameList && availablePrompNameList.length == 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%', pb: 3 }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={promptEmptySVG} alt='promptEmptySVG' />
</Box>
<div>No Available Prompts</div>
</Stack>
)}
{availablePrompNameList && availablePrompNameList.length > 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ width: '100%', p: 2 }}>
<Grid xs={12} container spacing={1} justifyContent='center' alignItems='center'>
<Grid xs={4} item sx={{ textAlign: 'left' }}>
<Box sx={{ width: '100%', maxWidth: 360 }}>
<Card variant='outlined' sx={{ height: 470, overflow: 'auto', borderRadius: 0 }}>
<CardContent sx={{ p: 1 }}>
<Typography sx={{ fontSize: 10 }} color='text.secondary' gutterBottom>
Available Prompts
</Typography>
<List component='nav' aria-label='secondary mailbox folder'>
{availablePrompNameList.map((item, index) => (
<ListItemButton
key={item.id}
selected={item.id === selectedPrompt?.id}
onClick={() => handleListItemClick(index)}
>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ fontSize: 16, p: 1, fontWeight: 500 }}>
{item.full_name}
</Typography>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 5
}}
>
{item.tags.map((tag, index) => (
<Chip
key={index}
label={tag}
style={{ marginRight: 5, marginBottom: 5 }}
/>
))}
</div>
</div>
</ListItemButton>
))}
</List>
</CardContent>
</Card>
</Box>
</Grid>
<Grid xs={8} item sx={{ textAlign: 'left' }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Card sx={{ height: 470, overflow: 'auto' }}>
<CardContent sx={{ p: 0.5 }}>
<Accordion
expanded={accordionExpanded.includes('prompt')}
onChange={handleAccordionChange('prompt')}
>
<AccordionSummary
aria-controls='panel2d-content'
expandIcon={<ExpandMoreIcon />}
id='panel2d-header'
>
<Typography>Prompt</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography sx={{ wordWrap: 'true' }} color='text.primary'>
{selectedPrompt?.detailed?.map((item) => (
<>
<Typography sx={{ fontSize: 12 }} color='text.secondary' gutterBottom>
{item.typeDisplay.toUpperCase()}
</Typography>
<Typography>
<p
style={{
whiteSpace: 'pre-wrap -moz-pre-wrap -pre-wrap -o-pre-wrap',
wordWrap: 'break-word',
fontFamily: 'inherit',
wordSpacing: '0.1rem',
lineHeight: '1.5rem'
}}
>
<NewLineToBr>{item.template}</NewLineToBr>
</p>
</Typography>
</>
))}
</Typography>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordionExpanded.includes('description')}
onChange={handleAccordionChange('description')}
>
<AccordionSummary
aria-controls='panel1d-content'
expandIcon={<ExpandMoreIcon />}
id='panel1d-header'
>
<Typography>Description</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography
sx={{ wordWrap: 'true', wordSpacing: '0.1rem', lineHeight: '1.5rem' }}
color='text.primary'
>
{selectedPrompt?.description}
</Typography>
</AccordionDetails>
</Accordion>
<Accordion
expanded={accordionExpanded.includes('readme')}
onChange={handleAccordionChange('readme')}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel3d-content'
id='panel3d-header'
>
<Typography>Readme</Typography>
</AccordionSummary>
<AccordionDetails>
<div
style={{
lineHeight: 1.75,
'& a': {
display: 'block',
marginRight: '2.5rem',
wordWrap: 'break-word',
color: '#16bed7',
fontWeight: 500
},
'& a:hover': { opacity: 0.8 },
'& code': {
color: '#0ab126',
fontWeight: 500,
whiteSpace: 'pre-wrap !important'
}
}}
>
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
isDialog={true}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{selectedPrompt?.readme}
</MemoizedReactMarkdown>
</div>
</AccordionDetails>
</Accordion>
</CardContent>
</Card>
</Box>
</Grid>
</Grid>
</Box>
</Stack>
)}
</DialogContent>
{availablePrompNameList && availablePrompNameList.length > 0 && (
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<StyledButton
disabled={!selectedPrompt?.detailed}
onClick={() => onSubmit(selectedPrompt.detailed)}
variant='contained'
>
Load
</StyledButton>
</DialogActions>
)}
</Dialog>
) : null
return createPortal(component, portalElement)
}
PromptLangsmithHubDialog.propTypes = {
promptType: PropTypes.string,
show: PropTypes.bool,
onCancel: PropTypes.func,
onSubmit: PropTypes.func
}
export default PromptLangsmithHubDialog
@@ -0,0 +1,252 @@
import { createPortal } from 'react-dom'
import { useDispatch } from 'react-redux'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions'
// material-ui
import {
Button,
IconButton,
Dialog,
DialogContent,
OutlinedInput,
DialogTitle,
DialogActions,
Box,
List,
InputAdornment
} from '@mui/material'
import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons'
// Project import
import { StyledButton } from 'ui-component/button/StyledButton'
// store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import useNotifier from 'utils/useNotifier'
// API
import chatflowsApi from 'api/chatflows'
const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [inputFields, setInputFields] = useState([
{
prompt: ''
}
])
const [chatbotConfig, setChatbotConfig] = useState({})
const addInputField = () => {
setInputFields([
...inputFields,
{
prompt: ''
}
])
}
const removeInputFields = (index) => {
const rows = [...inputFields]
rows.splice(index, 1)
setInputFields(rows)
}
const handleChange = (index, evnt) => {
const { name, value } = evnt.target
const list = [...inputFields]
list[index][name] = value
setInputFields(list)
}
const onSave = async () => {
try {
let value = {
starterPrompts: {
...inputFields
}
}
chatbotConfig.starterPrompts = value.starterPrompts
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
chatbotConfig: JSON.stringify(chatbotConfig)
})
if (saveResp.data) {
enqueueSnackbar({
message: 'Conversation Starter Prompts Saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
onConfirm()
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to save Conversation Starter Prompts: ${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 (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) {
try {
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
setChatbotConfig(chatbotConfig || {})
if (chatbotConfig.starterPrompts) {
let inputFields = []
Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => {
if (chatbotConfig.starterPrompts[key]) {
inputFields.push(chatbotConfig.starterPrompts[key])
}
})
setInputFields(inputFields)
} else {
setInputFields([
{
prompt: ''
}
])
}
} catch (e) {
setInputFields([
{
prompt: ''
}
])
}
}
return () => {}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title || 'Conversation Starter Prompts'}
</DialogTitle>
<DialogContent>
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
padding: 10
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
Starter prompts will only be shown when there is no messages on the chat
</span>
</div>
</div>
<Box sx={{ '& > :not(style)': { m: 1 }, pt: 2 }}>
<List>
{inputFields.map((data, index) => {
return (
<div key={index} style={{ display: 'flex', width: '100%' }}>
<Box sx={{ width: '95%', mb: 1 }}>
<OutlinedInput
sx={{ width: '100%' }}
key={index}
type='text'
onChange={(e) => handleChange(index, e)}
size='small'
value={data.prompt}
name='prompt'
endAdornment={
<InputAdornment position='end' sx={{ padding: '2px' }}>
{inputFields.length > 1 && (
<IconButton
sx={{ height: 30, width: 30 }}
size='small'
color='error'
disabled={inputFields.length === 1}
onClick={() => removeInputFields(index)}
edge='end'
>
<IconTrash />
</IconButton>
)}
</InputAdornment>
}
/>
</Box>
<Box sx={{ width: '5%', mb: 1 }}>
{index === inputFields.length - 1 && (
<IconButton color='primary' onClick={addInputField}>
<IconPlus />
</IconButton>
)}
</Box>
</div>
)
})}
</List>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</DialogActions>
</Dialog>
) : null
return createPortal(component, portalElement)
}
StarterPromptsDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default StarterPromptsDialog
@@ -147,8 +147,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
}
FlowListTable.propTypes = {
data: PropTypes.object,
images: PropTypes.array,
data: PropTypes.array,
images: PropTypes.object,
filterFunction: PropTypes.func,
updateFlowsApi: PropTypes.object
}
@@ -16,6 +16,7 @@ import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
import APICodeDialog from 'views/chatflows/APICodeDialog'
import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog'
import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog'
import StarterPromptsDialog from 'ui-component/dialog/StarterPromptsDialog'
// API
import chatflowsApi from 'api/chatflows'
@@ -45,6 +46,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [apiDialogProps, setAPIDialogProps] = useState({})
const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false)
const [analyseDialogProps, setAnalyseDialogProps] = useState({})
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
@@ -56,6 +59,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
if (setting === 'deleteChatflow') {
handleDeleteFlow()
} else if (setting === 'conversationStarters') {
setConversationStartersDialogProps({
title: 'Starter Prompts - ' + chatflow.name,
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
} else if (setting === 'analyseChatflow') {
setAnalyseDialogProps({
title: 'Analyse Chatflow',
@@ -376,6 +385,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
/>
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
<AnalyseFlowDialog show={analyseDialogOpen} dialogProps={analyseDialogProps} onCancel={() => setAnalyseDialogOpen(false)} />
<StarterPromptsDialog
show={conversationStartersDialogOpen}
dialogProps={conversationStartersDialogProps}
onConfirm={() => setConversationStartersDialogOpen(false)}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
@@ -6,6 +6,7 @@ import { useSelector } from 'react-redux'
// material-ui
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons'
@@ -31,6 +32,7 @@ import { getInputVariables } from 'utils/genericHelper'
// const
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
import PromptLangsmithHubDialog from '../../ui-component/dialog/PromptLangsmithHubDialog'
const EDITABLE_OPTIONS = ['selectedTool', 'selectedAssistant']
@@ -56,6 +58,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false)
const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({})
const [showPromptHubDialog, setShowPromptHubDialog] = useState(false)
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
@@ -69,6 +72,17 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
setShowExpandDialog(true)
}
const onShowPromptHubButtonClicked = () => {
setShowPromptHubDialog(true)
}
const onShowPromptHubButtonSubmit = (templates) => {
setShowPromptHubDialog(false)
for (const t of templates) {
if (Object.prototype.hasOwnProperty.call(data.inputs, t.type)) {
data.inputs[t.type] = t.template
}
}
}
const onFormatPromptValuesClicked = (value, inputParam) => {
// Preset values if the field is format prompt values
let inputValue = value
@@ -209,6 +223,31 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
</CustomWidthTooltip>
)}
<Box sx={{ p: 2 }}>
{(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') &&
(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
disabled={disabled}
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }}
variant='outlined'
onClick={() => onShowPromptHubButtonClicked()}
endIcon={<IconAutoFixHigh />}
>
Langchain Hub
</Button>
<PromptLangsmithHubDialog
promptType={inputParam.name}
show={showPromptHubDialog}
onCancel={() => setShowPromptHubDialog(false)}
onSubmit={onShowPromptHubButtonSubmit}
></PromptLangsmithHubDialog>
</>
)}
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
@@ -260,6 +299,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
}}
/>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
@@ -135,6 +135,8 @@ const ShareChatbot = ({ isSessionMemory }) => {
if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession
if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts
return obj
}
-1
View File
@@ -161,7 +161,6 @@ const Chatflows = () => {
variant='contained'
value='card'
title='Card View'
selectedColor='#00abc0'
>
<IconLayoutGrid />
</ToggleButton>
@@ -48,6 +48,7 @@ import { baseURL, maxScroll } from 'store/constant'
import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png'
import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper'
import DeleteIcon from '@mui/icons-material/Delete'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
@@ -75,6 +76,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([])
// drag & drop and file input
const fileUploadRef = useRef(null)
@@ -348,14 +352,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
}, 100)
}
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault()
const handlePromptClick = async (promptStarterInput) => {
setUserInput(promptStarterInput)
handleSubmit(undefined, promptStarterInput)
}
if (userInput.trim() === '') {
// Handle form submission
const handleSubmit = async (e, promptStarterInput) => {
if (e) e.preventDefault()
if (!promptStarterInput && userInput.trim() === '') {
return
}
let input = userInput
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
setLoading(true)
const urls = []
previews.map((item) => {
@@ -367,12 +380,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})
})
clearPreviews()
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage', fileUploads: urls }])
setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: urls }])
// Send user question and history to API
try {
const params = {
question: userInput,
question: input,
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'),
chatId
}
@@ -486,7 +499,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (getIsChatflowStreamingApi.data) {
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIsChatflowStreamingApi.data])
@@ -498,6 +510,24 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllowChatFlowUploads.data])
useEffect(() => {
if (getChatflowConfig.data) {
if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) {
let config = JSON.parse(getChatflowConfig.data?.chatbotConfig)
if (config.starterPrompts) {
let inputFields = []
Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => {
if (config.starterPrompts[key]) {
inputFields.push(config.starterPrompts[key])
}
})
setStarterPrompts(inputFields)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatflowConfig.data])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
@@ -517,6 +547,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
getChatmessageApi.request(chatflowid)
getIsChatflowStreamingApi.request(chatflowid)
getAllowChatFlowUploads.request(chatflowid)
getChatflowConfig.request(chatflowid)
scrollToBottom()
setIsRecording(false)
socket = socketIOClient(baseURL)
@@ -762,6 +793,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})}
</div>
</div>
<div style={{ position: 'relative' }}>
{messages && messages.length === 1 && (
<StarterPromptsCard starterPrompts={starterPrompts || []} onPromptClick={handlePromptClick} isGrid={isDialog} />
)}
<Divider />
</div>
<Divider />
<div>
{previews && previews.length > 0 && (