// Prussian Bot — chat view: message list, reasoning block, tool cards, composer. const { useState, useRef, useEffect, useLayoutEffect, useMemo } = React; const MACRON = /[āēīōūĀĒĪŌŪ]/; function Markdown({ text }) { const ref = useRef(null); const html = useMemo(() => text ? marked.parse(text) : '', [text]); useEffect(() => { const el = ref.current; if (!el) return; const iter = document.createNodeIterator(el, NodeFilter.SHOW_TEXT, null); const pending = []; let node; while ((node = iter.nextNode())) { if (node.textContent && MACRON.test(node.textContent)) { pending.push(node); } } for (const n of pending) { const wrap = document.createElement('span'); wrap.innerHTML = n.textContent.replace(/(\S*[āēīōūĀĒĪŌŪ]\S*)/g, '$1'); n.replaceWith(...wrap.childNodes); } }, [html]); if (!text) return null; return
; } /* ---- reasoning block (always visible, collapsible) ---- */ function Reasoning({ text, streaming }) { const [open, setOpen] = useState(true); if (!text) return null; return (
setOpen(!open)}> {t('reasoning.head')}{streaming ? ' …' : ''}
{open &&
{text}{streaming && }
}
); } /* ---- single tool call card ---- */ function ToolCard({ call }) { const [open, setOpen] = useState(false); const status = call.status || 'run'; // run | ok | err const led = status === 'ok' ? 'ok' : status === 'err' ? 'err' : 'run'; let preview = call.args || ''; try { if (call.args) preview = JSON.stringify(JSON.parse(call.args)); } catch (_) {} return (
setOpen(!open)}> {call.name} {preview} {call.ms != null && status !== 'run' && {call.ms} ms}
{open && (
{t('tool.args')}
{pretty(call.args)}
{status !== 'run' && (
{status === 'err' ? t('tool.error') : t('tool.result')}
{call.result != null ? call.result : (status === 'err' ? (call.error || t('tool.error')) : '…')}
)} {status === 'run' &&
{t('tool.runningLabel')}
{t('tool.running')}
}
)}
); } function pretty(s) { if (s == null || s === '') return '{}'; try { return JSON.stringify(JSON.parse(s), null, 2); } catch { return s; } } /* ---- error card with retry ---- */ function ErrorCard({ error, onRetry }) { return (
{error.isRateLimit ? t('error.rateLimit') : (error.source === 'mcp' ? t('error.mcp') : t('error.generic'))} {error.message}
{onRetry && (
)}
); } /* ---- one message (user or assistant) ---- */ function Message({ turn, isLast, streaming, onRetry }) { if (turn.role === 'user') { return (
{t('user.label')}
{turn.content}
); } const calls = turn.toolCalls || []; const reasoningStreaming = streaming && isLast && !turn.content && !turn.error; const contentStreaming = streaming && isLast && !!turn.content; return (
{t('bot.label')}
{turn.reasoning ? : null} {calls.length > 0 && (
{calls.map((c, i) => )}
)} {turn.content ? (
{contentStreaming && }
) : null} {turn.error ? : null} {streaming && isLast && !turn.content && !turn.reasoning && calls.length === 0 && !turn.error && (
{t('thinking')}
)}
); } /* ---- welcome / empty state ---- */ function Welcome({ onPick, configured }) { var welcomeQs = [ { q: t('welcome.q1'), a: null }, { q: t('welcome.q2'), a: null }, { q: t('welcome.q3'), a: null }, ]; return (

{t('welcome.title')}

{t('welcome.description')}

{!configured &&

{t('welcome.notConfigured')}

}
{welcomeQs.map(function(item, i) { return ( ); })}
); } /* ---- composer ---- */ function Composer({ onSend, busy, onStop, disabled }) { const [text, setText] = useState(''); const ref = useRef(null); function grow() { const el = ref.current; if (!el) return; el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 180) + 'px'; } useEffect(grow, [text]); function submit() { const t = text.trim(); if (!t || busy) return; onSend(t); setText(''); } function key(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } } return (