From 8bee34ec6bffed8896320f1670681da1a33986dd Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Fri, 14 Feb 2025 16:23:50 +0100 Subject: [PATCH 1/7] feat: working started for feedback implementation. TODO: - needs some refactoring. - needs some UI animations. --- api/models/schema/messageSchema.js | 4 ++ api/server/routes/messages.js | 13 ++++++ .../components/Chat/Messages/HoverButtons.tsx | 43 ++++++++++++++++++- .../Chat/Messages/ui/MessageRender.tsx | 4 ++ client/src/components/svg/ThumbDownIcon.tsx | 19 ++++++++ client/src/components/svg/ThumbUpIcon.tsx | 19 ++++++++ client/src/components/svg/index.ts | 2 + .../src/hooks/Messages/useMessageActions.tsx | 22 ++++++++++ packages/data-provider/src/api-endpoints.ts | 3 ++ packages/data-provider/src/data-service.ts | 8 ++++ .../src/react-query/react-query-service.ts | 22 ++++++++++ packages/data-provider/src/schemas.ts | 2 + 12 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 client/src/components/svg/ThumbDownIcon.tsx create mode 100644 client/src/components/svg/ThumbUpIcon.tsx diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index be711552955..3421434c7e1 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -137,6 +137,10 @@ const messageSchema = mongoose.Schema( expiredAt: { type: Date, }, + feedback: { + type: String, + default: null, + }, }, { timestamps: true }, ); diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index 54c4aab1c2d..07c5e8c6f0b 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -175,6 +175,19 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = } }); +router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => { + try { + const { conversationId, messageId } = req.params; + const { feedback } = req.body; + + const result = await updateMessage(req, { messageId, feedback }); + return res.status(200).json(result); + } catch (error) { + logger.error('Error updating message:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { messageId } = req.params; diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index 9558c5c8f10..341d3c4c298 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -1,7 +1,15 @@ import React, { useState } from 'react'; import { useRecoilState } from 'recoil'; import type { TConversation, TMessage } from 'librechat-data-provider'; -import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '~/components/svg'; +import { + EditIcon, + Clipboard, + CheckMark, + ContinueIcon, + RegenerateIcon, + ThumbUpIcon, + ThumbDownIcon, +} from '~/components/svg'; import { useGenerationsByLatest, useLocalize } from '~/hooks'; import { Fork } from '~/components/Conversations'; import MessageAudio from './MessageAudio'; @@ -20,6 +28,9 @@ type THoverButtons = { latestMessage: TMessage | null; isLast: boolean; index: number; + // Optional props for feedback callbacks + onFeedbackPositive?: () => void; + onFeedbackNegative?: () => void; }; export default function HoverButtons({ @@ -34,6 +45,8 @@ export default function HoverButtons({ handleContinue, latestMessage, isLast, + onFeedbackPositive, + onFeedbackNegative, }: THoverButtons) { const localize = useLocalize(); const { endpoint: _endpoint, endpointType } = conversation ?? {}; @@ -167,6 +180,34 @@ export default function HoverButtons({ ) : null} + {!isCreatedByUser && (onFeedbackPositive || onFeedbackNegative) && ( + <> + {onFeedbackPositive && ( + + )} + {onFeedbackNegative && ( + + )} + + )} ); } diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index dbbee5869f7..fd64c272050 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -50,6 +50,8 @@ const MessageRender = memo( copyToClipboard, setLatestMessage, regenerateMessage, + handleFeedbackPositive, + handleFeedbackNegative, } = useMessageActions({ message: msg, currentEditId, @@ -205,6 +207,8 @@ const MessageRender = memo( handleContinue={handleContinue} latestMessage={latestMessage} isLast={isLast} + onFeedbackPositive={handleFeedbackPositive} + onFeedbackNegative={handleFeedbackNegative} /> )} diff --git a/client/src/components/svg/ThumbDownIcon.tsx b/client/src/components/svg/ThumbDownIcon.tsx new file mode 100644 index 00000000000..9f8435a50af --- /dev/null +++ b/client/src/components/svg/ThumbDownIcon.tsx @@ -0,0 +1,19 @@ +import { cn } from '~/utils'; + +export default function ThumbDownIcon({ className = '', size = '1em' }) { + return ( + + + + + ); +} \ No newline at end of file diff --git a/client/src/components/svg/ThumbUpIcon.tsx b/client/src/components/svg/ThumbUpIcon.tsx new file mode 100644 index 00000000000..5fdc2dad9d0 --- /dev/null +++ b/client/src/components/svg/ThumbUpIcon.tsx @@ -0,0 +1,19 @@ +import { cn } from '~/utils'; + +export default function ThumbUpIcon({ className = '', size = '1em' }) { + return ( + + + + + ); +} \ No newline at end of file diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index 745c5210bde..26c625f3fce 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -56,3 +56,5 @@ export { default as SpeechIcon } from './SpeechIcon'; export { default as SaveIcon } from './SaveIcon'; export { default as CircleHelpIcon } from './CircleHelpIcon'; export { default as BedrockIcon } from './BedrockIcon'; +export { default as ThumbUpIcon } from './ThumbUpIcon'; +export { default as ThumbDownIcon } from './ThumbDownIcon'; diff --git a/client/src/hooks/Messages/useMessageActions.tsx b/client/src/hooks/Messages/useMessageActions.tsx index be80b404be5..4e159c7e470 100644 --- a/client/src/hooks/Messages/useMessageActions.tsx +++ b/client/src/hooks/Messages/useMessageActions.tsx @@ -12,6 +12,7 @@ import useCopyToClipboard from './useCopyToClipboard'; import { useAuthContext } from '~/hooks/AuthContext'; import useLocalize from '~/hooks/useLocalize'; import store from '~/store'; +import { useUpdateFeedbackMutation } from 'librechat-data-provider/react-query'; export type TMessageActions = Pick< TMessageProps, @@ -110,6 +111,25 @@ export default function useMessageActions(props: TMessageActions) { } }, [message, agent, assistant, UsernameDisplay, user, localize]); + const feedbackMutation = useUpdateFeedbackMutation(conversation?.conversationId || ''); + const handleFeedbackPositive = useCallback(() => { + if (conversation && message) { + feedbackMutation.mutate({ + messageId: message.messageId, + feedback: 'positive', + }); + } + }, [conversation, message, feedbackMutation]); + + const handleFeedbackNegative = useCallback(() => { + if (conversation && message) { + feedbackMutation.mutate({ + messageId: message.messageId, + feedback: 'negative', + }); + } + }, [conversation, message, feedbackMutation]); + return { ask, edit, @@ -125,5 +145,7 @@ export default function useMessageActions(props: TMessageActions) { copyToClipboard, setLatestMessage, regenerateMessage, + handleFeedbackPositive, + handleFeedbackNegative, }; } diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 27cc221d722..d44da1caacb 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -237,3 +237,6 @@ export const addTagToConversation = (conversationId: string) => export const userTerms = () => '/api/user/terms'; export const acceptUserTerms = () => '/api/user/terms/accept'; export const banner = () => '/api/banner'; + +export const feedback = (conversationId: string, messageId: string) => + `/api/messages/${conversationId}/${messageId}/feedback`; \ No newline at end of file diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 5af00fdcb9f..90c6feab5b3 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -774,3 +774,11 @@ export function acceptTerms(): Promise { export function getBanner(): Promise { return request.get(endpoints.banner()); } + +export function updateFeedback( + conversationId: string, + messageId: string, + feedback: string, +): Promise { + return request.put(endpoints.feedback(conversationId, messageId), { feedback }); +} \ No newline at end of file diff --git a/packages/data-provider/src/react-query/react-query-service.ts b/packages/data-provider/src/react-query/react-query-service.ts index 03a37d99a7f..373a03227cb 100644 --- a/packages/data-provider/src/react-query/react-query-service.ts +++ b/packages/data-provider/src/react-query/react-query-service.ts @@ -376,3 +376,25 @@ export const useGetCustomConfigSpeechQuery = ( }, ); }; + +export const useUpdateFeedbackMutation = ( + conversationId: string, +): UseMutationResult< + unknown, + unknown, + { messageId: string; feedback: string }, + unknown +> => { + const queryClient = useQueryClient(); + + return useMutation( + ({ messageId, feedback }: { messageId: string; feedback: string }) => + dataService.updateFeedback(conversationId, messageId, feedback), + { + onSuccess: () => { + // Invalidate messages for this conversation so that any UI shows the updated feedback. + queryClient.invalidateQueries([QueryKeys.messages, conversationId]); + }, + }, + ); +}; \ No newline at end of file diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 8ec61e5474f..a3525698425 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -483,6 +483,7 @@ export const tMessageSchema = z.object({ thread_id: z.string().optional(), /* frontend components */ iconURL: z.string().nullable().optional(), + feedback: z.string().nullable().optional(), }); export type TAttachmentMetadata = { messageId: string; toolCallId: string }; @@ -502,6 +503,7 @@ export type TMessage = z.input & { siblingIndex?: number; attachments?: TAttachment[]; clientTimestamp?: string; + feedback?: string; }; export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => { From c43ef59eade388b73b24bea2bab704ddfee432ff Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 15 Feb 2025 13:46:14 +0100 Subject: [PATCH 2/7] feat: working rate functionality --- api/models/schema/messageSchema.js | 27 ++++++- api/server/routes/messages.js | 54 +++++++++++++- .../Chat/Messages/FeedbackTagOptions.tsx | 54 ++++++++++++++ .../components/Chat/Messages/HoverButtons.tsx | 74 ++++++++++--------- .../Chat/Messages/ui/MessageRender.tsx | 38 ++++++++-- client/src/components/svg/ThumbDownIcon.tsx | 22 +++++- client/src/components/svg/ThumbUpIcon.tsx | 24 +++++- .../src/hooks/Messages/useMessageActions.tsx | 53 +++++++------ client/src/locales/en/translation.json | 2 + packages/data-provider/src/data-service.ts | 6 +- .../src/react-query/react-query-service.ts | 23 ++---- packages/data-provider/src/schemas.ts | 20 ++++- packages/data-provider/src/types.ts | 20 +++++ 13 files changed, 321 insertions(+), 96 deletions(-) create mode 100644 client/src/components/Chat/Messages/FeedbackTagOptions.tsx diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index 3421434c7e1..bfba8f9fde9 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -137,10 +137,35 @@ const messageSchema = mongoose.Schema( expiredAt: { type: Date, }, - feedback: { + rating: { type: String, + enum: ['thumbsUp', 'thumbsDown'], default: null, }, + ratingContent: { + tags: { + type: [String], + default: [], + }, + tagChoices: { + type: [String], + default: [ + 'Shouldn\'t have used Memory', + 'Don\'t like the style', + 'Not factually correct', + 'Didn\'t fully follow instructions', + 'Refused when it shouldn\'t have', + 'Being lazy', + 'Unsafe or problematic', + 'Biased', + 'Other', + ], + }, + text: { + type: String, + default: null, + }, + }, }, { timestamps: true }, ); diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index 07c5e8c6f0b..d87b4c6c260 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -178,12 +178,58 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => { try { const { conversationId, messageId } = req.params; - const { feedback } = req.body; + const { rating, ratingContent } = req.body; + + // Define default tag choices (for thumbsDown only). + const defaultTagChoices = [ + 'Shouldn\'t have used Memory', + 'Don\'t like the style', + 'Not factually correct', + 'Didn\'t fully follow instructions', + 'Refused when it shouldn\'t have', + 'Being lazy', + 'Unsafe or problematic', + 'Biased', + 'Other', + ]; + + // Update the message feedback. + const updatedMessage = await updateMessage(req, { + messageId, + rating, + ratingContent, + }); - const result = await updateMessage(req, { messageId, feedback }); - return res.status(200).json(result); + if (!updatedMessage) { + return res.status(400).json({ error: 'Failed to update feedback' }); + } + + // Build the response ratingContent. + // Start with whatever the updateMessage function returned. + let responseRatingContent = updatedMessage.ratingContent || {}; + + // For thumbsDown, if no tag choices are present, merge the default choices. + if (rating === 'thumbsDown') { + if (!responseRatingContent.tagChoices || responseRatingContent.tagChoices.length === 0) { + responseRatingContent.tagChoices = defaultTagChoices; + } + if (ratingContent && ratingContent.tags && !responseRatingContent.tags) { + responseRatingContent.tags = ratingContent.tags; + } + if (ratingContent && ratingContent.text && !responseRatingContent.text) { + responseRatingContent.text = ratingContent.text; + } + } + + // Return all the feedback details. + return res.status(200).json({ + messageId: updatedMessage.messageId, + conversationId: updatedMessage.conversationId, + rating: updatedMessage.rating || rating, + ratingContent: responseRatingContent, + }); } catch (error) { - logger.error('Error updating message:', error); + logger.error('Error updating message feedback:', error); res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/client/src/components/Chat/Messages/FeedbackTagOptions.tsx b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx new file mode 100644 index 00000000000..0164f3acbaa --- /dev/null +++ b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { cn } from '~/utils'; + +type FeedbackTagOptionsProps = { + tagChoices: string[]; + onSelectTag: (tag: string) => void; +}; + +const FeedbackTagOptions: React.FC = ({ tagChoices, onSelectTag }) => { + return ( + <> +
+
+
+
+ +
Tell us more:
+
+ {tagChoices.map((tag) => ( + + ))} +
+
+
+
+ + ); +}; + +export default FeedbackTagOptions; \ No newline at end of file diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index 341d3c4c298..d4b128c3b38 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useRecoilState } from 'recoil'; -import type { TConversation, TMessage } from 'librechat-data-provider'; +import type { TConversation, TMessage, TMessageFeedback } from 'librechat-data-provider'; import { EditIcon, Clipboard, @@ -28,9 +28,8 @@ type THoverButtons = { latestMessage: TMessage | null; isLast: boolean; index: number; - // Optional props for feedback callbacks - onFeedbackPositive?: () => void; - onFeedbackNegative?: () => void; + handleFeedback: (rating: 'thumbsUp' | 'thumbsDown') => void; + rated: TMessageFeedback | undefined; }; export default function HoverButtons({ @@ -45,8 +44,8 @@ export default function HoverButtons({ handleContinue, latestMessage, isLast, - onFeedbackPositive, - onFeedbackNegative, + handleFeedback, + rated, }: THoverButtons) { const localize = useLocalize(); const { endpoint: _endpoint, endpointType } = conversation ?? {}; @@ -77,6 +76,8 @@ export default function HoverButtons({ const { isCreatedByUser, error } = message; + const safeRated: TMessageFeedback = rated || { rating: null }; + const renderRegenerate = () => { if (!regenerateEnabled) { return null; @@ -127,6 +128,37 @@ export default function HoverButtons({ )} /> )} + {!isCreatedByUser && ( + <> + {safeRated.rating !== 'thumbsDown' && ( + + )} + + {safeRated.rating !== 'thumbsUp' && ( + + )} + + )} {isEditableEndpoint && ( ) : null} - {!isCreatedByUser && (onFeedbackPositive || onFeedbackNegative) && ( - <> - {onFeedbackPositive && ( - - )} - {onFeedbackNegative && ( - - )} - - )} ); -} +} \ No newline at end of file diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index fd64c272050..6cd8c5d234f 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -1,7 +1,8 @@ +import React, { useState, useCallback, useMemo, memo } from 'react'; import { useRecoilValue } from 'recoil'; -import { useCallback, useMemo, memo } from 'react'; import type { TMessage } from 'librechat-data-provider'; import type { TMessageProps, TMessageIcon } from '~/common'; +import FeedbackTagOptions from '~/components/Chat/Messages/FeedbackTagOptions'; import MessageContent from '~/components/Chat/Messages/Content/MessageContent'; import PlaceholderRow from '~/components/Chat/Messages/ui/PlaceholderRow'; import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch'; @@ -50,8 +51,8 @@ const MessageRender = memo( copyToClipboard, setLatestMessage, regenerateMessage, - handleFeedbackPositive, - handleFeedbackNegative, + handleFeedback, + rated, } = useMessageActions({ message: msg, currentEditId, @@ -60,6 +61,10 @@ const MessageRender = memo( }); const fontSize = useRecoilValue(store.fontSize); const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace); + + const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); + const [showThankYou, setShowThankYou] = useState(false); + const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]); const { isCreatedByUser, error, unfinished } = msg ?? {}; const hasNoChildren = !(msg?.children?.length ?? 0); @@ -207,15 +212,36 @@ const MessageRender = memo( handleContinue={handleContinue} latestMessage={latestMessage} isLast={isLast} - onFeedbackPositive={handleFeedbackPositive} - onFeedbackNegative={handleFeedbackNegative} + handleFeedback={handleFeedback} + rated={rated} /> )} + {!isCreatedByUser && rated?.rating === 'thumbsDown' && isLatestMessage && ( + + {!feedbackSubmitted ? ( + { + handleFeedback('thumbsDown', { ratingContent: { tags: [tag] } }); + setFeedbackSubmitted(true); + setShowThankYou(true); + setTimeout(() => { + setShowThankYou(false); + }, 3000); + }} + /> + ) : showThankYou ? ( +
+
Thanks for your feedback!
+
+ ) : null} +
+ )} ); }, ); -export default MessageRender; +export default MessageRender; \ No newline at end of file diff --git a/client/src/components/svg/ThumbDownIcon.tsx b/client/src/components/svg/ThumbDownIcon.tsx index 9f8435a50af..55ece5a4605 100644 --- a/client/src/components/svg/ThumbDownIcon.tsx +++ b/client/src/components/svg/ThumbDownIcon.tsx @@ -1,14 +1,30 @@ import { cn } from '~/utils'; -export default function ThumbDownIcon({ className = '', size = '1em' }) { - return ( +export default function ThumbDownIcon({ className = '', size = '1em', bold = false }) { + return bold ? ( + + + + ) : ( + + + + + ) : ( + ); -} \ No newline at end of file +} diff --git a/client/src/hooks/Messages/useMessageActions.tsx b/client/src/hooks/Messages/useMessageActions.tsx index 4e159c7e470..538c6c71cf9 100644 --- a/client/src/hooks/Messages/useMessageActions.tsx +++ b/client/src/hooks/Messages/useMessageActions.tsx @@ -1,6 +1,11 @@ import { useRecoilValue } from 'recoil'; -import { useCallback, useMemo } from 'react'; -import { isAssistantsEndpoint, isAgentsEndpoint } from 'librechat-data-provider'; +import { useCallback, useMemo, useState } from 'react'; +import { + isAssistantsEndpoint, + isAgentsEndpoint, + TMessageFeedback, + TUpdateFeedbackRequest, +} from 'librechat-data-provider'; import type { TMessageProps } from '~/common'; import { useChatContext, @@ -47,6 +52,7 @@ export default function useMessageActions(props: TMessageActions) { const { text, content, messageId = null, isCreatedByUser } = message ?? {}; const edit = useMemo(() => messageId === currentEditId, [messageId, currentEditId]); + const [rated, setRated] = useState({ rating: null }); const enterEdit = useCallback( (cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel === true ? -1 : messageId), @@ -111,24 +117,27 @@ export default function useMessageActions(props: TMessageActions) { } }, [message, agent, assistant, UsernameDisplay, user, localize]); - const feedbackMutation = useUpdateFeedbackMutation(conversation?.conversationId || ''); - const handleFeedbackPositive = useCallback(() => { - if (conversation && message) { - feedbackMutation.mutate({ - messageId: message.messageId, - feedback: 'positive', - }); - } - }, [conversation, message, feedbackMutation]); - - const handleFeedbackNegative = useCallback(() => { - if (conversation && message) { - feedbackMutation.mutate({ - messageId: message.messageId, - feedback: 'negative', - }); - } - }, [conversation, message, feedbackMutation]); + const feedbackMutation = useUpdateFeedbackMutation( + conversation?.conversationId || '', + message?.messageId || '', + ); + + // Updated: Always send feedback update if conversation and message exist. + const handleFeedback = useCallback( + (rating: 'thumbsUp' | 'thumbsDown', extraPayload?: Partial) => { + if (conversation?.conversationId && message?.messageId) { + feedbackMutation.mutate( + { rating, ...extraPayload }, + { + onSuccess: (data) => { + setRated(data); + }, + } + ); + } + }, + [conversation?.conversationId, message?.messageId, feedbackMutation] + ); return { ask, @@ -145,7 +154,7 @@ export default function useMessageActions(props: TMessageActions) { copyToClipboard, setLatestMessage, regenerateMessage, - handleFeedbackPositive, - handleFeedbackNegative, + handleFeedback, + rated, }; } diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 1daa6794d6f..9278c53050c 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -792,6 +792,8 @@ "com_ui_version_var": "Version {{0}}", "com_ui_versions": "Versions", "com_ui_view_source": "View source chat", + "com_ui_feedback_positive": "Good response", + "com_ui_feedback_negative": "Bad response", "com_ui_write": "Writing", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 90c6feab5b3..7a091d45cb2 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -778,7 +778,7 @@ export function getBanner(): Promise { export function updateFeedback( conversationId: string, messageId: string, - feedback: string, -): Promise { - return request.put(endpoints.feedback(conversationId, messageId), { feedback }); + payload: t.TUpdateFeedbackRequest, +): Promise { + return request.put(endpoints.feedback(conversationId, messageId), payload); } \ No newline at end of file diff --git a/packages/data-provider/src/react-query/react-query-service.ts b/packages/data-provider/src/react-query/react-query-service.ts index 373a03227cb..43a9ddeb565 100644 --- a/packages/data-provider/src/react-query/react-query-service.ts +++ b/packages/data-provider/src/react-query/react-query-service.ts @@ -379,22 +379,13 @@ export const useGetCustomConfigSpeechQuery = ( export const useUpdateFeedbackMutation = ( conversationId: string, -): UseMutationResult< - unknown, - unknown, - { messageId: string; feedback: string }, - unknown -> => { + messageId: string, +): UseMutationResult => { const queryClient = useQueryClient(); - - return useMutation( - ({ messageId, feedback }: { messageId: string; feedback: string }) => - dataService.updateFeedback(conversationId, messageId, feedback), - { - onSuccess: () => { - // Invalidate messages for this conversation so that any UI shows the updated feedback. - queryClient.invalidateQueries([QueryKeys.messages, conversationId]); - }, + return useMutation((payload: t.TUpdateFeedbackRequest) => + dataService.updateFeedback(conversationId, messageId,payload), { + onSuccess: () => { + queryClient.invalidateQueries([QueryKeys.messages, messageId]); }, - ); + }); }; \ No newline at end of file diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index a3525698425..7dcde4214b6 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -450,6 +450,15 @@ export const tAgentOptionsSchema = z.object({ temperature: z.number().default(agentOptionSettings.temperature.default), }); +export type TMessageFeedback = { + rating: 'thumbsUp' | 'thumbsDown' | null; + ratingContent?: { + tags?: string[]; + tagChoices?: string[]; + text?: string; + }; +}; + export const tMessageSchema = z.object({ messageId: z.string(), endpoint: z.string().optional(), @@ -483,7 +492,15 @@ export const tMessageSchema = z.object({ thread_id: z.string().optional(), /* frontend components */ iconURL: z.string().nullable().optional(), - feedback: z.string().nullable().optional(), + rating: z.enum(['thumbsUp', 'thumbsDown']).nullable().optional(), + ratingContent: z + .object({ + tags: z.array(z.string()).optional(), + tagChoices: z.array(z.string()).optional(), + text: z.string().optional(), + }) + .nullable() + .optional(), }); export type TAttachmentMetadata = { messageId: string; toolCallId: string }; @@ -503,7 +520,6 @@ export type TMessage = z.input & { siblingIndex?: number; attachments?: TAttachment[]; clientTimestamp?: string; - feedback?: string; }; export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => { diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index bf31a48cc05..e2b12223137 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -473,3 +473,23 @@ export type TAcceptTermsResponse = { }; export type TBannerResponse = TBanner | null; + +export type TUpdateFeedbackRequest = { + rating: 'thumbsUp' | 'thumbsDown'; + ratingContent?: { + tags?: string[]; + tagChoices?: string[]; + text?: string; + }; +}; + +export type TUpdateFeedbackResponse = { + messageId: string; + conversationId: string; + rating: 'thumbsUp' | 'thumbsDown'; + ratingContent?: { + tags?: string[]; + tagChoices?: string[]; + text?: string; + }; +}; \ No newline at end of file From fba52632b221045108339e4f54741904cda5b888 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 15 Feb 2025 14:31:55 +0100 Subject: [PATCH 3/7] feat: works now as well to reader the already rated responses from the server. --- .../components/Chat/Messages/HoverButtons.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/src/components/Chat/Messages/HoverButtons.tsx b/client/src/components/Chat/Messages/HoverButtons.tsx index d4b128c3b38..76a519cbcf2 100644 --- a/client/src/components/Chat/Messages/HoverButtons.tsx +++ b/client/src/components/Chat/Messages/HoverButtons.tsx @@ -28,7 +28,7 @@ type THoverButtons = { latestMessage: TMessage | null; isLast: boolean; index: number; - handleFeedback: (rating: 'thumbsUp' | 'thumbsDown') => void; + handleFeedback: (rating: 'thumbsUp' | 'thumbsDown', extraPayload?: any) => void; rated: TMessageFeedback | undefined; }; @@ -78,6 +78,10 @@ export default function HoverButtons({ const safeRated: TMessageFeedback = rated || { rating: null }; + // Use != null so that both null and undefined are treated as "no rating" + const currentRating = message.rating != null ? message.rating : safeRated.rating; + const disableFeedback = message.rating != null || safeRated.rating != null; + const renderRegenerate = () => { if (!regenerateEnabled) { return null; @@ -130,7 +134,7 @@ export default function HoverButtons({ )} {!isCreatedByUser && ( <> - {safeRated.rating !== 'thumbsDown' && ( + {currentRating !== 'thumbsDown' && ( )} - {safeRated.rating !== 'thumbsUp' && ( + {currentRating !== 'thumbsUp' && ( )} From 6bb348dea4d4db41d580986a378f6be929818b53 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 15 Feb 2025 16:38:14 +0100 Subject: [PATCH 4/7] feat: added the option to give feedback in text (optional) --- .../Chat/Messages/FeedbackTagOptions.tsx | 135 ++++++++++++++---- .../Chat/Messages/ui/MessageRender.tsx | 8 +- 2 files changed, 115 insertions(+), 28 deletions(-) diff --git a/client/src/components/Chat/Messages/FeedbackTagOptions.tsx b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx index 0164f3acbaa..29d8c7f8975 100644 --- a/client/src/components/Chat/Messages/FeedbackTagOptions.tsx +++ b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx @@ -1,52 +1,135 @@ -import React from 'react'; +import React, { useState } from 'react'; import { cn } from '~/utils'; +import { + OGDialog, + OGDialogContent, + OGDialogTrigger, + OGDialogHeader, + OGDialogTitle, +} from '~/components'; type FeedbackTagOptionsProps = { tagChoices: string[]; - onSelectTag: (tag: string) => void; + onSelectTag: (tag: string, text?: string) => void; }; const FeedbackTagOptions: React.FC = ({ tagChoices, onSelectTag }) => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [selectedTag, setSelectedTag] = useState(null); + const [text, setText] = useState(''); + + const inlineOptions = tagChoices.slice(0, 3); + const hasMore = tagChoices.length > 3; + + const handleInlineTagClick = (tag: string) => { + onSelectTag(tag); + }; + + const handleSubmit = () => { + if (selectedTag) { + onSelectTag(selectedTag, text); + setIsDialogOpen(false); + } + }; + return ( <> -
-
-
+
+
-
Tell us more:
- {tagChoices.map((tag) => ( + {inlineOptions.map((tag) => ( ))} + {hasMore && ( + + )}
+ + {/* Dialog for additional feedback */} + + + {/* Invisible trigger */} + + + + + + Provide additional feedback + + +
+
+
+ {tagChoices.map((tag) => ( + + ))} +
+
+
+ setText(e.target.value)} + /> +
+
+
+
+ +
+
+
+
); }; diff --git a/client/src/components/Chat/Messages/ui/MessageRender.tsx b/client/src/components/Chat/Messages/ui/MessageRender.tsx index 6cd8c5d234f..6d410d0a807 100644 --- a/client/src/components/Chat/Messages/ui/MessageRender.tsx +++ b/client/src/components/Chat/Messages/ui/MessageRender.tsx @@ -222,8 +222,12 @@ const MessageRender = memo( {!feedbackSubmitted ? ( { - handleFeedback('thumbsDown', { ratingContent: { tags: [tag] } }); + onSelectTag={(tag, text) => { + const ratingContent = { + tags: [tag], + ...(text ? { text } : {}), + }; + handleFeedback('thumbsDown', { ratingContent }); setFeedbackSubmitted(true); setShowThankYou(true); setTimeout(() => { From aa651bdfc7568928b5c5563c253fd71e2daf524f Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 15 Feb 2025 16:40:07 +0100 Subject: [PATCH 5/7] feat: added Dismiss option `x` to the `FeedbackTagOptions` --- .../Chat/Messages/FeedbackTagOptions.tsx | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/client/src/components/Chat/Messages/FeedbackTagOptions.tsx b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx index 29d8c7f8975..04433afbe9a 100644 --- a/client/src/components/Chat/Messages/FeedbackTagOptions.tsx +++ b/client/src/components/Chat/Messages/FeedbackTagOptions.tsx @@ -17,6 +17,7 @@ const FeedbackTagOptions: React.FC = ({ tagChoices, onS const [isDialogOpen, setIsDialogOpen] = useState(false); const [selectedTag, setSelectedTag] = useState(null); const [text, setText] = useState(''); + const [isDismissed, setIsDismissed] = useState(false); const inlineOptions = tagChoices.slice(0, 3); const hasMore = tagChoices.length > 3; @@ -34,38 +35,50 @@ const FeedbackTagOptions: React.FC = ({ tagChoices, onS return ( <> -
-
-
-
Tell us more:
-
- {inlineOptions.map((tag) => ( + {!isDismissed && ( +
+
+
+
+
Tell us more:
- ))} - {hasMore && ( - - )} +
+
+ {inlineOptions.map((tag) => ( + + ))} + {hasMore && ( + + )} +
-
+ )} {/* Dialog for additional feedback */} From fdd6977934d6f2a9e2cca1acb612197be6df3143 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 8 Mar 2025 21:27:20 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20rating=20and=20ra?= =?UTF-8?q?tingContent=20fields=20to=20message=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/schema/messageSchema.js | 30 ------------------ packages/data-schemas/src/schema/message.ts | 35 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/api/models/schema/messageSchema.js b/api/models/schema/messageSchema.js index caefee67cba..cf97b84eeae 100644 --- a/api/models/schema/messageSchema.js +++ b/api/models/schema/messageSchema.js @@ -2,36 +2,6 @@ const mongoose = require('mongoose'); const mongoMeili = require('~/models/plugins/mongoMeili'); const { messageSchema } = require('@librechat/data-schemas'); -// Needs to be moved to the new package -// rating: { -// type: String, -// enum: ['thumbsUp', 'thumbsDown'], -// default: null, -// }, -// ratingContent: { -// tags: { -// type: [String], -// default: [], -// }, -// tagChoices: { -// type: [String], -// default: [ -// 'Shouldn\'t have used Memory', -// 'Don\'t like the style', -// 'Not factually correct', -// 'Didn\'t fully follow instructions', -// 'Refused when it shouldn\'t have', -// 'Being lazy', -// 'Unsafe or problematic', -// 'Biased', -// 'Other', -// ], -// }, -// text: { -// type: String, -// default: null, -// }, - if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) { messageSchema.plugin(mongoMeili, { host: process.env.MEILI_HOST, diff --git a/packages/data-schemas/src/schema/message.ts b/packages/data-schemas/src/schema/message.ts index 82970de6de0..f62843b3701 100644 --- a/packages/data-schemas/src/schema/message.ts +++ b/packages/data-schemas/src/schema/message.ts @@ -20,6 +20,12 @@ export interface IMessage extends Document { unfinished?: boolean; error?: boolean; finish_reason?: string; + rating?: 'thumbsUp' | 'thumbsDown' | null; + ratingContent?: { + tags: string[]; + tagChoices: string[]; + text?: string | null; + }; _meiliIndex?: boolean; files?: unknown[]; plugin?: { @@ -110,6 +116,35 @@ const messageSchema: Schema = new Schema( finish_reason: { type: String, }, + rating: { + type: String, + enum: ['thumbsUp', 'thumbsDown'], + default: null, + }, + ratingContent: { + tags: { + type: [String], + default: [], + }, + tagChoices: { + type: [String], + default: [ + 'Shouldn\'t have used Memory', + 'Don\'t like the style', + 'Not factually correct', + 'Didn\'t fully follow instructions', + 'Refused when it shouldn\'t have', + 'Being lazy', + 'Unsafe or problematic', + 'Biased', + 'Other', + ], + }, + text: { + type: String, + default: null, + }, + }, _meiliIndex: { type: Boolean, required: false, From a92498602b402e8287f401fb55c4f66d803bc8a7 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 8 Mar 2025 21:30:25 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=94=A7=20chore:=20Bump=20version=20to?= =?UTF-8?q?=200.0.3=20in=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-schemas/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 82f398e31b2..4e3c7798542 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.2", + "version": "0.0.3", "type": "module", "description": "Mongoose schemas and models for LibreChat", "main": "dist/index.cjs",