// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates // SPDX-License-Identifier: MIT import { ExternalLink, Globe, Clock, Star } from "lucide-react"; import { useMemo } from "react"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "~/components/ui/hover-card"; import { cn } from "~/lib/utils"; import type { Citation } from "~/core/messages"; // Re-export Citation type as CitationData for backward compatibility export type CitationData = Citation; interface CitationLinkProps { href: string; children: React.ReactNode; citations: CitationData[]; className?: string; id?: string; } /** * Enhanced link component that shows citation metadata on hover. * Used within markdown content to provide rich citation information. */ export function CitationLink({ href, children, citations, className, id, }: CitationLinkProps) { // Find matching citation data for this URL const { citation, index } = useMemo(() => { if (!href || !citations) return { citation: null, index: -1 }; // Try exact match first let matchIndex = citations.findIndex((c) => c.url === href); // If not found, try versatile comparison using normalized URLs if (matchIndex === -1) { const normalizeUrl = (url: string) => { try { return decodeURIComponent(url).trim(); } catch { return url.trim(); } }; const normalizedHref = normalizeUrl(href); matchIndex = citations.findIndex( (c) => normalizeUrl(c.url) === normalizedHref ); } const match = matchIndex !== -1 ? citations[matchIndex] : null; return { citation: match, index: matchIndex }; }, [href, citations]); // If no citation data found, render as regular link if (!citation) { return ( {children} ); } const handleCitationClick = (e: React.MouseEvent) => { // If it's an internal-looking citation (e.g. [1]) // or if the user clicks the citation number in the text // we try to scroll to the reference list at the bottom if (index !== -1) { const targetId = `ref-${index + 1}`; const element = document.getElementById(targetId); if (element) { e.preventDefault(); element.scrollIntoView({ behavior: "smooth", block: "start" }); } } // If element not found or index is -1, let the default behavior (open URL) happen }; return ( {children} ); } interface CitationCardProps { citation: CitationData; compact?: boolean; } /** * Card component displaying citation metadata. */ export function CitationCard({ citation, compact = false }: CitationCardProps) { const { title, url, description, domain, relevance_score, accessed_at, source_type, } = citation; // Format access date const formattedDate = useMemo(() => { if (!accessed_at) return null; try { const date = new Date(accessed_at); return date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", }); } catch { return accessed_at.slice(0, 10); } }, [accessed_at]); // Format relevance score as percentage const relevancePercent = useMemo(() => { if (relevance_score == null || relevance_score <= 0) return null; return Math.round(relevance_score * 100); }, [relevance_score]); return ( {/* Title */} {title} {/* Domain and metadata row */} {domain && ( {domain} )} {formattedDate && ( {formattedDate} )} {relevancePercent != null && ( {relevancePercent}% match )} {/* Description/snippet */} {description && !compact && ( {description} )} {/* Source type badge */} {source_type && ( {source_type === "web_search" ? "Web" : source_type} )} {/* URL preview */} {url} ); } interface CitationListProps { citations: CitationData[]; title?: string; className?: string; } /** * List component for displaying all citations. */ export function CitationList({ citations, title = "Sources", className, }: CitationListProps) { if (!citations || citations.length === 0) { return null; } return (

{title}

{citations.map((citation, index) => (
{index + 1}
{citation.title} {citation.domain && (

{citation.domain}

)} {citation.description && (

{citation.description}

)}
))}
); } interface CitationBadgeProps { number: number; citation?: CitationData; onClick?: () => void; } /** * Small numbered badge for inline citations. */ export function CitationBadge({ number, citation, onClick }: CitationBadgeProps) { const badge = ( ); if (!citation) { return badge; } return ( {badge} ); }