Fix merge conflicts

This commit is contained in:
Ilango 2024-03-05 17:12:36 +05:30
commit 00bc63296b
18 changed files with 561 additions and 13 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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`)
}
}

View File

@ -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
]

View File

@ -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`)
}
}

View File

@ -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
]

View File

@ -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";`)
}
}

View File

@ -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
]

View File

@ -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<ChatMessageFeedback> = { 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<ChatMessage[]> {
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<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) {
try {
const chatflowid = req.params.id

View File

@ -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`)

View File

@ -0,0 +1,7 @@
import client from './client'
const getStatsFromChatflow = (id) => client.get(`/stats/${id}`)
export default {
getStatsFromChatflow
}

View File

@ -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

View File

@ -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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<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])
return (
<>
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<SwitchInput label='Enable chat feedback' onChange={handleChange} value={chatFeedbackStatus} />
</Box>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</>
)
}
ChatFeedback.propTypes = {
dialogProps: PropTypes.object
}
export default ChatFeedback

View File

@ -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 }) => {
<TabPanel key={index} value={tabValue} index={index}>
{item.id === 'rateLimiting' && <Configuration />}
{item.id === 'speechToText' ? <SpeechToText dialogProps={dialogProps} /> : null}
{item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null}
{item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null}
</TabPanel>
))}

View File

@ -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 }) => {
</DialogTitle>
<DialogContent>
<>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%', marginBottom: 10 }}>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gap: 10,
marginBottom: 16,
marginLeft: 8,
marginRight: 8
}}
>
<StatsCard title='Total Messages (API/Embed)' 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',
alignItems: 'center',
marginBottom: 16,
marginLeft: 8,
marginRight: 8
}}
>
<div style={{ marginRight: 10 }}>
<b style={{ marginRight: 10 }}>From Date</b>
<DatePicker
@ -812,6 +852,12 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
})}
</div>
)}
{message.type === 'apiMessage' && message.feedback ? (
<Feedback
content={message.feedback?.content || ''}
rating={message.feedback?.rating}
/>
) : null}
</div>
</Box>
)

View File

@ -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

View File

@ -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 (
<>
<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
disabled={disabled}
checked={myValue}
@ -22,6 +30,7 @@ export const SwitchInput = ({ value, onChange, disabled = false }) => {
}
SwitchInput.propTypes = {
label: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
onChange: PropTypes.func,
disabled: PropTypes.bool