import { memo, useEffect, useRef, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Copy, Volume2, Check } from 'lucide-react'; import { bridge } from '../../lib/bridge'; import { formatTime, preprocessMedia } from '../../lib/text'; import { speech } from '../../services/voice/speech'; import type { ChatMessage } from '../../state/chat'; import { ResilientImage } from './ResilientImage'; interface Props { message: ChatMessage; assistantName: string; userName: string; showReasoning: boolean; compact?: boolean; onOpenImage?: (src: string) => void; } function openLink(href: string): void { const api = bridge(); if (/^(file:\/\/|[a-zA-Z]:[\\/])/.test(href) && api) { api.files.openPath(href).catch(() => undefined); return; } window.open(href, '_blank', 'noopener'); } export const MessageBubble = memo(function MessageBubble({ message, assistantName, userName, showReasoning, compact, onOpenImage }: Props) { const [copied, setCopied] = useState(false); const copyTimer = useRef(null); useEffect(() => () => { if (copyTimer.current) window.clearTimeout(copyTimer.current); }, []); const isUser = message.role === 'user'; const who = isUser ? userName || 'VOUS' : message.role === 'system' ? 'SYSTÈME' : assistantName; const status = message.status ?? 'done'; const copy = () => { navigator.clipboard.writeText(message.content).then(() => { setCopied(true); if (copyTimer.current) window.clearTimeout(copyTimer.current); copyTimer.current = window.setTimeout(() => setCopied(false), 1200); }).catch(() => undefined); }; return (
{!compact && (
{who} {formatTime(message.timestamp)} {message.source && message.source !== 'eveflow' && {message.jobName ? `cron · ${message.jobName}` : message.source}} {message.transport && message.role === 'assistant' && {message.transport}} {message.usage?.total_tokens ? {message.usage.total_tokens} tok : null} {message.role === 'assistant' && status === 'done' && ( )}
)}
{message.images && message.images.length > 0 && (
{message.images.map((img, i) => ( ))}
)} {showReasoning && message.reasoning && (
raisonnement {message.reasoning}
)} {message.role === 'user' ? ( {message.content} ) : (
, a: ({ href, children }) => ( { e.preventDefault(); if (href) openLink(href); }} > {children} ) }} > {preprocessMedia(message.content) || (status === 'streaming' ? '' : '…')}
)}
); });