diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 0c5fa5a4e..91fe2a935 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -6,6 +6,11 @@ export enum chatType { INTERNAL = 'INTERNAL', EXTERNAL = 'EXTERNAL' } + +export enum ChatMessageRatingType { + THUMBS_UP = 'THUMBS_UP', + THUMBS_DOWN = 'THUMBS_DOWN' +} /** * Databases */ @@ -39,6 +44,16 @@ export interface IChatMessage { createdDate: Date } +export interface IChatMessageFeedback { + id: string + content?: string + chatflowid: string + chatId: string + messageId: string + rating: ChatMessageRatingType + createdDate: Date +} + export interface ITool { id: string name: string diff --git a/packages/server/src/database/entities/ChatMessageFeedback.ts b/packages/server/src/database/entities/ChatMessageFeedback.ts new file mode 100644 index 000000000..811f3104d --- /dev/null +++ b/packages/server/src/database/entities/ChatMessageFeedback.ts @@ -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 +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index af5c559f5..6a0e2d220 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -1,5 +1,6 @@ import { ChatFlow } from './ChatFlow' import { ChatMessage } from './ChatMessage' +import { ChatMessageFeedback } from './ChatMessageFeedback' import { Credential } from './Credential' import { Tool } from './Tool' import { Assistant } from './Assistant' @@ -8,6 +9,7 @@ import { Variable } from './Variable' export const entities = { ChatFlow, ChatMessage, + ChatMessageFeedback, Credential, Tool, Assistant, diff --git a/packages/server/src/database/migrations/mysql/1707213626553-AddFeedback.ts b/packages/server/src/database/migrations/mysql/1707213626553-AddFeedback.ts new file mode 100644 index 000000000..f2c86734a --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1707213626553-AddFeedback.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFeedback1707213626553 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE chat_message_feedback`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 549742a1f..a4baf4240 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddFeedback1707213626553 } from './1707213626553-AddFeedback' export const mysqlMigrations = [ Init1693840429259, @@ -29,5 +30,6 @@ export const mysqlMigrations = [ AddFileAnnotationsToChatMessage1700271021237, AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, - AddSpeechToText1706364937060 + AddSpeechToText1706364937060, + AddFeedback1707213626553 ] diff --git a/packages/server/src/database/migrations/postgres/1707213601923-AddFeedback.ts b/packages/server/src/database/migrations/postgres/1707213601923-AddFeedback.ts new file mode 100644 index 000000000..779de42e6 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1707213601923-AddFeedback.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFeedback1707213601923 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE chat_message_feedback`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index bd6319036..fbfac45ce 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddFeedback1707213601923 } from './1707213601923-AddFeedback' export const postgresMigrations = [ Init1693891895163, @@ -29,5 +30,6 @@ export const postgresMigrations = [ AddFileAnnotationsToChatMessage1700271021237, AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, - AddSpeechToText1706364937060 + AddSpeechToText1706364937060, + AddFeedback1707213601923 ] diff --git a/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts b/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts new file mode 100644 index 000000000..b9002697d --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFeedback1707213619308 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE IF EXISTS "chat_message_feedback";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index a50b0792e..0b4f3e051 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText' +import { AddFeedback1707213619308 } from './1707213619308-AddFeedback' export const sqliteMigrations = [ Init1693835579790, @@ -29,5 +30,6 @@ export const sqliteMigrations = [ AddFileAnnotationsToChatMessage1700271021237, AddFileUploadsToChatMessage1701788586491, AddVariableEntity1699325775451, - AddSpeechToText1706364937060 + AddSpeechToText1706364937060, + AddFeedback1707213619308 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d18111e30..52daef816 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -11,7 +11,7 @@ import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' import { v4 as uuidv4 } from 'uuid' import OpenAI from 'openai' -import { FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual } from 'typeorm' +import { FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual, Between } from 'typeorm' import { IChatFlow, IncomingInput, @@ -21,8 +21,10 @@ import { ICredentialReturnResponse, chatType, IChatMessage, + IChatMessageFeedback, IDepthQueue, INodeDirectedGraph, + ChatMessageRatingType, IUploadFileSizeAndTypes } from './Interface' import { @@ -57,6 +59,7 @@ import { getDataSource } from './DataSource' import { NodesPool } from './NodesPool' import { ChatFlow } from './database/entities/ChatFlow' import { ChatMessage } from './database/entities/ChatMessage' +import { ChatMessageFeedback } from './database/entities/ChatMessageFeedback' import { Credential } from './database/entities/Credential' import { Tool } from './database/entities/Tool' import { Assistant } from './database/entities/Assistant' @@ -184,6 +187,7 @@ export class App { '/api/v1/chatflows-streaming', '/api/v1/chatflows-uploads', '/api/v1/openai-assistants-file', + '/api/v1/feedback', '/api/v1/get-upload-file', '/api/v1/ip' ] @@ -556,6 +560,7 @@ export class App { 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 let chatTypeFilter = req.query?.chatType as chatType | undefined if (chatTypeFilter) { @@ -582,7 +587,8 @@ export class App { sessionId, startDate, endDate, - messageId + messageId, + feedback ) return res.json(chatmessages) }) @@ -640,6 +646,10 @@ export class App { if (sessionId) deleteOptions.sessionId = sessionId if (chatType) deleteOptions.chatType = chatType + // remove all related feedback records + const feedbackDeleteOptions: FindOptionsWhere = { chatId } + await this.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions) + // Delete all uploads corresponding to this chatflow/chatId if (chatId) { try { @@ -654,6 +664,68 @@ export class App { 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 + const chatTypeFilter = chatType.EXTERNAL + + const totalMessages = await this.AppDataSource.getRepository(ChatMessage).count({ + where: { + chatflowid, + chatType: chatTypeFilter + } + }) + + const chatMessageFeedbackRepo = this.AppDataSource.getRepository(ChatMessageFeedback) + + const totalFeedback = await chatMessageFeedbackRepo.count({ where: { chatflowid } }) + const positiveFeedback = await chatMessageFeedbackRepo.countBy({ chatflowid, rating: ChatMessageRatingType.THUMBS_UP }) + + const results = { + totalMessages, + totalFeedback, + positiveFeedback + } + + res.json(results) + }) + // ---------------------------------------- // Credentials // ---------------------------------------- @@ -1622,6 +1694,7 @@ export class App { * @param {string} sessionId * @param {string} startDate * @param {string} endDate + * @param {boolean} feedback */ async getChatMessage( chatflowid: string, @@ -1632,7 +1705,8 @@ export class App { sessionId?: string, startDate?: string, endDate?: string, - messageId?: string + messageId?: string, + feedback?: boolean ): Promise { const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => { const date = new Date(dateTimeStr) @@ -1649,6 +1723,40 @@ export class App { let toDate 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({ where: { chatflowid, @@ -1678,6 +1786,62 @@ export class App { 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 { + 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} chatMessageFeedback + */ + async addChatMessageFeedback(chatMessageFeedback: Partial): Promise { + 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} chatMessageFeedback + */ + async updateChatMessageFeedback(id: string, chatMessageFeedback: Partial) { + 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) { try { const chatflowid = req.params.id diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index f16512471..7941ccf5c 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -1,8 +1,9 @@ import client from './client' 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 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 getStoragePath = () => client.get(`/get-upload-path`) diff --git a/packages/ui/src/api/feedback.js b/packages/ui/src/api/feedback.js new file mode 100644 index 000000000..5b32ca26d --- /dev/null +++ b/packages/ui/src/api/feedback.js @@ -0,0 +1,7 @@ +import client from './client' + +const getStatsFromChatflow = (id) => client.get(`/stats/${id}`) + +export default { + getStatsFromChatflow +} diff --git a/packages/ui/src/ui-component/cards/StatsCard.js b/packages/ui/src/ui-component/cards/StatsCard.js new file mode 100644 index 000000000..57e8205df --- /dev/null +++ b/packages/ui/src/ui-component/cards/StatsCard.js @@ -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 ( + + + + {title} + + + {stat} + + + + ) +} + +StatsCard.propTypes = { + title: PropTypes.string, + stat: PropTypes.string +} + +export default StatsCard diff --git a/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.js b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.js new file mode 100644 index 000000000..493f1031b --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.js @@ -0,0 +1,107 @@ +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' + +// material-ui +import { Button, 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 useNotifier from 'utils/useNotifier' + +// API +import chatflowsApi from 'api/chatflows' + +const ChatFeedback = ({ dialogProps }) => { + 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) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } 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) => ( + + ) + } + }) + } + } + + 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]) + + return ( + <> + + + + + Save + + + ) +} + +ChatFeedback.propTypes = { + dialogProps: PropTypes.object +} + +export default ChatFeedback diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.js b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.js index bc2f6451f..13ca4a15e 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.js +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.js @@ -5,6 +5,7 @@ import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/materia import SpeechToText from './SpeechToTextDialog' import Configuration from 'views/chatflows/Configuration' import AllowedDomains from './AllowedDomainsDialog' +import ChatFeedback from './ChatFeedbackDialog' const CHATFLOW_CONFIGURATION_TABS = [ { @@ -86,6 +87,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { {item.id === 'rateLimiting' && } {item.id === 'speechToText' ? : null} + {item.id === 'chatFeedback' ? : null} {item.id === 'allowedDomains' ? : null} ))} diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 7456aa818..a0b3e396e 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -39,12 +39,15 @@ 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' +import StatsCard from 'ui-component/cards/StatsCard' +import Feedback from 'ui-component/extended/Feedback' // store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' // API import chatmessageApi from 'api/chatmessage' +import feedbackApi from 'api/feedback' import useApi from 'hooks/useApi' import useConfirm from 'hooks/useConfirm' @@ -91,6 +94,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [chatlogs, setChatLogs] = useState([]) const [allChatlogs, setAllChatLogs] = useState([]) const [chatMessages, setChatMessages] = useState([]) + const [stats, setStats] = useState([]) const [selectedMessageIndex, setSelectedMessageIndex] = useState(0) const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) @@ -100,6 +104,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) + const getStatsApi = useApi(feedbackApi.getStatsFromChatflow) const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath) let storagePath = '' @@ -162,6 +167,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments) if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools) if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations) + if (chatmsg.feedback) msg.feedback = chatmsg.feedback?.content if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) { obj[chatPK] = { @@ -238,6 +244,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } }) getChatmessageApi.request(chatflowid) + getStatsApi.request(chatflowid) // update stats } catch (error) { const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` enqueueSnackbar({ @@ -405,9 +412,16 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getChatmessageApi.data]) + useEffect(() => { + if (getStatsApi.data) { + setStats(getStatsApi.data) + } + }, [getStatsApi.data]) + useEffect(() => { if (dialogProps.chatflow) { getChatmessageApi.request(dialogProps.chatflow.id) + getStatsApi.request(dialogProps.chatflow.id) } return () => { @@ -449,7 +463,33 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { <> -
+
+ + + +
+
From Date { })}
)} + {message.type === 'apiMessage' && message.feedback ? ( + + ) : null}
) diff --git a/packages/ui/src/ui-component/extended/Feedback.js b/packages/ui/src/ui-component/extended/Feedback.js new file mode 100644 index 000000000..5a943ec57 --- /dev/null +++ b/packages/ui/src/ui-component/extended/Feedback.js @@ -0,0 +1,71 @@ +import { Alert, IconButton } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import PropTypes from 'prop-types' + +const ThumbsUpIcon = () => { + return ( + + + + + ) +} + +const ThumbsDownIcon = () => { + return ( + + + + + ) +} + +const Feedback = ({ content, rating }) => { + const theme = useTheme() + + return ( +
+ {content ? ( + : } + severity={rating === 'THUMBS_UP' ? 'success' : 'error'} + style={{ marginBottom: 14 }} + variant='outlined' + > + {content ? {content} : null} + + ) : ( + + {rating === 'THUMBS_UP' ? : } + + )} +
+ ) +} + +Feedback.propTypes = { + rating: PropTypes.oneOf(['THUMBS_UP', 'THUMBS_DOWN']), + content: PropTypes.string +} + +export default Feedback diff --git a/packages/ui/src/ui-component/switch/Switch.js b/packages/ui/src/ui-component/switch/Switch.js index 16a923f1c..50ace1197 100644 --- a/packages/ui/src/ui-component/switch/Switch.js +++ b/packages/ui/src/ui-component/switch/Switch.js @@ -1,13 +1,21 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' 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) + useEffect(() => { + setMyValue(value) + }, [value]) + return ( <> - + + {label && {label}} { } SwitchInput.propTypes = { + label: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), onChange: PropTypes.func, disabled: PropTypes.bool