import React, { PureComponent, createRef } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Tooltip from '@mui/material/Tooltip'; import ClickAwayListener from '@mui/material/ClickAwayListener'; import HandshakeRoundedIcon from '@mui/icons-material/HandshakeRounded'; import PsychologyRoundedIcon from '@mui/icons-material/PsychologyRounded'; import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; import ComputerRoundedIcon from '@mui/icons-material/ComputerRounded'; import ImageSearchRoundedIcon from '@mui/icons-material/ImageSearchRounded'; import CompareRoundedIcon from '@mui/icons-material/CompareRounded'; import SupportIcon from '@mui/icons-material/Support'; import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'; import PhonelinkIcon from '@mui/icons-material/Phonelink'; import SaveAsIcon from '@mui/icons-material/SaveAs'; import DescriptionIcon from '@mui/icons-material/Description'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import DeleteIcon from '@mui/icons-material/Delete'; import GoogleIcon from '@mui/icons-material/Google'; import WebIcon from '@mui/icons-material/Web'; import EngineeringIcon from '@mui/icons-material/Engineering'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import ManageSearchIcon from '@mui/icons-material/ManageSearch'; import LensBlurIcon from '@mui/icons-material/LensBlur'; import CalculateIcon from '@mui/icons-material/Calculate'; import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; import DirectionsIcon from '@mui/icons-material/Directions'; import EditNoteIcon from '@mui/icons-material/EditNote'; import PaidRoundedIcon from '@mui/icons-material/PaidRounded'; import Button from '@mui/material/Button'; import { enqueueSnackbar } from 'notistack'; import { router } from '../../appRouter'; import deliveryEditorAutomat from '../../automats/deliveryEditorAutomat'; import { setLastDeliveryPath } from '../../automats/navigationAutomat'; import HealingMarkdown from './HealingMarkdown'; import { VfsAttachmentLink, ExtractionPill, parseAttachmentsFromText, parseExtractionsFromText, stripUserEnv, } from './ChatAttachmentItems'; import { CHROMA, SHADOW_BUBBLE_ASSISTANT, SHADOW_BUBBLE_USER, SURFACE_RAISED, } from '../../colors'; import { TOOL_ICON_MAP, TOOL_GERMAN_NAMES, GoogleFaviconIcon, } from './chatTools.jsx'; import { InlineMentionText } from './ChatMentionPill'; export { TOOL_ICON_MAP, TOOL_GERMAN_NAMES, GoogleFaviconIcon }; function formatMessageTime(isoString) { if (!isoString) return ''; try { const d = new Date(isoString); if (Number.isNaN(d.getTime())) return ''; return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } } function parseMessagePayload(raw) { if (typeof raw !== 'string') return { content: raw }; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') return parsed; } catch { } return { content: raw }; } function formatUsageText(usage) { if (!usage) return ''; const input = Number(usage.input ?? usage.prompt_tokens ?? 0); const output = Number(usage.output ?? usage.completion_tokens ?? 0); const cached = Number(usage.cached ?? 0); const cost = typeof usage.cost === 'number' ? ` · $${usage.cost.toFixed(6)}` : ''; const fmt = (n) => n.toLocaleString('de-DE'); const cachedPart = cached > 0 ? ` / ${fmt(cached)} Cache` : ''; return `${fmt(input)} Prompt${cachedPart} · ${fmt(output)} Antwort${cost}`; } function formatUsageTooltip(usage) { if (!usage) return ''; const input = Number(usage.input ?? usage.prompt_tokens ?? 0); const output = Number(usage.output ?? usage.completion_tokens ?? 0); const cached = Number(usage.cached ?? 0); const cost = typeof usage.cost === 'number' ? `$${usage.cost.toFixed(6)}` : null; const fmt = (n) => n.toLocaleString('de-DE'); const lines = [ `Prompt: ${fmt(input)} Tokens`, ]; if (cached > 0) { lines.push(`Cache: ${fmt(cached)} Tokens`); } lines.push(`Antwort: ${fmt(output)} Tokens`); if (cost) { lines.push(`Kosten: ${cost}`); } return lines.join('\n'); } export function aggregateUsage(turns = []) { if (!Array.isArray(turns) || turns.length === 0) return null; let hasUsage = false; let totalInput = 0; let totalOutput = 0; let totalCached = 0; let totalCost = 0; let hasCost = false; for (const turn of turns) { const u = turn?.usage; if (!u) continue; hasUsage = true; totalInput += Number(u.input ?? u.prompt_tokens ?? 0); totalOutput += Number(u.output ?? u.completion_tokens ?? 0); totalCached += Number(u.cached ?? 0); if (typeof u.cost === 'number' && !Number.isNaN(u.cost)) { totalCost += u.cost; hasCost = true; } } if (!hasUsage) return null; return { input: totalInput, output: totalOutput, cached: totalCached, cost: hasCost ? totalCost : undefined, }; } function formatToolTooltip(content, result, parameters, prefix, error) { const parts = []; if (prefix) { parts.push(prefix); } if (parameters !== undefined && parameters !== null && parameters !== '') { try { const parsed = typeof parameters === 'string' ? JSON.parse(parameters) : parameters; parts.push(typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(parameters)); } catch { parts.push(String(parameters)); } } if (content !== undefined && content !== null && content !== '') { try { const parsed = typeof content === 'string' ? JSON.parse(content) : content; const str = typeof parsed === 'object' ? JSON.stringify(parsed, null, 2) : String(content); if (!parts.includes(str)) { parts.push(str); } } catch { const str = String(content); if (!parts.includes(str)) { parts.push(str); } } } if (result !== undefined && result !== null && result !== '') { try { const resStr = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result); if (!parts.includes(resStr)) { parts.push(resStr); } } catch { const resStr = String(result); if (!parts.includes(resStr)) { parts.push(resStr); } } } if (error !== undefined && error !== null && error !== '') { try { const errStr = typeof error === 'object' ? JSON.stringify(error, null, 2) : String(error); if (!parts.includes(errStr)) { parts.push(errStr); } } catch { const errStr = String(error); if (!parts.includes(errStr)) { parts.push(errStr); } } } return parts.join('\n\n') || ''; } class ClickTooltip extends PureComponent { state = { open: false }; handleClose = () => { if (this.state.open) this.setState({ open: false }); }; handleToggle = (e) => { e?.stopPropagation(); this.setState((prev) => ({ open: !prev.open })); }; render() { const { title, children } = this.props; return ( {children} ); } } function HandshakeIconView({ state }) { const isGreen = state === 'green'; const isBlack = state === 'black'; return ( ); } function extractReasoningText(msg, payload) { if (payload && typeof payload === 'object') { if (payload.plain !== undefined) { if (typeof payload.plain === 'string' && payload.plain.trim()) { return payload.plain.trim(); } return ''; } if (payload.encrypted) { return ''; } const candidate = payload.text || payload.content || payload.thinking || payload.reasoning; if (candidate && typeof candidate === 'string') { const trimmed = candidate.trim(); if (trimmed.startsWith('{') && (trimmed.includes('"encrypted"') || trimmed.includes('"plain"'))) { try { const inner = JSON.parse(trimmed); if (inner.plain && typeof inner.plain === 'string' && inner.plain.trim()) { return inner.plain.trim(); } } catch { } return ''; } return trimmed; } } if (typeof msg?.message === 'string') { const raw = msg.message.trim(); if (raw.startsWith('{') && (raw.includes('"encrypted"') || raw.includes('"plain"'))) { try { const parsed = JSON.parse(raw); if (parsed.plain && typeof parsed.plain === 'string' && parsed.plain.trim()) { return parsed.plain.trim(); } } catch { } return ''; } if (!raw.startsWith('{')) { return raw; } } return ''; } class ReasoningTooltipContent extends PureComponent { boxRef = createRef(); scrollToBottom = () => { const el = this.boxRef.current; if (el) { el.scrollTop = el.scrollHeight; requestAnimationFrame(() => { if (this.boxRef.current) { this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight; } }); } }; componentDidMount() { this.scrollToBottom(); } componentDidUpdate(prevProps) { if (this.props.text !== prevProps.text) { this.scrollToBottom(); } } render() { const { text } = this.props; return ( {text} ); } } function BrainIconView({ reasoning }) { const tooltipContent = reasoning?.text?.trim() || ''; if (!tooltipContent) return null; if (tooltipContent.startsWith('{') && (tooltipContent.includes('"encrypted"') || tooltipContent.includes('"plain"'))) { return null; } return ( } > ); } function extractUrlFromTool(tool) { if (!tool) return null; const candidates = [tool.parameters, tool.arguments, tool.content, tool.result]; for (const item of candidates) { if (!item) continue; if (typeof item === 'object') { const u = item.url || item.Url || item.uri || item.URI || item.link; if (typeof u === 'string' && u.trim()) return u.trim(); } const str = typeof item === 'string' ? item : JSON.stringify(item); const match = str.match(/https?:\/\/[^\s"'`<>{}|\\]+/); if (match) { return match[0].replace(/[.,;:)]+$/, ''); } } return null; } class UrlFaviconIcon extends PureComponent { state = { srcIndex: 0, hasError: false, }; sources = []; constructor(props) { super(props); this.sources = this.buildSources(props.url); } componentDidUpdate(prevProps) { if (this.props.url !== prevProps.url) { this.sources = this.buildSources(this.props.url); this.setState({ srcIndex: 0, hasError: false }); } } buildSources(urlStr) { if (!urlStr || typeof urlStr !== 'string') return []; try { const parsed = new URL( urlStr.startsWith('http://') || urlStr.startsWith('https://') ? urlStr : `https://${urlStr}` ); const hostname = parsed.hostname; const origin = parsed.origin; if (!hostname) return []; const list = []; if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local')) { list.push(`${origin}/favicon.ico`); } else { list.push(`https://icons.duckduckgo.com/ip3/${hostname}.ico`); list.push(`${origin}/favicon.ico`); } return list; } catch { return []; } } handleError = () => { if (this.state.srcIndex + 1 < this.sources.length) { this.setState((prev) => ({ srcIndex: prev.srcIndex + 1 })); } else { this.setState({ hasError: true }); } }; render() { const { sx } = this.props; const { srcIndex, hasError } = this.state; const currentSrc = this.sources[srcIndex]; if (!currentSrc || hasError) { return ; } return ( ); } } export function isDraftDeliveryManifestTool(name) { if (!name || typeof name !== 'string') return false; const n = name.trim().toLowerCase(); return n === 'draft_delivery_manifest' || n === 'draft-delivery-manifest'; } function tryExtractObject(val) { if (!val) return null; if (typeof val === 'object') return val; if (typeof val === 'string') { const trimmed = val.trim(); try { return JSON.parse(trimmed); } catch { } const match = trimmed.match(/\{[\s\S]*\}/); if (match) { try { return JSON.parse(match[0]); } catch { } } } return null; } export function parseDraftDeliveryManifest(tool) { if (!tool) return null; const candidates = [tool.parameters, tool.arguments, tool.content, tool.result, tool]; for (const c of candidates) { const obj = tryExtractObject(c); if (obj && typeof obj === 'object') { if (obj.manifest && typeof obj.manifest === 'object') { return obj.manifest; } if (obj.basedata || obj.baseData || obj.items || obj.lines || obj.lineitems) { return obj; } } } return null; } class DraftDeliveryManifestUseButton extends PureComponent { state = { applied: false, }; handleClick = async (e) => { e?.stopPropagation(); const { tool } = this.props; const manifest = parseDraftDeliveryManifest(tool); if (!manifest) { enqueueSnackbar('Keine gültigen Lieferscheindaten gefunden', { variant: 'warning' }); return; } deliveryEditorAutomat.actions.loadDraftManifest(manifest); setLastDeliveryPath('/lieferung/neu'); this.setState({ applied: true }); setTimeout(() => { this.setState({ applied: false }); }, 2500); const count = manifest.items?.numberOfItemsToAdd ?? (Array.isArray(manifest.items?.items) ? manifest.items.items.length : Array.isArray(manifest.items) ? manifest.items.length : Array.isArray(manifest.lines) ? manifest.lines.length : 0); enqueueSnackbar( count > 0 ? `${count} Positionen in den Lieferschein-Editor geladen` : 'Lieferschein in den Editor geladen', { variant: 'success' } ); try { if (router?.navigate) { const curPath = router.state?.location?.pathname || (typeof window !== 'undefined' ? window.location.pathname : ''); if (!curPath.startsWith('/lieferung/neu')) { void router.navigate('/lieferung/neu'); } } } catch (err) { console.warn('[DraftDeliveryManifestUseButton] Navigation failed:', err); } }; render() { const { applied } = this.state; return ( ); } } function WorkIconView({ tool }) { if (!tool || !tool.name) return null; const isGreen = tool?.state === 'green'; const isUrlGet = tool?.name === 'url_get'; const isDraftDelivery = isDraftDeliveryManifestTool(tool?.name); const ToolNameIcon = tool?.name ? TOOL_ICON_MAP[tool.name] : null; const prefix = ToolNameIcon ? tool?.name : null; const tooltipText = formatToolTooltip(tool?.content, tool?.result, tool?.parameters || tool?.arguments, prefix); const targetUrl = isUrlGet ? extractUrlFromTool(tool) : null; const iconElement = ( {tooltipText} } > {isUrlGet ? ( ) : ToolNameIcon ? ( ) : ( <> {tool?.name ? ( {tool.name} ) : null} )} ); if (isDraftDelivery) { return ( {iconElement} ); } return iconElement; } function UsageChipView({ usage, isSummary = false }) { const text = formatUsageText(usage); if (!text) return null; return ( {text} ); } function UsageIconView({ usage }) { if (!usage) return null; const tooltipText = formatUsageTooltip(usage); if (!tooltipText) return null; return ( {tooltipText} } > ); } function turnHasIcons(turn) { if (!turn) return false; const hasReasonings = Boolean( turn.reasonings && turn.reasonings.some((r) => Boolean(r.text && r.text.trim())) ); const hasTools = Boolean(turn.tools && turn.tools.length > 0); const hasUsage = Boolean(turn.usage); const hasContent = Boolean(turn.content); const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage; return showHandshake || hasReasonings || hasTools || hasUsage; } export function TurnIcons({ turn }) { if (!turn) return null; const hasReasonings = Boolean( turn.reasonings && turn.reasonings.some((r) => Boolean(r.text && r.text.trim())) ); const hasTools = Boolean(turn.tools && turn.tools.length > 0); const hasUsage = Boolean(turn.usage); const hasContent = Boolean(turn.content); const showHandshake = !hasReasonings && !hasTools && !hasContent && !hasUsage; const showStatus = showHandshake || hasReasonings || hasTools || hasUsage; if (!showStatus) return null; return ( {showHandshake ? : null} {turn.reasonings?.map((r, i) => ( ))} {turn.tools?.map((t, i) => ( ))} {hasUsage ? ( ) : null} ); } export function TurnsView({ turns = [], isLive = false }) { if (!Array.isArray(turns) || turns.length === 0) return null; // Segment turns into chunks where icons precede content const segments = []; let pendingTurns = []; for (let i = 0; i < turns.length; i++) { const turn = turns[i]; pendingTurns.push(turn); if (turn.content) { segments.push({ statusTurns: [...pendingTurns], contentTurn: turn, }); pendingTurns = []; } } if (pendingTurns.length > 0) { segments.push({ statusTurns: pendingTurns, contentTurn: null, }); } return ( {segments.map((seg, sIdx) => { const hasAnyIcons = seg.statusTurns.some(turnHasIcons); return ( {/* All turn icons in ONE line */} {hasAnyIcons ? ( {seg.statusTurns.map((t, tIdx) => ( ))} ) : null} {/* Assistant message content for this segment (if any) */} {seg.contentTurn?.content ? ( ({ maxWidth: '85%', px: 2, py: 1.25, borderRadius: '16px 16px 16px 4px', bgcolor: theme.palette.mode === 'dark' ? 'background.paper' : SURFACE_RAISED, color: theme.palette.text.primary, boxShadow: SHADOW_BUBBLE_ASSISTANT, border: `1px solid ${theme.palette.divider}`, wordBreak: 'break-word', fontSize: '0.875rem', lineHeight: 1.5, })} > ) : null} ); })} ); } export function TurnRow({ turn, isLive = false }) { if (!turn) return null; return ; } export function normalizeChatMessages(rawMessages = []) { if (!Array.isArray(rawMessages) || rawMessages.length === 0) return []; const result = []; let currentAssistantTurns = []; let currentTurn = null; const flushAssistant = () => { if (currentTurn) { if ( (currentTurn.reasonings && currentTurn.reasonings.length > 0) || (currentTurn.tools && currentTurn.tools.length > 0) || currentTurn.content || currentTurn.usage ) { currentAssistantTurns.push(currentTurn); } currentTurn = null; } if (currentAssistantTurns.length > 0) { const fullText = currentAssistantTurns .map((t) => t.content) .filter(Boolean) .join('\n\n'); result.push({ id: currentAssistantTurns[0]?.id ? `asst-${currentAssistantTurns[0].id}` : `asst-${result.length}`, role: 'assistant', message: fullText, turns: currentAssistantTurns, created_at: currentAssistantTurns[0]?.created_at, }); currentAssistantTurns = []; } }; let turnSeq = 0; const findExistingTool = (toolCallId) => { if (!toolCallId) return null; if (currentTurn?.tools) { const t = currentTurn.tools.find((tool) => tool.id === toolCallId); if (t) return t; } for (let i = currentAssistantTurns.length - 1; i >= 0; i--) { const t = currentAssistantTurns[i].tools?.find((tool) => tool.id === toolCallId); if (t) return t; } return null; }; const getOrCreateTurn = (msg) => { const shouldStartNew = !currentTurn || currentTurn.sealed || (msg?.role === 'reasoning' && (currentTurn.tools.length > 0 || Boolean(currentTurn.content))) || (msg?.role === 'tool' && Boolean(currentTurn.content)); if (!currentTurn) { turnSeq++; currentTurn = { id: msg?.id ? `turn-msg-${msg.id}` : `turn-${result.length}-${turnSeq}`, handshake: 'green', reasonings: [], tools: [], content: '', usage: null, sealed: false, created_at: msg?.created_at, }; } else if (shouldStartNew) { currentAssistantTurns.push(currentTurn); turnSeq++; currentTurn = { id: msg?.id ? `turn-msg-${msg.id}` : `turn-${result.length}-${turnSeq}`, handshake: 'green', reasonings: [], tools: [], content: '', usage: null, sealed: false, created_at: msg?.created_at, }; } return currentTurn; }; for (const msg of rawMessages) { if (!msg) continue; if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) { flushAssistant(); result.push(msg); continue; } const payload = parseMessagePayload(msg.message); const userText = payload.content !== undefined ? payload.content : msg.message; if (msg.role === 'user') { flushAssistant(); result.push({ ...msg, message: userText, }); continue; } if (msg.role === 'reasoning') { const text = extractReasoningText(msg, payload); if (text) { const turn = getOrCreateTurn(msg); turn.reasonings.push({ id: msg.id || `r-${turn.reasonings.length}`, text, sealed: true, }); } continue; } if (msg.role === 'tool') { const existingTool = payload.tool_call_id ? findExistingTool(payload.tool_call_id) : null; const isAskUser = payload.name === 'ask_user' || existingTool?.name === 'ask_user'; if (!isAskUser && existingTool) { existingTool.result = payload.result !== undefined ? payload.result : (payload.content !== undefined ? payload.content : ''); if (payload.parameters) { existingTool.parameters = payload.parameters; } if (payload.name && !existingTool.name) { existingTool.name = payload.name; } continue; } const turn = getOrCreateTurn(msg); turn.tools.push({ id: payload.tool_call_id || msg.id, state: 'green', name: isAskUser ? 'ask_user' : (payload.name || ''), content: payload.content !== undefined ? payload.content : (payload.answer || payload.parameters || ''), parameters: payload.parameters, result: payload.result, }); continue; } if (msg.role === 'usage') { const turn = getOrCreateTurn(msg); turn.usage = payload; turn.sealed = true; continue; } if (msg.role === 'assistant' || msg.role === 'model') { const toolCalls = Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? payload.tool_calls : null; if (toolCalls) { const turn = getOrCreateTurn(msg); for (const tc of toolCalls) { let args = {}; try { args = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : (tc.function?.arguments || {}); } catch { } const isAskUser = tc.function?.name === 'ask_user'; const existing = !isAskUser && tc.id ? findExistingTool(tc.id) : null; if (existing) { existing.parameters = tc.function?.arguments || args; if (tc.function?.name && !existing.name) existing.name = tc.function.name; } else { turn.tools.push({ id: tc.id || `tc-${Date.now()}`, state: 'green', name: tc.function?.name || 'tool', parameters: tc.function?.arguments || args, content: null, result: null, }); } } } const content = payload.content !== undefined ? payload.content : msg.message; if (content && typeof content === 'string') { const turn = getOrCreateTurn(msg); turn.content = content; } continue; } flushAssistant(); result.push(msg); } flushAssistant(); return result; } function MessageRow({ msg, showTime = true }) { // If the message has structured turns, render each turn if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) { const summaryUsage = aggregateUsage(msg.turns); const turnCount = msg.turns.length; return ( {summaryUsage ? ( ) : null} ); } const payload = parseMessagePayload(msg.message); let textContent = payload.content !== undefined ? payload.content : msg.message; if (typeof textContent === 'string' && textContent.startsWith('{')) { try { const nested = JSON.parse(textContent); if (nested && typeof nested === 'object' && nested.content !== undefined) { textContent = nested.content; } } catch { } } const { extractions, cleanText: textAfterExtractions } = parseExtractionsFromText(textContent); const { attachments: vfsAttachments, cleanText: rawCleanText } = parseAttachmentsFromText(textAfterExtractions); const cleanText = stripUserEnv(rawCleanText); if (msg.role === 'system') { return ( {textContent} ); } const isUser = msg.role === 'user'; const timeStr = showTime ? formatMessageTime(msg.created_at) : ''; return ( {timeStr ? ( {timeStr} ) : null} ({ px: 2, py: 1.25, borderRadius: isUser ? '16px 16px 4px 16px' : '16px 16px 16px 4px', bgcolor: isUser ? theme.palette.primary.main : theme.palette.mode === 'dark' ? 'background.paper' : SURFACE_RAISED, color: isUser ? theme.palette.primary.contrastText : theme.palette.text.primary, boxShadow: isUser ? SHADOW_BUBBLE_USER : SHADOW_BUBBLE_ASSISTANT, border: isUser ? 'none' : `1px solid ${theme.palette.divider}`, wordBreak: 'break-word', whiteSpace: isUser ? 'pre-wrap' : 'normal', fontSize: '0.875rem', lineHeight: 1.5, })} > {extractions.length > 0 && ( 0 ? 0.75 : 0, }} > {extractions.map((ext, idx) => ( ))} )} {vfsAttachments.length > 0 && ( {vfsAttachments.map((att, idx) => ( ))} )} {cleanText ? ( isUser ? ( ) : ( ) ) : null} ); } export default class ChatFlowList extends PureComponent { containerRef = createRef(); bottomRef = createRef(); scrollTimeout = null; componentDidMount() { this.scrollToBottom('auto'); } componentDidUpdate(prevProps) { if ( this.props.messages !== prevProps.messages || this.props.messages?.length !== prevProps.messages?.length || this.props.activeTurns !== prevProps.activeTurns ) { this.scrollToBottom('smooth'); } } componentWillUnmount() { if (this.scrollTimeout) { clearTimeout(this.scrollTimeout); this.scrollTimeout = null; } } scrollToBottom = (behavior = 'smooth') => { const doScroll = () => { if (this.bottomRef.current?.scrollIntoView) { this.bottomRef.current.scrollIntoView({ behavior, block: 'end' }); } if (this.containerRef.current) { this.containerRef.current.scrollTop = this.containerRef.current.scrollHeight; } }; // Immediate frame requestAnimationFrame(doScroll); // Delayed fallback for font/layout reflows if (this.scrollTimeout) clearTimeout(this.scrollTimeout); this.scrollTimeout = setTimeout(doScroll, 60); }; render() { const { messages = [], activeTurns = [] } = this.props; const normalizedMessages = normalizeChatMessages(messages); if (normalizedMessages.length === 0 && activeTurns.length === 0) { return null; } return ( {normalizedMessages.map((msg, index) => { const prevMsg = index > 0 ? normalizedMessages[index - 1] : null; const currTime = formatMessageTime(msg.created_at); const prevTime = prevMsg ? formatMessageTime(prevMsg.created_at) : ''; const showTime = Boolean(currTime && currTime !== prevTime); return ( ); })} {/* Live streaming turns */} {activeTurns && activeTurns.length > 0 ? ( <> {(() => { const activeSummary = aggregateUsage(activeTurns); if (!activeSummary) return null; return ( ); })()} ) : null}
); } }