import type { AssistantCitation } from "@t3tools/contracts"; import { serializeAssistantCitation } from "@t3tools/shared/assistantCitations"; import { Link, useNavigate } from "@tanstack/react-router"; import { PencilIcon, QuoteIcon, XIcon } from "lucide-react"; import { useEffect, useEffectEvent, useRef, type MouseEvent as ReactMouseEvent } from "react"; import { findAssistantCitationSourceAnchor, type AssistantCitationSourceAnchor, } from "~/lib/assistantTextSelection"; import { cn } from "~/lib/utils"; import { assistantCitationHash, assistantCitationNavigation, } from "../../lib/assistantCitationNavigation"; import { CHAT_INLINE_CHIP_CLASS_NAME, COMPOSER_INLINE_CHIP_CLASS_NAME, COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME, COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, } from "../composerInlineChip"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor"; import { observeAssistantCitationCommentSource } from "./AssistantCitationSource"; import { composerFloatingLayerProps } from "./composerEventScope"; const CITATION_ACTION_BUTTON_CLASS_NAME = cn( COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME, "text-primary/80 hover:bg-primary/10 hover:text-primary", ); export function AssistantCitationChip({ citation, onRemove, commentEditor, }: { citation: AssistantCitation; onRemove?: () => void; commentEditor?: { open: boolean; sourceAnchor?: AssistantCitationSourceAnchor | undefined; onOpenChange: (open: boolean) => void; onCancel?: () => void; onSave: (comment: string) => boolean; onSaveAndSend?: (comment: string) => boolean; }; }) { const navigate = useNavigate(); const commentInputRef = useRef(null); const commentOpen = commentEditor?.open ?? false; const sourceAnchor = commentEditor?.sourceAnchor; const onSourceUnavailable = useEffectEvent(() => { if (sourceAnchor) commentEditor?.onOpenChange(false); }); useEffect(() => { if (!commentOpen) return; const anchor = sourceAnchor ?? findAssistantCitationSourceAnchor(document, citation); if (!anchor) return; return observeAssistantCitationCommentSource({ anchor, citation, onUnavailable: onSourceUnavailable, }); }, [citation, commentOpen, sourceAnchor]); // A multi-line selection's bounding box spans the full message width; anchor // the bubble to the selection's last line, where the pointer released. const popupAnchor = sourceAnchor ? { contextElement: sourceAnchor.source, getBoundingClientRect: () => { const rects = sourceAnchor.range.getClientRects(); return rects.item(rects.length - 1) ?? sourceAnchor.range.getBoundingClientRect(); }, } : undefined; const preview = (citation.comment?.trim() || citation.text).replace(/\s+/g, " "); const label = preview.length > 64 ? `${preview.slice(0, 64)}…` : preview; const sourceLinkProps = { to: "/$environmentId/$threadId" as const, params: { environmentId: citation.environmentId, threadId: citation.threadId }, hash: assistantCitationHash(citation), "data-markdown-copy": serializeAssistantCitation(citation), resetScroll: false, onClick: (event: ReactMouseEvent) => { if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { return; } event.preventDefault(); void navigate(assistantCitationNavigation(citation)); }, }; const composerSourceLink = (