import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Box, Typography, Card, CardContent, TextField, InputAdornment, Button, Chip, Stack, FormControl, InputLabel, Select, MenuItem, OutlinedInput, Checkbox, ListItemText, Accordion, AccordionSummary, AccordionDetails, Slider, ToggleButton, ToggleButtonGroup, CircularProgress, Alert, Divider, IconButton, Tooltip, Autocomplete, LinearProgress, FormControlLabel, Switch, Paper, Skeleton, SelectChangeEvent, Menu, RadioGroup, Radio, Pagination, } from '@mui/material'; import Grid from '@mui/material/GridLegacy'; import { Search as SearchIcon, FilterList as FilterIcon, Clear as ClearIcon, ExpandMore as ExpandMoreIcon, Download as DownloadIcon, PictureAsPdf as PdfIcon, Image as ImageIcon, Description as DocIcon, TextSnippet as TextIcon, CalendarToday as DateIcon, Storage as SizeIcon, Tag as TagIcon, Visibility as ViewIcon, Settings as SettingsIcon, Speed as SpeedIcon, AccessTime as TimeIcon, TrendingUp as TrendingIcon, TextFormat as TextFormatIcon, } from '@mui/icons-material'; import { documentService, SearchRequest } from '../services/api'; import SearchGuidance from '../components/SearchGuidance'; import EnhancedSearchGuide from '../components/EnhancedSearchGuide'; import MimeTypeFacetFilter from '../components/MimeTypeFacetFilter'; import EnhancedSnippetViewer from '../components/EnhancedSnippetViewer'; import AdvancedSearchPanel from '../components/AdvancedSearchPanel'; interface Document { id: string; original_filename: string; filename?: string; file_size: number; mime_type: string; created_at: string; has_ocr_text?: boolean; tags: string[]; snippets?: Snippet[]; search_rank?: number; } interface Snippet { text: string; highlight_ranges?: HighlightRange[]; } interface HighlightRange { start: number; end: number; } interface SearchResponse { documents: Document[]; total: number; query_time_ms: number; suggestions?: string[]; } interface MimeTypeOption { value: string; label: string; } interface SearchFilters { tags?: string[]; mimeTypes?: string[]; dateRange?: number[]; fileSizeRange?: number[]; hasOcr?: string; } type SearchMode = 'simple' | 'phrase' | 'fuzzy' | 'boolean'; type OcrStatus = 'all' | 'yes' | 'no'; interface AdvancedSearchSettings { useEnhancedSearch: boolean; searchMode: SearchMode; includeSnippets: boolean; snippetLength: number; fuzzyThreshold: number; resultLimit: number; includeOcrText: boolean; includeFileContent: boolean; includeFilenames: boolean; boostRecentDocs: boolean; enableAutoCorrect: boolean; } type SnippetViewMode = 'compact' | 'detailed' | 'context'; type SnippetHighlightStyle = 'background' | 'underline' | 'bold'; interface SnippetSettings { viewMode: SnippetViewMode; highlightStyle: SnippetHighlightStyle; fontSize: number; contextLength: number; maxSnippetsToShow: number; } const SearchPage: React.FC = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || ''); const [searchResults, setSearchResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [queryTime, setQueryTime] = useState(0); const [totalResults, setTotalResults] = useState(0); const [suggestions, setSuggestions] = useState([]); const [isTyping, setIsTyping] = useState(false); const [searchProgress, setSearchProgress] = useState(0); const [quickSuggestions, setQuickSuggestions] = useState([]); const [showFilters, setShowFilters] = useState(false); const [searchTips] = useState([ 'Use quotes for exact phrases: "project plan"', 'Search by tags: tag:important or tag:invoice', 'Combine terms: contract AND payment', 'Use wildcards: proj* for project, projects, etc.' ]); // Search settings - consolidated into advanced settings const [showAdvanced, setShowAdvanced] = useState(false); const [advancedSettings, setAdvancedSettings] = useState({ useEnhancedSearch: true, searchMode: 'simple', includeSnippets: true, snippetLength: 200, fuzzyThreshold: 0.8, resultLimit: 100, includeOcrText: true, includeFileContent: true, includeFilenames: true, boostRecentDocs: false, enableAutoCorrect: true, }); // Global snippet settings const [snippetSettings, setSnippetSettings] = useState({ viewMode: 'detailed', highlightStyle: 'background', fontSize: 15, contextLength: 50, maxSnippetsToShow: 3, }); const [snippetSettingsAnchor, setSnippetSettingsAnchor] = useState(null); // Pagination states const [currentPage, setCurrentPage] = useState(1); const [resultsPerPage] = useState(20); // Filter states const [selectedTags, setSelectedTags] = useState([]); const [selectedMimeTypes, setSelectedMimeTypes] = useState([]); const [dateRange, setDateRange] = useState([0, 365]); // days const [fileSizeRange, setFileSizeRange] = useState([0, 100]); // MB const [hasOcr, setHasOcr] = useState('all'); // Available options (would typically come from API) const [availableTags, setAvailableTags] = useState([]); const mimeTypeOptions: MimeTypeOption[] = [ { value: 'application/pdf', label: 'PDF' }, { value: 'image/', label: 'Images' }, { value: 'text/', label: 'Text Files' }, { value: 'application/msword', label: 'Word Documents' }, { value: 'application/vnd.openxmlformats-officedocument', label: 'Office Documents' }, ]; // Enhanced debounced search with typing indicators const debounce = useCallback((func: (...args: any[]) => void, delay: number) => { let timeoutId: NodeJS.Timeout; return (...args: any[]) => { clearTimeout(timeoutId); setIsTyping(true); timeoutId = setTimeout(() => { setIsTyping(false); func.apply(null, args); }, delay); }; }, []); // Quick suggestions generator const generateQuickSuggestions = useCallback((query: string): void => { if (!query || query.length < 2) { setQuickSuggestions([]); return; } const suggestions: string[] = []; // Add exact phrase suggestion if (!query.includes('"')) { suggestions.push(`"${query}"`); } // Add tag suggestions if (!query.startsWith('tag:')) { suggestions.push(`tag:${query}`); } // Add wildcard suggestion if (!query.includes('*')) { suggestions.push(`${query}*`); } setQuickSuggestions(suggestions.slice(0, 3)); }, []); const performSearch = useCallback(async (query: string, filters: SearchFilters = {}, page: number = 1): Promise => { if (!query.trim()) { setSearchResults([]); setTotalResults(0); setQueryTime(0); setSuggestions([]); setQuickSuggestions([]); return; } try { setLoading(true); setError(null); setSearchProgress(0); // Simulate progressive loading for better UX const progressInterval = setInterval(() => { setSearchProgress(prev => Math.min(prev + 20, 90)); }, 100); const searchRequest: SearchRequest = { query: query.trim(), tags: filters.tags?.length ? filters.tags : undefined, mime_types: filters.mimeTypes?.length ? filters.mimeTypes : undefined, limit: resultsPerPage, offset: (page - 1) * resultsPerPage, include_snippets: advancedSettings.includeSnippets, snippet_length: advancedSettings.snippetLength, search_mode: advancedSettings.searchMode, }; const response = advancedSettings.useEnhancedSearch ? await documentService.enhancedSearch(searchRequest) : await documentService.search(searchRequest); // Apply additional client-side filters let results = response.data.documents || []; // Filter by date range if (filters.dateRange) { const now = new Date(); const [minDays, maxDays] = filters.dateRange; results = results.filter(doc => { const docDate = new Date(doc.created_at); const daysDiff = Math.ceil((now.getTime() - docDate.getTime()) / (1000 * 60 * 60 * 24)); return daysDiff >= minDays && daysDiff <= maxDays; }); } // Filter by file size if (filters.fileSizeRange) { const [minMB, maxMB] = filters.fileSizeRange; results = results.filter(doc => { const sizeMB = doc.file_size / (1024 * 1024); return sizeMB >= minMB && sizeMB <= maxMB; }); } // Filter by OCR status if (filters.hasOcr && filters.hasOcr !== 'all') { results = results.filter(doc => { return filters.hasOcr === 'yes' ? doc.has_ocr_text : !doc.has_ocr_text; }); } clearInterval(progressInterval); setSearchProgress(100); setSearchResults(results); setTotalResults(response.data.total || results.length); setQueryTime(response.data.query_time_ms || 0); setSuggestions(response.data.suggestions || []); // Extract unique tags for filter options const tags = [...new Set(results.flatMap(doc => doc.tags || []))].filter(tag => typeof tag === 'string'); setAvailableTags(tags); // Clear progress after a brief delay setTimeout(() => setSearchProgress(0), 500); } catch (err) { setSearchProgress(0); setError('Search failed. Please try again.'); console.error(err); } finally { setLoading(false); } }, [advancedSettings]); const debouncedSearch = useCallback( debounce((query: string, filters: SearchFilters, page: number = 1, resetPage: boolean = false) => { if (resetPage) { setCurrentPage(1); performSearch(query, filters, 1); } else { setCurrentPage(page); performSearch(query, filters, page); } }, 300), [performSearch] ); const quickSuggestionsDebounced = useCallback( debounce((query: string) => generateQuickSuggestions(query), 150), [generateQuickSuggestions] ); // Handle URL search params useEffect(() => { const queryFromUrl = searchParams.get('q'); if (queryFromUrl && queryFromUrl !== searchQuery) { setSearchQuery(queryFromUrl); } }, [searchParams]); useEffect(() => { const filters: SearchFilters = { tags: selectedTags, mimeTypes: selectedMimeTypes, dateRange: dateRange, fileSizeRange: fileSizeRange, hasOcr: hasOcr, }; // Reset to page 1 when search query or filters change const shouldResetPage = searchQuery !== searchParams.get('q') || JSON.stringify(filters) !== JSON.stringify({ tags: selectedTags, mimeTypes: selectedMimeTypes, dateRange: dateRange, fileSizeRange: fileSizeRange, hasOcr: hasOcr, }); debouncedSearch(searchQuery, filters, 1, shouldResetPage); quickSuggestionsDebounced(searchQuery); if (shouldResetPage) { setCurrentPage(1); } // Update URL params when search query changes if (searchQuery) { setSearchParams({ q: searchQuery }); } else { setSearchParams({}); } }, [searchQuery, selectedTags, selectedMimeTypes, dateRange, fileSizeRange, hasOcr, debouncedSearch, quickSuggestionsDebounced, setSearchParams]); const handleClearFilters = (): void => { setSelectedTags([]); setSelectedMimeTypes([]); setDateRange([0, 365]); setFileSizeRange([0, 100]); setHasOcr('all'); setCurrentPage(1); }; const getFileIcon = (mimeType: string): React.ReactElement => { if (mimeType.includes('pdf')) return ; if (mimeType.includes('image')) return ; if (mimeType.includes('text')) return ; return ; }; const formatFileSize = (bytes: number): string => { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }; const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); }; const handleDownload = async (doc: Document): Promise => { try { const response = await documentService.download(doc.id); const url = window.URL.createObjectURL(new Blob([response.data])); const link = window.document.createElement('a'); link.href = url; link.setAttribute('download', doc.original_filename); window.document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } catch (err) { console.error('Download failed:', err); } }; 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 parts.push( {text.substring(range.start, range.end)} ); lastIndex = range.end; }); // Add remaining text if (lastIndex < text.length) { parts.push( {text.substring(lastIndex)} ); } return parts; }; const handleSuggestionClick = (suggestion: string): void => { setSearchQuery(suggestion); }; const handleSearchModeChange = (event: React.MouseEvent, newMode: SearchMode | null): void => { if (newMode) { setAdvancedSettings(prev => ({ ...prev, searchMode: newMode })); } }; const handleTagsChange = (event: SelectChangeEvent): void => { const value = event.target.value; setSelectedTags(typeof value === 'string' ? value.split(',') : value); }; const handleMimeTypesChange = (event: SelectChangeEvent): void => { const value = event.target.value; setSelectedMimeTypes(typeof value === 'string' ? value.split(',') : value); }; const handleOcrChange = (event: SelectChangeEvent): void => { setHasOcr(event.target.value as OcrStatus); }; const handlePageChange = (event: React.ChangeEvent, page: number): void => { setCurrentPage(page); const filters: SearchFilters = { tags: selectedTags, mimeTypes: selectedMimeTypes, dateRange: dateRange, fileSizeRange: fileSizeRange, hasOcr: hasOcr, }; performSearch(searchQuery, filters, page); // Scroll to top of results const resultsElement = document.querySelector('.search-results-container'); if (resultsElement) { resultsElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; return ( {/* Header with Prominent Search */} Search Documents {/* Enhanced Search Bar */} setSearchQuery(e.target.value)} InputProps={{ startAdornment: ( ), endAdornment: ( {(loading || isTyping) && ( 0 ? "determinate" : "indeterminate"} value={searchProgress} /> )} {searchQuery && ( setSearchQuery('')} > )} setShowAdvanced(!showAdvanced)} color={showAdvanced ? 'primary' : 'default'} > setShowFilters(!showFilters)} color={showFilters ? 'primary' : 'default'} sx={{ display: { xs: 'inline-flex', md: 'none' } }} > ), }} sx={{ '& .MuiOutlinedInput-root': { '& fieldset': { borderWidth: 2, }, '&:hover fieldset': { borderColor: 'primary.main', }, '&.Mui-focused fieldset': { borderColor: 'primary.main', }, }, '& .MuiInputBase-input': { fontSize: '1.1rem', py: 2, }, }} /> {/* Enhanced Loading Progress Bar */} {(loading || isTyping || searchProgress > 0) && ( 0 ? "determinate" : "indeterminate"} value={searchProgress} sx={{ position: 'absolute', bottom: 0, left: 0, right: 0, borderRadius: '0 0 4px 4px', opacity: isTyping ? 0.5 : 1, transition: 'opacity 0.2s ease-in-out', }} /> )} {/* Quick Stats */} {(searchQuery && !loading) && ( } label={`${totalResults} results`} size="small" color="primary" variant="outlined" sx={{ flexShrink: 0 }} /> } label={`${queryTime}ms`} size="small" variant="outlined" sx={{ flexShrink: 0 }} /> {advancedSettings.useEnhancedSearch && ( } label="Enhanced" size="small" color="success" variant="outlined" sx={{ flexShrink: 0 }} /> )} {/* Simplified Search Mode Selector */} Smart Exact phrase Similar words Advanced )} {/* Quick Suggestions */} {quickSuggestions.length > 0 && searchQuery && !loading && ( Quick suggestions: {quickSuggestions.map((suggestion, index) => ( handleSuggestionClick(suggestion)} clickable variant="outlined" color="primary" sx={{ flexShrink: 0, '&:hover': { backgroundColor: 'primary.main', color: 'primary.contrastText', } }} /> ))} )} {/* Server Suggestions */} {suggestions.length > 0 && ( Related searches: {suggestions.map((suggestion, index) => ( handleSuggestionClick(suggestion)} clickable variant="outlined" sx={{ flexShrink: 0, '&:hover': { backgroundColor: 'primary.light', color: 'primary.contrastText', } }} /> ))} )} {/* Enhanced Search Guide when not in advanced mode */} {!showAdvanced && ( )} {/* Advanced Search Panel */} setAdvancedSettings(prev => ({ ...prev, ...newSettings })) } expanded={showAdvanced} onExpandedChange={setShowAdvanced} /> {/* Mobile Filters Drawer */} {showFilters && ( Filters {/* Mobile filter content would go here - simplified */} Mobile filters coming soon... )} {/* Desktop Filters Sidebar */} Filters {/* Tags Filter */} }> Tags Select Tags multiple value={selectedTags} onChange={handleTagsChange} input={} renderValue={(selected) => ( {selected.map((value) => ( ))} )} > {availableTags.map((tag) => ( -1} /> ))} {/* File Type Filter with Facets */} {/* OCR Filter */} }> OCR Status OCR Text {/* Date Range Filter */} }> Date Range Days ago: {dateRange[0]} - {dateRange[1]} setDateRange(newValue as number[])} valueLabelDisplay="auto" min={0} max={365} marks={[ { value: 0, label: 'Today' }, { value: 30, label: '30d' }, { value: 90, label: '90d' }, { value: 365, label: '1y' }, ]} /> {/* File Size Filter */} }> File Size Size: {fileSizeRange[0]}MB - {fileSizeRange[1]}MB setFileSizeRange(newValue as number[])} valueLabelDisplay="auto" min={0} max={100} marks={[ { value: 0, label: '0MB' }, { value: 10, label: '10MB' }, { value: 50, label: '50MB' }, { value: 100, label: '100MB' }, ]} /> {/* Search Results */} {/* Results Header */} {searchQuery && ( {loading ? 'Searching...' : `${searchResults.length} results found`} {/* Snippet Settings Button */} {/* Current Settings Preview */} {!loading && searchResults.length > 0 && ( Showing: )} )} {/* Results */} {loading && ( )} {error && ( {error} )} {!loading && !error && searchQuery && searchResults.length === 0 && ( No results found for "{searchQuery}" Try adjusting your search terms or filters {/* Helpful suggestions for no results */} Suggestions: • Try simpler or more general terms • Check spelling and try different keywords • Remove some filters to broaden your search • Use quotes for exact phrases )} {!loading && !error && !searchQuery && ( Start searching your documents Use the enhanced search bar above to find documents by content, filename, or tags {/* Search Tips */} Search Tips: {searchTips.map((tip, index) => ( {tip} ))} { setSearchQuery('invoice'); setCurrentPage(1); }} /> { setSearchQuery('contract'); setCurrentPage(1); }} /> { setSearchQuery('tag:important'); setCurrentPage(1); }} /> )} {!loading && !error && searchResults.length > 0 && ( <> {searchResults.map((doc) => ( {getFileIcon(doc.mime_type)} {doc.original_filename} {formatFileSize(doc.file_size)} • {formatDate(doc.created_at)} {doc.has_ocr_text && ' • OCR'} {doc.tags.length > 0 && ( Tags: {doc.tags.slice(0, 3).map((tag, index) => ( ))} {doc.tags.length > 3 && ( +{doc.tags.length - 3} more )} )} {/* Enhanced Search Snippets */} {doc.snippets && doc.snippets.length > 0 && ( { console.log('Snippet clicked:', snippet, index); }} /> )} navigate(`/documents/${doc.id}`)} > handleDownload(doc)} > ))} {/* Pagination */} {totalResults > resultsPerPage && ( )} {/* Results Summary */} Showing {((currentPage - 1) * resultsPerPage) + 1}-{Math.min(currentPage * resultsPerPage, totalResults)} of {totalResults} results )} {/* Global Snippet Settings Menu */} setSnippetSettingsAnchor(null)} PaperProps={{ sx: { width: 320, p: 2 } }} > Text Display Settings View Mode setSnippetSettings(prev => ({ ...prev, viewMode: e.target.value as SnippetViewMode }))} > } label="Compact" /> } label="Detailed" /> } label="Context Focus" /> Highlight Style setSnippetSettings(prev => ({ ...prev, highlightStyle: e.target.value as SnippetHighlightStyle }))} > } label="Background Color" /> } label="Underline" /> } label="Bold Text" /> Font Size: {snippetSettings.fontSize}px setSnippetSettings(prev => ({ ...prev, fontSize: value as number }))} min={12} max={20} marks valueLabelDisplay="auto" /> Snippets per result: {snippetSettings.maxSnippetsToShow} setSnippetSettings(prev => ({ ...prev, maxSnippetsToShow: value as number }))} min={1} max={5} marks valueLabelDisplay="auto" /> {snippetSettings.viewMode === 'context' && ( <> Context Length: {snippetSettings.contextLength} characters setSnippetSettings(prev => ({ ...prev, contextLength: value as number }))} min={20} max={200} step={10} marks valueLabelDisplay="auto" /> )} ); }; export default SearchPage;