"use client"; import { getAnnotationData, type Message } from "@llamaindex/chat-ui"; import { IconBrandDiscord, IconBrandGithub, IconBrandNotion, IconBrandSlack, IconBrandYoutube, } from "@tabler/icons-react"; import { BookOpen, Calendar, CheckSquare, ExternalLink, FileText, Globe, Link2, Mail, Puzzle, } 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"; interface Source { id: string; title: string; description: string; url: 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) { switch (type) { // GitHub case "USER_SELECTED_GITHUB_CONNECTOR": case "GITHUB_CONNECTOR": return ; // Notion case "USER_SELECTED_NOTION_CONNECTOR": case "NOTION_CONNECTOR": return ; // Slack case "USER_SELECTED_SLACK_CONNECTOR": case "SLACK_CONNECTOR": return ; // Discord case "USER_SELECTED_DISCORD_CONNECTOR": case "DISCORD_CONNECTOR": return ; // Google Calendar case "USER_SELECTED_GOOGLE_CALENDAR_CONNECTOR": case "GOOGLE_CALENDAR_CONNECTOR": return ; // Google Gmail case "USER_SELECTED_GOOGLE_GMAIL_CONNECTOR": case "GOOGLE_GMAIL_CONNECTOR": return ; // YouTube case "USER_SELECTED_YOUTUBE_VIDEO": case "YOUTUBE_VIDEO": return ; // Linear case "USER_SELECTED_LINEAR_CONNECTOR": case "LINEAR_CONNECTOR": return ; // Jira case "USER_SELECTED_JIRA_CONNECTOR": case "JIRA_CONNECTOR": return ; // Confluence case "USER_SELECTED_CONFLUENCE_CONNECTOR": case "CONFLUENCE_CONNECTOR": return ; // ClickUp case "USER_SELECTED_CLICKUP_CONNECTOR": case "CLICKUP_CONNECTOR": return ; // Files case "USER_SELECTED_FILE": case "FILE": return ; // Extension case "USER_SELECTED_EXTENSION": case "EXTENSION": return ; // Crawled URL case "USER_SELECTED_CRAWLED_URL": case "CRAWLED_URL": return ; // Default for any other source type default: return ; } } function SourceCard({ source }: { source: Source }) { const hasUrl = source.url && source.url.trim() !== ""; // Clean up the description for better display const cleanDescription = source.description .replace(/## Metadata\n\n/g, "") .replace(/\n+/g, " ") .trim(); return ( {source.title} {hasUrl && ( window.open(source.url, "_blank")} > )} {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 || "", })), }); } }); } 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) => ( ))} ))} ); }