feat(client): paginate search results and they look much better now

This commit is contained in:
perf3ct 2025-06-26 19:13:34 +00:00
parent e57935a4b5
commit ff50fe1155
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
2 changed files with 498 additions and 208 deletions

View File

@ -49,6 +49,11 @@ interface EnhancedSnippetViewerProps {
searchQuery?: string;
maxSnippetsToShow?: number;
onSnippetClick?: (snippet: Snippet, index: number) => void;
viewMode?: ViewMode;
highlightStyle?: HighlightStyle;
fontSize?: number;
contextLength?: number;
showSettings?: boolean;
}
type ViewMode = 'compact' | 'detailed' | 'context';
@ -59,15 +64,28 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
searchQuery,
maxSnippetsToShow = 3,
onSnippetClick,
viewMode: propViewMode,
highlightStyle: propHighlightStyle,
fontSize: propFontSize,
contextLength: propContextLength,
showSettings = true,
}) => {
const [expanded, setExpanded] = useState(false);
const [viewMode, setViewMode] = useState<ViewMode>('detailed');
const [highlightStyle, setHighlightStyle] = useState<HighlightStyle>('background');
const [fontSize, setFontSize] = useState<number>(14);
const [contextLength, setContextLength] = useState<number>(50);
const [viewMode, setViewMode] = useState<ViewMode>(propViewMode || 'detailed');
const [highlightStyle, setHighlightStyle] = useState<HighlightStyle>(propHighlightStyle || 'background');
const [fontSize, setFontSize] = useState<number>(propFontSize || 15);
const [contextLength, setContextLength] = useState<number>(propContextLength || 50);
const [settingsAnchor, setSettingsAnchor] = useState<null | HTMLElement>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
// Update local state when props change
React.useEffect(() => {
if (propViewMode) setViewMode(propViewMode);
if (propHighlightStyle) setHighlightStyle(propHighlightStyle);
if (propFontSize) setFontSize(propFontSize);
if (propContextLength) setContextLength(propContextLength);
}, [propViewMode, propHighlightStyle, propFontSize, propContextLength]);
const visibleSnippets = expanded ? snippets : snippets.slice(0, maxSnippetsToShow);
const handleCopySnippet = (text: string, index: number) => {
@ -187,16 +205,16 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
key={index}
variant="outlined"
sx={{
p: isCompact ? 1 : 2,
mb: 1.5,
p: isCompact ? 1 : 1.5,
mb: 0.75,
backgroundColor: (theme) => theme.palette.mode === 'light' ? 'grey.50' : 'grey.900',
borderLeft: '3px solid',
borderLeft: '2px 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)',
transform: 'translateX(2px)',
} : {},
}}
onClick={() => onSnippetClick?.(snippet, index)}
@ -204,28 +222,12 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
<Box display="flex" alignItems="flex-start" justifyContent="space-between">
<Box flex={1}>
{!isCompact && (
<Box display="flex" alignItems="center" gap={1} mb={1}>
<Chip
icon={getSourceIcon(snippet.source)}
label={getSourceLabel(snippet.source)}
size="small"
variant="outlined"
/>
{snippet.page_number && (
<Chip
label={`Page ${snippet.page_number}`}
size="small"
variant="outlined"
/>
)}
{snippet.confidence && snippet.confidence < 0.8 && (
<Chip
label={`${(snippet.confidence * 100).toFixed(0)}% confidence`}
size="small"
color="warning"
variant="outlined"
/>
)}
<Box display="flex" alignItems="center" gap={1} mb={0.5}>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{getSourceLabel(snippet.source)}
{snippet.page_number && ` • Page ${snippet.page_number}`}
{snippet.confidence && snippet.confidence < 0.8 && `${(snippet.confidence * 100).toFixed(0)}% confidence`}
</Typography>
</Box>
)}
@ -237,74 +239,94 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
color: 'text.primary',
wordWrap: 'break-word',
fontFamily: viewMode === 'context' ? 'monospace' : 'inherit',
mt: 0,
}}
>
{renderHighlightedText(snippet.text, snippet.highlight_ranges)}
</Typography>
</Box>
<Box display="flex" gap={0.5} ml={2}>
<Tooltip title="Copy snippet">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleCopySnippet(snippet.text, index);
}}
sx={{
color: copiedIndex === index ? 'success.main' : 'text.secondary'
}}
>
<CopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
{!isCompact && (
<Box display="flex" gap={0.5} ml={2}>
<Tooltip title="Copy snippet">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleCopySnippet(snippet.text, index);
}}
sx={{
color: copiedIndex === index ? 'success.main' : 'text.secondary',
p: 0.5,
}}
>
<CopyIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
</Paper>
);
};
return (
<Box>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle2" fontWeight="bold">
Search Results
</Typography>
{snippets.length > 0 && (
<Chip
label={`${snippets.length > 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' } }}
/>
)}
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Tooltip title="Snippet settings">
<IconButton
size="small"
onClick={(e) => setSettingsAnchor(e.currentTarget)}
>
<SettingsIcon fontSize="small" />
</IconButton>
</Tooltip>
<Box sx={{ mt: 0.5 }}>
{showSettings && (
<Box display="flex" alignItems="center" justifyContent="space-between" mb={1}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle2" fontWeight="bold">
Search Results
</Typography>
{snippets.length > 0 && (
<Chip
label={`${snippets.length > 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' } }}
/>
)}
</Box>
{snippets.length > maxSnippetsToShow && (
<Button
size="small"
onClick={() => setExpanded(!expanded)}
endIcon={expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
>
{expanded ? 'Show Less' : `Show All (${snippets.length})`}
</Button>
)}
<Box display="flex" alignItems="center" gap={1}>
<Tooltip title="Snippet settings">
<IconButton
size="small"
onClick={(e) => setSettingsAnchor(e.currentTarget)}
>
<SettingsIcon fontSize="small" />
</IconButton>
</Tooltip>
{snippets.length > maxSnippetsToShow && (
<Button
size="small"
onClick={() => setExpanded(!expanded)}
endIcon={expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
>
{expanded ? 'Show Less' : `Show All (${snippets.length})`}
</Button>
)}
</Box>
</Box>
</Box>
)}
{!showSettings && snippets.length > maxSnippetsToShow && (
<Box display="flex" alignItems="center" justifyContent="flex-end" mb={0.5}>
<Button
size="small"
variant="text"
onClick={() => setExpanded(!expanded)}
endIcon={expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
sx={{ fontSize: '0.75rem', minHeight: 'auto', py: 0.25 }}
>
{expanded ? 'Show Less' : `Show All (${snippets.length})`}
</Button>
</Box>
)}
{searchQuery && (
{showSettings && searchQuery && (
<Box mb={2}>
<Typography variant="caption" color="text.secondary">
Showing matches for: <strong>{searchQuery}</strong>
@ -323,12 +345,13 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
)}
{/* Settings Menu */}
<Menu
anchorEl={settingsAnchor}
open={Boolean(settingsAnchor)}
onClose={() => setSettingsAnchor(null)}
PaperProps={{ sx: { width: 320, p: 2 } }}
>
{showSettings && (
<Menu
anchorEl={settingsAnchor}
open={Boolean(settingsAnchor)}
onClose={() => setSettingsAnchor(null)}
PaperProps={{ sx: { width: 320, p: 2 } }}
>
<Typography variant="subtitle2" sx={{ mb: 2 }}>
Snippet Display Settings
</Typography>
@ -422,7 +445,8 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
</Box>
</>
)}
</Menu>
</Menu>
)}
</Box>
);
};

View File

@ -35,6 +35,10 @@ import {
Paper,
Skeleton,
SelectChangeEvent,
Menu,
RadioGroup,
Radio,
Pagination,
} from '@mui/material';
import Grid from '@mui/material/GridLegacy';
import {
@ -55,6 +59,7 @@ import {
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';
@ -123,6 +128,17 @@ interface AdvancedSearchSettings {
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();
@ -160,6 +176,20 @@ const SearchPage: React.FC = () => {
enableAutoCorrect: true,
});
// Global snippet settings
const [snippetSettings, setSnippetSettings] = useState<SnippetSettings>({
viewMode: 'detailed',
highlightStyle: 'background',
fontSize: 15,
contextLength: 50,
maxSnippetsToShow: 3,
});
const [snippetSettingsAnchor, setSnippetSettingsAnchor] = useState<null | HTMLElement>(null);
// Pagination states
const [currentPage, setCurrentPage] = useState<number>(1);
const [resultsPerPage] = useState<number>(20);
// Filter states
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedMimeTypes, setSelectedMimeTypes] = useState<string[]>([]);
@ -217,7 +247,7 @@ const SearchPage: React.FC = () => {
setQuickSuggestions(suggestions.slice(0, 3));
}, []);
const performSearch = useCallback(async (query: string, filters: SearchFilters = {}): Promise<void> => {
const performSearch = useCallback(async (query: string, filters: SearchFilters = {}, page: number = 1): Promise<void> => {
if (!query.trim()) {
setSearchResults([]);
setTotalResults(0);
@ -241,8 +271,8 @@ const SearchPage: React.FC = () => {
query: query.trim(),
tags: filters.tags?.length ? filters.tags : undefined,
mime_types: filters.mimeTypes?.length ? filters.mimeTypes : undefined,
limit: advancedSettings.resultLimit,
offset: 0,
limit: resultsPerPage,
offset: (page - 1) * resultsPerPage,
include_snippets: advancedSettings.includeSnippets,
snippet_length: advancedSettings.snippetLength,
search_mode: advancedSettings.searchMode,
@ -307,7 +337,15 @@ const SearchPage: React.FC = () => {
}, [advancedSettings]);
const debouncedSearch = useCallback(
debounce((query: string, filters: SearchFilters) => performSearch(query, filters), 300),
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]
);
@ -332,9 +370,23 @@ const SearchPage: React.FC = () => {
fileSizeRange: fileSizeRange,
hasOcr: hasOcr,
};
debouncedSearch(searchQuery, filters);
// 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 });
@ -349,13 +401,14 @@ const SearchPage: React.FC = () => {
setDateRange([0, 365]);
setFileSizeRange([0, 100]);
setHasOcr('all');
setCurrentPage(1);
};
const getFileIcon = (mimeType: string): React.ReactElement => {
if (mimeType.includes('pdf')) return <PdfIcon color="error" />;
if (mimeType.includes('image')) return <ImageIcon color="primary" />;
if (mimeType.includes('text')) return <TextIcon color="info" />;
return <DocIcon color="secondary" />;
if (mimeType.includes('pdf')) return <PdfIcon color="error" sx={{ fontSize: '1.2rem' }} />;
if (mimeType.includes('image')) return <ImageIcon color="primary" sx={{ fontSize: '1.2rem' }} />;
if (mimeType.includes('text')) return <TextIcon color="info" sx={{ fontSize: '1.2rem' }} />;
return <DocIcon color="secondary" sx={{ fontSize: '1.2rem' }} />;
};
const formatFileSize = (bytes: number): string => {
@ -464,6 +517,24 @@ const SearchPage: React.FC = () => {
setHasOcr(event.target.value as OcrStatus);
};
const handlePageChange = (event: React.ChangeEvent<unknown>, 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 (
<Box sx={{ p: 3 }}>
@ -526,13 +597,15 @@ const SearchPage: React.FC = () => {
<ClearIcon />
</IconButton>
)}
<IconButton
size="small"
onClick={() => setShowAdvanced(!showAdvanced)}
color={showAdvanced ? 'primary' : 'default'}
>
<SettingsIcon />
</IconButton>
<Tooltip title="Search Settings">
<IconButton
size="small"
onClick={() => setShowAdvanced(!showAdvanced)}
color={showAdvanced ? 'primary' : 'default'}
>
<SettingsIcon />
</IconButton>
</Tooltip>
<IconButton
size="small"
onClick={() => setShowFilters(!showFilters)}
@ -621,17 +694,19 @@ const SearchPage: React.FC = () => {
</Box>
{/* Simplified Search Mode Selector */}
<ToggleButtonGroup
value={advancedSettings.searchMode}
exclusive
onChange={handleSearchModeChange}
size="small"
>
<ToggleButton value="simple">Smart</ToggleButton>
<ToggleButton value="phrase">Exact phrase</ToggleButton>
<ToggleButton value="fuzzy">Similar words</ToggleButton>
<ToggleButton value="boolean">Advanced</ToggleButton>
</ToggleButtonGroup>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<ToggleButtonGroup
value={advancedSettings.searchMode}
exclusive
onChange={handleSearchModeChange}
size="small"
>
<ToggleButton value="simple">Smart</ToggleButton>
<ToggleButton value="phrase">Exact phrase</ToggleButton>
<ToggleButton value="fuzzy">Similar words</ToggleButton>
<ToggleButton value="boolean">Advanced</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
)}
@ -883,14 +958,74 @@ const SearchPage: React.FC = () => {
</Grid>
{/* Search Results */}
<Grid item xs={12} md={9}>
<Grid item xs={12} md={9} className="search-results-container">
{/* Results Header */}
{searchQuery && (
<Box sx={{ mb: 3 }}>
<Typography variant="body2" color="text.secondary">
{loading ? 'Searching...' : `${searchResults.length} results found`}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2, mb: 1 }}>
<Typography variant="body2" color="text.secondary">
{loading ? 'Searching...' : `${searchResults.length} results found`}
</Typography>
{/* Snippet Settings Button */}
<Button
variant="outlined"
size="small"
startIcon={<TextFormatIcon />}
onClick={(e) => setSnippetSettingsAnchor(e.currentTarget)}
sx={{
flexShrink: 0,
position: 'relative',
}}
>
Display Settings
{/* Show indicator if settings are customized */}
{(snippetSettings.viewMode !== 'detailed' ||
snippetSettings.highlightStyle !== 'background' ||
snippetSettings.fontSize !== 15 ||
snippetSettings.maxSnippetsToShow !== 3) && (
<Box
sx={{
position: 'absolute',
top: -4,
right: -4,
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'primary.main',
}}
/>
)}
</Button>
</Box>
{/* Current Settings Preview */}
{!loading && searchResults.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
<Typography variant="caption" color="text.secondary">
Showing:
</Typography>
<Chip
label={`${snippetSettings.maxSnippetsToShow} snippets`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem' }}
/>
<Chip
label={`${snippetSettings.fontSize}px font`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem' }}
/>
<Chip
label={snippetSettings.viewMode}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', textTransform: 'capitalize' }}
/>
</Box>
)}
</Box>
)}
@ -995,29 +1130,39 @@ const SearchPage: React.FC = () => {
size="small"
variant="outlined"
clickable
onClick={() => setSearchQuery('invoice')}
onClick={() => {
setSearchQuery('invoice');
setCurrentPage(1);
}}
/>
<Chip
label="Try: contract"
size="small"
variant="outlined"
clickable
onClick={() => setSearchQuery('contract')}
onClick={() => {
setSearchQuery('contract');
setCurrentPage(1);
}}
/>
<Chip
label="Try: tag:important"
size="small"
variant="outlined"
clickable
onClick={() => setSearchQuery('tag:important')}
onClick={() => {
setSearchQuery('tag:important');
setCurrentPage(1);
}}
/>
</Stack>
</Box>
)}
{!loading && !error && searchResults.length > 0 && (
<Grid container spacing={1}>
{searchResults.map((doc) => (
<>
<Grid container spacing={1}>
{searchResults.map((doc) => (
<Grid
item
xs={12}
@ -1050,7 +1195,7 @@ const SearchPage: React.FC = () => {
gap: 1,
width: '100%'
}}>
<Box sx={{ mr: 1, mt: 0.5 }}>
<Box sx={{ mr: 1.5, mt: 0.5, flexShrink: 0 }}>
{getFileIcon(doc.mime_type)}
</Box>
@ -1058,14 +1203,15 @@ const SearchPage: React.FC = () => {
<Typography
variant="h6"
sx={{
fontSize: '0.95rem',
fontSize: '1.05rem',
fontWeight: 600,
mb: 0.5,
mb: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
width: '100%',
color: 'text.primary',
}}
title={doc.original_filename}
>
@ -1076,44 +1222,29 @@ const SearchPage: React.FC = () => {
mb: 0.5,
display: 'flex',
flexWrap: 'wrap',
gap: 0.5,
overflow: 'hidden'
gap: 0.75,
overflow: 'hidden',
alignItems: 'center',
}}>
<Chip
className="search-chip"
label={formatFileSize(doc.file_size)}
size="small"
variant="outlined"
sx={{ flexShrink: 0 }}
/>
<Chip
className="search-chip"
label={formatDate(doc.created_at)}
size="small"
variant="outlined"
sx={{ flexShrink: 0 }}
/>
{doc.has_ocr_text && (
<Chip
className="search-chip"
label="OCR"
size="small"
color="success"
variant="outlined"
sx={{ flexShrink: 0 }}
/>
)}
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
{formatFileSize(doc.file_size)} {formatDate(doc.created_at)}
{doc.has_ocr_text && ' • OCR'}
</Typography>
</Box>
{doc.tags.length > 0 && (
<Box sx={{
mb: 0.5,
mb: 1,
display: 'flex',
flexWrap: 'wrap',
gap: 0.5,
overflow: 'hidden'
overflow: 'hidden',
alignItems: 'center',
}}>
{doc.tags.slice(0, 2).map((tag, index) => (
<Typography variant="caption" color="text.secondary" sx={{ mr: 0.5 }}>
Tags:
</Typography>
{doc.tags.slice(0, 3).map((tag, index) => (
<Chip
key={index}
className="search-chip"
@ -1134,14 +1265,10 @@ const SearchPage: React.FC = () => {
}}
/>
))}
{doc.tags.length > 2 && (
<Chip
className="search-chip"
label={`+${doc.tags.length - 2}`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: '18px', flexShrink: 0 }}
/>
{doc.tags.length > 3 && (
<Typography variant="caption" color="text.secondary">
+{doc.tags.length - 3} more
</Typography>
)}
</Box>
)}
@ -1149,68 +1276,54 @@ const SearchPage: React.FC = () => {
{/* Enhanced Search Snippets */}
{doc.snippets && doc.snippets.length > 0 && (
<Box sx={{
mt: 1,
mb: 0.5
mt: 0.5,
mb: 1
}}>
<EnhancedSnippetViewer
snippets={doc.snippets}
searchQuery={searchQuery}
maxSnippetsToShow={2}
maxSnippetsToShow={snippetSettings.maxSnippetsToShow}
viewMode={snippetSettings.viewMode}
highlightStyle={snippetSettings.highlightStyle}
fontSize={snippetSettings.fontSize}
contextLength={snippetSettings.contextLength}
showSettings={false}
onSnippetClick={(snippet, index) => {
// Could navigate to document with snippet highlighted
console.log('Snippet clicked:', snippet, index);
}}
/>
</Box>
)}
{/* Search Rank */}
{doc.search_rank && (
<Box sx={{
mt: 0.5,
overflow: 'hidden'
}}>
<Chip
className="search-chip"
label={`Relevance: ${(doc.search_rank * 100).toFixed(1)}%`}
size="small"
color="info"
variant="outlined"
sx={{
fontSize: '0.7rem',
height: '18px',
flexShrink: 0,
maxWidth: '150px',
'& .MuiChip-label': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}
}}
/>
</Box>
)}
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
ml: 'auto',
ml: 2,
gap: 0.5,
alignItems: 'center'
alignItems: 'center',
justifyContent: 'flex-start',
pt: 0.5,
}}>
<Tooltip title="View Details">
<IconButton
className="search-filter-button search-focusable"
size="small"
sx={{
p: 0.5,
minWidth: 28,
minHeight: 28
p: 0.75,
minWidth: 32,
minHeight: 32,
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
bgcolor: 'primary.dark',
}
}}
onClick={() => navigate(`/documents/${doc.id}`)}
>
<ViewIcon sx={{ fontSize: '1rem' }} />
<ViewIcon sx={{ fontSize: '1.1rem' }} />
</IconButton>
</Tooltip>
<Tooltip title="Download">
@ -1218,13 +1331,17 @@ const SearchPage: React.FC = () => {
className="search-filter-button search-focusable"
size="small"
sx={{
p: 0.5,
minWidth: 28,
minHeight: 28
p: 0.75,
minWidth: 32,
minHeight: 32,
bgcolor: 'action.hover',
'&:hover': {
bgcolor: 'action.selected',
}
}}
onClick={() => handleDownload(doc)}
>
<DownloadIcon sx={{ fontSize: '1rem' }} />
<DownloadIcon sx={{ fontSize: '1.1rem' }} />
</IconButton>
</Tooltip>
</Box>
@ -1232,11 +1349,160 @@ const SearchPage: React.FC = () => {
</CardContent>
</Card>
</Grid>
))}
</Grid>
))}
</Grid>
{/* Pagination */}
{totalResults > resultsPerPage && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4, mb: 2 }}>
<Pagination
count={Math.ceil(totalResults / resultsPerPage)}
page={currentPage}
onChange={handlePageChange}
color="primary"
size="large"
showFirstButton
showLastButton
siblingCount={1}
boundaryCount={1}
sx={{
'& .MuiPagination-ul': {
flexWrap: 'wrap',
justifyContent: 'center',
},
}}
/>
</Box>
)}
{/* Results Summary */}
<Box sx={{ textAlign: 'center', mt: 2, mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Showing {((currentPage - 1) * resultsPerPage) + 1}-{Math.min(currentPage * resultsPerPage, totalResults)} of {totalResults} results
</Typography>
</Box>
</>
)}
</Grid>
</Grid>
{/* Global Snippet Settings Menu */}
<Menu
anchorEl={snippetSettingsAnchor}
open={Boolean(snippetSettingsAnchor)}
onClose={() => setSnippetSettingsAnchor(null)}
PaperProps={{ sx: { width: 320, p: 2 } }}
>
<Typography variant="subtitle2" sx={{ mb: 2 }}>
Text Display Settings
</Typography>
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom>
View Mode
</Typography>
<RadioGroup
value={snippetSettings.viewMode}
onChange={(e) => setSnippetSettings(prev => ({ ...prev, viewMode: e.target.value as SnippetViewMode }))}
>
<FormControlLabel
value="compact"
control={<Radio size="small" />}
label="Compact"
/>
<FormControlLabel
value="detailed"
control={<Radio size="small" />}
label="Detailed"
/>
<FormControlLabel
value="context"
control={<Radio size="small" />}
label="Context Focus"
/>
</RadioGroup>
</Box>
<Divider sx={{ my: 2 }} />
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom>
Highlight Style
</Typography>
<RadioGroup
value={snippetSettings.highlightStyle}
onChange={(e) => setSnippetSettings(prev => ({ ...prev, highlightStyle: e.target.value as SnippetHighlightStyle }))}
>
<FormControlLabel
value="background"
control={<Radio size="small" />}
label="Background Color"
/>
<FormControlLabel
value="underline"
control={<Radio size="small" />}
label="Underline"
/>
<FormControlLabel
value="bold"
control={<Radio size="small" />}
label="Bold Text"
/>
</RadioGroup>
</Box>
<Divider sx={{ my: 2 }} />
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom>
Font Size: {snippetSettings.fontSize}px
</Typography>
<Slider
value={snippetSettings.fontSize}
onChange={(_, value) => setSnippetSettings(prev => ({ ...prev, fontSize: value as number }))}
min={12}
max={20}
marks
valueLabelDisplay="auto"
/>
</Box>
<Divider sx={{ my: 2 }} />
<Box mb={2}>
<Typography variant="caption" color="text.secondary" gutterBottom>
Snippets per result: {snippetSettings.maxSnippetsToShow}
</Typography>
<Slider
value={snippetSettings.maxSnippetsToShow}
onChange={(_, value) => setSnippetSettings(prev => ({ ...prev, maxSnippetsToShow: value as number }))}
min={1}
max={5}
marks
valueLabelDisplay="auto"
/>
</Box>
{snippetSettings.viewMode === 'context' && (
<>
<Divider sx={{ my: 2 }} />
<Box>
<Typography variant="caption" color="text.secondary" gutterBottom>
Context Length: {snippetSettings.contextLength} characters
</Typography>
<Slider
value={snippetSettings.contextLength}
onChange={(_, value) => setSnippetSettings(prev => ({ ...prev, contextLength: value as number }))}
min={20}
max={200}
step={10}
marks
valueLabelDisplay="auto"
/>
</Box>
</>
)}
</Menu>
</Box>
);
};