customer-support-agent/components/RightSidebar.tsx (185 lines of code) (raw):

"use client"; import React, { useState, useEffect } from "react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { FileIcon, MessageCircleIcon } from "lucide-react"; import FullSourceModal from "./FullSourceModal"; interface RAGSource { id: string; fileName: string; snippet: string; score: number; timestamp?: string; } interface RAGHistoryItem { sources: RAGSource[]; timestamp: string; query: string; } interface DebugInfo { context_used: boolean; } interface SidebarEvent { id: string; content: string; user_mood?: string; debug?: DebugInfo; } const truncateSnippet = (text: string): string => { return text?.length > 150 ? `${text.slice(0, 100)}...` : text || ""; }; const getScoreColor = (score: number): string => { if (score > 0.6) return "bg-green-100 text-green-800"; if (score > 0.4) return "bg-yellow-100 text-yellow-800"; return "bg-red-100 text-red-800"; }; const MAX_HISTORY = 15; const RightSidebar: React.FC = () => { const [ragHistory, setRagHistory] = useState<RAGHistoryItem[]>([]); const [shouldShowSources, setShouldShowSources] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedSource, setSelectedSource] = useState<RAGSource | null>(null); useEffect(() => { const updateRAGSources = ( event: CustomEvent<{ sources: RAGSource[]; query: string; debug?: DebugInfo; }>, ) => { console.log("🔍 RAG event received:", event.detail); const { sources, query, debug } = event.detail; const shouldDisplaySources = debug?.context_used; if ( Array.isArray(sources) && sources.length > 0 && shouldDisplaySources ) { const cleanedSources = sources.map((source) => ({ ...source, snippet: source.snippet || "No preview available", fileName: (source.fileName || "").replace(/_/g, " ").replace(".txt", "") || "Unnamed", timestamp: new Date().toISOString(), })); const historyItem: RAGHistoryItem = { sources: cleanedSources, timestamp: new Date().toISOString(), query: query || "Unknown query", }; setRagHistory((prev) => { const newHistory = [historyItem, ...prev]; return newHistory.slice(0, MAX_HISTORY); }); console.log( "🔍 Sources displayed:", shouldDisplaySources ? "YES" : "NO", ); } }; const updateDebug = (event: CustomEvent<SidebarEvent>) => { const debug = event.detail.debug; const shouldShow = debug?.context_used ?? false; setShouldShowSources(shouldShow); }; window.addEventListener( "updateRagSources" as any, updateRAGSources as EventListener, ); window.addEventListener( "updateSidebar" as any, updateDebug as EventListener, ); return () => { window.removeEventListener( "updateRagSources" as any, updateRAGSources as EventListener, ); window.removeEventListener( "updateSidebar" as any, updateDebug as EventListener, ); }; }, []); const handleViewFullSource = (source: RAGSource) => { setSelectedSource(source); setIsModalOpen(true); }; const fadeInUpClass = "animate-fade-in-up"; const fadeStyle = { animationDuration: "600ms", animationFillMode: "backwards", animationTimingFunction: "cubic-bezier(0.2, 0.8, 0.2, 1)", }; return ( <aside className="w-[380px] pr-4 overflow-hidden pb-4"> <Card className={`${fadeInUpClass} h-full overflow-hidden`} style={fadeStyle} > <CardHeader> <CardTitle className="text-sm font-medium leading-none"> Knowledge Base History </CardTitle> </CardHeader> <CardContent className="overflow-y-auto h-[calc(100%-45px)]"> {ragHistory.length === 0 && ( <div className="text-sm text-muted-foreground"> The assistant will display sources here once finding them </div> )} {ragHistory.map((historyItem, index) => ( <div key={historyItem.timestamp} className={`mb-6 ${fadeInUpClass}`} style={{ ...fadeStyle, animationDelay: `${index * 50}ms` }} > <div className="flex items-center text-xs text-muted-foreground mb-2 gap-1"> <MessageCircleIcon size={14} className="text-muted-foreground" /> <span>{historyItem.query}</span> </div> {historyItem.sources.map((source, sourceIndex) => ( <Card key={source.id} className={`mb-2 ${fadeInUpClass}`} style={{ ...fadeStyle, animationDelay: `${index * 100 + sourceIndex * 75}ms`, }} > <CardContent className="py-4"> <p className="text-sm text-muted-foreground"> {truncateSnippet(source.snippet)} </p> <div className="flex flex-col gap-2"> <div className={`${getScoreColor(source.score)} px-2 py-1 mt-4 rounded-full text-xs inline-block w-fit`} > {(source.score * 100).toFixed(0)}% match </div> <div className="inline-flex items-center mr-2 mt-2 text-muted-foreground text-xs py-0 cursor-pointer hover:text-gray-600" onClick={() => handleViewFullSource(source)} > <FileIcon className="w-4 h-4 min-w-[12px] min-h-[12px] mr-2" /> <span className="text-xs underline"> {truncateSnippet(source.fileName || "Unnamed")} </span> </div> </div> </CardContent> </Card> ))} </div> ))} </CardContent> </Card> <FullSourceModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} source={selectedSource} /> </aside> ); }; export default RightSidebar;