SPB Git

spb/uqo-chat

Public
4.6 KB · 78 lines tsx
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
Files · cd08d441f7c277e8fedd6f3d3be82f02b44553ba
Menu

Theme