Merge pull request #44 from readur/feat/improve-dashboard-and-theme
feat(client): update/fix dashboard and theme
This commit is contained in:
commit
182ba7cf18
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue