// SPDX-License-Identifier: MIT // Copyright contributors to the openassistant project import { forwardRef, HTMLAttributes, ReactNode, useCallback, useRef, useState, } from 'react'; import { cn } from '@heroui/react'; import { useClipboard } from '@heroui/use-clipboard'; import { MessagePayload, StreamMessage, ToolCallComponents, } from '@openassistant/core'; import { AvatarBadge } from './avatar-badge'; import { MessageContent } from './message-content'; import { MessageActions } from './message-action'; import { AttemptsNavigation, AttemptFeedback } from './message-feedback'; export type MessageCardProps = HTMLAttributes & { index: number; avatar?: ReactNode | string; showFeedback?: boolean; message?: StreamMessage; components?: ToolCallComponents; customMessage?: MessagePayload; currentAttempt?: number; status?: 'success' | 'failed' | 'pending'; attempts?: number; messageClassName?: string; botMessageBackground?: string; onAttemptChange?: (attempt: number) => void; onMessageCopy?: (content: string | string[]) => void; onFeedback?: (index: number) => void; onAttemptFeedback?: (feedback: 'like' | 'dislike' | 'same') => void; githubIssueLink?: string; isMessageDraggable?: boolean; useMarkdown?: boolean; showTools?: boolean; }; const MessageCard = forwardRef( ( { index, avatar, message, components, customMessage, showFeedback, attempts = 1, currentAttempt = 1, status, onMessageCopy, onAttemptChange, onFeedback, onAttemptFeedback, messageClassName, githubIssueLink, useMarkdown = true, isMessageDraggable = false, showTools = true, ...props }, ref ) => { const [feedback, setFeedback] = useState<'like' | 'dislike'>(); const [attemptFeedback, setAttemptFeedback] = useState< 'like' | 'dislike' | 'same' >(); const messageRef = useRef(null); const { copied, copy } = useClipboard(); const failedMessageClassName = status === 'failed' ? 'bg-danger-100/50 border border-danger-100 text-foreground' : ''; const hasFailed = status === 'failed'; const handleCopy = useCallback(() => { let stringValue = ''; if (typeof message === 'string') { stringValue = message; } else if (Array.isArray(message)) { message.forEach((child) => { const childString = typeof child === 'string' ? child : child?.props?.children?.toString(); if (childString) { stringValue += childString + '\n'; } }); } const valueToCopy = stringValue || messageRef.current?.textContent || ''; copy(valueToCopy); onMessageCopy?.(valueToCopy); }, [copy, message, onMessageCopy]); const handleFeedback = useCallback( (index: number, liked: boolean) => { setFeedback(liked ? 'like' : 'dislike'); if (liked === false) { onFeedback?.(index); } }, [onFeedback] ); const handleAttemptFeedback = useCallback( (feedback: 'like' | 'dislike' | 'same') => { setAttemptFeedback(feedback); onAttemptFeedback?.(feedback); }, [onAttemptFeedback] ); return (
{showFeedback && !hasFailed && status !== 'pending' && ( )}
{attempts > 1 && !hasFailed && ( )}
{showFeedback && attempts > 1 && ( )}
); } ); export default MessageCard; MessageCard.displayName = 'MessageCard';