"use client"; import { getAnnotationData, type Message } from "@llamaindex/chat-ui"; import { ExternalLink, FileText } from "lucide-react"; import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { SourceDetailSheet } from "./SourceDetailSheet"; interface Source { id: string; title: string; description: string; url: string; sourceType: string; } interface SourceGroup { id: number; name: string; type: string; sources: Source[]; } // New interfaces for the updated data format interface NodeMetadata { title: string; source_type: string; group_name: string; } interface SourceNode { id: string; text: string; url: string; metadata: NodeMetadata; } function getSourceIcon(type: string) { // Handle USER_SELECTED_ prefix const normalizedType = type.startsWith("USER_SELECTED_") ? type.replace("USER_SELECTED_", "") : type; return getConnectorIcon(normalizedType, "h-4 w-4"); } function SourceCard({ source }: { source: Source }) { const hasUrl = source.url && source.url.trim() !== ""; const chunkId = Number(source.id); const sourceType = source.sourceType; const [isOpen, setIsOpen] = useState(false); // Clean up the description for better display const cleanDescription = source.description .replace(/## Metadata\n\n/g, "") .replace(/\n+/g, " ") .trim(); const handleUrlClick = (e: React.MouseEvent, url: string) => { e.preventDefault(); e.stopPropagation(); window.open(url, "_blank", "noopener,noreferrer"); }; return ( {source.title} #{chunkId} {hasUrl && ( handleUrlClick(e, source.url)} > )} {cleanDescription} ); } export default function ChatSourcesDisplay({ message }: { message: Message }) { const [open, setOpen] = useState(false); const annotations = getAnnotationData(message, "sources"); // Transform the new data format to the expected SourceGroup format const sourceGroups: SourceGroup[] = []; if (Array.isArray(annotations) && annotations.length > 0) { // Extract all nodes from the response const allNodes: SourceNode[] = []; annotations.forEach((item) => { if (item && typeof item === "object" && "nodes" in item && Array.isArray(item.nodes)) { allNodes.push(...item.nodes); } }); // Group nodes by source_type const groupedByType = allNodes.reduce( (acc, node) => { const sourceType = node.metadata.source_type; if (!acc[sourceType]) { acc[sourceType] = []; } acc[sourceType].push(node); return acc; }, {} as Record ); // Convert grouped nodes to SourceGroup format Object.entries(groupedByType).forEach(([sourceType, nodes], index) => { if (nodes.length > 0) { const firstNode = nodes[0]; sourceGroups.push({ id: index + 100, // Generate unique ID name: firstNode.metadata.group_name, type: sourceType, sources: nodes.map((node) => ({ id: node.id, title: node.metadata.title, description: node.text, url: node.url || "", sourceType: sourceType, })), }); } }); } if (sourceGroups.length === 0) { return null; } const totalSources = sourceGroups.reduce((acc, group) => acc + group.sources.length, 0); return ( View Sources ({totalSources}) Sources {totalSources} {totalSources === 1 ? "source" : "sources"} {sourceGroups.map((group) => ( {getSourceIcon(group.type)} {group.name} {group.sources.length} ))} {sourceGroups.map((group) => ( {group.sources.map((source) => ( ))} ))} ); }