import React, { useState, useMemo, memo } from 'react'; import { useRecoilState } from 'recoil'; 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'; type THoverButtons = { isEditing: boolean; enterEdit: (cancel?: boolean) => void; copyToClipboard: (setIsCopied: React.Dispatch>) => void; conversation: TConversation | null; isSubmitting: boolean; message: TMessage; regenerate: () => void; handleContinue: (e: React.MouseEvent) => void; latestMessage: TMessage | null; isLast: boolean; index: number; handleFeedback: ({ feedback }: { feedback: TFeedback | undefined }) => void; }; type HoverButtonProps = { onClick: (e?: React.MouseEvent) => 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 text-text-secondary-alt transition-colors duration-200', 'hover:text-text-primary hover:bg-surface-hover', '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-text-primary bg-surface-hover', className, ); return ( ); }, ); HoverButton.displayName = 'HoverButton'; const HoverButtons = ({ index, isEditing, enterEdit, copyToClipboard, conversation, isSubmitting, message, regenerate, handleContinue, latestMessage, isLast, handleFeedback, }: THoverButtons) => { const localize = useLocalize(); const [isCopied, setIsCopied] = useState(false); const [TextToSpeech] = useRecoilState(store.textToSpeech); const endpoint = useMemo(() => { if (!conversation) { return ''; } return conversation.endpointType ?? conversation.endpoint; }, [conversation]); const generationCapabilities = useGenerationsByLatest({ isEditing, isSubmitting, error: message.error, endpoint: endpoint ?? '', messageId: message.messageId, searchResult: message.searchResult, finish_reason: message.finish_reason, isCreatedByUser: message.isCreatedByUser, latestMessageId: latestMessage?.messageId, }); const { hideEditButton, regenerateEnabled, continueSupported, forkingSupported, isEditableEndpoint, } = generationCapabilities; if (!conversation) { return null; } const { isCreatedByUser, error } = message; if (error === true) { return (
{regenerateEnabled && ( } isLast={isLast} /> )}
); } const onEdit = () => { if (isEditing) { return enterEdit(true); } enterEdit(); }; const handleCopy = () => copyToClipboard(setIsCopied); return (
{/* Text to Speech */} {TextToSpeech && ( ( )} /> )} {/* Copy Button */} : } 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 && ( } isActive={isEditing} isVisible={!hideEditButton} isDisabled={hideEditButton} isLast={isLast} className={isCreatedByUser ? '' : 'active'} /> )} {/* Fork Button */} {/* Feedback Buttons */} {!isCreatedByUser && ( )} {/* Regenerate Button */} {regenerateEnabled && ( } isLast={isLast} className="active" /> )} {/* Continue Button */} {continueSupported && ( e && handleContinue(e)} title={localize('com_ui_continue')} icon={} isLast={isLast} className="active" /> )}
); }; export default memo(HoverButtons);