import React, { useState } from 'react'; import { Box, Typography, Paper, IconButton, Collapse, Chip, Button, Menu, MenuItem, ListItemIcon, ListItemText, Tooltip, RadioGroup, FormControlLabel, Radio, Slider, Divider, } from '@mui/material'; import { ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, FormatSize as FontSizeIcon, ViewModule as ViewModeIcon, ContentCopy as CopyIcon, FormatQuote as QuoteIcon, Code as CodeIcon, WrapText as WrapIcon, Search as SearchIcon, Settings as SettingsIcon, } from '@mui/icons-material'; interface HighlightRange { start: number; end: number; } interface Snippet { text: string; highlight_ranges?: HighlightRange[]; source?: 'content' | 'ocr_text' | 'filename'; page_number?: number; confidence?: number; } interface EnhancedSnippetViewerProps { snippets: Snippet[]; searchQuery?: string; maxSnippetsToShow?: number; onSnippetClick?: (snippet: Snippet, index: number) => void; } type ViewMode = 'compact' | 'detailed' | 'context'; type HighlightStyle = 'background' | 'underline' | 'bold'; const EnhancedSnippetViewer: React.FC = ({ snippets, searchQuery, maxSnippetsToShow = 3, onSnippetClick, }) => { const [expanded, setExpanded] = useState(false); const [viewMode, setViewMode] = useState('detailed'); const [highlightStyle, setHighlightStyle] = useState('background'); const [fontSize, setFontSize] = useState(14); const [contextLength, setContextLength] = useState(50); const [settingsAnchor, setSettingsAnchor] = useState(null); const [copiedIndex, setCopiedIndex] = useState(null); const visibleSnippets = expanded ? snippets : snippets.slice(0, maxSnippetsToShow); const handleCopySnippet = (text: string, index: number) => { navigator.clipboard.writeText(text); setCopiedIndex(index); setTimeout(() => setCopiedIndex(null), 2000); }; const renderHighlightedText = (text: string, highlightRanges?: HighlightRange[]): React.ReactNode => { if (!highlightRanges || highlightRanges.length === 0) { return text; } const parts: React.ReactNode[] = []; let lastIndex = 0; highlightRanges.forEach((range, index) => { // Add text before highlight if (range.start > lastIndex) { parts.push( {text.substring(lastIndex, range.start)} ); } // Add highlighted text const highlightedText = text.substring(range.start, range.end); parts.push( {highlightedText} ); lastIndex = range.end; }); // Add remaining text if (lastIndex < text.length) { parts.push( {text.substring(lastIndex)} ); } return parts; }; const getSourceIcon = (source?: string) => { switch (source) { case 'ocr_text': return ; case 'filename': return ; default: return ; } }; const getSourceLabel = (source?: string) => { switch (source) { case 'ocr_text': return 'OCR Text'; case 'filename': return 'Filename'; default: return 'Document Content'; } }; const renderSnippet = (snippet: Snippet, index: number) => { const isCompact = viewMode === 'compact'; const showContext = viewMode === 'context'; // Extract context around highlights if in context mode let displayText = snippet.text; if (showContext && snippet.highlight_ranges && snippet.highlight_ranges.length > 0) { const firstHighlight = snippet.highlight_ranges[0]; const lastHighlight = snippet.highlight_ranges[snippet.highlight_ranges.length - 1]; const contextStart = Math.max(0, firstHighlight.start - contextLength); const contextEnd = Math.min(snippet.text.length, lastHighlight.end + contextLength); displayText = (contextStart > 0 ? '...' : '') + snippet.text.substring(contextStart, contextEnd) + (contextEnd < snippet.text.length ? '...' : ''); // Adjust highlight ranges for the new substring if (snippet.highlight_ranges) { snippet = { ...snippet, text: displayText, highlight_ranges: snippet.highlight_ranges.map(range => ({ start: range.start - contextStart + (contextStart > 0 ? 3 : 0), end: range.end - contextStart + (contextStart > 0 ? 3 : 0), })), }; } } return ( theme.palette.mode === 'light' ? 'grey.50' : 'grey.900', borderLeft: '3px solid', borderLeftColor: snippet.source === 'ocr_text' ? 'warning.main' : 'primary.main', cursor: onSnippetClick ? 'pointer' : 'default', transition: 'all 0.2s', '&:hover': onSnippetClick ? { backgroundColor: (theme) => theme.palette.mode === 'light' ? 'grey.100' : 'grey.800', transform: 'translateX(4px)', } : {}, }} onClick={() => onSnippetClick?.(snippet, index)} > {!isCompact && ( {snippet.page_number && ( )} {snippet.confidence && snippet.confidence < 0.8 && ( )} )} {renderHighlightedText(snippet.text, snippet.highlight_ranges)} { e.stopPropagation(); handleCopySnippet(snippet.text, index); }} sx={{ color: copiedIndex === index ? 'success.main' : 'text.secondary' }} > ); }; return ( Search Results {snippets.length > 0 && ( 999 ? `${Math.floor(snippets.length/1000)}K` : snippets.length} matches`} size="small" color="primary" variant="outlined" sx={{ maxWidth: '100px', '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' } }} /> )} setSettingsAnchor(e.currentTarget)} > {snippets.length > maxSnippetsToShow && ( )} {searchQuery && ( Showing matches for: {searchQuery} )} {visibleSnippets.map((snippet, index) => renderSnippet(snippet, index))} {snippets.length === 0 && ( No text snippets available for this search result )} {/* Settings Menu */} setSettingsAnchor(null)} PaperProps={{ sx: { width: 320, p: 2 } }} > Snippet Display Settings View Mode setViewMode(e.target.value as ViewMode)} > } label="Compact" /> } label="Detailed (with metadata)" /> } label="Context Focus" /> Highlight Style setHighlightStyle(e.target.value as HighlightStyle)} > } label="Background Color" /> } label="Underline" /> } label="Bold Text" /> Font Size: {fontSize}px setFontSize(value as number)} min={12} max={20} marks valueLabelDisplay="auto" /> {viewMode === 'context' && ( <> Context Length: {contextLength} characters setContextLength(value as number)} min={20} max={200} step={10} marks valueLabelDisplay="auto" /> )} ); }; export default EnhancedSnippetViewer;