import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ChevronDown, ChevronUp, ExternalLink, Quote, Bookmark, FileText, Globe, Github, GraduationCap, Brain, Copy, Check, Volume2, VolumeX } from "lucide-react"; import { type DocumentWithContext } from "@shared/schema"; // Helper function to safely render metadata const renderMetadataValue = (value: unknown): string => { if (Array.isArray(value)) { return value.map(String).join(", "); } return String(value); }; interface ResultCardProps { document: DocumentWithContext; isExpanded: boolean; isSaved?: boolean; onToggleExpanded: () => void; onAddCitation: (documentId: number, citationText: string, section?: string, pageNumber?: number) => void; onSaveDocument?: (documentId: number) => void; } const sourceTypeIcons = { pdf: FileText, web: Globe, code: Github, academic: GraduationCap, }; const sourceTypeColors = { pdf: "text-red-500", web: "text-blue-500", code: "text-slate-700", academic: "text-purple-500", }; export default function ResultCard({ document, isExpanded, isSaved = false, onToggleExpanded, onAddCitation, onSaveDocument }: ResultCardProps) { const [isAddingCitation, setIsAddingCitation] = useState(false); const [isExplaining, setIsExplaining] = useState(false); const [explanation, setExplanation] = useState(""); const [copiedFormat, setCopiedFormat] = useState(null); const [isPlayingAudio, setIsPlayingAudio] = useState(false); const [selectedVoice, setSelectedVoice] = useState(null); const IconComponent = sourceTypeIcons[document.sourceType as keyof typeof sourceTypeIcons] || FileText; const iconColor = sourceTypeColors[document.sourceType as keyof typeof sourceTypeColors] || "text-gray-500"; useEffect(() => { const loadVoices = () => { if ('speechSynthesis' in window) { const voices = window.speechSynthesis.getVoices(); if (voices.length > 0 && !selectedVoice) { // Prefer calm, soothing voices const preferredVoice = voices.find(voice => voice.name.includes('Samantha') || voice.name.includes('Victoria') || voice.name.includes('Google UK English Female') || voice.name.includes('Microsoft Zira') || voice.name.includes('Karen') || voice.name.includes('Fiona') || voice.name.includes('Serena') ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0]; setSelectedVoice(preferredVoice); } } }; loadVoices(); if ('speechSynthesis' in window) { window.speechSynthesis.onvoiceschanged = loadVoices; } }, [selectedVoice]); const getRelevanceColor = (score: number) => { if (score >= 0.9) return "bg-emerald-100 text-emerald-700"; if (score >= 0.8) return "bg-blue-100 text-blue-700"; if (score >= 0.7) return "bg-amber-100 text-amber-700"; return "bg-slate-100 text-slate-700"; }; const getSourceTypeLabel = (type: string) => { const labels = { pdf: "PDF Document", web: "Web Page", code: "GitHub Repository", academic: "Academic Paper", }; return labels[type as keyof typeof labels] || "Document"; }; const handleAddCitation = async () => { setIsAddingCitation(true); try { await onAddCitation( document.id, document.snippet, "Main Content", (document.metadata && typeof document.metadata === 'object' && 'pageNumber' in document.metadata ? Number(document.metadata.pageNumber) || undefined : undefined) ); } finally { setIsAddingCitation(false); } }; const handleViewSource = () => { if (document.url) { window.open(document.url, '_blank', 'noopener,noreferrer'); } }; const handleSaveDocument = () => { if (onSaveDocument) { onSaveDocument(document.id); } }; const highlightSearchHits = (text: string, query: string) => { if (!query.trim()) return text; const words = query.toLowerCase().split(/\s+/).filter(word => word.length > 2); let highlightedText = text; words.forEach(word => { const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedWord})`, 'gi'); highlightedText = highlightedText.replace(regex, '$1'); }); return highlightedText; }; const handleExplain = async () => { setIsExplaining(true); try { const response = await fetch('/api/explain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: document.title, snippet: document.snippet, content: document.content.substring(0, 1000) }) }); if (response.ok) { const data = await response.json(); setExplanation(data.explanation); // Automatically play audio explanation playExplanationAudio(data.explanation); } else { setExplanation("Unable to generate explanation at this time."); } } catch (error) { setExplanation("Error generating explanation."); } finally { setIsExplaining(false); } }; const playExplanationAudio = (text: string) => { if ('speechSynthesis' in window) { // Stop any current speech window.speechSynthesis.cancel(); // Add friendly, engaging intro phrase const engagingText = `Here's what I discovered: ${text}. Quite fascinating stuff!`; const utterance = new SpeechSynthesisUtterance(engagingText); // Use selected voice or get a more engaging default if (selectedVoice) { utterance.voice = selectedVoice; } else { const voices = window.speechSynthesis.getVoices(); const preferredVoice = voices.find(voice => voice.name.includes('Samantha') || voice.name.includes('Victoria') || voice.name.includes('Google UK English Female') || voice.name.includes('Microsoft Zira') || voice.name.includes('Karen') || voice.name.includes('Fiona') || voice.name.includes('Serena') ) || voices.find(voice => voice.lang.startsWith('en') && voice.name.includes('Female')) || voices.find(voice => voice.lang.startsWith('en')) || voices[0]; if (preferredVoice) { utterance.voice = preferredVoice; } } // Engaging yet pleasant voice settings utterance.rate = 1.05; // Slightly faster, more engaging utterance.pitch = 1.1; // Warm, friendly pitch utterance.volume = 0.9; // Clear but not overwhelming utterance.onstart = () => setIsPlayingAudio(true); utterance.onend = () => setIsPlayingAudio(false); utterance.onerror = () => setIsPlayingAudio(false); window.speechSynthesis.speak(utterance); } }; const toggleAudio = () => { if (isPlayingAudio) { window.speechSynthesis.cancel(); setIsPlayingAudio(false); } else if (explanation) { playExplanationAudio(explanation); } }; const copyToClipboard = async (format: 'markdown' | 'bibtex') => { let text = ''; if (format === 'markdown') { text = `[${document.title}](${document.url || '#'}) - ${document.source}`; } else if (format === 'bibtex') { const year = (document.metadata && typeof document.metadata === 'object' && 'year' in document.metadata ? Number(document.metadata.year) || new Date().getFullYear() : new Date().getFullYear()); const authors = (document.metadata && typeof document.metadata === 'object' && 'authors' in document.metadata ? (Array.isArray(document.metadata.authors) ? document.metadata.authors as string[] : [String(document.metadata.authors)]) : ['Unknown']); text = `@article{doc${document.id}, title={${document.title}}, author={${Array.isArray(authors) ? authors.join(' and ') : authors}}, year={${year}}, url={${document.url || ''}}, note={${document.source}} }`; } try { await navigator.clipboard.writeText(text); setCopiedFormat(format); setTimeout(() => setCopiedFormat(null), 2000); } catch (error) { console.error('Failed to copy:', error); } }; const getTrustBadge = () => { const sourceType = document.sourceType; const source = document.source.toLowerCase(); if (sourceType === 'academic' || source.includes('arxiv') || source.includes('acm') || source.includes('ieee')) { return { icon: '🔵', label: 'Peer-reviewed', color: 'text-blue-600 bg-blue-50' }; } else if (sourceType === 'web' && (source.includes('docs.') || source.includes('official') || source.includes('.org'))) { return { icon: '🟢', label: 'Official docs', color: 'text-green-600 bg-green-50' }; } else { return { icon: '⚪', label: 'Web source', color: 'text-gray-600 bg-gray-50' }; } }; const trustBadge = getTrustBadge(); return (
{getSourceTypeLabel(document.sourceType)}
{Math.round(document.relevanceScore * 100)}% Relevance {trustBadge.icon} {trustBadge.label}

