"use client"; import { Fragment, useState } from "react"; import { useTranslations } from "next-intl"; import { ChevronDown, HelpCircle } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { ApiDataInterface } from "../../../../../core"; import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../../shadcnui/ui/tooltip"; import { cn } from "@/lib/utils"; import { RelevanceMeter } from "../RelevanceMeter"; import { useEntityLabel } from "./useEntityLabel"; interface Props { citations: (ChunkInterface & ChunkRelationshipMeta)[]; /** * Resolved source entities keyed by `chunk.nodeId`. When provided, the row * label is the entity's `name`; otherwise it falls back to the nodeType + a * short id. */ sources?: Map; } export function CitationsTab({ citations, sources }: Props) { const t = useTranslations(); const entityLabel = useEntityLabel(); const [expanded, setExpanded] = useState>(new Set()); if (citations.length === 0) return null; const sorted = [...citations].sort((a, b) => (b.relevance ?? 0) - (a.relevance ?? 0)); const toggle = (id: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; return ( {t("features.assistant.message.sources.source")} {t("features.assistant.message.sources.relevance")} {sorted.map((chunk) => { const isOpen = expanded.has(chunk.id); const resolved = chunk.nodeId ? sources?.get(chunk.nodeId) : undefined; // `nodeType` is a JSON:API wire type (`npcs`), not UI copy — translate // it the same way the References tab does before showing it. const typeLabel = chunk.nodeType ? entityLabel(chunk.nodeType) : t("features.assistant.message.sources.source"); const fallbackName = chunk.nodeId ? `${typeLabel} ${chunk.nodeId.slice(0, 8)}` : typeLabel; const sourceName = (resolved as any)?.name ?? fallbackName; return (
toggle(chunk.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(chunk.id); } }} aria-expanded={isOpen} className="flex w-full cursor-pointer items-center justify-start gap-x-2" > {sourceName} {chunk.reason && ( {chunk.reason} )}
{isOpen && ( {/* `max-w-0` is the table-layout trick: without it the cell sizes to its widest line, so the panel either scrolls sideways or clips. Pinning the cell's intrinsic max width to zero makes the browser take its width from the table instead, which lets the content below actually wrap. */} {/* Prose wraps; only genuinely wide children scroll. The wrapper used to be `overflow-x-auto`, which turned every long sentence into a horizontal scrollbar instead of breaking onto the next line. */}
{chunk.content}
)}
); })}
); }