import { UIMessage as VercelMessage } from '@ai-sdk/react' import { CheckIcon, Loader2, Pencil, Trash2 } from 'lucide-react' import { createContext, memo, PropsWithChildren, ReactNode, useMemo, useState } from 'react' import ReactMarkdown from 'react-markdown' import { Components } from 'react-markdown/lib/ast-to-react' import remarkGfm from 'remark-gfm' import { toast } from 'sonner' import { ProfileImage } from 'components/ui/ProfileImage' import { useProfile } from 'lib/profile' import { cn, markdownComponents, WarningIcon } from 'ui' import { ButtonTooltip } from '../ButtonTooltip' import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock' import { DeleteMessageConfirmModal } from './DeleteMessageConfirmModal' import { DisplayBlockRenderer } from './DisplayBlockRenderer' import { Heading3, Hyperlink, InlineCode, ListItem, MarkdownPre, OrderedList, } from './MessageMarkdown' import { Reasoning } from './elements/Reasoning' interface MessageContextType { isLoading: boolean readOnly?: boolean } export const MessageContext = createContext({ isLoading: false }) const baseMarkdownComponents: Partial = { ol: OrderedList, li: ListItem, h3: Heading3, code: InlineCode, a: Hyperlink, img: ({ src }) => [Image: {src}], } interface MessageProps { id: string message: VercelMessage isLoading: boolean readOnly?: boolean status?: string action?: ReactNode variant?: 'default' | 'warning' onResults: ({ messageId, resultId, results, }: { messageId: string resultId?: string results: any[] }) => void onDelete: (id: string) => void onEdit: (id: string) => void isAfterEditedMessage: boolean isBeingEdited: boolean onCancelEdit: () => void } const Message = function Message({ id, message, isLoading, readOnly, action = null, variant = 'default', onResults, onDelete, onEdit, isAfterEditedMessage = false, isBeingEdited = false, status, onCancelEdit, }: PropsWithChildren) { const { profile } = useProfile() const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false) const allMarkdownComponents: Partial = useMemo( () => ({ ...markdownComponents, ...baseMarkdownComponents, pre: ({ children }) => ( {children} ), }), [id, onResults] ) if (!message) { console.error(`Message component received undefined message prop for id: ${id}`) return null } // For backwards compatibility: some stored messages may have a 'content' property const { role, parts } = message const hasContent = (msg: VercelMessage): msg is VercelMessage & { content: string } => 'content' in msg && typeof msg.content === 'string' const content = hasContent(message) ? message.content : undefined const isUser = role === 'user' const shouldUsePartsRendering = parts && parts.length > 0 const hasTextContent = content && content.trim().length > 0 return (
{variant === 'warning' && } {action}
{isUser && ( )}
{shouldUsePartsRendering ? ( (() => { return parts.map( (part: NonNullable[number], index: number) => { switch (part.type) { case 'dynamic-tool': { return (
{part.state === 'input-streaming' ? ( ) : ( )} {`${part.toolName}`}
) } case 'reasoning': return ( {part.text} ) case 'text': return ( div]:my-4 prose-h1:text-xl prose-h1:mt-6 prose-h2:text-lg prose-h3:no-underline prose-h3:text-base prose-h3:mb-4 prose-strong:font-medium prose-strong:text-foreground prose-ol:space-y-3 prose-ul:space-y-3 prose-li:my-0 break-words [&>p:not(:last-child)]:!mb-2 [&>*>p:first-child]:!mt-0 [&>*>p:last-child]:!mb-0 [&>*>*>p:first-child]:!mt-0 [&>*>*>p:last-child]:!mb-0 [&>ol>li]:!pl-4', isUser && 'text-foreground [&>p]:font-medium', isBeingEdited && 'animate-pulse' )} remarkPlugins={[remarkGfm]} components={allMarkdownComponents} > {part.text} ) case 'tool-display_query': { const { toolCallId, state, input } = part if (state === 'input-streaming' || state === 'input-available') { return (
{`Calling display_query...`}
) } if (state === 'output-available') { return ( ) } return null } case 'tool-display_edge_function': { const { toolCallId, state, input } = part if (state === 'input-streaming' || state === 'input-available') { return (
{`Calling display_edge_function...`}
) } if (state === 'output-available') { return (
) } return null } case 'source-url': case 'source-document': case 'file': return null default: return null } } ) })() ) : hasTextContent ? ( {content} ) : ( Assistant is thinking... )} {/* Action button - only show for user messages on hover */}
{message.role === 'user' && ( <> } onClick={ isBeingEdited || isAfterEditedMessage ? onCancelEdit : () => onEdit(id) } className="text-foreground-light hover:text-foreground p-1 rounded" aria-label={ isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message' } tooltip={{ content: { side: 'bottom', text: isBeingEdited || isAfterEditedMessage ? 'Cancel editing' : 'Edit message', }, }} /> } tooltip={{ content: { side: 'bottom', text: 'Delete message' } }} onClick={() => setShowDeleteConfirmModal(true)} className="text-foreground-light hover:text-foreground p-1 rounded" title="Delete message" aria-label="Delete message" /> )}
{ onDelete(id) setShowDeleteConfirmModal(false) toast.success('Message deleted successfully') }} onCancel={() => setShowDeleteConfirmModal(false)} />
) } export const MemoizedMessage = memo( ({ message, status, onResults, onDelete, onEdit, isAfterEditedMessage, isBeingEdited, onCancelEdit, }: { message: VercelMessage status: string onResults: ({ messageId, resultId, results, }: { messageId: string resultId?: string results: any[] }) => void onDelete: (id: string) => void onEdit: (id: string) => void isAfterEditedMessage: boolean isBeingEdited: boolean onCancelEdit: () => void }) => { return ( ) } ) MemoizedMessage.displayName = 'MemoizedMessage'