import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Box, Typography, Grid, 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, } from '@mui/material'; import { Search as SearchIcon, FilterList as FilterIcon, Clear as ClearIcon, ExpandMore as ExpandMoreIcon, GridView as GridViewIcon, ViewList as ListViewIcon, 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, } from '@mui/icons-material'; import { documentService, SearchRequest } from '../services/api'; import SearchGuidance from '../components/SearchGuidance'; 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 ViewMode = 'grid' | 'list'; type SearchMode = 'simple' | 'phrase' | 'fuzzy' | 'boolean'; type OcrStatus = 'all' | 'yes' | 'no'; 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 [viewMode, setViewMode] = useState('grid'); 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 const [useEnhancedSearch, setUseEnhancedSearch] = useState(true); const [searchMode, setSearchMode] = useState('simple'); const [includeSnippets, setIncludeSnippets] = useState(true); const [snippetLength, setSnippetLength] = useState(200); const [showAdvanced, setShowAdvanced] = useState(false); // 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 = {}): 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: 100, offset: 0, include_snippets: includeSnippets, snippet_length: snippetLength, search_mode: searchMode, }; const response = 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))]; setAvailableTags(tags); // Clear progress after a brief delay setTimeout(() => setSearchProgress(0), 500); } catch (err) { clearInterval(progressInterval); setSearchProgress(0); setError('Search failed. Please try again.'); console.error(err); } finally { setLoading(false); } }, [useEnhancedSearch, includeSnippets, snippetLength, searchMode]); const debouncedSearch = useCallback( debounce((query: string, filters: SearchFilters) => performSearch(query, filters), 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, }; debouncedSearch(searchQuery, filters); quickSuggestionsDebounced(searchQuery); // 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'); }; 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 = document.createElement('a'); link.href = url; link.setAttribute('download', doc.original_filename); 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 handleViewModeChange = (event: React.MouseEvent, newView: ViewMode | null): void => { if (newView) { setViewMode(newView); } }; const handleSearchModeChange = (event: React.MouseEvent, newMode: SearchMode | null): void => { if (newMode) { setSearchMode(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 handleSnippetLengthChange = (event: SelectChangeEvent): void => { setSnippetLength(event.target.value as number); }; 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" /> } label={`${queryTime}ms`} size="small" variant="outlined" /> {useEnhancedSearch && ( } label="Enhanced" size="small" color="success" variant="outlined" /> )} {/* 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={{ '&:hover': { backgroundColor: 'primary.main', color: 'primary.contrastText', } }} /> ))} )} {/* Server Suggestions */} {suggestions.length > 0 && ( Related searches: {suggestions.map((suggestion, index) => ( handleSuggestionClick(suggestion)} clickable variant="outlined" sx={{ '&:hover': { backgroundColor: 'primary.light', color: 'primary.contrastText', } }} /> ))} )} {/* Advanced Search Options */} {showAdvanced && ( Search Options setUseEnhancedSearch(e.target.checked)} color="primary" /> } label="Enhanced Search" /> setIncludeSnippets(e.target.checked)} color="primary" /> } label="Show Snippets" /> Snippet Length )} {/* 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 {/* File Type Filter */} }> File Types Select Types {/* 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 */} {/* Toolbar */} {searchQuery && ( {loading ? 'Searching...' : `${searchResults.length} results found`} )} {/* 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')} /> setSearchQuery('contract')} /> setSearchQuery('tag:important')} /> )} {!loading && !error && searchResults.length > 0 && ( {searchResults.map((doc) => ( {viewMode === 'grid' && ( {getFileIcon(doc.mime_type)} )} {viewMode === 'list' && ( {getFileIcon(doc.mime_type)} )} {doc.original_filename} {doc.has_ocr_text && ( )} {doc.tags.length > 0 && ( {doc.tags.slice(0, 2).map((tag, index) => ( ))} {doc.tags.length > 2 && ( )} )} {/* Search Snippets */} {doc.snippets && doc.snippets.length > 0 && ( {doc.snippets.slice(0, 2).map((snippet, index) => ( theme.palette.mode === 'light' ? 'grey.50' : 'grey.800', borderLeft: '3px solid', borderLeftColor: 'primary.main', }} > ...{renderHighlightedText(snippet.text, snippet.highlight_ranges)}... ))} {doc.snippets.length > 2 && ( +{doc.snippets.length - 2} more matches )} )} {/* Search Rank */} {doc.search_rank && ( )} navigate(`/documents/${doc.id}`)} > handleDownload(doc)} > ))} )} ); }; export default SearchPage;