mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 10:50:14 +01:00
📈 feat: Chat rating for feedback (#5878)
* feat: working started for feedback implementation. TODO: - needs some refactoring. - needs some UI animations. * feat: working rate functionality * feat: works now as well to reader the already rated responses from the server. * feat: added the option to give feedback in text (optional) * feat: added Dismiss option `x` to the `FeedbackTagOptions` * ✨ feat: Add rating and ratingContent fields to message schema * 🔧 chore: Bump version to 0.0.3 in package.json * ✨ feat: Enhance feedback localization and update UI elements * 🚀 feat: Implement feedback tagging system with thumbs up/down options * 🚀 feat: Add data-provider package to unused i18n keys detection * 🎨 style: update HoverButtons' style * 🎨 style: Update HoverButtons and Fork components for improved styling and visibility * 🔧 feat: Implement feedback system with rating and content options * 🔧 feat: Enhance feedback handling with improved rating toggle and tag options * 🔧 feat: Integrate toast notifications for feedback submission and clean up unused state * 🔧 feat: Remove unused feedback tag options from translation file * ✨ refactor: clean up Feedback component and improve HoverButtons structure * ✨ refactor: remove unused settings switches for auto scroll, hide side panel, and user message markdown * refactor: reorganize import order * ✨ refactor: enhance HoverButtons and Fork components with improved styles and animations * ✨ refactor: update feedback response phrases for improved user engagement * ✨ refactor: add CheckboxOption component and streamline fork options rendering * Refactor feedback components and logic - Consolidated feedback handling into a single Feedback component, removing FeedbackButtons and FeedbackTagOptions. - Introduced new feedback tagging system with detailed tags for both thumbs up and thumbs down ratings. - Updated feedback schema to include new tags and improved type definitions. - Enhanced user interface for feedback collection, including a dialog for additional comments. - Removed obsolete files and adjusted imports accordingly. - Updated translations for new feedback tags and placeholders. * ✨ refactor: update feedback handling by replacing rating fields with feedback in message updates * fix: add missing validateMessageReq middleware to feedback route and refactor feedback system * 🗑️ chore: Remove redundant fork option explanations from translation file * 🔧 refactor: Remove unused dependency from feedback callback * 🔧 refactor: Simplify message update response structure and improve error logging * Chore: removed unused tests. --------- Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
parent
4808c5be48
commit
4cbab86b45
76 changed files with 1592 additions and 835 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo, memo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import type { TConversation, TMessage } from 'librechat-data-provider';
|
||||
import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '~/components/svg';
|
||||
import type { TConversation, TMessage, TFeedback } from 'librechat-data-provider';
|
||||
import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '~/components';
|
||||
import { useGenerationsByLatest, useLocalize } from '~/hooks';
|
||||
import { Fork } from '~/components/Conversations';
|
||||
import MessageAudio from './MessageAudio';
|
||||
import Feedback from './Feedback';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
|
|
@ -20,9 +21,97 @@ type THoverButtons = {
|
|||
latestMessage: TMessage | null;
|
||||
isLast: boolean;
|
||||
index: number;
|
||||
handleFeedback: ({ feedback }: { feedback: TFeedback | undefined }) => void;
|
||||
};
|
||||
|
||||
export default function HoverButtons({
|
||||
type HoverButtonProps = {
|
||||
onClick: (e?: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
isVisible?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isLast?: boolean;
|
||||
className?: string;
|
||||
buttonStyle?: string;
|
||||
};
|
||||
|
||||
const extractMessageContent = (message: TMessage): string => {
|
||||
if (typeof message.content === 'string') {
|
||||
return message.content;
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map((part) => {
|
||||
if (typeof part === 'string') {
|
||||
return part;
|
||||
}
|
||||
if ('text' in part) {
|
||||
return part.text || '';
|
||||
}
|
||||
if ('think' in part) {
|
||||
const think = part.think;
|
||||
if (typeof think === 'string') {
|
||||
return think;
|
||||
}
|
||||
return think && 'text' in think ? think.text || '' : '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
return message.text || '';
|
||||
};
|
||||
|
||||
const HoverButton = memo(
|
||||
({
|
||||
onClick,
|
||||
title,
|
||||
icon,
|
||||
isActive = false,
|
||||
isVisible = true,
|
||||
isDisabled = false,
|
||||
isLast = false,
|
||||
className = '',
|
||||
}: HoverButtonProps) => {
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5',
|
||||
|
||||
'hover:bg-gray-100 hover:text-gray-500',
|
||||
|
||||
'dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200',
|
||||
'disabled:dark:hover:text-gray-400',
|
||||
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
!isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100',
|
||||
!isVisible && 'opacity-0',
|
||||
|
||||
'focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white focus-visible:outline-none',
|
||||
|
||||
isActive && isVisible && 'active text-gray-700 dark:text-gray-200 bg-gray-100 bg-gray-700',
|
||||
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={buttonStyle}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
title={title}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
HoverButton.displayName = 'HoverButton';
|
||||
|
||||
const HoverButtons = ({
|
||||
index,
|
||||
isEditing,
|
||||
enterEdit,
|
||||
|
|
@ -34,20 +123,20 @@ export default function HoverButtons({
|
|||
handleContinue,
|
||||
latestMessage,
|
||||
isLast,
|
||||
}: THoverButtons) {
|
||||
handleFeedback,
|
||||
}: THoverButtons) => {
|
||||
const localize = useLocalize();
|
||||
const { endpoint: _endpoint, endpointType } = conversation ?? {};
|
||||
const endpoint = endpointType ?? _endpoint;
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [TextToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
||||
|
||||
const {
|
||||
hideEditButton,
|
||||
regenerateEnabled,
|
||||
continueSupported,
|
||||
forkingSupported,
|
||||
isEditableEndpoint,
|
||||
} = useGenerationsByLatest({
|
||||
const endpoint = useMemo(() => {
|
||||
if (!conversation) {
|
||||
return '';
|
||||
}
|
||||
return conversation.endpointType ?? conversation.endpoint;
|
||||
}, [conversation]);
|
||||
|
||||
const generationCapabilities = useGenerationsByLatest({
|
||||
isEditing,
|
||||
isSubmitting,
|
||||
error: message.error,
|
||||
|
|
@ -58,38 +147,44 @@ export default function HoverButtons({
|
|||
isCreatedByUser: message.isCreatedByUser,
|
||||
latestMessageId: latestMessage?.messageId,
|
||||
});
|
||||
|
||||
const {
|
||||
hideEditButton,
|
||||
regenerateEnabled,
|
||||
continueSupported,
|
||||
forkingSupported,
|
||||
isEditableEndpoint,
|
||||
} = generationCapabilities;
|
||||
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isCreatedByUser, error } = message;
|
||||
|
||||
const renderRegenerate = () => {
|
||||
if (!regenerateEnabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={regenerate}
|
||||
type="button"
|
||||
title={localize('com_ui_regenerate')}
|
||||
>
|
||||
<RegenerateIcon
|
||||
className="hover:text-gray-500 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400"
|
||||
size="19"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5',
|
||||
'hover:bg-gray-100 hover:text-gray-500',
|
||||
'dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200',
|
||||
'disabled:dark:hover:text-gray-400',
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
!isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100',
|
||||
'focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white focus-visible:outline-none',
|
||||
'active text-gray-700 dark:text-gray-200 bg-gray-100 bg-gray-700',
|
||||
);
|
||||
|
||||
// If message has an error, only show regenerate button
|
||||
if (error === true) {
|
||||
return (
|
||||
<div className="visible mt-0 flex justify-center gap-1 self-end text-gray-500 lg:justify-start">
|
||||
{renderRegenerate()}
|
||||
<div className="visible flex justify-center self-end lg:justify-start">
|
||||
{regenerateEnabled && (
|
||||
<HoverButton
|
||||
onClick={regenerate}
|
||||
title={localize('com_ui_regenerate')}
|
||||
icon={<RegenerateIcon size="19" />}
|
||||
isLast={isLast}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -101,72 +196,92 @@ export default function HoverButtons({
|
|||
enterEdit();
|
||||
};
|
||||
|
||||
const handleCopy = () => copyToClipboard(setIsCopied);
|
||||
|
||||
return (
|
||||
<div className="visible mt-0 flex justify-center gap-1 self-end text-gray-500 lg:justify-start">
|
||||
<div className="group visible flex justify-center gap-0.5 self-end focus-within:outline-none lg:justify-start">
|
||||
{/* Text to Speech */}
|
||||
{TextToSpeech && (
|
||||
<MessageAudio
|
||||
index={index}
|
||||
messageId={message.messageId}
|
||||
content={message.content ?? message.text}
|
||||
content={extractMessageContent(message)}
|
||||
isLast={isLast}
|
||||
className={cn(
|
||||
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
renderButton={(props) => (
|
||||
<HoverButton
|
||||
onClick={props.onClick}
|
||||
title={props.title}
|
||||
icon={props.icon}
|
||||
isActive={props.isActive}
|
||||
isLast={isLast}
|
||||
className={props.className}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isEditableEndpoint && (
|
||||
<button
|
||||
id={`edit-${message.messageId}`}
|
||||
className={cn(
|
||||
'hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
isCreatedByUser ? '' : 'active',
|
||||
hideEditButton ? 'opacity-0' : '',
|
||||
isEditing ? 'active text-gray-700 dark:text-gray-200' : '',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={onEdit}
|
||||
type="button"
|
||||
title={localize('com_ui_edit')}
|
||||
disabled={hideEditButton}
|
||||
>
|
||||
<EditIcon size="19" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={cn(
|
||||
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
|
||||
isSubmitting && isCreatedByUser ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={() => copyToClipboard(setIsCopied)}
|
||||
type="button"
|
||||
|
||||
{/* Copy Button */}
|
||||
<HoverButton
|
||||
onClick={handleCopy}
|
||||
title={
|
||||
isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_to_clipboard')
|
||||
}
|
||||
>
|
||||
{isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
|
||||
</button>
|
||||
{renderRegenerate()}
|
||||
<Fork
|
||||
icon={isCopied ? <CheckMark className="h-[18px] w-[18px]" /> : <Clipboard size="19" />}
|
||||
isLast={isLast}
|
||||
className={`ml-0 flex items-center gap-1.5 text-xs ${isSubmitting && isCreatedByUser ? 'md:opacity-0 md:group-hover:opacity-100' : ''}`}
|
||||
/>
|
||||
|
||||
{/* Edit Button */}
|
||||
{isEditableEndpoint && (
|
||||
<HoverButton
|
||||
onClick={onEdit}
|
||||
title={localize('com_ui_edit')}
|
||||
icon={<EditIcon size="19" />}
|
||||
isActive={isEditing}
|
||||
isVisible={!hideEditButton}
|
||||
isDisabled={hideEditButton}
|
||||
isLast={isLast}
|
||||
className={isCreatedByUser ? '' : 'active'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fork Button */}
|
||||
<Fork
|
||||
messageId={message.messageId}
|
||||
conversationId={conversation.conversationId}
|
||||
forkingSupported={forkingSupported}
|
||||
latestMessageId={latestMessage?.messageId}
|
||||
isLast={isLast}
|
||||
/>
|
||||
{continueSupported === true ? (
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
|
||||
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={handleContinue}
|
||||
type="button"
|
||||
|
||||
{/* Feedback Buttons */}
|
||||
{!isCreatedByUser && (
|
||||
<Feedback handleFeedback={handleFeedback} feedback={message.feedback} isLast={isLast} />
|
||||
)}
|
||||
|
||||
{/* Regenerate Button */}
|
||||
{regenerateEnabled && (
|
||||
<HoverButton
|
||||
onClick={regenerate}
|
||||
title={localize('com_ui_regenerate')}
|
||||
icon={<RegenerateIcon size="19" />}
|
||||
isLast={isLast}
|
||||
className="active"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Continue Button */}
|
||||
{continueSupported && (
|
||||
<HoverButton
|
||||
onClick={(e) => e && handleContinue(e)}
|
||||
title={localize('com_ui_continue')}
|
||||
>
|
||||
<ContinueIcon className="h-4 w-4 hover:text-gray-500 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
) : null}
|
||||
icon={<ContinueIcon className="w-19 h-19 -rotate-180" />}
|
||||
isLast={isLast}
|
||||
className="active"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(HoverButtons);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue