| 1 | import { memo, useEffect, useMemo, useRef } from "react"; |
| 2 | import { reasoningSummaryText } from "../lib/reasoningSummary"; |
| 3 | |
| 4 | export const ReasoningSummary = memo(function ReasoningSummary({ |
| 5 | text, |
| 6 | streaming = false, |
| 7 | className = "", |
| 8 | onOpen, |
| 9 | maxChars, |
| 10 | }: { |
| 11 | text: string; |
| 12 | streaming?: boolean; |
| 13 | className?: string; |
| 14 | onOpen?: () => void; |
| 15 | maxChars?: number; |
| 16 | }) { |
| 17 | const summary = useMemo(() => reasoningSummaryText(text, { streaming, maxChars }), [text, streaming, maxChars]); |
| 18 | const ref = useRef<HTMLElement>(null); |
| 19 | |
| 20 | // While streaming, keep the single-line summary pinned to the line tail so |
| 21 | // the newest text stays visible; rAF coalesces rapid token updates. A |
| 22 | // settled summary resets to the line start. |
| 23 | useEffect(() => { |
| 24 | const el = ref.current; |
| 25 | if (!el) return; |
| 26 | if (!streaming) { |
| 27 | el.scrollLeft = 0; |
| 28 | return; |
| 29 | } |
| 30 | const frame = requestAnimationFrame(() => { |
| 31 | el.scrollLeft = el.scrollWidth; |
| 32 | }); |
| 33 | return () => cancelAnimationFrame(frame); |
| 34 | }, [summary, streaming]); |
| 35 | |
| 36 | if (!summary) return null; |
| 37 | const cls = `reasoning-summary${className ? ` ${className}` : ""}`; |
| 38 | const followEnd = streaming ? { "data-follow-end": "" } : {}; |
| 39 | if (onOpen) { |
| 40 | return <button ref={ref as React.RefObject<HTMLButtonElement>} type="button" className={cls} onClick={onOpen} {...followEnd}>{summary}</button>; |
| 41 | } |
| 42 | return <span ref={ref as React.RefObject<HTMLSpanElement>} className={cls} {...followEnd}>{summary}</span>; |
| 43 | }); |
| 44 |