import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; import type { ChatMessage as ChatMessageType, MessageBlock } from '../types'; import { ThinkingBlock } from './ThinkingBlock'; import { ToolCall } from './ToolCall'; import { Bot, User, Wrench } from 'lucide-react'; // ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage function formatTimestamp(ts: number): string { const date = new Date(ts); const now = new Date(); const time = date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); const isToday = date.toDateString() === now.toDateString(); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const isYesterday = date.toDateString() === yesterday.toDateString(); if (isToday) return time; if (isYesterday) return `Hier ${time}`; return `${date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} ${time}`; } /** Guess a language hint from content patterns */ function guessLanguage(lines: string[]): string { const joined = lines.join('\n'); if (/^import .+ from ['"]/.test(joined) || /^export (function|const|default|class|interface|type) /.test(joined) || /React\./.test(joined) || /<\w+[\s/>]/.test(joined) && /className=/.test(joined)) return 'tsx'; if (/^(import|export|const|let|var|function|class|interface|type) /.test(joined) || /=>\s*{/.test(joined) || /: (string|number|boolean|any)\b/.test(joined)) return 'typescript'; if (/^(use |fn |let mut |pub |impl |struct |enum |mod |crate::)/.test(joined) || /-> (Self|Result|Option|Vec|String|bool|i32|u32)/.test(joined)) return 'rust'; if (/^(def |class |import |from .+ import |if __name__)/.test(joined) || /self\.\w+/.test(joined) && !/this\./.test(joined)) return 'python'; if (/^\s*(server|location|upstream|proxy_pass|listen \d)/.test(joined)) return 'nginx'; if (/^\[.*\]\s*$/.test(lines[0] || '') && /=/.test(joined)) return 'ini'; if (/^(apiVersion|kind|metadata|spec):/.test(joined)) return 'yaml'; if (/^\{/.test(joined.trim()) && /\}$/.test(joined.trim())) return 'json'; if (/^#!\/(bin|usr)/.test(joined) || /^\s*(if \[|then|fi|echo |export |source )/.test(joined)) return 'bash'; if (/^(\s*[{(]/, /\.\w+\(.*\)\s*[;,]?\s*$/, ]; for (const line of lines) { for (const pat of patterns) { if (pat.test(line)) { codeSignals++; break; } } } return codeSignals / lines.length > 0.3; } /** Auto-wrap unformatted code/terminal output in fenced code blocks */ function autoFormatText(text: string): string { // Already has code fences — leave as-is if (text.includes('```')) return text; const lines = text.split('\n'); // If most of the text looks like code, wrap the whole thing const nonEmptyLines = lines.filter(l => l.trim()); if (nonEmptyLines.length >= 3 && looksLikeCode(nonEmptyLines)) { const lang = guessLanguage(nonEmptyLines); return '```' + lang + '\n' + text + '\n```'; } // Otherwise, detect contiguous code blocks within prose const result: string[] = []; let codeBuffer: string[] = []; const flushCode = () => { if (codeBuffer.length >= 3 && looksLikeCode(codeBuffer)) { const lang = guessLanguage(codeBuffer); result.push('```' + lang); result.push(...codeBuffer); result.push('```'); } else { result.push(...codeBuffer); } codeBuffer = []; }; const isCodeLine = (line: string): boolean => { return /^[\s]+(import|export|const|let|var|function|return|if|else|for)/.test(line) || /[{};]\s*$/.test(line) || /^\s*(\/\/|#)/.test(line) || /[├└│┬─]──/.test(line) || /^\s+\w+\(.*\)/.test(line); }; for (const line of lines) { if (isCodeLine(line) || (codeBuffer.length > 0 && (line.trim() === '' || /^\s{2,}/.test(line)))) { codeBuffer.push(line); } else { flushCode(); result.push(line); } } flushCode(); return result.join('\n'); } function getTextBlocks(blocks: MessageBlock[]): MessageBlock[] { return blocks.filter(b => b.type === 'text' && b.text.trim()); } function getInternalBlocks(blocks: MessageBlock[]): MessageBlock[] { return blocks.filter(b => b.type === 'thinking' || b.type === 'tool_use' || b.type === 'tool_result'); } function renderTextBlocks(blocks: MessageBlock[]) { return getTextBlocks(blocks).map((block, i) => (