Response collapse Folding a long answer after a lead that stands on its own, with the control naming the size of what is hidden. Cutting at a fixed line count lands mid-sentence, and a bare show more leaves the reader nothing to decide with. npx shadcn@latest add button npm i lucide-react Tokens this needs: --card, --card-foreground, --background, --accent, --accent-foreground, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // ResponseCollapse.tsx ──────────────────────────────────────────────────────────────────────── import { useEffect, useRef, useState, type ReactNode } from "react"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Button } from "@/components/ui/button"; type Section = { heading: string; body: string }; type Props = { /** Written to stand alone. Ask the model for one, rather than hoping the * first lines happen to serve. */ lead: string; /** Everything after the lead, cut on structure rather than on a line count. */ sections: Section[]; render?: (body: string) => ReactNode; }; const words = (s: string) => s.trim().split(/\s+/).length; export function ResponseCollapse({ lead, sections, render = (b) =>

{b}

}: Props) { const [open, setOpen] = useState(false); const fold = useRef(null); const hidden = sections.reduce((n, s) => n + words(s.heading) + words(s.body), 0); // The fold stays in the DOM, hidden until found. Browser find then reaches // the conclusion, and beforematch tells us it did so the control agrees. // `hidden={!open}` in the JSX is what the server renders, so the fold // arrives closed; the effect then upgrades the attribute to until-found, // which React only knows how to set as a string via the DOM. useEffect(() => { const el = fold.current; if (!el) return; if (!open) el.setAttribute("hidden", "until-found"); const onMatch = () => setOpen(true); el.addEventListener("beforematch", onMatch); return () => el.removeEventListener("beforematch", onMatch); }, [open]); return (

{lead}

); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { ResponseCollapse } from "./ResponseCollapse"; /** The lead was asked for; the sections are the answer cut on its own headings. */ export default function Demo() { return (
); }