Merge pull request #44 from readur/feat/improve-dashboard-and-theme

feat(client): update/fix dashboard and theme
This commit is contained in:
Jon Fuller 2025-06-25 16:13:22 -07:00 committed by GitHub
commit 182ba7cf18
7 changed files with 161 additions and 42 deletions

View File

@ -38,7 +38,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import api from '../../services/api'; import api, { documentService } from '../../services/api';
interface Document { interface Document {
id: string; id: string;
@ -82,19 +82,24 @@ interface QuickAction {
// Stats Card Component // Stats Card Component
const StatsCard: React.FC<StatsCardProps> = ({ title, value, subtitle, icon: Icon, color, trend }) => { const StatsCard: React.FC<StatsCardProps> = ({ title, value, subtitle, icon: Icon, color, trend }) => {
const theme = useTheme(); const theme = useTheme();
const isDarkMode = theme.palette.mode === 'dark';
return ( return (
<Card <Card
elevation={0} elevation={0}
sx={{ sx={{
background: `linear-gradient(135deg, ${color} 0%, ${alpha(color, 0.85)} 100%)`, background: isDarkMode
color: 'white', ? `linear-gradient(135deg, ${alpha(color, 0.15)} 0%, ${alpha(color, 0.08)} 100%)`
: `linear-gradient(135deg, ${color} 0%, ${alpha(color, 0.85)} 100%)`,
color: isDarkMode ? theme.palette.text.primary : 'white',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
borderRadius: 3, borderRadius: 3,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
cursor: 'pointer', cursor: 'pointer',
border: '1px solid rgba(255,255,255,0.1)', border: isDarkMode
? `1px solid ${alpha(color, 0.3)}`
: '1px solid rgba(255,255,255,0.1)',
backdropFilter: 'blur(20px)', backdropFilter: 'blur(20px)',
'&:hover': { '&:hover': {
transform: 'translateY(-4px)', transform: 'translateY(-4px)',
@ -107,7 +112,9 @@ const StatsCard: React.FC<StatsCardProps> = ({ title, value, subtitle, icon: Ico
right: 0, right: 0,
width: '120px', width: '120px',
height: '120px', height: '120px',
background: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%)', background: isDarkMode
? `linear-gradient(135deg, ${alpha(color, 0.15)} 0%, ${alpha(color, 0.05)} 100%)`
: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%)',
borderRadius: '50%', borderRadius: '50%',
transform: 'translate(40px, -40px)', transform: 'translate(40px, -40px)',
}, },
@ -118,7 +125,9 @@ const StatsCard: React.FC<StatsCardProps> = ({ title, value, subtitle, icon: Ico
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)', background: isDarkMode
? `linear-gradient(135deg, ${alpha(color, 0.08)} 0%, ${alpha(color, 0.03)} 100%)`
: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
}, },
}} }}
@ -177,16 +186,21 @@ const StatsCard: React.FC<StatsCardProps> = ({ title, value, subtitle, icon: Ico
width: 64, width: 64,
height: 64, height: 64,
borderRadius: 3, borderRadius: 3,
background: 'linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.15) 100%)', background: isDarkMode
? `linear-gradient(135deg, ${alpha(color, 0.25)} 0%, ${alpha(color, 0.15)} 100%)`
: 'linear-gradient(135deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0.15) 100%)',
backdropFilter: 'blur(20px)', backdropFilter: 'blur(20px)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
border: '1px solid rgba(255,255,255,0.2)', border: isDarkMode
? `1px solid ${alpha(color, 0.4)}`
: '1px solid rgba(255,255,255,0.2)',
boxShadow: '0 8px 32px rgba(0,0,0,0.1)', boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
}}> }}>
<Icon sx={{ <Icon sx={{
fontSize: 32, fontSize: 32,
color: isDarkMode ? color : 'inherit',
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))', filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))',
}} /> }} />
</Box> </Box>
@ -363,9 +377,12 @@ const RecentDocuments: React.FC<RecentDocumentsProps> = ({ documents = [] }) =>
</IconButton> </IconButton>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={async () => {
const downloadUrl = `/api/documents/${doc.id}/download`; try {
window.open(downloadUrl, '_blank'); await documentService.downloadFile(doc.id, doc.original_filename || doc.filename);
} catch (error) {
console.error('Download failed:', error);
}
}} }}
> >
<DownloadIcon fontSize="small" /> <DownloadIcon fontSize="small" />
@ -522,8 +539,20 @@ const Dashboard: React.FC = () => {
// Fetch documents with better error handling // Fetch documents with better error handling
let docs: Document[] = []; let docs: Document[] = [];
try { try {
const docsResponse = await api.get<Document[]>('/documents'); const docsResponse = await api.get('/documents', {
docs = Array.isArray(docsResponse.data) ? docsResponse.data : []; params: {
limit: 10,
offset: 0,
}
});
// Handle both direct array response and paginated response
if (Array.isArray(docsResponse.data)) {
docs = docsResponse.data;
} else if (docsResponse.data?.documents) {
docs = docsResponse.data.documents;
} else {
docs = [];
}
} catch (docError) { } catch (docError) {
console.error('Failed to fetch documents:', docError); console.error('Failed to fetch documents:', docError);
// Continue with empty documents array // Continue with empty documents array
@ -623,7 +652,7 @@ const Dashboard: React.FC = () => {
subtitle="Files in your library" subtitle="Files in your library"
icon={DocumentIcon} icon={DocumentIcon}
color="#6366f1" color="#6366f1"
trend="+12% this month" trend={stats.totalDocuments > 0 ? `${stats.totalDocuments} total` : 'No documents yet'}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6} lg={3}> <Grid item xs={12} sm={6} lg={3}>
@ -633,7 +662,7 @@ const Dashboard: React.FC = () => {
subtitle="Total file size" subtitle="Total file size"
icon={StorageIcon} icon={StorageIcon}
color="#10b981" color="#10b981"
trend="+2.4 GB this week" trend={stats.totalSize > 0 ? `${formatBytes(stats.totalSize)} used` : 'No storage used'}
/> />
</Grid> </Grid>
<Grid item xs={12} sm={6} lg={3}> <Grid item xs={12} sm={6} lg={3}>
@ -653,7 +682,7 @@ const Dashboard: React.FC = () => {
subtitle="Ready for search" subtitle="Ready for search"
icon={SearchableIcon} icon={SearchableIcon}
color="#8b5cf6" color="#8b5cf6"
trend="100% indexed" trend={stats.searchablePages > 0 ? `${stats.searchablePages} indexed` : 'Nothing indexed yet'}
/> />
</Grid> </Grid>
</Grid> </Grid>

View File

@ -273,10 +273,11 @@ const EnhancedSnippetViewer: React.FC<EnhancedSnippetViewerProps> = ({
</Typography> </Typography>
{snippets.length > 0 && ( {snippets.length > 0 && (
<Chip <Chip
label={`${snippets.length} matches`} label={`${snippets.length > 999 ? `${Math.floor(snippets.length/1000)}K` : snippets.length} matches`}
size="small" size="small"
color="primary" color="primary"
variant="outlined" variant="outlined"
sx={{ maxWidth: '100px', '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' } }}
/> />
)} )}
</Box> </Box>

View File

@ -230,10 +230,11 @@ const MimeTypeFacetFilter: React.FC<MimeTypeFacetFilterProps> = ({
{group.label} {group.label}
</Typography> </Typography>
<Chip <Chip
label={totalCount} label={totalCount > 999 ? `${Math.floor(totalCount/1000)}K` : totalCount}
size="small" size="small"
variant={selectedCount > 0 ? "filled" : "outlined"} variant={selectedCount > 0 ? "filled" : "outlined"}
color={selectedCount > 0 ? "primary" : "default"} color={selectedCount > 0 ? "primary" : "default"}
sx={{ maxWidth: '60px', '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' } }}
/> />
</Box> </Box>
</Box> </Box>
@ -254,7 +255,12 @@ const MimeTypeFacetFilter: React.FC<MimeTypeFacetFilterProps> = ({
<Typography variant="body2"> <Typography variant="body2">
{getMimeTypeLabel(facet.value)} {getMimeTypeLabel(facet.value)}
</Typography> </Typography>
<Chip label={facet.count} size="small" variant="outlined" /> <Chip
label={facet.count > 999 ? `${Math.floor(facet.count/1000)}K` : facet.count}
size="small"
variant="outlined"
sx={{ maxWidth: '50px', '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' } }}
/>
</Box> </Box>
} }
sx={{ display: 'flex', width: '100%', mb: 0.5 }} sx={{ display: 'flex', width: '100%', mb: 0.5 }}
@ -285,7 +291,12 @@ const MimeTypeFacetFilter: React.FC<MimeTypeFacetFilterProps> = ({
<Typography variant="body2"> <Typography variant="body2">
{getMimeTypeLabel(facet.value)} {getMimeTypeLabel(facet.value)}
</Typography> </Typography>
<Chip label={facet.count} size="small" variant="outlined" /> <Chip
label={facet.count > 999 ? `${Math.floor(facet.count/1000)}K` : facet.count}
size="small"
variant="outlined"
sx={{ maxWidth: '50px', '& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' } }}
/>
</Box> </Box>
} }
sx={{ display: 'flex', width: '100%', mb: 0.5 }} sx={{ display: 'flex', width: '100%', mb: 0.5 }}

View File

@ -547,8 +547,8 @@ const DocumentsPage: React.FC = () => {
gap: 2, gap: 2,
color: 'primary.contrastText' color: 'primary.contrastText'
}}> }}>
<Typography variant="body2" sx={{ flexGrow: 1 }}> <Typography variant="body2" sx={{ flexGrow: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{selectedDocuments.size} of {sortedDocuments.length} documents selected {selectedDocuments.size > 999 ? `${Math.floor(selectedDocuments.size/1000)}K` : selectedDocuments.size} of {sortedDocuments.length > 999 ? `${Math.floor(sortedDocuments.length/1000)}K` : sortedDocuments.length} documents selected
</Typography> </Typography>
<Button <Button
variant="text" variant="text"
@ -567,7 +567,7 @@ const DocumentsPage: React.FC = () => {
size="small" size="small"
color="error" color="error"
> >
Delete Selected ({selectedDocuments.size}) Delete Selected ({selectedDocuments.size > 999 ? `${Math.floor(selectedDocuments.size/1000)}K` : selectedDocuments.size})
</Button> </Button>
</Box> </Box>
)} )}
@ -785,7 +785,16 @@ const DocumentsPage: React.FC = () => {
size="small" size="small"
color="primary" color="primary"
variant="outlined" variant="outlined"
sx={{ fontSize: '0.7rem', height: '20px' }} sx={{
fontSize: '0.7rem',
height: '20px',
maxWidth: '120px',
'& .MuiChip-label': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}}
/> />
))} ))}
{doc.tags.length > 3 && ( {doc.tags.length > 3 && (

View File

@ -473,7 +473,13 @@ const FailedOcrPage: React.FC = () => {
<Tooltip title="Download Document"> <Tooltip title="Download Document">
<IconButton <IconButton
size="small" size="small"
onClick={() => window.open(`/api/documents/${document.id}/download`, '_blank')} onClick={async () => {
try {
await documentService.downloadFile(document.id, document.original_filename || document.filename);
} catch (error) {
console.error('Download failed:', error);
}
}}
> >
<DownloadIcon /> <DownloadIcon />
</IconButton> </IconButton>
@ -484,7 +490,12 @@ const FailedOcrPage: React.FC = () => {
<TableRow> <TableRow>
<TableCell sx={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> <TableCell sx={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={expandedRows.has(document.id)} timeout="auto" unmountOnExit> <Collapse in={expandedRows.has(document.id)} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1, p: 2, bgcolor: 'grey.50' }}> <Box sx={{
margin: 1,
p: 2,
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.900' : 'grey.50',
borderRadius: 1
}}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Error Details Error Details
</Typography> </Typography>
@ -504,7 +515,7 @@ const FailedOcrPage: React.FC = () => {
variant="body2" variant="body2"
sx={{ sx={{
fontFamily: 'monospace', fontFamily: 'monospace',
bgcolor: 'grey.100', bgcolor: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.100',
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
fontSize: '0.75rem', fontSize: '0.75rem',
@ -755,7 +766,13 @@ const FailedOcrPage: React.FC = () => {
<Tooltip title="Download Document"> <Tooltip title="Download Document">
<IconButton <IconButton
size="small" size="small"
onClick={() => window.open(`/api/documents/${doc.id}/download`, '_blank')} onClick={async () => {
try {
await documentService.downloadFile(doc.id, doc.original_filename || doc.filename);
} catch (error) {
console.error('Download failed:', error);
}
}}
sx={{ color: theme.palette.secondary.main }} sx={{ color: theme.palette.secondary.main }}
> >
<DownloadIcon /> <DownloadIcon />

View File

@ -576,16 +576,19 @@ const SourcesPage: React.FC = () => {
icon: React.ReactNode; icon: React.ReactNode;
label: string; label: string;
value: string | number; value: string | number;
color?: 'primary' | 'success' | 'warning' | 'error' color?: 'primary' | 'success' | 'warning' | 'error' | 'info'
}) => ( }) => (
<Box <Box
sx={{ sx={{
p: 3, p: 2.5,
borderRadius: 3, borderRadius: 3,
background: `linear-gradient(135deg, ${alpha(theme.palette[color].main, 0.1)} 0%, ${alpha(theme.palette[color].main, 0.05)} 100%)`, background: `linear-gradient(135deg, ${alpha(theme.palette[color].main, 0.1)} 0%, ${alpha(theme.palette[color].main, 0.05)} 100%)`,
border: `1px solid ${alpha(theme.palette[color].main, 0.2)}`, border: `1px solid ${alpha(theme.palette[color].main, 0.2)}`,
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
height: '100px',
display: 'flex',
alignItems: 'center',
'&::before': { '&::before': {
content: '""', content: '""',
position: 'absolute', position: 'absolute',
@ -597,22 +600,41 @@ const SourcesPage: React.FC = () => {
} }
}} }}
> >
<Stack direction="row" alignItems="center" spacing={2}> <Stack direction="row" alignItems="center" spacing={2} sx={{ width: '100%', overflow: 'hidden' }}>
<Avatar <Avatar
sx={{ sx={{
bgcolor: alpha(theme.palette[color].main, 0.15), bgcolor: alpha(theme.palette[color].main, 0.15),
color: theme.palette[color].main, color: theme.palette[color].main,
width: 48, width: 40,
height: 48, height: 40,
flexShrink: 0
}} }}
> >
{icon} {icon}
</Avatar> </Avatar>
<Box> <Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="h4" fontWeight="bold" color={theme.palette[color].main}> <Typography
variant="h5"
fontWeight="bold"
color={theme.palette[color].main}
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{typeof value === 'number' ? value.toLocaleString() : value} {typeof value === 'number' ? value.toLocaleString() : value}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography
variant="body2"
color="text.secondary"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '0.75rem'
}}
>
{label} {label}
</Typography> </Typography>
</Box> </Box>
@ -782,8 +804,8 @@ const SourcesPage: React.FC = () => {
</Stack> </Stack>
{/* Stats Grid */} {/* Stats Grid */}
<Grid container spacing={3} mb={3}> <Grid container spacing={2} mb={3}>
<Grid item xs={6} sm={3}> <Grid item xs={6} sm={4} md={3}>
<StatCard <StatCard
icon={<TrendingUpIcon />} icon={<TrendingUpIcon />}
label="Files Synced" label="Files Synced"
@ -791,7 +813,7 @@ const SourcesPage: React.FC = () => {
color="success" color="success"
/> />
</Grid> </Grid>
<Grid item xs={6} sm={3}> <Grid item xs={6} sm={4} md={3}>
<StatCard <StatCard
icon={<SpeedIcon />} icon={<SpeedIcon />}
label="Files Pending" label="Files Pending"
@ -799,15 +821,23 @@ const SourcesPage: React.FC = () => {
color="warning" color="warning"
/> />
</Grid> </Grid>
<Grid item xs={6} sm={3}> <Grid item xs={6} sm={4} md={3}>
<StatCard
icon={<AssessmentIcon />}
label="OCR Processed"
value={source.total_files_synced}
color="info"
/>
</Grid>
<Grid item xs={6} sm={4} md={3}>
<StatCard <StatCard
icon={<StorageIcon />} icon={<StorageIcon />}
label="Total Size" label="Total Size (Downloaded)"
value={formatBytes(source.total_size_bytes)} value={formatBytes(source.total_size_bytes)}
color="primary" color="primary"
/> />
</Grid> </Grid>
<Grid item xs={6} sm={3}> <Grid item xs={6} sm={4} md={3}>
<StatCard <StatCard
icon={<TimelineIcon />} icon={<TimelineIcon />}
label="Last Sync" label="Last Sync"

View File

@ -151,6 +151,28 @@ export const documentService = {
}) })
}, },
downloadFile: async (id: string, filename?: string) => {
try {
const response = await api.get(`/documents/${id}/download`, {
responseType: 'blob',
});
// Create blob URL and trigger download
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename || `document-${id}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Download failed:', error);
throw error;
}
},
getOcrText: (id: string) => { getOcrText: (id: string) => {
return api.get<OcrResponse>(`/documents/${id}/ocr`) return api.get<OcrResponse>(`/documents/${id}/ocr`)
}, },