"use client"; import { memo, useState } from "react"; import { ChevronDown, X, type LucideIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; /** * One row in the reference tree: an attachment, a Space reference, a * persona, etc. All kinds render identically (monochrome icon + kind * prefix + label) — visual uniformity is the point. */ export interface ContextTreeItem { key: string; // A lucide icon, or any glyph with the same call signature (brand SVG marks // are cast to this at the call site). icon: LucideIcon; /** Type prefix ("Book", "Notebook", ...), already translated. */ kind: string; /** Item title; truncates. */ label: string; /** 16px thumbnail for image attachments — replaces the icon. */ thumbnailUrl?: string; /** Optional click action (e.g. open attachment preview). */ onClick?: () => void; /** Optional remove action (composer only). */ onRemove?: () => void; } /** * Connector glyph: a thin rounded elbow line (NOT a bordered box — that * reads as a todo checkbox). Drawn for the "up" flavor (┌: rises from * the textarea, turns right); the mirrored/down flavors derive from it * via CSS transforms. */ function ElbowMark({ direction, mirrored, }: { direction: "up" | "down"; mirrored: boolean; }) { const flip = [ direction === "down" ? "-scale-y-100" : "", mirrored ? "-scale-x-100" : "", ] .join(" ") .trim(); return ( {/* vertical arm from the box edge, rounded corner, horizontal arm */} ); } /** * Folded-content mark for the summary toggle: three quiet dots — the * "…" idiom for collapsed lines. The toggle is a control, not a ref, * so it gets no elbow; once expanded nothing is hidden anymore and the * mark yields to a blank spacer that keeps the text column aligned. */ function FoldMark({ visible }: { visible: boolean }) { return ( {visible && ( <> )} ); } /** * Claude-Code-style attachment tree: an elbow connector + a quiet * collapsed summary ("N references"), expandable to one row per item. * * direction="up" is the composer flavor — the block sits above the * textarea and the elbows read as the input box extending upward. * direction="down" + align="right" is the sent-message flavor: ┘ * elbows hug the bubble's right edge and the rows extend leftward. */ export default memo(function ContextReferenceTree({ items, direction, align = "left", summaryNoun, }: { items: ContextTreeItem[]; direction: "up" | "down"; align?: "left" | "right"; /** Translated noun for the collapsed summary ("attachments" / "references"). */ summaryNoun: string; }) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); if (items.length === 0) return null; // A single item needs no collapse ceremony — show it directly. const collapsible = items.length > 1; const showRows = !collapsible || expanded; const mirrored = align === "right"; const alignClass = mirrored ? "items-end" : "items-start"; // Mirrored rows reverse the flex order so the elbow sits at the right // edge (hugging the bubble) and content extends leftward. const rowDirClass = mirrored ? "flex-row-reverse" : ""; const rows = showRows ? items.map((item) => { const Inner = ( <> {item.thumbnailUrl ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( )} {item.kind} {item.label} ); return ( {item.onClick ? ( ) : ( Inner )} {item.onRemove ? ( ) : null} ); }) : null; const summary = collapsible ? ( ) : null; return (
{/* "up" reads bottom-to-top: rows stack above the toggle so the block grows away from the textarea. "down" is the inverse. */} {direction === "up" ? ( <> {rows} {summary} ) : ( <> {summary} {rows} )}
); });