Edit and resend Fixing the question rather than arguing with the answer. Editing a user turn branches everything below it, so the count of turns about to move has to be on screen before the reader commits. npx shadcn@latest add textarea button Tokens this needs: --background, --foreground, --card, --muted-foreground, --border, --input, --status-warn, --chart-1 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // EditTurn.tsx ──────────────────────────────────────────────────────────────────────── import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; type Props = { /** Every version of this question, oldest first. Editing appends one; it * never replaces, because the old limb of the conversation still hangs * off the old wording. */ versions: string[]; /** How many turns sit below this one. Required, because saving moves all of * them to the other branch and the viewer has to read that before they * commit, never after. */ turnsBelow: number; onSave: (text: string) => void; onCancel?: () => void; /** Which version the transcript continues from. */ onSelect?: (index: number) => void; defaultEditing?: boolean; /** Which version shows first. Defaults to the newest. */ defaultIndex?: number; }; export function EditTurn({ versions, turnsBelow, onSave, onCancel, onSelect, defaultEditing = false, defaultIndex }: Props) { const n = versions.length; const [i, setI] = useState(defaultIndex ?? n - 1); const [editing, setEditing] = useState(defaultEditing); const [draft, setDraft] = useState(versions[i]); const ref = useRef(null); const go = (next: number) => { setI(next); setDraft(versions[next]); onSelect?.(next); }; // A saved edit lands on the new version, the same as a regenerate lands on // the new answer. const seen = useRef(n); useEffect(() => { if (n > seen.current) go(n - 1); seen.current = n; }, [n]); // Pre-filled with the cursor at the end. The viewer is here to change a // word or add a clause, and the caret at the start makes them arrow past // the whole question first. useEffect(() => { const el = ref.current; if (!editing || !el) return; el.focus(); el.setSelectionRange(el.value.length, el.value.length); }, [editing]); const cancel = () => { setEditing(false); setDraft(versions[i]); onCancel?.(); }; const save = () => { const text = draft.trim(); if (!text || text === versions[i]) { cancel(); return; } setEditing(false); onSave(text); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); cancel(); } if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); save(); } }; const pager = n > 1 && (
{i + 1} of {n}
); return (
{editing ? (