// 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 (
{lang} Copy
{children}
); } 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 (