// Messages.jsx — message thread + composer
function Message({ role, time, children, streaming }) {
return (
{role === 'asst'
?

:
L}
{role === 'asst' ? 'ZIGU · proof-α' : 'You'}
{time}
{streaming && }
{children}
{role === 'asst' && !streaming && (
)}
);
}
function CodeBlock({ lang, children }) {
return (
);
}
function Thread({ messages }) {
const ref = React.useRef(null);
React.useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [messages]);
return (
{messages.map((m, i) => {m.body})}
);
}
function Composer({ onSend, onStop, streaming }) {
const [val, setVal] = React.useState('');
const taRef = React.useRef(null);
const click = () => {
if (streaming) { onStop && onStop(); return; }
if (val.trim()) { onSend(val); setVal(''); }
};
const onKey = e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); click(); }
};
React.useEffect(() => {
const ta = taRef.current; if (!ta) return;
ta.style.height = 'auto';
ta.style.height = Math.min(ta.scrollHeight, 220) + 'px';
}, [val]);
// Re-focus the textarea when streaming ends (the `disabled` attribute drops
// focus when it flips on, which the browser does not restore automatically
// when it flips back off). Keeps the input ready for the next turn.
React.useEffect(() => {
if (!streaming && taRef.current) taRef.current.focus();
}, [streaming]);
const disabled = !streaming && !val.trim();
return (
);
}
window.Thread = Thread;
window.Composer = Composer;
window.Message = Message;
window.CodeBlock = CodeBlock;