import type { Message } from "@langchain/langgraph-sdk"; import { ExternalLinkIcon, LinkIcon } from "lucide-react"; import { useParams } from "next/navigation"; import { memo, useMemo } from "react"; import { InlineCitationCard, InlineCitationCardBody, InlineCitationSource, } from "@/components/ai-elements/inline-citation"; import { Message as AIElementMessage, MessageContent as AIElementMessageContent, MessageResponse as AIElementMessageResponse, MessageToolbar, } from "@/components/ai-elements/message"; import { Badge } from "@/components/ui/badge"; import { HoverCardTrigger } from "@/components/ui/hover-card"; import { resolveArtifactURL } from "@/core/artifacts/utils"; import { type Citation, buildCitationMap, extractDomainFromUrl, parseCitations, } from "@/core/citations"; import { extractContentFromMessage, extractReasoningContentFromMessage, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { cn } from "@/lib/utils"; import { CopyButton } from "../copy-button"; export function MessageListItem({ className, message, isLoading, }: { className?: string; message: Message; isLoading?: boolean; }) { return (
); } function MessageContent_({ className, message, isLoading = false, }: { className?: string; message: Message; isLoading?: boolean; }) { const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); // Extract and parse citations from message content const { citations, cleanContent } = useMemo(() => { const reasoningContent = extractReasoningContentFromMessage(message); const rawContent = extractContentFromMessage(message); if (!isLoading && reasoningContent && !rawContent) { return { citations: [], cleanContent: reasoningContent }; } return parseCitations(rawContent ?? ""); }, [isLoading, message]); // Build citation map for quick URL lookup const citationMap = useMemo( () => buildCitationMap(citations), [citations], ); const { thread_id } = useParams<{ thread_id: string }>(); return ( {/* Citations list at the top */} {citations.length > 0 && } ) => { if (!href) { return {children}; } // Check if this link matches a citation const citation = citationMap.get(href); if (citation) { return ( {children} ); } // Regular external link return ( {children} ); }, img: ({ src, alt }: React.ImgHTMLAttributes) => { if (!src) return null; if (typeof src !== "string") { return ( {alt} ); } let url = src; if (src.startsWith("/mnt/")) { url = resolveArtifactURL(src, thread_id); } return ( {alt} ); }, }} > {cleanContent} ); } /** * Citations list component that displays all sources at the top */ function CitationsList({ citations }: { citations: Citation[] }) { if (citations.length === 0) return null; return (
Sources ({citations.length})
{citations.map((citation) => ( ))}
); } /** * Single citation badge in the citations list */ function CitationBadge({ citation }: { citation: Citation }) { const domain = extractDomainFromUrl(citation.url); return ( {domain}
Visit source
); } /** * Citation link component that renders as a hover card badge */ function CitationLink({ citation, href, children, }: { citation: Citation; href: string; children: React.ReactNode; }) { const domain = extractDomainFromUrl(href); return ( e.stopPropagation()} > {children ?? domain}
Visit source
); } const MessageContent = memo(MessageContent_);