diff --git a/README.md b/README.md index 6ada9a2f..7c5473de 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Flowise has 3 different modules in a single mono repository. ### Prerequisite -- Install [Yarn v1](https://pnpm.io/installation) +- Install [PNPM](https://pnpm.io/installation) ```bash npm i -g pnpm ``` diff --git a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts index 16493adc..94c1e14f 100644 --- a/packages/components/nodes/chains/ConversationChain/ConversationChain.ts +++ b/packages/components/nodes/chains/ConversationChain/ConversationChain.ts @@ -199,14 +199,15 @@ const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: MessageConte const messages: BaseMessagePromptTemplateLike[] = [ SystemMessagePromptTemplate.fromTemplate(prompt ? prompt : systemMessage), - new MessagesPlaceholder(memory.memoryKey ?? 'chat_history') + new MessagesPlaceholder(memory.memoryKey ?? 'chat_history'), + HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`) ] // OpenAI works better when separate images into standalone human messages if (model instanceof ChatOpenAI && humanImageMessages.length) { - messages.push(HumanMessagePromptTemplate.fromTemplate(`{${inputKey}}`)) messages.push(new HumanMessage({ content: [...humanImageMessages] })) } else if (humanImageMessages.length) { + messages.pop() messages.push(HumanMessagePromptTemplate.fromTemplate([`{${inputKey}}`, ...humanImageMessages])) } diff --git a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts index 31d78270..ab5bc0df 100644 --- a/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts +++ b/packages/components/nodes/chatmodels/AWSBedrock/AWSChatBedrock.ts @@ -6,10 +6,6 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' /** - * I had to run the following to build the component - * and get the icon copied over to the dist directory - * Flowise/packages/components > yarn build - * * @author Michael Connor */ class AWSChatBedrock_ChatModels implements INode { diff --git a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts index 7b095fb9..4516e044 100644 --- a/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts +++ b/packages/components/nodes/llms/AWSBedrock/AWSBedrock.ts @@ -6,10 +6,6 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../ import { BaseBedrockInput } from '@langchain/community/dist/utils/bedrock' /** - * I had to run the following to build the component - * and get the icon copied over to the dist directory - * Flowise/packages/components > yarn build - * * @author Michael Connor */ class AWSBedrock_LLMs implements INode { diff --git a/packages/components/nodes/llms/HuggingFaceInference/core.ts b/packages/components/nodes/llms/HuggingFaceInference/core.ts index 0d74bbe7..eb99d4a3 100644 --- a/packages/components/nodes/llms/HuggingFaceInference/core.ts +++ b/packages/components/nodes/llms/HuggingFaceInference/core.ts @@ -107,7 +107,7 @@ export class HuggingFaceInference extends LLM implements HFInput { const { HfInference } = await import('@huggingface/inference') return { HfInference } } catch (e) { - throw new Error('Please install huggingface as a dependency with, e.g. `yarn add @huggingface/inference`') + throw new Error('Please install huggingface as a dependency with, e.g. `pnpm add @huggingface/inference`') } } } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c2b5f9ad..a976e6a1 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) }) @@ -703,19 +723,41 @@ export class App { // 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 + let chatTypeFilter = req.query?.chatType as chatType | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined - const totalMessages = await this.AppDataSource.getRepository(ChatMessage).count({ - where: { - chatflowid, - chatType: chatTypeFilter + 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 chatMessageFeedbackRepo = this.AppDataSource.getRepository(ChatMessageFeedback) + const chatmessages = (await this.getChatMessage( + chatflowid, + chatTypeFilter, + undefined, + undefined, + undefined, + undefined, + startDate, + endDate, + '', + true + )) as Array + const totalMessages = chatmessages.length - const totalFeedback = await chatMessageFeedbackRepo.count({ where: { chatflowid } }) - const positiveFeedback = await chatMessageFeedbackRepo.countBy({ chatflowid, rating: ChatMessageRatingType.THUMBS_UP }) + const totalFeedback = chatmessages.filter((message) => message?.feedback).length + const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length const results = { totalMessages, diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index cd8a7e12..96cbcd6d 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -162,39 +162,22 @@ export const constructGraphs = ( * @param {string} endNodeId */ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) => { - const visited = new Set() - const queue: Array<[string, number]> = [[endNodeId, 0]] const depthQueue: IDepthQueue = { [endNodeId]: 0 } - let maxDepth = 0 - let startingNodeIds: string[] = [] - - while (queue.length > 0) { - const [currentNode, depth] = queue.shift()! - - if (visited.has(currentNode)) { - continue - } - - visited.add(currentNode) - - if (depth > maxDepth) { - maxDepth = depth - startingNodeIds = [currentNode] - } else if (depth === maxDepth) { - startingNodeIds.push(currentNode) - } - - for (const neighbor of graph[currentNode]) { - if (!visited.has(neighbor)) { - queue.push([neighbor, depth + 1]) - depthQueue[neighbor] = depth + 1 - } - } + // Assuming that this is a directed acyclic graph, there will be no infinite loop problem. + const walkGraph = (nodeId: string) => { + const depth = depthQueue[nodeId] + graph[nodeId].flatMap((id) => { + depthQueue[id] = Math.max(depthQueue[id] ?? 0, depth + 1) + walkGraph(id) + }) } + walkGraph(endNodeId) + + const maxDepth = Math.max(...Object.values(depthQueue)) const depthQueueReversed: IDepthQueue = {} for (const nodeId in depthQueue) { if (Object.prototype.hasOwnProperty.call(depthQueue, nodeId)) { @@ -202,6 +185,10 @@ export const getStartingNodes = (graph: INodeDirectedGraph, endNodeId: string) = } } + const startingNodeIds = Object.entries(depthQueueReversed) + .filter(([_, depth]) => depth === 0) + .map(([id, _]) => id) + return { startingNodeIds, depthQueue: depthQueueReversed } } diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index 7941ccf5..8760ce07 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 00000000..1916cfed --- /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/api/feedback.js b/packages/ui/src/api/feedback.js index 5b32ca26..4c2becf8 100644 --- a/packages/ui/src/api/feedback.js +++ b/packages/ui/src/api/feedback.js @@ -1,6 +1,6 @@ import client from './client' -const getStatsFromChatflow = (id) => client.get(`/stats/${id}`) +const getStatsFromChatflow = (id, params) => client.get(`/stats/${id}`, { params: { ...params } }) export default { getStatsFromChatflow diff --git a/packages/ui/src/ui-component/button/CopyToClipboardButton.jsx b/packages/ui/src/ui-component/button/CopyToClipboardButton.jsx new file mode 100644 index 00000000..ee9cc752 --- /dev/null +++ b/packages/ui/src/ui-component/button/CopyToClipboardButton.jsx @@ -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.jsx b/packages/ui/src/ui-component/button/ThumbsDownButton.jsx new file mode 100644 index 00000000..6ee9e09a --- /dev/null +++ b/packages/ui/src/ui-component/button/ThumbsDownButton.jsx @@ -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.jsx b/packages/ui/src/ui-component/button/ThumbsUpButton.jsx new file mode 100644 index 00000000..c1b5106e --- /dev/null +++ b/packages/ui/src/ui-component/button/ThumbsUpButton.jsx @@ -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/ChatFeedback.jsx b/packages/ui/src/ui-component/dialog/ChatFeedback.jsx index 3a15e8a1..1706d624 100644 --- a/packages/ui/src/ui-component/dialog/ChatFeedback.jsx +++ b/packages/ui/src/ui-component/dialog/ChatFeedback.jsx @@ -1,7 +1,6 @@ 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' @@ -12,6 +11,7 @@ import { StyledButton } from '@/ui-component/button/StyledButton' import { SwitchInput } from '@/ui-component/switch/Switch' // store +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' import useNotifier from '@/utils/useNotifier' // API diff --git a/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.jsx b/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.jsx new file mode 100644 index 00000000..6b34aeca --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ChatFeedbackContentDialog.jsx @@ -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 ? ( + + + Provide additional feedback + + + + + + + + + + Submit Feedback + + + + ) : null + + return createPortal(component, portalElement) +} + +export default ChatFeedbackContentDialog diff --git a/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx new file mode 100644 index 00000000..1706d624 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx @@ -0,0 +1,107 @@ +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' + +// 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 { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' +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/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 5dafb322..80d0b45f 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -115,6 +115,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { endDate: endDate, chatType: chatTypeFilter.length ? chatTypeFilter : undefined }) + getStatsApi.request(dialogProps.chatflow.id, { + startDate: date, + endDate: endDate, + chatType: chatTypeFilter.length ? chatTypeFilter : undefined + }) } const onEndDateSelected = (date) => { @@ -124,6 +129,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { startDate: startDate, chatType: chatTypeFilter.length ? chatTypeFilter : undefined }) + getStatsApi.request(dialogProps.chatflow.id, { + endDate: date, + startDate: startDate, + chatType: chatTypeFilter.length ? chatTypeFilter : undefined + }) } const onChatTypeSelected = (chatTypes) => { @@ -133,6 +143,11 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { startDate: startDate, endDate: endDate }) + getStatsApi.request(dialogProps.chatflow.id, { + chatType: chatTypes.length ? chatTypes : undefined, + startDate: startDate, + endDate: endDate + }) } const exportMessages = async () => { @@ -432,6 +447,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setSelectedMessageIndex(0) setStartDate(new Date().setMonth(new Date().getMonth() - 1)) setEndDate(new Date()) + setStats([]) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -463,23 +479,6 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { <> -
- - - -
{
+
+ + + +
{chatlogs && chatlogs.length == 0 && ( diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index 7ec6a5f7..e083552a 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -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,31 @@ 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} + + + ) : null} {message.fileAnnotations && (
{message.fileAnnotations.map((fileAnnotation, index) => { @@ -993,6 +1119,11 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews )}
setSourceDialogOpen(false)} /> + setShowFeedbackContentDialog(false)} + onConfirm={submitFeedbackContent} + />
) }