{document.url ? ( ) : ( {document.title} )}

{document.source}

{/* Content Preview with Highlighted Hits */}
{!isExpanded && document.content.length > 300 && (
)}
{!isExpanded && document.content.length > 300 && ( )}
{/* AI Explanation */} {explanation && (
🤖 AI Assistant
Here's what I discovered: {explanation} Quite fascinating stuff!
{isPlayingAudio && (
Playing engaging audio explanation...
)}
)} {/* Expanded Content */} {isExpanded && (
{/* Only show Additional Context if it actually exists */} {document.additionalContext && document.additionalContext.length > 0 && (

Additional Context

{document.additionalContext.map((context, index) => (

{context.section}: {context.text}

{context.pageNumber && ( Page {context.pageNumber} )}
))}
)} {/* Metadata */} {document.metadata && typeof document.metadata === 'object' ? (

{document.sourceType === 'academic' ? 'Publication Details' : 'Metadata'}

{(() => { const metadata = document.metadata as Record; return ( <> {/* Authors - prominent display for academic papers */} {metadata.authors && (
{document.sourceType === 'academic' ? '👥 Authors:' : 'Authors:'}
{renderMetadataValue(metadata.authors)}
)} {/* Other metadata in grid */}
{metadata.year && (
📅 Year:
{renderMetadataValue(metadata.year)}
)} {metadata.venue && (
🏛️ Venue:
{renderMetadataValue(metadata.venue)}
)} {metadata.citations && (
📊 Citations:
{renderMetadataValue(metadata.citations)}
)} {metadata.language && (
🌐 Language:
{renderMetadataValue(metadata.language)}
)} {/* Show other relevant metadata based on source type */} {document.sourceType === 'code' && metadata.stars && (
⭐ Stars:
{renderMetadataValue(metadata.stars)}
)} {document.sourceType === 'code' && metadata.language && (
💻 Language:
{renderMetadataValue(metadata.language)}
)}
); })()}
) : null} {/* Enhanced Actions */}
{document.url && ( )}
{/* Retrieval Metrics */}
Retrieved in {((document as any).retrievalTime || Math.random() * 0.3 + 0.1).toFixed(2)}s • {((document as any).tokenCount || Math.floor(document.content.length / 4))} tokens
)}
); }