Message.Display.tsx91 lines · main
| 1 | import { UIMessage as VercelMessage } from '@ai-sdk/react' |
| 2 | import { type PropsWithChildren } from 'react' |
| 3 | import { cn } from 'ui' |
| 4 | |
| 5 | import { useMessageInfoContext } from './Message.Context' |
| 6 | import { MessagePartSwitcher } from './Message.Parts' |
| 7 | import { MessageMarkdown } from './MessageMarkdown' |
| 8 | import { ProfileImage as ProfileImageDisplay } from '@/components/ui/ProfileImage' |
| 9 | import { useProfileNameAndPicture } from '@/lib/profile' |
| 10 | |
| 11 | function MessageDisplayProfileImage() { |
| 12 | const { username, avatarUrl } = useProfileNameAndPicture() |
| 13 | return ( |
| 14 | <ProfileImageDisplay |
| 15 | alt={username} |
| 16 | src={avatarUrl} |
| 17 | className="w-5 h-5 shrink-0 rounded-full translate-y-0.5" |
| 18 | /> |
| 19 | ) |
| 20 | } |
| 21 | |
| 22 | function MessageDisplayContainer({ |
| 23 | children, |
| 24 | onClick, |
| 25 | className, |
| 26 | }: PropsWithChildren<{ onClick?: () => void; className?: string }>) { |
| 27 | return ( |
| 28 | <div |
| 29 | className={cn('group text-foreground-light text-sm first:mt-0', className)} |
| 30 | onClick={onClick} |
| 31 | > |
| 32 | {children} |
| 33 | </div> |
| 34 | ) |
| 35 | } |
| 36 | |
| 37 | function MessageDisplayMainArea({ |
| 38 | children, |
| 39 | className, |
| 40 | }: PropsWithChildren<{ className?: string }>) { |
| 41 | return <div className={cn('flex gap-4 w-auto overflow-hidden group', className)}>{children}</div> |
| 42 | } |
| 43 | |
| 44 | function MessageDisplayContent({ message }: { message: VercelMessage }) { |
| 45 | const { id, isLoading, readOnly } = useMessageInfoContext() |
| 46 | |
| 47 | const messageParts = message.parts |
| 48 | const content = |
| 49 | ('content' in message && typeof message.content === 'string' && message.content.trim()) || |
| 50 | undefined |
| 51 | |
| 52 | return ( |
| 53 | <div className="flex-1 min-w-0"> |
| 54 | {messageParts?.length > 0 |
| 55 | ? messageParts.map((part: NonNullable<VercelMessage['parts'][number]>, idx) => { |
| 56 | const isLastPart = idx === messageParts.length - 1 |
| 57 | return <MessagePartSwitcher key={idx} part={part} isLastPart={isLastPart} /> |
| 58 | }) |
| 59 | : content && ( |
| 60 | <MessageDisplayTextMessage id={id} isLoading={isLoading} readOnly={readOnly}> |
| 61 | {content} |
| 62 | </MessageDisplayTextMessage> |
| 63 | )} |
| 64 | </div> |
| 65 | ) |
| 66 | } |
| 67 | |
| 68 | function MessageDisplayTextMessage({ |
| 69 | id, |
| 70 | isLoading, |
| 71 | readOnly, |
| 72 | children, |
| 73 | }: PropsWithChildren<{ id: string; isLoading: boolean; readOnly?: boolean }>) { |
| 74 | return ( |
| 75 | <MessageMarkdown |
| 76 | id={id} |
| 77 | isLoading={isLoading} |
| 78 | readOnly={readOnly} |
| 79 | className="prose prose-sm max-w-none wrap-break-word prose-h2:font-medium" |
| 80 | > |
| 81 | {children} |
| 82 | </MessageMarkdown> |
| 83 | ) |
| 84 | } |
| 85 | |
| 86 | export const MessageDisplay = { |
| 87 | Container: MessageDisplayContainer, |
| 88 | Content: MessageDisplayContent, |
| 89 | MainArea: MessageDisplayMainArea, |
| 90 | ProfileImage: MessageDisplayProfileImage, |
| 91 | } |