"use client"; import { ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, GitBranchPlusIcon, SparklesIcon, XIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type FormEvent, useEffect, useId, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; function useReducedMotionAfterMount() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export interface BranchMessage { id: string; parentId: string | null; role: "user" | "assistant"; text: string; } export interface ChatBranchesProps { title?: string; messages: BranchMessage[]; defaultLeafId?: string; respond?: (text: string, history: BranchMessage[]) => string; assistantName?: string; className?: string; } const spring = { type: "spring", stiffness: 480, damping: 38 } as const; const STEP = 16; const ROW = 26; const cannedReplies = [ "Here's a tighter take: “Work offline in Tideline 2.4. Your edits wait patiently and sync the moment you're back.”", "Sure. I led with the speed this time: “Sync is now up to 3× faster, and it keeps going when your Wi-Fi doesn't.”", "Done. It's under 120 characters now, so it fits the in-app banner without truncating.", ]; function useTree(messages: BranchMessage[]) { return useMemo(() => { const byId = new Map(messages.map((m) => [m.id, m])); const children = new Map(); for (const m of messages) { const list = children.get(m.parentId) ?? []; list.push(m); children.set(m.parentId, list); } const leaves = messages.filter((m) => !children.has(m.id)); const pathTo = (id: string | null) => { const out: BranchMessage[] = []; let cur = id ? byId.get(id) : undefined; while (cur) { out.unshift(cur); cur = cur.parentId ? byId.get(cur.parentId) : undefined; } return out; }; const lane = new Map(); const depth = new Map(); leaves.forEach((leaf, i) => { pathTo(leaf.id).forEach((m, d) => { depth.set(m.id, d); if (!lane.has(m.id)) lane.set(m.id, i); }); }); const tipOf = (id: string) => { for (let i = leaves.length - 1; i >= 0; i--) { if (pathTo(leaves[i].id).some((m) => m.id === id)) return leaves[i].id; } return id; }; return { byId, children, leaves, pathTo, lane, depth, tipOf }; }, [messages]); } export function ChatBranches({ title = "Release note, v2.4", messages: initial, defaultLeafId, respond, assistantName = "Lumen", className, }: ChatBranchesProps) { const reduce = useReducedMotionAfterMount(); const inputId = useId(); const [messages, setMessages] = useState(initial); const tree = useTree(messages); const [leafId, setLeafId] = useState(defaultLeafId ?? tree.leaves[0]?.id ?? ""); const [forkFrom, setForkFrom] = useState(null); const [draft, setDraft] = useState(""); const [pending, setPending] = useState(false); const replyCount = useRef(0); const scrollRef = useRef(null); const inputRef = useRef(null); const timer = useRef>(undefined); useEffect(() => () => clearTimeout(timer.current), []); const viewTip = forkFrom ?? leafId; const path = tree.pathTo(viewTip); const activeIds = new Set(tree.pathTo(leafId).map((m) => m.id)); const activeLane = tree.lane.get(leafId) ?? 0; useEffect(() => { const el = scrollRef.current; if (el) el.scrollTo({ top: el.scrollHeight, behavior: reduce ? "auto" : "smooth" }); }, [viewTip, pending, reduce]); const hop = (id: string) => { if (forkFrom) cancelFork(); setLeafId(tree.tipOf(id)); }; const startFork = (id: string) => { const m = tree.byId.get(id); if (m?.role === "user") setDraft(m.text); else if (forkSource?.role === "user") setDraft(""); setForkFrom(id); requestAnimationFrame(() => { inputRef.current?.focus(); inputRef.current?.select(); }); }; const cancelFork = () => { if (forkSource?.role === "user") setDraft(""); setForkFrom(null); }; const send = (e: FormEvent) => { e.preventDefault(); const text = draft.trim(); if (!text || pending) return; const from = forkFrom ? tree.byId.get(forkFrom) : null; const parentId = from ? (from.role === "user" ? from.parentId : from.id) : leafId; const userMsg: BranchMessage = { id: `u-${Date.now()}`, parentId, role: "user", text }; const history = [...tree.pathTo(parentId), userMsg]; setMessages((m) => [...m, userMsg]); setLeafId(userMsg.id); setForkFrom(null); setDraft(""); setPending(true); timer.current = setTimeout(() => { const reply = respond?.(text, history) ?? cannedReplies[replyCount.current++ % cannedReplies.length]; const botMsg: BranchMessage = { id: `a-${Date.now()}`, parentId: userMsg.id, role: "assistant", text: reply }; setMessages((m) => [...m, botMsg]); setLeafId(botMsg.id); setPending(false); }, 1100); }; const forkSource = forkFrom ? tree.byId.get(forkFrom) : null; return (

{title}

{tree.leaves.length} {tree.leaves.length === 1 ? "branch" : "branches"}
    {path.map((m) => { const siblings = tree.children.get(m.parentId) ?? []; const index = siblings.findIndex((s) => s.id === m.id); return ( startFork(m.id)} /> {siblings.length > 1 && (
    hop(siblings[index - 1].id)} > {index + 1}/{siblings.length} hop(siblings[index + 1].id)} >
    )}
    ); })} {pending && ( {assistantName} is replying {[0, 1, 2].map((i) => ( ))} )}
{forkSource && (
{forkSource.role === "user" ? "New branch instead of " : "New branch after "} “{forkSource.text}”
)}
setDraft(e.target.value)} onKeyDown={(e) => e.key === "Escape" && forkFrom && cancelFork()} placeholder={forkFrom ? "Take it somewhere else…" : "Reply, or branch from any message"} ref={inputRef} value={draft} />
); } function PagerButton({ label, disabled, onClick, children, }: { label: string; disabled?: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); } function AssistantMark() { return ( ); } function Message({ message, assistantName, forking, onFork, }: { message: BranchMessage; assistantName: string; forking: boolean; onFork: () => void; }) { const user = message.role === "user"; const forkButton = message.parentId === null ? null : ( ); if (user) { return (
{forkButton}

{message.text}

); } return (
{assistantName}:

{message.text}

{forkButton}
); } function Minimap({ tree, activeIds, activeLane, onHop, reduce, }: { tree: ReturnType; activeIds: Set; activeLane: number; onHop: (id: string) => void; reduce: boolean | null; }) { const rowId = useId(); const nodes = [...tree.byId.values()]; const maxDepth = Math.max(0, ...nodes.map((n) => tree.depth.get(n.id) ?? 0)); const width = (maxDepth + 1) * STEP; const pos = (id: string) => ({ x: STEP / 2 + (tree.depth.get(id) ?? 0) * STEP, y: ROW / 2 + (tree.lane.get(id) ?? 0) * ROW, }); const branchName = (leaf: BranchMessage, lane: number) => { if (lane === 0) return "Main"; const first = tree.pathTo(leaf.id).find((m) => tree.lane.get(m.id) === lane && m.role === "user"); return first?.text ?? "Branch"; }; return ( ); }