mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 23:01:09 +03:00
Merge pull request #1733 from 0xi4o/feature/chat-message-feedback
Feature: Chat message feedback
This commit is contained in:
@@ -6,6 +6,11 @@ export enum chatType {
|
|||||||
INTERNAL = 'INTERNAL',
|
INTERNAL = 'INTERNAL',
|
||||||
EXTERNAL = 'EXTERNAL'
|
EXTERNAL = 'EXTERNAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ChatMessageRatingType {
|
||||||
|
THUMBS_UP = 'THUMBS_UP',
|
||||||
|
THUMBS_DOWN = 'THUMBS_DOWN'
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Databases
|
* Databases
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +44,16 @@ export interface IChatMessage {
|
|||||||
createdDate: Date
|
createdDate: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChatMessageFeedback {
|
||||||
|
id: string
|
||||||
|
content?: string
|
||||||
|
chatflowid: string
|
||||||
|
chatId: string
|
||||||
|
messageId: string
|
||||||
|
rating: ChatMessageRatingType
|
||||||
|
createdDate: Date
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITool {
|
export interface ITool {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index, Unique } from 'typeorm'
|
||||||
|
import { IChatMessageFeedback, ChatMessageRatingType } from '../../Interface'
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique(['messageId'])
|
||||||
|
export class ChatMessageFeedback implements IChatMessageFeedback {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
chatflowid: string
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
chatId: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
messageId: string
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
rating: ChatMessageRatingType
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text' })
|
||||||
|
content?: string
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdDate: Date
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ChatFlow } from './ChatFlow'
|
import { ChatFlow } from './ChatFlow'
|
||||||
import { ChatMessage } from './ChatMessage'
|
import { ChatMessage } from './ChatMessage'
|
||||||
|
import { ChatMessageFeedback } from './ChatMessageFeedback'
|
||||||
import { Credential } from './Credential'
|
import { Credential } from './Credential'
|
||||||
import { Tool } from './Tool'
|
import { Tool } from './Tool'
|
||||||
import { Assistant } from './Assistant'
|
import { Assistant } from './Assistant'
|
||||||
@@ -8,6 +9,7 @@ import { Variable } from './Variable'
|
|||||||
export const entities = {
|
export const entities = {
|
||||||
ChatFlow,
|
ChatFlow,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
|
ChatMessageFeedback,
|
||||||
Credential,
|
Credential,
|
||||||
Tool,
|
Tool,
|
||||||
Assistant,
|
Assistant,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFeedback1707213626553 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`chat_message_feedback\` (
|
||||||
|
\`id\` varchar(36) NOT NULL,
|
||||||
|
\`chatflowid\` varchar(255) NOT NULL,
|
||||||
|
\`content\` text,
|
||||||
|
\`chatId\` varchar(255) NOT NULL,
|
||||||
|
\`messageId\` varchar(255) NOT NULL,
|
||||||
|
\`rating\` varchar(255) NOT NULL,
|
||||||
|
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (\`id\`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE chat_message_feedback`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
|
|||||||
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
||||||
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
||||||
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
||||||
|
import { AddFeedback1707213626553 } from './1707213626553-AddFeedback'
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
Init1693840429259,
|
Init1693840429259,
|
||||||
@@ -29,5 +30,6 @@ export const mysqlMigrations = [
|
|||||||
AddFileAnnotationsToChatMessage1700271021237,
|
AddFileAnnotationsToChatMessage1700271021237,
|
||||||
AddFileUploadsToChatMessage1701788586491,
|
AddFileUploadsToChatMessage1701788586491,
|
||||||
AddVariableEntity1699325775451,
|
AddVariableEntity1699325775451,
|
||||||
AddSpeechToText1706364937060
|
AddSpeechToText1706364937060,
|
||||||
|
AddFeedback1707213626553
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFeedback1707213601923 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS chat_message_feedback (
|
||||||
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"chatflowid" varchar NOT NULL,
|
||||||
|
"content" text,
|
||||||
|
"chatId" varchar NOT NULL,
|
||||||
|
"messageId" varchar NOT NULL,
|
||||||
|
"rating" varchar NOT NULL,
|
||||||
|
"createdDate" timestamp NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "PK_98419043dd704f54-9830ab78f8" PRIMARY KEY (id)
|
||||||
|
);`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE chat_message_feedback`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
|
|||||||
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
||||||
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
||||||
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
||||||
|
import { AddFeedback1707213601923 } from './1707213601923-AddFeedback'
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
Init1693891895163,
|
Init1693891895163,
|
||||||
@@ -29,5 +30,6 @@ export const postgresMigrations = [
|
|||||||
AddFileAnnotationsToChatMessage1700271021237,
|
AddFileAnnotationsToChatMessage1700271021237,
|
||||||
AddFileUploadsToChatMessage1701788586491,
|
AddFileUploadsToChatMessage1701788586491,
|
||||||
AddVariableEntity1699325775451,
|
AddVariableEntity1699325775451,
|
||||||
AddSpeechToText1706364937060
|
AddSpeechToText1706364937060,
|
||||||
|
AddFeedback1707213601923
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||||
|
|
||||||
|
export class AddFeedback1707213619308 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS "chat_message_feedback" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "chatId" varchar NOT NULL, "messageId" varchar NOT NULL, "rating" varchar NOT NULL, "content" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')));`
|
||||||
|
)
|
||||||
|
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_e574527322272fd838f4f0f3d3" ON "chat_message_feedback" ("chatflowid") ;`)
|
||||||
|
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_e574527322272fd838f4f0f3d3" ON "chat_message_feedback" ("chatId") ;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "chat_message_feedback";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
|
|||||||
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
|
||||||
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
|
||||||
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
|
||||||
|
import { AddFeedback1707213619308 } from './1707213619308-AddFeedback'
|
||||||
|
|
||||||
export const sqliteMigrations = [
|
export const sqliteMigrations = [
|
||||||
Init1693835579790,
|
Init1693835579790,
|
||||||
@@ -29,5 +30,6 @@ export const sqliteMigrations = [
|
|||||||
AddFileAnnotationsToChatMessage1700271021237,
|
AddFileAnnotationsToChatMessage1700271021237,
|
||||||
AddFileUploadsToChatMessage1701788586491,
|
AddFileUploadsToChatMessage1701788586491,
|
||||||
AddVariableEntity1699325775451,
|
AddVariableEntity1699325775451,
|
||||||
AddSpeechToText1706364937060
|
AddSpeechToText1706364937060,
|
||||||
|
AddFeedback1707213619308
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import logger from './utils/logger'
|
|||||||
import { expressRequestLogger } from './utils/logger'
|
import { expressRequestLogger } from './utils/logger'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
import { DataSource, FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual } from 'typeorm'
|
import { DataSource, FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual, Between } from 'typeorm'
|
||||||
import {
|
import {
|
||||||
IChatFlow,
|
IChatFlow,
|
||||||
IncomingInput,
|
IncomingInput,
|
||||||
@@ -21,8 +21,10 @@ import {
|
|||||||
ICredentialReturnResponse,
|
ICredentialReturnResponse,
|
||||||
chatType,
|
chatType,
|
||||||
IChatMessage,
|
IChatMessage,
|
||||||
|
IChatMessageFeedback,
|
||||||
IDepthQueue,
|
IDepthQueue,
|
||||||
INodeDirectedGraph,
|
INodeDirectedGraph,
|
||||||
|
ChatMessageRatingType,
|
||||||
IUploadFileSizeAndTypes
|
IUploadFileSizeAndTypes
|
||||||
} from './Interface'
|
} from './Interface'
|
||||||
import {
|
import {
|
||||||
@@ -57,6 +59,7 @@ import { getDataSource } from './DataSource'
|
|||||||
import { NodesPool } from './NodesPool'
|
import { NodesPool } from './NodesPool'
|
||||||
import { ChatFlow } from './database/entities/ChatFlow'
|
import { ChatFlow } from './database/entities/ChatFlow'
|
||||||
import { ChatMessage } from './database/entities/ChatMessage'
|
import { ChatMessage } from './database/entities/ChatMessage'
|
||||||
|
import { ChatMessageFeedback } from './database/entities/ChatMessageFeedback'
|
||||||
import { Credential } from './database/entities/Credential'
|
import { Credential } from './database/entities/Credential'
|
||||||
import { Tool } from './database/entities/Tool'
|
import { Tool } from './database/entities/Tool'
|
||||||
import { Assistant } from './database/entities/Assistant'
|
import { Assistant } from './database/entities/Assistant'
|
||||||
@@ -184,6 +187,7 @@ export class App {
|
|||||||
'/api/v1/chatflows-streaming',
|
'/api/v1/chatflows-streaming',
|
||||||
'/api/v1/chatflows-uploads',
|
'/api/v1/chatflows-uploads',
|
||||||
'/api/v1/openai-assistants-file',
|
'/api/v1/openai-assistants-file',
|
||||||
|
'/api/v1/feedback',
|
||||||
'/api/v1/get-upload-file',
|
'/api/v1/get-upload-file',
|
||||||
'/api/v1/ip'
|
'/api/v1/ip'
|
||||||
]
|
]
|
||||||
@@ -556,6 +560,7 @@ export class App {
|
|||||||
const messageId = req.query?.messageId as string | undefined
|
const messageId = req.query?.messageId as string | undefined
|
||||||
const startDate = req.query?.startDate as string | undefined
|
const startDate = req.query?.startDate as string | undefined
|
||||||
const endDate = req.query?.endDate as string | undefined
|
const endDate = req.query?.endDate as string | undefined
|
||||||
|
const feedback = req.query?.feedback as boolean | undefined
|
||||||
let chatTypeFilter = req.query?.chatType as chatType | undefined
|
let chatTypeFilter = req.query?.chatType as chatType | undefined
|
||||||
|
|
||||||
if (chatTypeFilter) {
|
if (chatTypeFilter) {
|
||||||
@@ -582,14 +587,35 @@ export class App {
|
|||||||
sessionId,
|
sessionId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
messageId
|
messageId,
|
||||||
|
feedback
|
||||||
)
|
)
|
||||||
return res.json(chatmessages)
|
return res.json(chatmessages)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get internal chatmessages from chatflowid
|
// Get internal chatmessages from chatflowid
|
||||||
this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
|
this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
|
||||||
const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL)
|
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 messageId = req.query?.messageId as string | undefined
|
||||||
|
const startDate = req.query?.startDate as string | undefined
|
||||||
|
const endDate = req.query?.endDate as string | undefined
|
||||||
|
const feedback = req.query?.feedback as boolean | undefined
|
||||||
|
|
||||||
|
const chatmessages = await this.getChatMessage(
|
||||||
|
req.params.id,
|
||||||
|
chatType.INTERNAL,
|
||||||
|
sortOrder,
|
||||||
|
chatId,
|
||||||
|
memoryType,
|
||||||
|
sessionId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
messageId,
|
||||||
|
feedback
|
||||||
|
)
|
||||||
return res.json(chatmessages)
|
return res.json(chatmessages)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -640,6 +666,10 @@ export class App {
|
|||||||
if (sessionId) deleteOptions.sessionId = sessionId
|
if (sessionId) deleteOptions.sessionId = sessionId
|
||||||
if (chatType) deleteOptions.chatType = chatType
|
if (chatType) deleteOptions.chatType = chatType
|
||||||
|
|
||||||
|
// remove all related feedback records
|
||||||
|
const feedbackDeleteOptions: FindOptionsWhere<ChatMessageFeedback> = { chatId }
|
||||||
|
await this.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions)
|
||||||
|
|
||||||
// Delete all uploads corresponding to this chatflow/chatId
|
// Delete all uploads corresponding to this chatflow/chatId
|
||||||
if (chatId) {
|
if (chatId) {
|
||||||
try {
|
try {
|
||||||
@@ -654,6 +684,90 @@ export class App {
|
|||||||
return res.json(results)
|
return res.json(results)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Chat Message Feedback
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// Get all chatmessage feedback from chatflowid
|
||||||
|
this.app.get('/api/v1/feedback/:id', async (req: Request, res: Response) => {
|
||||||
|
const chatflowid = req.params.id
|
||||||
|
const chatId = req.query?.chatId as string | undefined
|
||||||
|
const sortOrder = req.query?.order as string | undefined
|
||||||
|
const startDate = req.query?.startDate as string | undefined
|
||||||
|
const endDate = req.query?.endDate as string | undefined
|
||||||
|
|
||||||
|
const feedback = await this.getChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate)
|
||||||
|
|
||||||
|
return res.json(feedback)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add chatmessage feedback for chatflowid
|
||||||
|
this.app.post('/api/v1/feedback/:id', async (req: Request, res: Response) => {
|
||||||
|
const body = req.body
|
||||||
|
const results = await this.addChatMessageFeedback(body)
|
||||||
|
return res.json(results)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update chatmessage feedback for id
|
||||||
|
this.app.put('/api/v1/feedback/:id', async (req: Request, res: Response) => {
|
||||||
|
const id = req.params.id
|
||||||
|
const body = req.body
|
||||||
|
await this.updateChatMessageFeedback(id, body)
|
||||||
|
return res.json({ status: 'OK' })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// stats
|
||||||
|
// ----------------------------------------
|
||||||
|
//
|
||||||
|
// get stats for showing in chatflow
|
||||||
|
this.app.get('/api/v1/stats/:id', async (req: Request, res: Response) => {
|
||||||
|
const chatflowid = req.params.id
|
||||||
|
let chatTypeFilter = req.query?.chatType as chatType | undefined
|
||||||
|
const startDate = req.query?.startDate as string | undefined
|
||||||
|
const endDate = req.query?.endDate as string | 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(
|
||||||
|
chatflowid,
|
||||||
|
chatTypeFilter,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
'',
|
||||||
|
true
|
||||||
|
)) as Array<ChatMessage & { feedback?: ChatMessageFeedback }>
|
||||||
|
const totalMessages = chatmessages.length
|
||||||
|
|
||||||
|
const totalFeedback = chatmessages.filter((message) => message?.feedback).length
|
||||||
|
const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
totalMessages,
|
||||||
|
totalFeedback,
|
||||||
|
positiveFeedback
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(results)
|
||||||
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Credentials
|
// Credentials
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
@@ -1629,6 +1743,7 @@ export class App {
|
|||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
* @param {string} startDate
|
* @param {string} startDate
|
||||||
* @param {string} endDate
|
* @param {string} endDate
|
||||||
|
* @param {boolean} feedback
|
||||||
*/
|
*/
|
||||||
async getChatMessage(
|
async getChatMessage(
|
||||||
chatflowid: string,
|
chatflowid: string,
|
||||||
@@ -1639,7 +1754,8 @@ export class App {
|
|||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
startDate?: string,
|
startDate?: string,
|
||||||
endDate?: string,
|
endDate?: string,
|
||||||
messageId?: string
|
messageId?: string,
|
||||||
|
feedback?: boolean
|
||||||
): Promise<ChatMessage[]> {
|
): Promise<ChatMessage[]> {
|
||||||
const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
|
const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
|
||||||
const date = new Date(dateTimeStr)
|
const date = new Date(dateTimeStr)
|
||||||
@@ -1656,6 +1772,40 @@ export class App {
|
|||||||
let toDate
|
let toDate
|
||||||
if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end')
|
if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end')
|
||||||
|
|
||||||
|
if (feedback) {
|
||||||
|
const query = this.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message')
|
||||||
|
|
||||||
|
// do the join with chat message feedback based on messageId for each chat message in the chatflow
|
||||||
|
query
|
||||||
|
.leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
|
||||||
|
.where('chat_message.chatflowid = :chatflowid', { chatflowid })
|
||||||
|
|
||||||
|
// based on which parameters are available add `andWhere` clauses to the query
|
||||||
|
if (chatType) {
|
||||||
|
query.andWhere('chat_message.chatType = :chatType', { chatType })
|
||||||
|
}
|
||||||
|
if (chatId) {
|
||||||
|
query.andWhere('chat_message.chatId = :chatId', { chatId })
|
||||||
|
}
|
||||||
|
if (memoryType) {
|
||||||
|
query.andWhere('chat_message.memoryType = :memoryType', { memoryType })
|
||||||
|
}
|
||||||
|
if (sessionId) {
|
||||||
|
query.andWhere('chat_message.sessionId = :sessionId', { sessionId })
|
||||||
|
}
|
||||||
|
|
||||||
|
// set date range
|
||||||
|
query.andWhere('chat_message.createdDate BETWEEN :fromDate AND :toDate', {
|
||||||
|
fromDate: fromDate ?? new Date().setMonth(new Date().getMonth() - 1),
|
||||||
|
toDate: toDate ?? new Date()
|
||||||
|
})
|
||||||
|
// sort
|
||||||
|
query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC')
|
||||||
|
|
||||||
|
const messages = await query.getMany()
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
return await this.AppDataSource.getRepository(ChatMessage).find({
|
return await this.AppDataSource.getRepository(ChatMessage).find({
|
||||||
where: {
|
where: {
|
||||||
chatflowid,
|
chatflowid,
|
||||||
@@ -1687,6 +1837,62 @@ export class App {
|
|||||||
return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
|
return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that get chat messages.
|
||||||
|
* @param {string} chatflowid
|
||||||
|
* @param {string} sortOrder
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} startDate
|
||||||
|
* @param {string} endDate
|
||||||
|
*/
|
||||||
|
async getChatMessageFeedback(
|
||||||
|
chatflowid: string,
|
||||||
|
chatId?: string,
|
||||||
|
sortOrder: string = 'ASC',
|
||||||
|
startDate?: string,
|
||||||
|
endDate?: string
|
||||||
|
): Promise<ChatMessageFeedback[]> {
|
||||||
|
let fromDate
|
||||||
|
if (startDate) fromDate = new Date(startDate)
|
||||||
|
|
||||||
|
let toDate
|
||||||
|
if (endDate) toDate = new Date(endDate)
|
||||||
|
return await this.AppDataSource.getRepository(ChatMessageFeedback).find({
|
||||||
|
where: {
|
||||||
|
chatflowid,
|
||||||
|
chatId,
|
||||||
|
createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that add chat message feedback.
|
||||||
|
* @param {Partial<IChatMessageFeedback>} chatMessageFeedback
|
||||||
|
*/
|
||||||
|
async addChatMessageFeedback(chatMessageFeedback: Partial<IChatMessageFeedback>): Promise<ChatMessageFeedback> {
|
||||||
|
const newChatMessageFeedback = new ChatMessageFeedback()
|
||||||
|
Object.assign(newChatMessageFeedback, chatMessageFeedback)
|
||||||
|
|
||||||
|
const feedback = this.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback)
|
||||||
|
return await this.AppDataSource.getRepository(ChatMessageFeedback).save(feedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that updates chat message feedback.
|
||||||
|
* @param {string} id
|
||||||
|
* @param {Partial<IChatMessageFeedback>} chatMessageFeedback
|
||||||
|
*/
|
||||||
|
async updateChatMessageFeedback(id: string, chatMessageFeedback: Partial<IChatMessageFeedback>) {
|
||||||
|
const newChatMessageFeedback = new ChatMessageFeedback()
|
||||||
|
Object.assign(newChatMessageFeedback, chatMessageFeedback)
|
||||||
|
|
||||||
|
await this.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback)
|
||||||
|
}
|
||||||
|
|
||||||
async upsertVector(req: Request, res: Response, isInternal: boolean = false) {
|
async upsertVector(req: Request, res: Response, isInternal: boolean = false) {
|
||||||
try {
|
try {
|
||||||
const chatflowid = req.params.id
|
const chatflowid = req.params.id
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import client from './client'
|
import client from './client'
|
||||||
|
|
||||||
const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`)
|
const getInternalChatmessageFromChatflow = (id, params = {}) =>
|
||||||
const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } })
|
client.get(`/internal-chatmessage/${id}`, { params: { feedback: true, ...params } })
|
||||||
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } })
|
const getAllChatmessageFromChatflow = (id, params = {}) =>
|
||||||
|
client.get(`/chatmessage/${id}`, { params: { order: 'DESC', feedback: true, ...params } })
|
||||||
|
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', feedback: true, ...params } })
|
||||||
const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } })
|
const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } })
|
||||||
const getStoragePath = () => client.get(`/get-upload-path`)
|
const getStoragePath = () => client.get(`/get-upload-path`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const addFeedback = (id, body) => client.post(`/feedback/${id}`, body)
|
||||||
|
const updateFeedback = (id, body) => client.put(`/feedback/${id}`, body)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
addFeedback,
|
||||||
|
updateFeedback
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const getStatsFromChatflow = (id, params) => client.get(`/stats/${id}`, { params: { ...params } })
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getStatsFromChatflow
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
IconMessage,
|
IconMessage,
|
||||||
IconPictureInPictureOff,
|
IconPictureInPictureOff,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconMicrophone
|
IconMicrophone,
|
||||||
|
IconThumbUp
|
||||||
} from '@tabler/icons'
|
} from '@tabler/icons'
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
@@ -21,7 +22,8 @@ const icons = {
|
|||||||
IconMessage,
|
IconMessage,
|
||||||
IconPictureInPictureOff,
|
IconPictureInPictureOff,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconMicrophone
|
IconMicrophone,
|
||||||
|
IconThumbUp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
||||||
@@ -45,6 +47,13 @@ const settings = {
|
|||||||
url: '',
|
url: '',
|
||||||
icon: icons.IconMessage
|
icon: icons.IconMessage
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'chatFeedback',
|
||||||
|
title: 'Chat Feedback',
|
||||||
|
type: 'item',
|
||||||
|
url: '',
|
||||||
|
icon: icons.IconThumbUp
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'allowedDomains',
|
id: 'allowedDomains',
|
||||||
title: 'Allowed Domains',
|
title: 'Allowed Domains',
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconClipboard } from '@tabler/icons'
|
||||||
|
|
||||||
|
const CopyToClipboardButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Copy to clipboard'
|
||||||
|
>
|
||||||
|
<IconClipboard
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.isLoading ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyToClipboardButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyToClipboardButton
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconThumbDown } from '@tabler/icons'
|
||||||
|
|
||||||
|
const ThumbsDownButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Thumbs Down'
|
||||||
|
>
|
||||||
|
<IconThumbDown
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.rating === 'THUMBS_DOWN' ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbsDownButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
rating: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThumbsDownButton
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import { IconThumbUp } from '@tabler/icons'
|
||||||
|
|
||||||
|
const ThumbsUpButton = (props) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
onClick={props.onClick}
|
||||||
|
size='small'
|
||||||
|
sx={{ background: 'transparent', border: 'none' }}
|
||||||
|
title='Thumbs Up'
|
||||||
|
>
|
||||||
|
<IconThumbUp
|
||||||
|
style={{ width: '20px', height: '20px' }}
|
||||||
|
color={props.rating === 'THUMBS_UP' ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbsUpButton.propTypes = {
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
rating: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThumbsUpButton
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
const StatsCard = ({ title, stat }) => {
|
||||||
|
const customization = useSelector((state) => state.customization)
|
||||||
|
return (
|
||||||
|
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ fontSize: 14 }} color='text.primary' gutterBottom>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontSize: 30, fontWeight: 500 }} color='text.primary'>
|
||||||
|
{stat}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsCard.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
stat: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatsCard
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Box, OutlinedInput } from '@mui/material'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// Project import
|
||||||
|
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||||
|
|
||||||
|
// store
|
||||||
|
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||||
|
|
||||||
|
const ChatFeedbackContentDialog = ({ show, onCancel, onConfirm }) => {
|
||||||
|
const portalElement = document.getElementById('portal')
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const [feedbackContent, setFeedbackContent] = useState('')
|
||||||
|
|
||||||
|
const onChange = useCallback((e) => setFeedbackContent(e.target.value), [setFeedbackContent])
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
onConfirm(feedbackContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||||
|
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||||
|
return () => {
|
||||||
|
dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||||
|
setFeedbackContent('')
|
||||||
|
}
|
||||||
|
}, [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'>
|
||||||
|
Provide additional feedback
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<OutlinedInput
|
||||||
|
// eslint-disable-next-line
|
||||||
|
autoFocus
|
||||||
|
id='feedbackContentInput'
|
||||||
|
multiline={true}
|
||||||
|
name='feedbackContentInput'
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder='What do you think of the response?'
|
||||||
|
rows={4}
|
||||||
|
value={feedbackContent}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
<StyledButton variant='contained' onClick={onSave}>
|
||||||
|
Submit Feedback
|
||||||
|
</StyledButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatFeedbackContentDialog
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Box } from '@mui/material'
|
||||||
|
import { IconX } from '@tabler/icons'
|
||||||
|
|
||||||
|
// Project import
|
||||||
|
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||||
|
import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||||
|
|
||||||
|
// store
|
||||||
|
import {
|
||||||
|
enqueueSnackbar as enqueueSnackbarAction,
|
||||||
|
closeSnackbar as closeSnackbarAction,
|
||||||
|
SET_CHATFLOW,
|
||||||
|
HIDE_CANVAS_DIALOG,
|
||||||
|
SHOW_CANVAS_DIALOG
|
||||||
|
} from '@/store/actions'
|
||||||
|
import useNotifier from '@/utils/useNotifier'
|
||||||
|
|
||||||
|
// API
|
||||||
|
import chatflowsApi from '@/api/chatflows'
|
||||||
|
|
||||||
|
const ChatFeedbackDialog = ({ 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 [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
|
||||||
|
const [chatbotConfig, setChatbotConfig] = useState({})
|
||||||
|
|
||||||
|
const handleChange = (value) => {
|
||||||
|
setChatFeedbackStatus(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSave = async () => {
|
||||||
|
try {
|
||||||
|
let value = {
|
||||||
|
chatFeedback: {
|
||||||
|
status: chatFeedbackStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chatbotConfig.chatFeedback = value.chatFeedback
|
||||||
|
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
|
||||||
|
chatbotConfig: JSON.stringify(chatbotConfig)
|
||||||
|
})
|
||||||
|
if (saveResp.data) {
|
||||||
|
enqueueSnackbar({
|
||||||
|
message: 'Chat Feedback Settings 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 Chat Feedback Settings: ${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) {
|
||||||
|
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
|
||||||
|
setChatbotConfig(chatbotConfig || {})
|
||||||
|
if (chatbotConfig.chatFeedback) {
|
||||||
|
setChatFeedbackStatus(chatbotConfig.chatFeedback.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || 'Chat Feedback'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<SwitchInput label='Enable chat feedback' onChange={handleChange} value={chatFeedbackStatus} />
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel}>Cancel</Button>
|
||||||
|
<StyledButton variant='contained' onClick={onSave}>
|
||||||
|
Save
|
||||||
|
</StyledButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
return createPortal(component, portalElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatFeedbackDialog.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
dialogProps: PropTypes.object,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onConfirm: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatFeedbackDialog
|
||||||
@@ -39,12 +39,15 @@ import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
|||||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||||
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
|
||||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||||
|
import StatsCard from '@/ui-component/cards/StatsCard'
|
||||||
|
import Feedback from '@/ui-component/extended/Feedback'
|
||||||
|
|
||||||
// store
|
// store
|
||||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
import chatmessageApi from '@/api/chatmessage'
|
import chatmessageApi from '@/api/chatmessage'
|
||||||
|
import feedbackApi from '@/api/feedback'
|
||||||
import useApi from '@/hooks/useApi'
|
import useApi from '@/hooks/useApi'
|
||||||
import useConfirm from '@/hooks/useConfirm'
|
import useConfirm from '@/hooks/useConfirm'
|
||||||
|
|
||||||
@@ -91,6 +94,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
const [chatlogs, setChatLogs] = useState([])
|
const [chatlogs, setChatLogs] = useState([])
|
||||||
const [allChatlogs, setAllChatLogs] = useState([])
|
const [allChatlogs, setAllChatLogs] = useState([])
|
||||||
const [chatMessages, setChatMessages] = useState([])
|
const [chatMessages, setChatMessages] = useState([])
|
||||||
|
const [stats, setStats] = useState([])
|
||||||
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
|
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
|
||||||
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
|
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
|
||||||
const [sourceDialogProps, setSourceDialogProps] = useState({})
|
const [sourceDialogProps, setSourceDialogProps] = useState({})
|
||||||
@@ -100,6 +104,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
|
|
||||||
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
|
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
|
||||||
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
|
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
|
||||||
|
const getStatsApi = useApi(feedbackApi.getStatsFromChatflow)
|
||||||
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
|
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
|
||||||
let storagePath = ''
|
let storagePath = ''
|
||||||
|
|
||||||
@@ -110,6 +115,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
||||||
})
|
})
|
||||||
|
getStatsApi.request(dialogProps.chatflow.id, {
|
||||||
|
startDate: date,
|
||||||
|
endDate: endDate,
|
||||||
|
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEndDateSelected = (date) => {
|
const onEndDateSelected = (date) => {
|
||||||
@@ -119,6 +129,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
||||||
})
|
})
|
||||||
|
getStatsApi.request(dialogProps.chatflow.id, {
|
||||||
|
endDate: date,
|
||||||
|
startDate: startDate,
|
||||||
|
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChatTypeSelected = (chatTypes) => {
|
const onChatTypeSelected = (chatTypes) => {
|
||||||
@@ -128,6 +143,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate
|
endDate: endDate
|
||||||
})
|
})
|
||||||
|
getStatsApi.request(dialogProps.chatflow.id, {
|
||||||
|
chatType: chatTypes.length ? chatTypes : undefined,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportMessages = async () => {
|
const exportMessages = async () => {
|
||||||
@@ -162,6 +182,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||||
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
||||||
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||||
|
if (chatmsg.feedback) msg.feedback = chatmsg.feedback?.content
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
|
||||||
obj[chatPK] = {
|
obj[chatPK] = {
|
||||||
@@ -238,6 +259,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
getChatmessageApi.request(chatflowid)
|
getChatmessageApi.request(chatflowid)
|
||||||
|
getStatsApi.request(chatflowid) // update stats
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||||
enqueueSnackbar({
|
enqueueSnackbar({
|
||||||
@@ -405,9 +427,16 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [getChatmessageApi.data])
|
}, [getChatmessageApi.data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getStatsApi.data) {
|
||||||
|
setStats(getStatsApi.data)
|
||||||
|
}
|
||||||
|
}, [getStatsApi.data])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogProps.chatflow) {
|
if (dialogProps.chatflow) {
|
||||||
getChatmessageApi.request(dialogProps.chatflow.id)
|
getChatmessageApi.request(dialogProps.chatflow.id)
|
||||||
|
getStatsApi.request(dialogProps.chatflow.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -418,6 +447,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
setSelectedMessageIndex(0)
|
setSelectedMessageIndex(0)
|
||||||
setStartDate(new Date().setMonth(new Date().getMonth() - 1))
|
setStartDate(new Date().setMonth(new Date().getMonth() - 1))
|
||||||
setEndDate(new Date())
|
setEndDate(new Date())
|
||||||
|
setStats([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -449,7 +479,16 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%', marginBottom: 10 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
marginLeft: 8,
|
||||||
|
marginRight: 8
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ marginRight: 10 }}>
|
<div style={{ marginRight: 10 }}>
|
||||||
<b style={{ marginRight: 10 }}>From Date</b>
|
<b style={{ marginRight: 10 }}>From Date</b>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -496,6 +535,23 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }}></div>
|
<div style={{ flex: 1 }}></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
|
||||||
|
gap: 10,
|
||||||
|
marginBottom: 16,
|
||||||
|
marginLeft: 8,
|
||||||
|
marginRight: 8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StatsCard title='Total Messages' stat={`${stats.totalMessages}`} />
|
||||||
|
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback}`} />
|
||||||
|
<StatsCard
|
||||||
|
title='Positive Feedback'
|
||||||
|
stat={`${((stats.positiveFeedback / stats.totalFeedback) * 100 || 0).toFixed(2)}%`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
{chatlogs && chatlogs.length == 0 && (
|
{chatlogs && chatlogs.length == 0 && (
|
||||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
|
||||||
@@ -812,6 +868,12 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{message.type === 'apiMessage' && message.feedback ? (
|
||||||
|
<Feedback
|
||||||
|
content={message.feedback?.content || ''}
|
||||||
|
rating={message.feedback?.rating}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Alert, IconButton } from '@mui/material'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const ThumbsUpIcon = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width='20'
|
||||||
|
height='20'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth='2'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
>
|
||||||
|
<path d='M7 10v12' />
|
||||||
|
<path d='M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2h0a3.13 3.13 0 0 1 3 3.88Z' />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThumbsDownIcon = () => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width='20'
|
||||||
|
height='20'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
fill='none'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeWidth='2'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
>
|
||||||
|
<path d='M17 14V2' />
|
||||||
|
<path d='M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22h0a3.13 3.13 0 0 1-3-3.88Z' />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Feedback = ({ content, rating }) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'start' }}>
|
||||||
|
{content ? (
|
||||||
|
<Alert
|
||||||
|
icon={rating === 'THUMBS_UP' ? <ThumbsUpIcon /> : <ThumbsDownIcon />}
|
||||||
|
severity={rating === 'THUMBS_UP' ? 'success' : 'error'}
|
||||||
|
style={{ marginBottom: 14 }}
|
||||||
|
variant='outlined'
|
||||||
|
>
|
||||||
|
{content ? <span style={{ color: theme.palette.text.primary }}>{content}</span> : null}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<IconButton color={rating === 'THUMBS_UP' ? 'success' : 'error'} style={{ marginBottom: 14 }}>
|
||||||
|
{rating === 'THUMBS_UP' ? <ThumbsUpIcon /> : <ThumbsDownIcon />}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Feedback.propTypes = {
|
||||||
|
rating: PropTypes.oneOf(['THUMBS_UP', 'THUMBS_DOWN']),
|
||||||
|
content: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Feedback
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { FormControl, Switch } from '@mui/material'
|
import { FormControl, Switch, Typography } from '@mui/material'
|
||||||
|
|
||||||
export const SwitchInput = ({ value, onChange, disabled = false }) => {
|
export const SwitchInput = ({ label, value, onChange, disabled = false }) => {
|
||||||
const [myValue, setMyValue] = useState(!!value ?? false)
|
const [myValue, setMyValue] = useState(!!value ?? false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyValue(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
<FormControl
|
||||||
|
sx={{ mt: 1, width: '100%', display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{label && <Typography>{label}</Typography>}
|
||||||
<Switch
|
<Switch
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={myValue}
|
checked={myValue}
|
||||||
@@ -22,6 +30,7 @@ export const SwitchInput = ({ value, onChange, disabled = false }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SwitchInput.propTypes = {
|
SwitchInput.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
disabled: PropTypes.bool
|
disabled: PropTypes.bool
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import AnalyseFlowDialog from '@/ui-component/dialog/AnalyseFlowDialog'
|
|||||||
import ViewMessagesDialog from '@/ui-component/dialog/ViewMessagesDialog'
|
import ViewMessagesDialog from '@/ui-component/dialog/ViewMessagesDialog'
|
||||||
import StarterPromptsDialog from '@/ui-component/dialog/StarterPromptsDialog'
|
import StarterPromptsDialog from '@/ui-component/dialog/StarterPromptsDialog'
|
||||||
import SpeechToTextDialog from '@/ui-component/dialog/SpeechToTextDialog'
|
import SpeechToTextDialog from '@/ui-component/dialog/SpeechToTextDialog'
|
||||||
|
import ChatFeedbackDialog from '@/ui-component/dialog/ChatFeedbackDialog'
|
||||||
import AllowedDomainsDialog from '@/ui-component/dialog/AllowedDomainsDialog'
|
import AllowedDomainsDialog from '@/ui-component/dialog/AllowedDomainsDialog'
|
||||||
|
|
||||||
// API
|
// API
|
||||||
@@ -54,6 +55,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||||||
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
|
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
|
||||||
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
|
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
|
||||||
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
|
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
|
||||||
|
const [chatFeedbackDialogOpen, setChatFeedbackDialogOpen] = useState(false)
|
||||||
|
const [chatFeedbackDialogProps, setChatFeedbackDialogProps] = useState({})
|
||||||
const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false)
|
const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false)
|
||||||
const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({})
|
const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({})
|
||||||
|
|
||||||
@@ -71,6 +74,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||||||
chatflow: chatflow
|
chatflow: chatflow
|
||||||
})
|
})
|
||||||
setConversationStartersDialogOpen(true)
|
setConversationStartersDialogOpen(true)
|
||||||
|
} else if (setting === 'chatFeedback') {
|
||||||
|
setChatFeedbackDialogProps({
|
||||||
|
title: `Chat Feedback - ${chatflow.name}`,
|
||||||
|
chatflow: chatflow
|
||||||
|
})
|
||||||
|
setChatFeedbackDialogOpen(true)
|
||||||
} else if (setting === 'allowedDomains') {
|
} else if (setting === 'allowedDomains') {
|
||||||
setAllowedDomainsDialogProps({
|
setAllowedDomainsDialogProps({
|
||||||
title: 'Allowed Domains - ' + chatflow.name,
|
title: 'Allowed Domains - ' + chatflow.name,
|
||||||
@@ -414,6 +423,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
|||||||
onConfirm={() => setConversationStartersDialogOpen(false)}
|
onConfirm={() => setConversationStartersDialogOpen(false)}
|
||||||
onCancel={() => setConversationStartersDialogOpen(false)}
|
onCancel={() => setConversationStartersDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<ChatFeedbackDialog
|
||||||
|
show={chatFeedbackDialogOpen}
|
||||||
|
dialogProps={chatFeedbackDialogProps}
|
||||||
|
onConfirm={() => setChatFeedbackDialogOpen(false)}
|
||||||
|
onCancel={() => setChatFeedbackDialogOpen(false)}
|
||||||
|
/>
|
||||||
<AllowedDomainsDialog
|
<AllowedDomainsDialog
|
||||||
show={allowedDomainsDialogOpen}
|
show={allowedDomainsDialogOpen}
|
||||||
dialogProps={allowedDomainsDialogProps}
|
dialogProps={allowedDomainsDialogProps}
|
||||||
|
|||||||
@@ -32,9 +32,13 @@ import audioUploadSVG from '@/assets/images/wave-sound.jpg'
|
|||||||
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
|
||||||
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
|
||||||
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
|
||||||
|
import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'
|
||||||
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
|
||||||
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
|
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
|
||||||
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'
|
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'
|
||||||
|
import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'
|
||||||
|
import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'
|
||||||
|
import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'
|
||||||
import './ChatMessage.css'
|
import './ChatMessage.css'
|
||||||
import './audio-recording.css'
|
import './audio-recording.css'
|
||||||
|
|
||||||
@@ -42,6 +46,7 @@ import './audio-recording.css'
|
|||||||
import chatmessageApi from '@/api/chatmessage'
|
import chatmessageApi from '@/api/chatmessage'
|
||||||
import chatflowsApi from '@/api/chatflows'
|
import chatflowsApi from '@/api/chatflows'
|
||||||
import predictionApi from '@/api/prediction'
|
import predictionApi from '@/api/prediction'
|
||||||
|
import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import useApi from '@/hooks/useApi'
|
import useApi from '@/hooks/useApi'
|
||||||
@@ -86,6 +91,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
|
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
|
||||||
|
|
||||||
const [starterPrompts, setStarterPrompts] = useState([])
|
const [starterPrompts, setStarterPrompts] = useState([])
|
||||||
|
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
|
||||||
|
const [feedbackId, setFeedbackId] = useState('')
|
||||||
|
const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false)
|
||||||
|
|
||||||
// drag & drop and file input
|
// drag & drop and file input
|
||||||
const fileUploadRef = useRef(null)
|
const fileUploadRef = useRef(null)
|
||||||
@@ -318,6 +326,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
let allMessages = [...cloneDeep(prevMessages)]
|
let allMessages = [...cloneDeep(prevMessages)]
|
||||||
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
|
||||||
allMessages[allMessages.length - 1].message += text
|
allMessages[allMessages.length - 1].message += text
|
||||||
|
allMessages[allMessages.length - 1].feedback = null
|
||||||
return allMessages
|
return allMessages
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -389,6 +398,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
if (response.data) {
|
if (response.data) {
|
||||||
const data = response.data
|
const data = response.data
|
||||||
|
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
let allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
if (allMessages[allMessages.length - 1].type === 'apiMessage') {
|
||||||
|
allMessages[allMessages.length - 1].id = data?.chatMessageId
|
||||||
|
}
|
||||||
|
return allMessages
|
||||||
|
})
|
||||||
|
|
||||||
if (!chatId) setChatId(data.chatId)
|
if (!chatId) setChatId(data.chatId)
|
||||||
|
|
||||||
if (input === '' && data.question) {
|
if (input === '' && data.question) {
|
||||||
@@ -412,10 +429,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
...prevMessages,
|
...prevMessages,
|
||||||
{
|
{
|
||||||
message: text,
|
message: text,
|
||||||
|
id: data?.chatMessageId,
|
||||||
sourceDocuments: data?.sourceDocuments,
|
sourceDocuments: data?.sourceDocuments,
|
||||||
usedTools: data?.usedTools,
|
usedTools: data?.usedTools,
|
||||||
fileAnnotations: data?.fileAnnotations,
|
fileAnnotations: data?.fileAnnotations,
|
||||||
type: 'apiMessage'
|
type: 'apiMessage',
|
||||||
|
feedback: null
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -474,7 +493,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
setChatId(chatId)
|
setChatId(chatId)
|
||||||
const loadedMessages = getChatmessageApi.data.map((message) => {
|
const loadedMessages = getChatmessageApi.data.map((message) => {
|
||||||
const obj = {
|
const obj = {
|
||||||
|
id: message.id,
|
||||||
message: message.content,
|
message: message.content,
|
||||||
|
feedback: message.feedback,
|
||||||
type: message.role
|
type: message.role
|
||||||
}
|
}
|
||||||
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
|
||||||
@@ -527,6 +548,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
})
|
})
|
||||||
setStarterPrompts(inputFields)
|
setStarterPrompts(inputFields)
|
||||||
}
|
}
|
||||||
|
if (config.chatFeedback) {
|
||||||
|
setChatFeedbackStatus(config.chatFeedback.status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -604,6 +628,83 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [previews])
|
}, [previews])
|
||||||
|
|
||||||
|
const copyMessageToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text || '')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error copying to clipboard:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onThumbsUpClick = async (messageId) => {
|
||||||
|
const body = {
|
||||||
|
chatflowid,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
rating: 'THUMBS_UP',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body)
|
||||||
|
if (result.data) {
|
||||||
|
const data = result.data
|
||||||
|
let id = ''
|
||||||
|
if (data && data.id) id = data.id
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
return allMessages.map((message) => {
|
||||||
|
if (message.id === messageId) {
|
||||||
|
message.feedback = {
|
||||||
|
rating: 'THUMBS_UP'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setFeedbackId(id)
|
||||||
|
setShowFeedbackContentDialog(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onThumbsDownClick = async (messageId) => {
|
||||||
|
const body = {
|
||||||
|
chatflowid,
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
rating: 'THUMBS_DOWN',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body)
|
||||||
|
if (result.data) {
|
||||||
|
const data = result.data
|
||||||
|
let id = ''
|
||||||
|
if (data && data.id) id = data.id
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const allMessages = [...cloneDeep(prevMessages)]
|
||||||
|
return allMessages.map((message) => {
|
||||||
|
if (message.id === messageId) {
|
||||||
|
message.feedback = {
|
||||||
|
rating: 'THUMBS_DOWN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setFeedbackId(id)
|
||||||
|
setShowFeedbackContentDialog(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFeedbackContent = async (text) => {
|
||||||
|
const body = {
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body)
|
||||||
|
if (result.data) {
|
||||||
|
setFeedbackId('')
|
||||||
|
setShowFeedbackContentDialog(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onDragEnter={handleDrag}>
|
<div onDragEnter={handleDrag}>
|
||||||
{isDragActive && (
|
{isDragActive && (
|
||||||
@@ -747,6 +848,31 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
{message.message}
|
{message.message}
|
||||||
</MemoizedReactMarkdown>
|
</MemoizedReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
{message.type === 'apiMessage' && message.id && chatFeedbackStatus ? (
|
||||||
|
<>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 1 }}>
|
||||||
|
<CopyToClipboardButton onClick={() => copyMessageToClipboard(message.message)} />
|
||||||
|
{!message.feedback ||
|
||||||
|
message.feedback.rating === '' ||
|
||||||
|
message.feedback.rating === 'THUMBS_UP' ? (
|
||||||
|
<ThumbsUpButton
|
||||||
|
isDisabled={message.feedback && message.feedback.rating === 'THUMBS_UP'}
|
||||||
|
rating={message.feedback ? message.feedback.rating : ''}
|
||||||
|
onClick={() => onThumbsUpClick(message.id)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!message.feedback ||
|
||||||
|
message.feedback.rating === '' ||
|
||||||
|
message.feedback.rating === 'THUMBS_DOWN' ? (
|
||||||
|
<ThumbsDownButton
|
||||||
|
isDisabled={message.feedback && message.feedback.rating === 'THUMBS_DOWN'}
|
||||||
|
rating={message.feedback ? message.feedback.rating : ''}
|
||||||
|
onClick={() => onThumbsDownClick(message.id)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{message.fileAnnotations && (
|
{message.fileAnnotations && (
|
||||||
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
|
||||||
{message.fileAnnotations.map((fileAnnotation, index) => {
|
{message.fileAnnotations.map((fileAnnotation, index) => {
|
||||||
@@ -993,6 +1119,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
|
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
|
||||||
|
<ChatFeedbackContentDialog
|
||||||
|
show={showFeedbackContentDialog}
|
||||||
|
onCancel={() => setShowFeedbackContentDialog(false)}
|
||||||
|
onConfirm={submitFeedbackContent}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user