({ message }: MessageBubbleProps)
| 10 | } |
| 11 | |
| 12 | export function MessageBubble({ message }: MessageBubbleProps) { |
| 13 | const isUser = message.role === "user"; |
| 14 | const isError = message.status === "error"; |
| 15 | const text = extractTextContent(message.content); |
| 16 | |
| 17 | return ( |
| 18 | <article |
| 19 | className={cn( |
| 20 | "flex gap-3 animate-fade-in", |
| 21 | isUser && "flex-row-reverse" |
| 22 | )} |
| 23 | aria-label={isUser ? "You" : isError ? "Error from Claude" : "Claude"} |
| 24 | > |
| 25 | {/* Avatar — purely decorative, role conveyed by article label */} |
| 26 | <div |
| 27 | aria-hidden="true" |
| 28 | className={cn( |
| 29 | "w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5", |
| 30 | isUser |
| 31 | ? "bg-brand-600 text-white" |
| 32 | : isError |
| 33 | ? "bg-red-900 text-red-300" |
| 34 | : "bg-surface-700 text-surface-300" |
| 35 | )} |
| 36 | > |
| 37 | {isUser ? ( |
| 38 | <User className="w-4 h-4" aria-hidden="true" /> |
| 39 | ) : isError ? ( |
| 40 | <AlertCircle className="w-4 h-4" aria-hidden="true" /> |
| 41 | ) : ( |
| 42 | <Bot className="w-4 h-4" aria-hidden="true" /> |
| 43 | )} |
| 44 | </div> |
| 45 | |
| 46 | {/* Content */} |
| 47 | <div |
| 48 | className={cn( |
| 49 | "flex-1 min-w-0 max-w-2xl", |
| 50 | isUser && "flex justify-end" |
| 51 | )} |
| 52 | > |
| 53 | <div |
| 54 | className={cn( |
| 55 | "rounded-2xl px-4 py-3 text-sm", |
| 56 | isUser |
| 57 | ? "bg-brand-600 text-white rounded-tr-sm" |
| 58 | : isError |
| 59 | ? "bg-red-950 border border-red-800 text-red-200 rounded-tl-sm" |
| 60 | : "bg-surface-800 text-surface-100 rounded-tl-sm" |
| 61 | )} |
| 62 | > |
| 63 | {isUser ? ( |
| 64 | <p className="whitespace-pre-wrap break-words">{text}</p> |
| 65 | ) : ( |
| 66 | <MarkdownContent content={text} /> |
| 67 | )} |
| 68 | {message.status === "streaming" && ( |
| 69 | <span |
nothing calls this directly
no test coverage detected