diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index f087e40be..c40581bf5 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -595,7 +595,27 @@ export class App {
// Get internal chatmessages from chatflowid
this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
- const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL)
+ 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)
})
diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js
index 7941ccf5c..8760ce077 100644
--- a/packages/ui/src/api/chatmessage.js
+++ b/packages/ui/src/api/chatmessage.js
@@ -1,6 +1,7 @@
import client from './client'
-const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`)
+const getInternalChatmessageFromChatflow = (id, params = {}) =>
+ client.get(`/internal-chatmessage/${id}`, { params: { feedback: true, ...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 } })
diff --git a/packages/ui/src/api/chatmessagefeedback.js b/packages/ui/src/api/chatmessagefeedback.js
new file mode 100644
index 000000000..1916cfeda
--- /dev/null
+++ b/packages/ui/src/api/chatmessagefeedback.js
@@ -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
+}
diff --git a/packages/ui/src/ui-component/button/CopyToClipboardButton.js b/packages/ui/src/ui-component/button/CopyToClipboardButton.js
new file mode 100644
index 000000000..ee9cc7524
--- /dev/null
+++ b/packages/ui/src/ui-component/button/CopyToClipboardButton.js
@@ -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 (
+
+
+
+ )
+}
+
+CopyToClipboardButton.propTypes = {
+ isDisabled: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ onClick: PropTypes.func
+}
+
+export default CopyToClipboardButton
diff --git a/packages/ui/src/ui-component/button/ThumbsDownButton.js b/packages/ui/src/ui-component/button/ThumbsDownButton.js
new file mode 100644
index 000000000..6ee9e09a6
--- /dev/null
+++ b/packages/ui/src/ui-component/button/ThumbsDownButton.js
@@ -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 (
+
+
+
+ )
+}
+
+ThumbsDownButton.propTypes = {
+ isDisabled: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ onClick: PropTypes.func,
+ rating: PropTypes.string
+}
+
+export default ThumbsDownButton
diff --git a/packages/ui/src/ui-component/button/ThumbsUpButton.js b/packages/ui/src/ui-component/button/ThumbsUpButton.js
new file mode 100644
index 000000000..c1b5106e9
--- /dev/null
+++ b/packages/ui/src/ui-component/button/ThumbsUpButton.js
@@ -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 (
+
+
+
+ )
+}
+
+ThumbsUpButton.propTypes = {
+ isDisabled: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ onClick: PropTypes.func,
+ rating: PropTypes.string
+}
+
+export default ThumbsUpButton
diff --git a/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.js b/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.js
new file mode 100644
index 000000000..56d81d141
--- /dev/null
+++ b/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.js
@@ -0,0 +1,73 @@
+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 })
+ }, [show, dispatch])
+
+ const component = show ? (
+
+ ) : null
+
+ return createPortal(component, portalElement)
+}
+
+export default ChatFeedbackContentDialog
diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js
index 75a466d3b..55a157d11 100644
--- a/packages/ui/src/views/chatmessage/ChatMessage.js
+++ b/packages/ui/src/views/chatmessage/ChatMessage.js
@@ -32,9 +32,13 @@ import audioUploadSVG from 'assets/images/wave-sound.jpg'
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
+import ChatFeedbackContentDialog from 'ui-component/dialog/ChatFeedbackContentDialog'
import StarterPromptsCard from 'ui-component/cards/StarterPromptsCard'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
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 './audio-recording.css'
@@ -42,6 +46,7 @@ import './audio-recording.css'
import chatmessageApi from 'api/chatmessage'
import chatflowsApi from 'api/chatflows'
import predictionApi from 'api/prediction'
+import chatmessagefeedbackApi from 'api/chatmessagefeedback'
// Hooks
import useApi from 'hooks/useApi'
@@ -86,6 +91,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([])
+ const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
+ const [feedbackId, setFeedbackId] = useState('')
+ const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false)
// drag & drop and file input
const fileUploadRef = useRef(null)
@@ -318,6 +326,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].message += text
+ allMessages[allMessages.length - 1].feedback = null
return allMessages
})
}
@@ -389,6 +398,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
if (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 (input === '' && data.question) {
@@ -412,10 +429,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
...prevMessages,
{
message: text,
+ id: data?.chatMessageId,
sourceDocuments: data?.sourceDocuments,
usedTools: data?.usedTools,
fileAnnotations: data?.fileAnnotations,
- type: 'apiMessage'
+ type: 'apiMessage',
+ feedback: null
}
])
}
@@ -474,7 +493,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
setChatId(chatId)
const loadedMessages = getChatmessageApi.data.map((message) => {
const obj = {
+ id: message.id,
message: message.content,
+ feedback: message.feedback,
type: message.role
}
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
@@ -527,6 +548,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
})
setStarterPrompts(inputFields)
}
+ if (config.chatFeedback) {
+ setChatFeedbackStatus(config.chatFeedback.status)
+ }
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -604,6 +628,83 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
// eslint-disable-next-line
}, [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 (
{isDragActive && (
@@ -747,6 +848,36 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
{message.message}
+ {message.type === 'apiMessage' && message.id && chatFeedbackStatus ? (
+ <>
+
+ copyMessageToClipboard(message.message)} />
+ {!message.feedback ||
+ message.feedback.rating === '' ||
+ message.feedback.rating === 'THUMBS_UP' ? (
+ onThumbsUpClick(message.id)}
+ />
+ ) : null}
+ {!message.feedback ||
+ message.feedback.rating === '' ||
+ message.feedback.rating === 'THUMBS_DOWN' ? (
+ onThumbsDownClick(message.id)}
+ />
+ ) : null}
+
+ setShowFeedbackContentDialog(false)}
+ onConfirm={submitFeedbackContent}
+ />
+ >
+ ) : null}
{message.fileAnnotations && (
{message.fileAnnotations.map((fileAnnotation, index) => {