From bc7d95cf9d6c4a095fab5dcede8328ab7f9f945e Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Thu, 14 Sep 2023 14:50:56 +0800 Subject: [PATCH 1/8] add migration query for chat history --- packages/server/src/Interface.ts | 5 ++++- .../src/database/entities/ChatMessage.ts | 9 +++++++++ .../mysql/1694099200729-AddApiConfig.ts | 3 ++- .../mysql/1694432361423-AddAnalytic.ts | 3 ++- .../mysql/1694658767766-AddChatHistory.ts | 19 +++++++++++++++++++ .../src/database/migrations/mysql/index.ts | 4 +++- .../postgres/1694099183389-AddApiConfig.ts | 2 +- .../postgres/1694432361423-AddAnalytic.ts | 2 +- .../postgres/1694658756136-AddChatHistory.ts | 13 +++++++++++++ .../src/database/migrations/postgres/index.ts | 4 +++- .../sqlite/1694657778173-AddChatHistory.ts | 15 +++++++++++++++ .../src/database/migrations/sqlite/index.ts | 4 +++- 12 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts create mode 100644 packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts create mode 100644 packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 58740b86..f57c8311 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -24,8 +24,11 @@ export interface IChatMessage { role: MessageType content: string chatflowid: string - createdDate: Date sourceDocuments?: string + chatType: string + memoryType?: string + sessionId?: string + createdDate: Date } export interface ITool { diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index 4b5306ee..d1fdee72 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -20,6 +20,15 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) sourceDocuments?: string + @Column() + chatType: string + + @Column({ nullable: true }) + memoryType?: string + + @Column({ nullable: true }) + sessionId?: string + @CreateDateColumn() createdDate: Date } diff --git a/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts b/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts index c82b36ea..4509c5bb 100644 --- a/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts +++ b/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts @@ -2,7 +2,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddApiConfig1694099200729 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`apiConfig\` TEXT;`) + const columnExists = await queryRunner.hasColumn('chat_flow', 'apiConfig') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`apiConfig\` TEXT;`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts b/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts index a5e088fa..5fed5753 100644 --- a/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts +++ b/packages/server/src/database/migrations/mysql/1694432361423-AddAnalytic.ts @@ -2,7 +2,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddAnalytic1694432361423 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`analytic\` TEXT;`) + const columnExists = await queryRunner.hasColumn('chat_flow', 'analytic') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`analytic\` TEXT;`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts b/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts new file mode 100644 index 00000000..43d0fd3d --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddChatHistory1694658767766 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const chatTypeColumnExists = await queryRunner.hasColumn('chat_message', 'chatType') + if (!chatTypeColumnExists) + await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`chatType\` VARCHAR(255) NOT NULL DEFAULT 'INTERNAL';`) + const memoryTypeColumnExists = await queryRunner.hasColumn('chat_message', 'memoryType') + if (!memoryTypeColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`memoryType\` VARCHAR(255);`) + const sessionIdColumnExists = await queryRunner.hasColumn('chat_message', 'sessionId') + if (!sessionIdColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`sessionId\` VARCHAR(255);`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`chat_message\` DROP COLUMN \`chatType\`, DROP COLUMN \`memoryType\`, DROP COLUMN \`sessionId\`;` + ) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index ea26789e..aa34fa55 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -5,6 +5,7 @@ import { ModifyCredential1693999261583 } from './1693999261583-ModifyCredential' import { ModifyTool1694001465232 } from './1694001465232-ModifyTool' import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' +import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' export const mysqlMigrations = [ Init1693840429259, @@ -13,5 +14,6 @@ export const mysqlMigrations = [ ModifyCredential1693999261583, ModifyTool1694001465232, AddApiConfig1694099200729, - AddAnalytic1694432361423 + AddAnalytic1694432361423, + AddChatHistory1694658767766 ] diff --git a/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts b/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts index 832c2fa3..840bcc24 100644 --- a/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts +++ b/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts @@ -2,7 +2,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddApiConfig1694099183389 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "apiConfig" TEXT;`) + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "apiConfig" TEXT;`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts b/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts index ed83c833..e95bd68c 100644 --- a/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts +++ b/packages/server/src/database/migrations/postgres/1694432361423-AddAnalytic.ts @@ -2,7 +2,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddAnalytic1694432361423 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "analytic" TEXT;`) + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "analytic" TEXT;`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts b/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts new file mode 100644 index 00000000..f5862ebe --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddChatHistory1694658756136 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', ADD COLUMN IF NOT EXISTS "memoryType" VARCHAR, ADD COLUMN IF NOT EXISTS "sessionId" VARCHAR;` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "chatType", DROP COLUMN "memoryType", DROP COLUMN "sessionId";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 4a78556b..e16d9107 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -5,6 +5,7 @@ import { ModifyCredential1693997070000 } from './1693997070000-ModifyCredential' import { ModifyTool1693997339912 } from './1693997339912-ModifyTool' import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' +import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' export const postgresMigrations = [ Init1693891895163, @@ -13,5 +14,6 @@ export const postgresMigrations = [ ModifyCredential1693997070000, ModifyTool1693997339912, AddApiConfig1694099183389, - AddAnalytic1694432361423 + AddAnalytic1694432361423, + AddChatHistory1694658756136 ] diff --git a/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts b/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts new file mode 100644 index 00000000..3a6887e5 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddChatHistory1694657778173 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL';`) + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "memoryType" VARCHAR;`) + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "sessionId" VARCHAR;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "chatType";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "memoryType";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "sessionId";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index bff926b0..680f762b 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -5,6 +5,7 @@ import { ModifyCredential1693923551694 } from './1693923551694-ModifyCredential' import { ModifyTool1693924207475 } from './1693924207475-ModifyTool' import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig' import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' +import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' export const sqliteMigrations = [ Init1693835579790, @@ -13,5 +14,6 @@ export const sqliteMigrations = [ ModifyCredential1693923551694, ModifyTool1693924207475, AddApiConfig1694090982460, - AddAnalytic1694432361423 + AddAnalytic1694432361423, + AddChatHistory1694657778173 ] From 7b54f17a58d5c50c21849dfc06a5522399d7d0bb Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Fri, 15 Sep 2023 21:25:28 +0800 Subject: [PATCH 2/8] modify addChatMessage --- packages/server/src/Interface.ts | 4 ++ packages/server/src/index.ts | 38 +++++++++++++++---- packages/ui/src/api/chatmessage.js | 3 -- .../ui/src/views/chatmessage/ChatMessage.js | 19 ---------- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index f57c8311..36e02bee 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -2,6 +2,10 @@ import { ICommonObject, INode, INodeData as INodeDataFromComponent, INodeParams export type MessageType = 'apiMessage' | 'userMessage' +export enum chatType { + INTERNAL = 'INTERNAL', + EXTERNAL = 'EXTERNAL' +} /** * Databases */ diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index db5ecf38..78efe657 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -16,7 +16,8 @@ import { IReactFlowObject, INodeData, IDatabaseExport, - ICredentialReturnResponse + ICredentialReturnResponse, + chatType } from './Interface' import { getNodeModulesPackagePath, @@ -404,12 +405,7 @@ export class App { // Add chatmessages for chatflowid this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const body = req.body - const newChatMessage = new ChatMessage() - Object.assign(newChatMessage, body) - - const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage) - const results = await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) - + const results = await this.addChatMessage(body) return res.json(results) }) @@ -818,6 +814,18 @@ export class App { } } + /** + * Add Chat Message + * @param {any} chatMessage + */ + async addChatMessage(chatMessage: any) { + const newChatMessage = new ChatMessage() + Object.assign(newChatMessage, chatMessage) + + const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage) + return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) + } + /** * Process Prediction * @param {Request} req @@ -844,6 +852,13 @@ export class App { await this.validateKey(req, res, chatflow) } + await this.addChatMessage({ + role: 'userMessage', + content: incomingInput.question, + chatflowid: chatflowid, + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL + }) + let isStreamValid = false const files = (req.files as any[]) || [] @@ -991,6 +1006,15 @@ export class App { analytic: chatflow.analytic }) + const apiMessage: any = { + role: 'apiMessage', + content: typeof result === 'string' ? result : result.text, + chatflowid: chatflowid, + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL + } + if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) + await this.addChatMessage(apiMessage) + logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) return res.json(result) } catch (e: any) { diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index d93068e6..72a186a6 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -2,12 +2,9 @@ import client from './client' const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`) -const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body) - const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`) export default { getChatmessageFromChatflow, - createNewChatmessage, deleteChatmessage } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 7a15d9ff..cf5cea6c 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -104,20 +104,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]) - const addChatMessage = async (message, type, sourceDocuments) => { - try { - const newChatMessageBody = { - role: type, - content: message, - chatflowid: chatflowid - } - if (sourceDocuments) newChatMessageBody.sourceDocuments = JSON.stringify(sourceDocuments) - await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody) - } catch (error) { - console.error(error) - } - } - const updateLastMessage = (text) => { setMessages((prevMessages) => { let allMessages = [...cloneDeep(prevMessages)] @@ -140,7 +126,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { 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`, '') setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]) - addChatMessage(message, 'apiMessage') setLoading(false) setUserInput('') setTimeout(() => { @@ -158,8 +143,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setLoading(true) setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }]) - // waiting for first chatmessage saved, the first chatmessage will be used in sendMessageAndGetPrediction - await addChatMessage(userInput, 'userMessage') // Send user question and history to API try { @@ -183,12 +166,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { { message: data.text, sourceDocuments: data.sourceDocuments, type: 'apiMessage' } ]) } - addChatMessage(data.text, 'apiMessage', data.sourceDocuments) } else { if (!isChatFlowAvailableToStream) { setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }]) } - addChatMessage(data, 'apiMessage') } setLoading(false) setUserInput('') From d6cf5fed4222c26070b9bce65eef1af278cac2fd Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Mon, 18 Sep 2023 21:45:51 +0800 Subject: [PATCH 3/8] add chatId column --- packages/server/src/Interface.ts | 2 ++ .../src/database/entities/ChatMessage.ts | 3 ++ .../mysql/1694658767766-AddChatHistory.ts | 24 +++++++++++++- .../postgres/1694658756136-AddChatHistory.ts | 23 ++++++++++++-- .../sqlite/1694657778173-AddChatHistory.ts | 31 +++++++++++++++++-- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 36e02bee..120c07be 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -30,6 +30,7 @@ export interface IChatMessage { chatflowid: string sourceDocuments?: string chatType: string + chatId: string memoryType?: string sessionId?: string createdDate: Date @@ -153,6 +154,7 @@ export interface IncomingInput { history: IMessage[] overrideConfig?: ICommonObject socketIOClientId?: string + chatId?: string } export interface IActiveChatflows { diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index d1fdee72..3efd7fbb 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage { @Column() chatType: string + @Column() + chatId: string + @Column({ nullable: true }) memoryType?: string diff --git a/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts b/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts index 43d0fd3d..317d6188 100644 --- a/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts +++ b/packages/server/src/database/migrations/mysql/1694658767766-AddChatHistory.ts @@ -5,15 +5,37 @@ export class AddChatHistory1694658767766 implements MigrationInterface { const chatTypeColumnExists = await queryRunner.hasColumn('chat_message', 'chatType') if (!chatTypeColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`chatType\` VARCHAR(255) NOT NULL DEFAULT 'INTERNAL';`) + + const chatIdColumnExists = await queryRunner.hasColumn('chat_message', 'chatId') + if (!chatIdColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`chatId\` VARCHAR(255);`) + const results: { id: string; chatflowid: string }[] = await queryRunner.query(`WITH RankedMessages AS ( + SELECT + \`chatflowid\`, + \`id\`, + \`createdDate\`, + ROW_NUMBER() OVER (PARTITION BY \`chatflowid\` ORDER BY \`createdDate\`) AS row_num + FROM \`chat_message\` + ) + SELECT \`chatflowid\`, \`id\` + FROM RankedMessages + WHERE row_num = 1;`) + for (const chatMessage of results) { + await queryRunner.query( + `UPDATE \`chat_message\` SET \`chatId\` = '${chatMessage.id}' WHERE \`chatflowid\` = '${chatMessage.chatflowid}'` + ) + } + await queryRunner.query(`ALTER TABLE \`chat_message\` MODIFY \`chatId\` VARCHAR(255) NOT NULL;`) + const memoryTypeColumnExists = await queryRunner.hasColumn('chat_message', 'memoryType') if (!memoryTypeColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`memoryType\` VARCHAR(255);`) + const sessionIdColumnExists = await queryRunner.hasColumn('chat_message', 'sessionId') if (!sessionIdColumnExists) await queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`sessionId\` VARCHAR(255);`) } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE \`chat_message\` DROP COLUMN \`chatType\`, DROP COLUMN \`memoryType\`, DROP COLUMN \`sessionId\`;` + `ALTER TABLE \`chat_message\` DROP COLUMN \`chatType\`, DROP COLUMN \`chatId\`, DROP COLUMN \`memoryType\`, DROP COLUMN \`sessionId\`;` ) } } diff --git a/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts b/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts index f5862ebe..cba0e1e4 100644 --- a/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts +++ b/packages/server/src/database/migrations/postgres/1694658756136-AddChatHistory.ts @@ -3,11 +3,30 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddChatHistory1694658756136 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', ADD COLUMN IF NOT EXISTS "memoryType" VARCHAR, ADD COLUMN IF NOT EXISTS "sessionId" VARCHAR;` + `ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', ADD COLUMN IF NOT EXISTS "chatId" VARCHAR, ADD COLUMN IF NOT EXISTS "memoryType" VARCHAR, ADD COLUMN IF NOT EXISTS "sessionId" VARCHAR;` ) + const results: { id: string; chatflowid: string }[] = await queryRunner.query(`WITH RankedMessages AS ( + SELECT + "chatflowid", + "id", + "createdDate", + ROW_NUMBER() OVER (PARTITION BY "chatflowid" ORDER BY "createdDate") AS row_num + FROM "chat_message" + ) + SELECT "chatflowid", "id" + FROM RankedMessages + WHERE row_num = 1;`) + for (const chatMessage of results) { + await queryRunner.query( + `UPDATE "chat_message" SET "chatId" = '${chatMessage.id}' WHERE "chatflowid" = '${chatMessage.chatflowid}'` + ) + } + await queryRunner.query(`ALTER TABLE "chat_message" ALTER COLUMN "chatId" SET NOT NULL;`) } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "chatType", DROP COLUMN "memoryType", DROP COLUMN "sessionId";`) + await queryRunner.query( + `ALTER TABLE "chat_message" DROP COLUMN "chatType", DROP COLUMN "chatId", DROP COLUMN "memoryType", DROP COLUMN "sessionId";` + ) } } diff --git a/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts b/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts index 3a6887e5..67703ae0 100644 --- a/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts +++ b/packages/server/src/database/migrations/sqlite/1694657778173-AddChatHistory.ts @@ -2,13 +2,38 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddChatHistory1694657778173 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL';`) - await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "memoryType" VARCHAR;`) - await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "sessionId" VARCHAR;`) + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN "chatId" VARCHAR;`) + const results: { id: string; chatflowid: string }[] = await queryRunner.query(`WITH RankedMessages AS ( + SELECT + "chatflowid", + "id", + "createdDate", + ROW_NUMBER() OVER (PARTITION BY "chatflowid" ORDER BY "createdDate") AS row_num + FROM "chat_message" + ) + SELECT "chatflowid", "id" + FROM RankedMessages + WHERE row_num = 1;`) + for (const chatMessage of results) { + await queryRunner.query( + `UPDATE "chat_message" SET "chatId" = '${chatMessage.id}' WHERE "chatflowid" = '${chatMessage.chatflowid}'` + ) + } + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "createdDate", "chatId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "createdDate", "chatId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "chatType";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "chatId";`) await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "memoryType";`) await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "sessionId";`) } From f7178074ad05ecb2516f513aaedcb0cb91237e48 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Tue, 19 Sep 2023 14:07:33 +0800 Subject: [PATCH 4/8] add retrieve internal chat message --- packages/server/package.json | 1 + packages/server/src/index.ts | 96 ++++++++++++++----- packages/server/src/utils/index.ts | 3 +- packages/ui/src/api/chatmessage.js | 2 +- .../ui/src/views/chatmessage/ChatMessage.js | 32 +++---- 5 files changed, 92 insertions(+), 42 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 4d4293b0..4fcdfc9d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -64,6 +64,7 @@ "socket.io": "^4.6.1", "sqlite3": "^5.1.6", "typeorm": "^0.3.6", + "uuid": "^9.0.1", "winston": "^3.9.0" }, "devDependencies": { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 78efe657..6da37330 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -8,6 +8,7 @@ import basicAuth from 'express-basic-auth' import { Server } from 'socket.io' import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' +import { v4 as uuidv4 } from 'uuid' import { IChatFlow, @@ -391,14 +392,13 @@ export class App { // Get all chatmessages from chatflowid this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const chatmessages = await this.AppDataSource.getRepository(ChatMessage).find({ - where: { - chatflowid: req.params.id - }, - order: { - createdDate: 'ASC' - } - }) + const chatmessages = await this.getChatMessage(req.params.id, undefined) + return res.json(chatmessages) + }) + + // Get internal chatmessages from chatflowid + this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { + const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) @@ -815,10 +815,31 @@ export class App { } /** - * Add Chat Message - * @param {any} chatMessage + * Method that get chat messages. + * + * @param chatflowid - + * @param chatType - + * */ - async addChatMessage(chatMessage: any) { + async getChatMessage(chatflowid: string, chatType: chatType | undefined): Promise { + return await this.AppDataSource.getRepository(ChatMessage).find({ + where: { + chatflowid: chatflowid, + chatType: chatType + }, + order: { + createdDate: 'ASC' + } + }) + } + + /** + * Method that add chat messages. + * + * @param chatMessage - + * + */ + async addChatMessage(chatMessage: any): Promise { const newChatMessage = new ChatMessage() Object.assign(newChatMessage, chatMessage) @@ -826,6 +847,20 @@ export class App { return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) } + /** + * Method that find memory label. + * + * @param nodes - + * + */ + findMemoryLabel(nodes: any[]): string | undefined { + const memoryNode = nodes.find((node) => { + return node.data.category === 'Memory' + }) + + return memoryNode ? memoryNode.data.label : undefined + } + /** * Process Prediction * @param {Request} req @@ -845,20 +880,13 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) - let chatId = await getChatId(chatflow.id) - if (!chatId) chatId = chatflowid + const chatId = incomingInput.chatId ?? uuidv4() + const userMessageDateTime = new Date() if (!isInternal) { await this.validateKey(req, res, chatflow) } - await this.addChatMessage({ - role: 'userMessage', - content: incomingInput.question, - chatflowid: chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL - }) - let isStreamValid = false const files = (req.files as any[]) || [] @@ -986,9 +1014,12 @@ export class App { logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - if (nodeToExecuteData.instance) checkMemorySessionId(nodeToExecuteData.instance, chatId) + let sessionId = undefined + let memoryLabel = undefined + if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId) + if (sessionId) memoryLabel = this.findMemoryLabel(nodes) - const result = isStreamValid + let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { chatHistory: incomingInput.history, socketIO, @@ -1006,16 +1037,33 @@ export class App { analytic: chatflow.analytic }) + result = typeof result === 'string' ? { text: result } : result + + await this.addChatMessage({ + role: 'userMessage', + content: incomingInput.question, + chatflowid: chatflowid, + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatId: chatId, + memoryType: memoryLabel ? memoryLabel : undefined, + sessionId: sessionId ? sessionId : undefined, + createdDate: userMessageDateTime + }) + const apiMessage: any = { role: 'apiMessage', - content: typeof result === 'string' ? result : result.text, + content: result.text, chatflowid: chatflowid, - chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL + chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, + chatId: chatId, + memoryType: memoryLabel ? memoryLabel : undefined, + sessionId: sessionId ? sessionId : undefined } if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) + result.chatId = chatId return res.json(result) } catch (e: any) { logger.error('[server]: Error:', e) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index b1f7e5a2..0ece0a22 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -922,8 +922,9 @@ export const redactCredentialWithPasswordType = ( * @param {any} instance * @param {string} chatId */ -export const checkMemorySessionId = (instance: any, chatId: string) => { +export const checkMemorySessionId = (instance: any, chatId: string): string => { if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { instance.memory.sessionId = chatId } + return instance.memory.sessionId } diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index 72a186a6..4dcc7c88 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -1,6 +1,6 @@ import client from './client' -const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`) +const getChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`) const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index cf5cea6c..f5940932 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -50,6 +50,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false) const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) + const [chatId, setChatId] = useState(undefined) const inputRef = useRef(null) const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow) @@ -148,7 +149,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { try { const params = { 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?'), + chatId } if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId @@ -159,18 +161,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { data = handleVectaraMetadata(data) - if (typeof data === 'object' && data.text && data.sourceDocuments) { - if (!isChatFlowAvailableToStream) { - setMessages((prevMessages) => [ - ...prevMessages, - { message: data.text, sourceDocuments: data.sourceDocuments, type: 'apiMessage' } - ]) - } - } else { - if (!isChatFlowAvailableToStream) { - setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }]) - } + if (!chatId) { + setChatId(data.chatId) } + if (!isChatFlowAvailableToStream) { + setMessages((prevMessages) => [ + ...prevMessages, + { message: data.text, sourceDocuments: data?.sourceDocuments, type: 'apiMessage' } + ]) + } + setLoading(false) setUserInput('') setTimeout(() => { @@ -201,15 +201,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data) { - const loadedMessages = [] - for (const message of getChatmessageApi.data) { + setChatId(getChatmessageApi.data[0]?.chatId) + const loadedMessages = getChatmessageApi.data.map((message) => { const obj = { message: message.content, type: message.role } if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) - loadedMessages.push(obj) - } + return obj + }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) } From 51f9f0be0468a10e59b06223297a42fbf200b7c3 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Tue, 19 Sep 2023 21:39:37 +0800 Subject: [PATCH 5/8] modify delete chat message method --- packages/server/src/index.ts | 19 +++++++++++++------ .../ui/src/views/chatmessage/ChatMessage.js | 7 +++++-- .../ui/src/views/chatmessage/ChatPopUp.js | 4 +++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6da37330..c2f3cb38 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -409,22 +409,29 @@ export class App { return res.json(results) }) - // Delete all chatmessages from chatflowid + // Delete all chatmessages from chatId this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + const chatId = req.params.id + const chatMessage = await this.AppDataSource.getRepository(ChatMessage).findOneBy({ + chatId: chatId + }) + if (!chatMessage) { + res.status(404).send(`chatmessage ${chatId} not found`) + return + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id + id: chatMessage.chatflowid }) if (!chatflow) { - res.status(404).send(`Chatflow ${req.params.id} not found`) + res.status(404).send(`Chatflow ${chatMessage.chatflowid} not found`) return } const flowData = chatflow.flowData const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - let chatId = await getChatId(chatflow.id) - if (!chatId) chatId = chatflow.id + clearSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, req.query.sessionId as string) - const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: req.params.id }) + const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatId: chatId }) return res.json(results) }) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index f5940932..836a29c3 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -163,6 +163,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { if (!chatId) { setChatId(data.chatId) + localStorage.setItem(`${chatflowid}_INTERNAL`, data.chatId) } if (!isChatFlowAvailableToStream) { setMessages((prevMessages) => [ @@ -200,8 +201,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { // Get chatmessages successful useEffect(() => { - if (getChatmessageApi.data) { - setChatId(getChatmessageApi.data[0]?.chatId) + if (getChatmessageApi.data?.length) { + const chatId = getChatmessageApi.data[0]?.chatId + setChatId(chatId) + localStorage.setItem(`${chatflowid}_INTERNAL`, chatId) const loadedMessages = getChatmessageApi.data.map((message) => { const obj = { message: message.content, diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.js b/packages/ui/src/views/chatmessage/ChatPopUp.js index 93050c3a..5f1d413c 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.js +++ b/packages/ui/src/views/chatmessage/ChatPopUp.js @@ -85,7 +85,9 @@ export const ChatPopUp = ({ chatflowid }) => { if (isConfirmed) { try { - await chatmessageApi.deleteChatmessage(chatflowid) + const chatId = localStorage.getItem(`${chatflowid}_INTERNAL`) + await chatmessageApi.deleteChatmessage(chatId) + localStorage.removeItem(`${chatflowid}_INTERNAL`) resetChatDialog() enqueueSnackbar({ message: 'Succesfully cleared all chat history', From ea1f3f67890ef8ca28193709ecc816d23caa3af5 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 17 Oct 2023 16:24:21 +0100 Subject: [PATCH 6/8] add fix for backed chat message --- packages/server/src/index.ts | 2 +- packages/server/src/utils/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7dc8cb8c..c4594efa 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1077,7 +1077,7 @@ export class App { await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - result.chatId = chatId + if (incomingInput.chatId) result.chatId = chatId return res.json(result) } catch (e: any) { logger.error('[server]: Error:', e) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 749d8c55..d636a6cd 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -931,6 +931,7 @@ export const redactCredentialWithPasswordType = ( export const checkMemorySessionId = (instance: any, chatId: string): string => { if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) { instance.memory.sessionId = chatId + instance.memory.chatHistory.sessionId = chatId } return instance.memory.sessionId } From 4bc827ca6891c243728cb96469006b09d13343d0 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 18 Oct 2023 12:30:56 +0100 Subject: [PATCH 7/8] update share chatbot config --- .../ui/src/views/chatflows/ShareChatbot.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/ui/src/views/chatflows/ShareChatbot.js b/packages/ui/src/views/chatflows/ShareChatbot.js index 0f05c28a..dc6c0621 100644 --- a/packages/ui/src/views/chatflows/ShareChatbot.js +++ b/packages/ui/src/views/chatflows/ShareChatbot.js @@ -57,6 +57,9 @@ const ShareChatbot = ({ isSessionMemory }) => { const [isPublicChatflow, setChatflowIsPublic] = useState(chatflow.isPublic ?? false) const [generateNewSession, setGenerateNewSession] = useState(chatbotConfig?.generateNewSession ?? false) + const [title, setTitle] = useState(chatbotConfig?.title ?? '') + const [titleAvatarSrc, setTitleAvatarSrc] = useState(chatbotConfig?.titleAvatarSrc ?? '') + const [welcomeMessage, setWelcomeMessage] = useState(chatbotConfig?.welcomeMessage ?? '') const [backgroundColor, setBackgroundColor] = useState(chatbotConfig?.backgroundColor ?? defaultConfig.backgroundColor) const [fontSize, setFontSize] = useState(chatbotConfig?.fontSize ?? defaultConfig.fontSize) @@ -108,6 +111,8 @@ const ShareChatbot = ({ isSessionMemory }) => { textInput: {}, overrideConfig: {} } + if (title) obj.title = title + if (titleAvatarSrc) obj.titleAvatarSrc = titleAvatarSrc if (welcomeMessage) obj.welcomeMessage = welcomeMessage if (backgroundColor) obj.backgroundColor = backgroundColor if (fontSize) obj.fontSize = fontSize @@ -252,6 +257,12 @@ const ShareChatbot = ({ isSessionMemory }) => { const onTextChanged = (value, fieldName) => { switch (fieldName) { + case 'title': + setTitle(value) + break + case 'titleAvatarSrc': + setTitleAvatarSrc(value) + break case 'welcomeMessage': setWelcomeMessage(value) break @@ -395,6 +406,14 @@ const ShareChatbot = ({ isSessionMemory }) => { /> + {textField(title, 'title', 'Title', 'string', 'Flowise Assistant')} + {textField( + titleAvatarSrc, + 'titleAvatarSrc', + 'Title Avatar Link', + 'string', + `https://raw.githubusercontent.com/FlowiseAI/Flowise/main/assets/FloWiseAI_dark.png` + )} {textField(welcomeMessage, 'welcomeMessage', 'Welcome Message', 'string', 'Hello! This is custom welcome message')} {colorField(backgroundColor, 'backgroundColor', 'Background Color')} {textField(fontSize, 'fontSize', 'Font Size', 'number')} From 4f6cab47f82be2cf5ca8e20cfee4e921e1b102e7 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 3 Nov 2023 01:33:08 +0000 Subject: [PATCH 8/8] code cleanup --- packages/server/src/index.ts | 190 +++-- packages/server/src/utils/index.ts | 45 +- packages/ui/package.json | 2 +- packages/ui/src/api/chatmessage.js | 11 +- .../ui/src/assets/images/message_empty.svg | 1 + .../MainLayout/Header/ProfileSection/index.js | 2 +- packages/ui/src/menu-items/settings.js | 11 +- packages/ui/src/themes/palette.js | 3 + .../ui-component/dialog/ViewMessagesDialog.js | 695 ++++++++++++++++++ .../ui-component/dropdown/MultiDropdown.js | 5 +- packages/ui/src/utils/genericHelper.js | 17 + packages/ui/src/views/canvas/CanvasHeader.js | 16 +- .../ui/src/views/chatmessage/ChatMessage.css | 10 + .../ui/src/views/chatmessage/ChatMessage.js | 19 +- .../ui/src/views/chatmessage/ChatPopUp.js | 2 +- packages/ui/src/views/tools/ToolDialog.js | 2 +- 16 files changed, 942 insertions(+), 89 deletions(-) create mode 100644 packages/ui/src/assets/images/message_empty.svg create mode 100644 packages/ui/src/ui-component/dialog/ViewMessagesDialog.js diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 98ed89d1..ad82675b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -9,7 +9,7 @@ import { Server } from 'socket.io' import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' import { v4 as uuidv4 } from 'uuid' - +import { Between, IsNull, FindOptionsWhere } from 'typeorm' import { IChatFlow, IncomingInput, @@ -18,7 +18,9 @@ import { INodeData, IDatabaseExport, ICredentialReturnResponse, - chatType + chatType, + IChatMessage, + IReactFlowEdge } from './Interface' import { getNodeModulesPackagePath, @@ -42,10 +44,11 @@ import { getApiKey, transformToCredentialEntity, decryptCredentialData, - clearSessionMemory, + clearAllSessionMemory, replaceInputsWithConfig, getEncryptionKey, - checkMemorySessionId + checkMemorySessionId, + clearSessionMemoryFromViewMessageDialog } from './utils' import { cloneDeep, omit } from 'lodash' import { getDataSource } from './DataSource' @@ -397,7 +400,39 @@ export class App { // Get all chatmessages from chatflowid this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const chatmessages = await this.getChatMessage(req.params.id, undefined) + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined + + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL + } + } catch (e) { + return res.status(500).send(e) + } + } + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) return res.json(chatmessages) }) @@ -416,27 +451,41 @@ export class App { // Delete all chatmessages from chatId this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const chatId = req.params.id - const chatMessage = await this.AppDataSource.getRepository(ChatMessage).findOneBy({ - chatId: chatId - }) - if (!chatMessage) { - res.status(404).send(`chatmessage ${chatId} not found`) - return - } + const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatMessage.chatflowid + id: chatflowid }) if (!chatflow) { - res.status(404).send(`Chatflow ${chatMessage.chatflowid} not found`) + res.status(404).send(`Chatflow ${chatflowid} not found`) return } + const chatId = (req.query?.chatId as string) ?? (await getChatId(chatflowid)) + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const chatType = req.query?.chatType as string | undefined + const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined + const flowData = chatflow.flowData const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes - clearSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, req.query.sessionId as string) - const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatId: chatId }) + if (isClearFromViewMessageDialog) + clearSessionMemoryFromViewMessageDialog( + nodes, + this.nodesPool.componentNodes, + chatId, + this.AppDataSource, + sessionId, + memoryType + ) + else clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId) + + const deleteOptions: FindOptionsWhere = { chatflowid, chatId } + if (memoryType) deleteOptions.memoryType = memoryType + if (sessionId) deleteOptions.sessionId = sessionId + if (chatType) deleteOptions.chatType = chatType + + const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions) return res.json(results) }) @@ -831,30 +880,51 @@ export class App { /** * Method that get chat messages. - * - * @param chatflowid - - * @param chatType - - * + * @param {string} chatflowid + * @param {chatType} chatType + * @param {string} sortOrder + * @param {string} chatId + * @param {string} memoryType + * @param {string} sessionId + * @param {string} startDate + * @param {string} endDate */ - async getChatMessage(chatflowid: string, chatType: chatType | undefined): Promise { + async getChatMessage( + chatflowid: string, + chatType: chatType | undefined, + sortOrder: string = 'ASC', + chatId?: string, + memoryType?: string, + sessionId?: string, + startDate?: string, + endDate?: string + ): Promise { + let fromDate + if (startDate) fromDate = new Date(startDate) + + let toDate + if (endDate) toDate = new Date(endDate) + return await this.AppDataSource.getRepository(ChatMessage).find({ where: { - chatflowid: chatflowid, - chatType: chatType + chatflowid, + chatType, + chatId, + memoryType: memoryType ?? (chatId ? IsNull() : undefined), + sessionId: sessionId ?? (chatId ? IsNull() : undefined), + createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined }, order: { - createdDate: 'ASC' + createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' } }) } /** * Method that add chat messages. - * - * @param chatMessage - - * + * @param {Partial} chatMessage */ - async addChatMessage(chatMessage: any): Promise { + async addChatMessage(chatMessage: Partial): Promise { const newChatMessage = new ChatMessage() Object.assign(newChatMessage, chatMessage) @@ -863,17 +933,23 @@ export class App { } /** - * Method that find memory label. - * - * @param nodes - - * + * Method that find memory label that is connected within chatflow + * In a chatflow, there should only be 1 memory node + * @param {IReactFlowNode[]} nodes + * @param {IReactFlowEdge[]} edges + * @returns {string | undefined} */ - findMemoryLabel(nodes: any[]): string | undefined { - const memoryNode = nodes.find((node) => { - return node.data.category === 'Memory' - }) + findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): string | undefined { + const memoryNodes = nodes.filter((node) => node.data.category === 'Memory') + const memoryNodeIds = memoryNodes.map((mem) => mem.data.id) - return memoryNode ? memoryNode.data.label : undefined + for (const edge of edges) { + if (memoryNodeIds.includes(edge.source)) { + const memoryNode = nodes.find((node) => node.data.id === edge.source) + return memoryNode ? memoryNode.data.label : undefined + } + } + return undefined } /** @@ -883,7 +959,7 @@ export class App { * @param {Server} socketIO * @param {boolean} isInternal */ - async processPrediction(req: Request, res: Response, socketIO?: Server, isInternal = false) { + async processPrediction(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false) { try { const chatflowid = req.params.id let incomingInput: IncomingInput = req.body @@ -895,7 +971,7 @@ export class App { }) if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) - const chatId = incomingInput.chatId ?? uuidv4() + const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4() const userMessageDateTime = new Date() if (!isInternal) { @@ -1033,9 +1109,9 @@ export class App { logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) let sessionId = undefined - let memoryLabel = undefined if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId) - if (sessionId) memoryLabel = this.findMemoryLabel(nodes) + + const memoryType = this.findMemoryLabel(nodes, edges) let result = isStreamValid ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, { @@ -1057,31 +1133,35 @@ export class App { result = typeof result === 'string' ? { text: result } : result - await this.addChatMessage({ + const userMessage: Omit = { role: 'userMessage', content: incomingInput.question, - chatflowid: chatflowid, + chatflowid, chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - chatId: chatId, - memoryType: memoryLabel ? memoryLabel : undefined, - sessionId: sessionId ? sessionId : undefined, + chatId, + memoryType, + sessionId, createdDate: userMessageDateTime - }) + } + await this.addChatMessage(userMessage) - const apiMessage: any = { + const apiMessage: Omit = { role: 'apiMessage', content: result.text, - chatflowid: chatflowid, + chatflowid, chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL, - chatId: chatId, - memoryType: memoryLabel ? memoryLabel : undefined, - sessionId: sessionId ? sessionId : undefined + chatId, + memoryType, + sessionId } if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments) await this.addChatMessage(apiMessage) logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) - if (incomingInput.chatId) result.chatId = chatId + + // Only return ChatId when its Internal OR incoming input has ChatId, to avoid confusion when calling API + if (incomingInput.chatId || isInternal) result.chatId = chatId + return res.json(result) } catch (e: any) { logger.error('[server]: Error:', e) @@ -1104,7 +1184,7 @@ export class App { * @param {string} chatflowid * @returns {string} */ -export async function getChatId(chatflowid: string) { +export async function getChatId(chatflowid: string): Promise { // first chatmessage id as the unique chat id const firstChatMessage = await getDataSource() .getRepository(ChatMessage) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index d636a6cd..63d19989 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -298,14 +298,14 @@ export const buildLangchain = async ( } /** - * Clear memory + * Clear all session memories on the canvas * @param {IReactFlowNode[]} reactFlowNodes * @param {IComponentNodes} componentNodes * @param {string} chatId * @param {DataSource} appDataSource * @param {string} sessionId */ -export const clearSessionMemory = async ( +export const clearAllSessionMemory = async ( reactFlowNodes: IReactFlowNode[], componentNodes: IComponentNodes, chatId: string, @@ -317,9 +317,46 @@ export const clearSessionMemory = async ( const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() + if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId - if (newNodeInstance.clearSessionMemory) + + if (newNodeInstance.clearSessionMemory) { await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) + } + } +} + +/** + * Clear specific session memory from View Message Dialog UI + * @param {IReactFlowNode[]} reactFlowNodes + * @param {IComponentNodes} componentNodes + * @param {string} chatId + * @param {DataSource} appDataSource + * @param {string} sessionId + * @param {string} memoryType + */ +export const clearSessionMemoryFromViewMessageDialog = async ( + reactFlowNodes: IReactFlowNode[], + componentNodes: IComponentNodes, + chatId: string, + appDataSource: DataSource, + sessionId?: string, + memoryType?: string +) => { + if (!sessionId) return + for (const node of reactFlowNodes) { + if (node.data.category !== 'Memory') continue + if (node.data.label !== memoryType) continue + const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId + + if (newNodeInstance.clearSessionMemory) { + await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger }) + return + } } } @@ -933,5 +970,5 @@ export const checkMemorySessionId = (instance: any, chatId: string): string => { instance.memory.sessionId = chatId instance.memory.chatHistory.sessionId = chatId } - return instance.memory.sessionId + return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined } diff --git a/packages/ui/package.json b/packages/ui/package.json index 41ad515f..5244151c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -31,7 +31,7 @@ "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", "react-color": "^2.19.3", - "react-datepicker": "^4.8.0", + "react-datepicker": "^4.21.0", "react-device-detect": "^1.17.0", "react-dom": "^18.2.0", "react-markdown": "^8.0.6", diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index 4dcc7c88..5f1a4bad 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -1,10 +1,13 @@ import client from './client' -const getChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`) - -const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`) +const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`) +const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } }) +const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } }) +const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } }) export default { - getChatmessageFromChatflow, + getInternalChatmessageFromChatflow, + getAllChatmessageFromChatflow, + getChatmessageFromPK, deleteChatmessage } diff --git a/packages/ui/src/assets/images/message_empty.svg b/packages/ui/src/assets/images/message_empty.svg new file mode 100644 index 00000000..d7c9df25 --- /dev/null +++ b/packages/ui/src/assets/images/message_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js index 41de3dd4..c10b3289 100644 --- a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js +++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js @@ -71,7 +71,7 @@ const ProfileSection = ({ username, handleLogout }) => { try { const response = await databaseApi.getExportDatabase() const exportItems = response.data - let dataStr = JSON.stringify(exportItems) + let dataStr = JSON.stringify(exportItems, null, 2) let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) let exportFileDefaultName = `DB.json` diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 038dc740..307bd0bd 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -11,6 +11,13 @@ const settings = { title: '', type: 'group', children: [ + { + id: 'viewMessages', + title: 'View Messages', + type: 'item', + url: '', + icon: icons.IconMessage + }, { id: 'duplicateChatflow', title: 'Duplicate Chatflow', diff --git a/packages/ui/src/themes/palette.js b/packages/ui/src/themes/palette.js index 19a7df11..66ec3d01 100644 --- a/packages/ui/src/themes/palette.js +++ b/packages/ui/src/themes/palette.js @@ -80,6 +80,9 @@ export default function themePalette(theme) { asyncSelect: { main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50 }, + timeMessage: { + main: theme.customization.isDarkMode ? theme.colors?.darkLevel2 : theme.colors?.grey200 + }, canvasHeader: { deployLight: theme.colors?.primaryLight, deployDark: theme.colors?.primaryDark, diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js new file mode 100644 index 00000000..6d147dee --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -0,0 +1,695 @@ +import { createPortal } from 'react-dom' +import { useDispatch, useSelector } from 'react-redux' +import { useState, useEffect, forwardRef } from 'react' +import PropTypes from 'prop-types' +import moment from 'moment' +import rehypeMathjax from 'rehype-mathjax' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +// material-ui +import { + Button, + Tooltip, + ListItemButton, + Box, + Stack, + Dialog, + DialogContent, + DialogTitle, + ListItem, + ListItemText, + Chip +} from '@mui/material' +import { useTheme } from '@mui/material/styles' +import DatePicker from 'react-datepicker' + +import robotPNG from 'assets/images/robot.png' +import userPNG from 'assets/images/account.png' +import msgEmptySVG from 'assets/images/message_empty.svg' +import { IconFileExport, IconEraser, IconX } from '@tabler/icons' + +// Project import +import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' +import { CodeBlock } from 'ui-component/markdown/CodeBlock' +import SourceDocDialog from 'ui-component/dialog/SourceDocDialog' +import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' +import { StyledButton } from 'ui-component/button/StyledButton' + +// store +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' + +// API +import chatmessageApi from 'api/chatmessage' +import useApi from 'hooks/useApi' +import useConfirm from 'hooks/useConfirm' + +// Utils +import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' +import useNotifier from 'utils/useNotifier' + +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions' + +import 'views/chatmessage/ChatMessage.css' +import 'react-datepicker/dist/react-datepicker.css' + +const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { + return ( + + {value} + + ) +}) + +DatePickerCustomInput.propTypes = { + value: PropTypes.string, + onClick: PropTypes.func +} + +const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const dispatch = useDispatch() + const theme = useTheme() + const customization = useSelector((state) => state.customization) + const { confirm } = useConfirm() + + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [chatlogs, setChatLogs] = useState([]) + const [allChatlogs, setAllChatLogs] = useState([]) + const [chatMessages, setChatMessages] = useState([]) + const [selectedMessageIndex, setSelectedMessageIndex] = useState(0) + const [sourceDialogOpen, setSourceDialogOpen] = useState(false) + const [sourceDialogProps, setSourceDialogProps] = useState({}) + const [chatTypeFilter, setChatTypeFilter] = useState([]) + const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1)) + const [endDate, setEndDate] = useState(new Date()) + + const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) + const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) + + const onStartDateSelected = (date) => { + setStartDate(date) + getChatmessageApi.request(dialogProps.chatflow.id, { + startDate: date, + endDate: endDate, + chatType: chatTypeFilter.length ? chatTypeFilter : undefined + }) + } + + const onEndDateSelected = (date) => { + setEndDate(date) + getChatmessageApi.request(dialogProps.chatflow.id, { + endDate: date, + startDate: startDate, + chatType: chatTypeFilter.length ? chatTypeFilter : undefined + }) + } + + const onChatTypeSelected = (chatTypes) => { + setChatTypeFilter(chatTypes) + getChatmessageApi.request(dialogProps.chatflow.id, { + chatType: chatTypes.length ? chatTypes : undefined, + startDate: startDate, + endDate: endDate + }) + } + + const exportMessages = () => { + const obj = {} + for (let i = 0; i < allChatlogs.length; i += 1) { + const chatmsg = allChatlogs[i] + const chatPK = getChatPK(chatmsg) + const msg = { + content: chatmsg.content, + role: chatmsg.role === 'apiMessage' ? 'bot' : 'user', + time: chatmsg.createdDate + } + if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) + + if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) { + obj[chatPK] = { + id: chatmsg.chatId, + source: chatmsg.chatType === 'INTERNAL' ? 'UI' : 'API/Embed', + sessionId: chatmsg.sessionId ?? null, + memoryType: chatmsg.memoryType ?? null, + messages: [msg] + } + } else if (Object.prototype.hasOwnProperty.call(obj, chatPK)) { + obj[chatPK].messages = [...obj[chatPK].messages, msg] + } + } + + const exportMessages = [] + for (const key in obj) { + exportMessages.push({ + ...obj[key] + }) + } + + for (let i = 0; i < exportMessages.length; i += 1) { + exportMessages[i].messages = exportMessages[i].messages.reverse() + } + + const dataStr = JSON.stringify(exportMessages, null, 2) + const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + + const exportFileDefaultName = `${dialogProps.chatflow.id}-Message.json` + + let linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } + + const clearChat = async (chatmsg) => { + const description = + chatmsg.sessionId && chatmsg.memoryType + ? `Are you sure you want to clear session id: ${chatmsg.sessionId} from ${chatmsg.memoryType}?` + : `Are you sure you want to clear messages?` + const confirmPayload = { + title: `Clear Session`, + description, + confirmButtonName: 'Clear', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + const chatflowid = dialogProps.chatflow.id + if (isConfirmed) { + try { + const obj = { chatflowid, isClearFromViewMessageDialog: true } + if (chatmsg.chatId) obj.chatId = chatmsg.chatId + if (chatmsg.chatType) obj.chatType = chatmsg.chatType + if (chatmsg.memoryType) obj.memoryType = chatmsg.memoryType + if (chatmsg.sessionId) obj.sessionId = chatmsg.sessionId + + await chatmessageApi.deleteChatmessage(chatflowid, obj) + const description = + chatmsg.sessionId && chatmsg.memoryType + ? `Succesfully cleared session id: ${chatmsg.sessionId} from ${chatmsg.memoryType}` + : `Succesfully cleared messages` + enqueueSnackbar({ + message: description, + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + getChatmessageApi.request(chatflowid) + } 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) => ( + + ) + } + }) + } + } + } + + const getChatMessages = (chatmessages) => { + let prevDate = '' + const loadedMessages = [] + for (let i = 0; i < chatmessages.length; i += 1) { + const chatmsg = chatmessages[i] + if (!prevDate) { + prevDate = chatmsg.createdDate.split('T')[0] + loadedMessages.push({ + message: chatmsg.createdDate, + type: 'timeMessage' + }) + } else { + const currentDate = chatmsg.createdDate.split('T')[0] + if (currentDate !== prevDate) { + prevDate = currentDate + loadedMessages.push({ + message: chatmsg.createdDate, + type: 'timeMessage' + }) + } + } + const obj = { + ...chatmsg, + message: chatmsg.content, + type: chatmsg.role + } + if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) + loadedMessages.push(obj) + } + setChatMessages(loadedMessages) + } + + const getChatPK = (chatmsg) => { + const chatId = chatmsg.chatId + const memoryType = chatmsg.memoryType ?? 'null' + const sessionId = chatmsg.sessionId ?? 'null' + return `${chatId}_${memoryType}_${sessionId}` + } + + const transformChatPKToParams = (chatPK) => { + const chatId = chatPK.split('_')[0] + const memoryType = chatPK.split('_')[1] + const sessionId = chatPK.split('_')[2] + + const params = { chatId } + if (memoryType !== 'null') params.memoryType = memoryType + if (sessionId !== 'null') params.sessionId = sessionId + + return params + } + + const processChatLogs = (allChatMessages) => { + const seen = {} + const filteredChatLogs = [] + for (let i = 0; i < allChatMessages.length; i += 1) { + const PK = getChatPK(allChatMessages[i]) + + const item = allChatMessages[i] + if (!Object.prototype.hasOwnProperty.call(seen, PK)) { + seen[PK] = { + counter: 1, + item: allChatMessages[i] + } + } else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) { + seen[PK] = { + counter: 2, + item: { + ...seen[PK].item, + apiContent: + seen[PK].item.role === 'apiMessage' ? `Bot: ${seen[PK].item.content}` : `User: ${seen[PK].item.content}`, + userContent: item.role === 'apiMessage' ? `Bot: ${item.content}` : `User: ${item.content}` + } + } + filteredChatLogs.push(seen[PK].item) + } + } + setChatLogs(filteredChatLogs) + if (filteredChatLogs.length) return getChatPK(filteredChatLogs[0]) + return undefined + } + + const handleItemClick = (idx, chatmsg) => { + setSelectedMessageIndex(idx) + getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(getChatPK(chatmsg))) + } + + const onURLClick = (data) => { + window.open(data, '_blank') + } + + const onSourceDialogClick = (data) => { + setSourceDialogProps({ data }) + setSourceDialogOpen(true) + } + + useEffect(() => { + if (getChatmessageFromPKApi.data) { + getChatMessages(getChatmessageFromPKApi.data) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatmessageFromPKApi.data]) + + useEffect(() => { + if (getChatmessageApi.data) { + setAllChatLogs(getChatmessageApi.data) + const chatPK = processChatLogs(getChatmessageApi.data) + setSelectedMessageIndex(0) + if (chatPK) getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(chatPK)) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatmessageApi.data]) + + useEffect(() => { + if (dialogProps.chatflow) { + getChatmessageApi.request(dialogProps.chatflow.id) + } + + return () => { + setChatLogs([]) + setAllChatLogs([]) + setChatMessages([]) + setChatTypeFilter([]) + setSelectedMessageIndex(0) + setStartDate(new Date().setMonth(new Date().getMonth() - 1)) + setEndDate(new Date()) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [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 ? ( + + +
+ {dialogProps.title} +
+ +
+ + + <> +
+
+ From Date + onStartDateSelected(date)} + selectsStart + startDate={startDate} + endDate={endDate} + customInput={} + /> +
+
+ To Date + onEndDateSelected(date)} + selectsEnd + startDate={startDate} + endDate={endDate} + minDate={startDate} + maxDate={new Date()} + customInput={} + /> +
+
+ Source + onChatTypeSelected(newValue)} + value={chatTypeFilter} + formControlSx={{ mt: 0 }} + /> +
+
+
+
+ {chatlogs && chatlogs.length == 0 && ( + + + msgEmptySVG + +
No Messages
+
+ )} + {chatlogs && chatlogs.length > 0 && ( +
+ + {chatlogs.map((chatmsg, index) => ( + handleItemClick(index, chatmsg)} + > + + + {chatmsg?.userContent} +
+ {chatmsg?.apiContent} +
+
+ } + secondary={moment(chatmsg.createdDate).format('MMMM Do YYYY, h:mm:ss a')} + /> + + + ))} + +
+ )} + {chatlogs && chatlogs.length > 0 && ( +
+ {chatMessages && chatMessages.length > 1 && ( +
+
+ {chatMessages[1].sessionId && ( +
+ Session Id: {chatMessages[1].sessionId} +
+ )} + {chatMessages[1].chatType && ( +
+ Source: {chatMessages[1].chatType === 'INTERNAL' ? 'UI' : 'API/Embed'} +
+ )} + {chatMessages[1].memoryType && ( +
+ Memory: {chatMessages[1].memoryType} +
+ )} +
+
+ clearChat(chatMessages[1])} + startIcon={} + > + Clear + + {chatMessages[1].sessionId && ( + +
+ Why my session is not deleted? +
+
+ )} +
+
+ )} +
+
+ {chatMessages && + chatMessages.map((message, index) => { + if (message.type === 'apiMessage' || message.type === 'userMessage') { + return ( + + {/* Display the correct icon depending on the message type */} + {message.type === 'apiMessage' ? ( + AI + ) : ( + Me + )} +
+
+ {/* Messages are being rendered in Markdown format */} + + ) : ( + + {children} + + ) + } + }} + > + {message.message} + +
+ {message.sourceDocuments && ( +
+ {removeDuplicateURL(message).map((source, index) => { + const URL = isValidURL(source.metadata.source) + return ( + + URL + ? onURLClick(source.metadata.source) + : onSourceDialogClick(source) + } + /> + ) + })} +
+ )} +
+
+ ) + } else { + return ( + + {moment(message.message).format('MMMM Do YYYY, h:mm:ss a')} + + ) + } + })} +
+
+
+ )} +
+ setSourceDialogOpen(false)} /> + + +
+ ) : null + + return createPortal(component, portalElement) +} + +ViewMessagesDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default ViewMessagesDialog diff --git a/packages/ui/src/ui-component/dropdown/MultiDropdown.js b/packages/ui/src/ui-component/dropdown/MultiDropdown.js index ff07b89f..9b96e55c 100644 --- a/packages/ui/src/ui-component/dropdown/MultiDropdown.js +++ b/packages/ui/src/ui-component/dropdown/MultiDropdown.js @@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({ } }) -export const MultiDropdown = ({ name, value, options, onSelect, disabled = false, disableClearable = false }) => { +export const MultiDropdown = ({ name, value, options, onSelect, formControlSx = {}, disabled = false, disableClearable = false }) => { const customization = useSelector((state) => state.customization) const findMatchingOptions = (options = [], internalValue) => { let values = [] @@ -30,7 +30,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, disabled = false let [internalValue, setInternalValue] = useState(value ?? []) return ( - + { return inputVariables } +export const removeDuplicateURL = (message) => { + const visitedURLs = [] + const newSourceDocuments = [] + + if (!message.sourceDocuments) return newSourceDocuments + + message.sourceDocuments.forEach((source) => { + if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { + visitedURLs.push(source.metadata.source) + newSourceDocuments.push(source) + } else if (!isValidURL(source.metadata.source)) { + newSourceDocuments.push(source) + } + }) + return newSourceDocuments +} + export const isValidURL = (url) => { try { return new URL(url) diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index a9d2f39e..56365ba8 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -15,6 +15,7 @@ import Settings from 'views/settings' 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' // API import chatflowsApi from 'api/chatflows' @@ -44,6 +45,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [apiDialogProps, setAPIDialogProps] = useState({}) const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false) const [analyseDialogProps, setAnalyseDialogProps] = useState({}) + const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) + const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({}) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const canvas = useSelector((state) => state.canvas) @@ -59,6 +62,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl chatflow: chatflow }) setAnalyseDialogOpen(true) + } else if (setting === 'viewMessages') { + setViewMessagesDialogProps({ + title: 'View Messages', + chatflow: chatflow + }) + setViewMessagesDialogOpen(true) } else if (setting === 'duplicateChatflow') { try { localStorage.setItem('duplicatedFlowData', chatflow.flowData) @@ -69,7 +78,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl } else if (setting === 'exportChatflow') { try { const flowData = JSON.parse(chatflow.flowData) - let dataStr = JSON.stringify(generateExportFlowData(flowData)) + let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) let exportFileDefaultName = `${chatflow.name} Chatflow.json` @@ -367,6 +376,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl /> setAPIDialogOpen(false)} /> setAnalyseDialogOpen(false)} /> + setViewMessagesDialogOpen(false)} + /> ) } diff --git a/packages/ui/src/views/chatmessage/ChatMessage.css b/packages/ui/src/views/chatmessage/ChatMessage.css index 3b006c1d..2298fee6 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.css +++ b/packages/ui/src/views/chatmessage/ChatMessage.css @@ -134,3 +134,13 @@ justify-content: center; align-items: center; } + +.cloud-message { + width: 100%; + height: calc(100vh - 260px); + overflow-y: scroll; + border-radius: 0.5rem; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 7b186358..ce5a32d3 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -30,7 +30,7 @@ import { baseURL, maxScroll } from 'store/constant' import robotPNG from 'assets/images/robot.png' import userPNG from 'assets/images/account.png' -import { isValidURL } from 'utils/genericHelper' +import { isValidURL, removeDuplicateURL } from 'utils/genericHelper' export const ChatMessage = ({ open, chatflowid, isDialog }) => { const theme = useTheme() @@ -53,7 +53,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { const [chatId, setChatId] = useState(undefined) const inputRef = useRef(null) - const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow) + const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) const onSourceDialogClick = (data) => { @@ -65,21 +65,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { window.open(data, '_blank') } - const removeDuplicateURL = (message) => { - const visitedURLs = [] - const newSourceDocuments = [] - - message.sourceDocuments.forEach((source) => { - if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { - visitedURLs.push(source.metadata.source) - newSourceDocuments.push(source) - } else if (!isValidURL(source.metadata.source)) { - newSourceDocuments.push(source) - } - }) - return newSourceDocuments - } - const scrollToBottom = () => { if (ps.current) { ps.current.scrollTo({ top: maxScroll }) diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.js b/packages/ui/src/views/chatmessage/ChatPopUp.js index 5f1d413c..1b87ac30 100644 --- a/packages/ui/src/views/chatmessage/ChatPopUp.js +++ b/packages/ui/src/views/chatmessage/ChatPopUp.js @@ -86,7 +86,7 @@ export const ChatPopUp = ({ chatflowid }) => { if (isConfirmed) { try { const chatId = localStorage.getItem(`${chatflowid}_INTERNAL`) - await chatmessageApi.deleteChatmessage(chatId) + await chatmessageApi.deleteChatmessage(chatflowid, { chatId, chatType: 'INTERNAL' }) localStorage.removeItem(`${chatflowid}_INTERNAL`) resetChatDialog() enqueueSnackbar({ diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js index 2b67f6d4..1fa92ec9 100644 --- a/packages/ui/src/views/tools/ToolDialog.js +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -227,7 +227,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = delete toolData.id delete toolData.createdDate delete toolData.updatedDate - let dataStr = JSON.stringify(toolData) + let dataStr = JSON.stringify(toolData, null, 2) let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) let exportFileDefaultName = `${toolName}-CustomTool.json`