1import { useState } from 'react';2import { Check, Copy, RefreshCw, ThumbsDown, ThumbsUp, Paperclip, AlertTriangle } from 'lucide-react';3import type { Message } from '@/lib/types';4import { StreamingText } from './streaming-text';5import { ToolCallTimeline } from './tool-call-timeline';6import { cn } from '@/lib/cn';7import { fmtDate } from '@/lib/format';89export function MessageBubble({ msg, conversationId, initials, onRegenerate, onFeedback, isLast }: {10 msg: Message;11 conversationId: string;12 initials: string;13 onRegenerate?: () => void;14 onFeedback?: (fb: 'up' | 'down' | null) => void;15 isLast: boolean;16}) {17 const [copied, setCopied] = useState(false);18 const copy = async () => {19 await navigator.clipboard.writeText(msg.content);20 setCopied(true);21 setTimeout(() => setCopied(false), 1200);22 };2324 if (msg.role === 'user') {25 return (26 <div className="flex justify-end gap-2 animate-fadein">27 <div className="max-w-[88%] sm:max-w-[75%]">28 <div className="rounded-2xl rounded-br-md bg-uqo-blue text-white px-4 py-3 text-[15px] whitespace-pre-wrap break-words">{msg.content}</div>29 {msg.attachments && msg.attachments.length > 0 && (30 <div className="mt-1 text-xs text-neutral-muted inline-flex items-center gap-1 justify-end w-full"><Paperclip size={12} /> {msg.attachments.length} fichier(s)</div>31 )}32 <div className="mt-1 text-[11px] text-neutral-muted text-right">{fmtDate(msg.created_at)}</div>33 </div>34 <span className="hidden sm:inline-flex h-8 w-8 rounded-full bg-uqo-blue-light text-uqo-blue-dark items-center justify-center text-xs font-semibold shrink-0 mt-1">{initials}</span>35 </div>36 );37 }3839 const empty = !msg.content && !(msg.tool_calls && msg.tool_calls.length);40 return (41 <div className="flex gap-2 sm:gap-3 animate-fadein group">42 <span className="h-8 w-8 rounded-full bg-uqo-blue text-white items-center justify-center text-[11px] font-bold shrink-0 mt-1 hidden sm:inline-flex">UQ</span>43 <div className="min-w-0 flex-1 max-w-[900px]">44 {msg.tool_calls && msg.tool_calls.length > 0 && <ToolCallTimeline calls={msg.tool_calls} conversationId={conversationId} />}45 <div className={cn('rounded-2xl rounded-tl-md bg-white border border-neutral-line shadow-card px-4 py-3 text-[15px]', empty && msg.streaming && 'inline-block')}>46 {empty && msg.streaming ? (47 <span className="inline-flex gap-1 items-center h-5" aria-label="Le tuteur réfléchit">48 <i className="h-2 w-2 rounded-full bg-uqo-blue/60 animate-bounce [animation-delay:-0.2s]" />49 <i className="h-2 w-2 rounded-full bg-uqo-blue/60 animate-bounce [animation-delay:-0.1s]" />50 <i className="h-2 w-2 rounded-full bg-uqo-blue/60 animate-bounce" />51 </span>52 ) : (53 <StreamingText text={msg.content} streaming={msg.streaming} conversationId={conversationId} />54 )}55 {msg.error && (56 <div className="mt-2 rounded-lg bg-red-50 text-semantic-error text-sm px-3 py-2 inline-flex items-center gap-2"><AlertTriangle size={16} /> {msg.error}</div>57 )}58 </div>59 {!msg.streaming && (60 <div className="mt-1 flex items-center gap-0.5 text-neutral-muted">61 <button onClick={copy} className="h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface" aria-label="Copier">{copied ? <Check size={15} className="text-uqo-green" /> : <Copy size={15} />}</button>62 {onFeedback && (63 <>64 <button onClick={() => onFeedback(msg.feedback === 'up' ? null : 'up')} className={cn('h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface', msg.feedback === 'up' && 'text-uqo-green')} aria-label="Réponse utile"><ThumbsUp size={15} /></button>65 <button onClick={() => onFeedback(msg.feedback === 'down' ? null : 'down')} className={cn('h-9 w-9 inline-flex items-center justify-center rounded-lg hover:bg-neutral-surface', msg.feedback === 'down' && 'text-semantic-error')} aria-label="Réponse à améliorer"><ThumbsDown size={15} /></button>66 </>67 )}68 {isLast && onRegenerate && (69 <button onClick={onRegenerate} className="h-9 px-2 inline-flex items-center gap-1 rounded-lg hover:bg-neutral-surface text-xs" aria-label="Régénérer"><RefreshCw size={14} /> Régénérer</button>70 )}71 <span className="ml-auto text-[11px]">{msg.model ? msg.model.split('/').pop() : ''} · {fmtDate(msg.created_at)}</span>72 </div>73 )}74 </div>75 </div>76 );77}78