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 ? ( + + + Provide additional feedback + + + + + + + + + + Submit Feedback + + + + ) : 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) => {