349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
import { useCallback, useState, useEffect, useRef } from 'react'
|
|
import { useDropzone } from 'react-dropzone'
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
CircularProgress,
|
|
Alert,
|
|
Button,
|
|
Chip,
|
|
LinearProgress,
|
|
Card,
|
|
CardContent,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
ListItemIcon,
|
|
Divider
|
|
} from '@mui/material'
|
|
import {
|
|
CloudUpload,
|
|
CheckCircle,
|
|
Error,
|
|
HourglassEmpty,
|
|
Visibility,
|
|
Description,
|
|
Image,
|
|
PictureAsPdf
|
|
} from '@mui/icons-material'
|
|
import { useAppDispatch, useAppSelector } from '../store'
|
|
import { uploadDocument, removeDocument, addDocuments, setCurrentDocument } from '../store/documentSlice'
|
|
import { Layout } from '../components/Layout'
|
|
import { FilePreview } from '../components/FilePreview'
|
|
import { getTestFilesList, loadTestFile, filterSupportedFiles } from '../services/testFilesApi'
|
|
import type { Document } from '../types'
|
|
|
|
export default function UploadView() {
|
|
const dispatch = useAppDispatch()
|
|
const { documents, error, extractionById } = useAppSelector((state) => state.document)
|
|
const [previewDocument, setPreviewDocument] = useState<Document | null>(null)
|
|
const [bootstrapped, setBootstrapped] = useState(false)
|
|
const [bootstrapInProgress, setBootstrapInProgress] = useState(false)
|
|
const bootstrapTriggered = useRef(false)
|
|
const [isProcessing, setIsProcessing] = useState(false)
|
|
const [processedCount, setProcessedCount] = useState(0)
|
|
const [totalFiles, setTotalFiles] = useState(0)
|
|
|
|
const onDrop = useCallback(
|
|
async (acceptedFiles: File[]) => {
|
|
setIsProcessing(true)
|
|
setTotalFiles(acceptedFiles.length)
|
|
setProcessedCount(0)
|
|
|
|
// Traitement en parallèle de tous les fichiers
|
|
const uploadPromises = acceptedFiles.map(async (file) => {
|
|
try {
|
|
const doc = await dispatch(uploadDocument(file)).unwrap()
|
|
|
|
// Déclencher l'extraction immédiatement
|
|
if (!extractionById[doc.id]) {
|
|
const { extractDocument } = await import('../store/documentSlice')
|
|
dispatch(extractDocument(doc.id))
|
|
}
|
|
|
|
setProcessedCount(prev => prev + 1)
|
|
return doc
|
|
} catch (error) {
|
|
console.error(`Erreur lors du traitement de ${file.name}:`, error)
|
|
setProcessedCount(prev => prev + 1)
|
|
return null
|
|
}
|
|
})
|
|
|
|
// Attendre que tous les fichiers soient traités
|
|
await Promise.all(uploadPromises)
|
|
setIsProcessing(false)
|
|
},
|
|
[dispatch, extractionById]
|
|
)
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
onDrop,
|
|
accept: {
|
|
'application/pdf': ['.pdf'],
|
|
'image/*': ['.png', '.jpg', '.jpeg', '.tiff'],
|
|
},
|
|
multiple: true,
|
|
})
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <CheckCircle color="success" />
|
|
case 'error':
|
|
return <Error color="error" />
|
|
case 'processing':
|
|
return <CircularProgress size={20} />
|
|
default:
|
|
return <HourglassEmpty color="action" />
|
|
}
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return 'success'
|
|
case 'error':
|
|
return 'error'
|
|
case 'processing':
|
|
return 'warning'
|
|
default:
|
|
return 'default'
|
|
}
|
|
}
|
|
|
|
// Bootstrap: charger automatiquement les fichiers de test et les traiter en parallèle
|
|
useEffect(() => {
|
|
if (bootstrapped || bootstrapInProgress || bootstrapTriggered.current || !import.meta.env.DEV) return
|
|
|
|
const load = async () => {
|
|
bootstrapTriggered.current = true
|
|
setBootstrapInProgress(true)
|
|
console.log('🔄 [BOOTSTRAP] Chargement automatique des fichiers de test...')
|
|
|
|
try {
|
|
// Récupérer la liste des fichiers disponibles
|
|
const testFiles = await getTestFilesList()
|
|
console.log('📁 [BOOTSTRAP] Fichiers trouvés:', testFiles.map(f => f.name))
|
|
|
|
// Filtrer les fichiers supportés
|
|
const supportedFiles = filterSupportedFiles(testFiles)
|
|
console.log('✅ [BOOTSTRAP] Fichiers supportés:', supportedFiles.map(f => f.name))
|
|
|
|
if (supportedFiles.length === 0) {
|
|
console.log('⚠️ [BOOTSTRAP] Aucun fichier de test supporté trouvé')
|
|
setBootstrapped(true)
|
|
return
|
|
}
|
|
|
|
// Démarrer le traitement en parallèle
|
|
setIsProcessing(true)
|
|
setTotalFiles(supportedFiles.length)
|
|
setProcessedCount(0)
|
|
|
|
// Traitement en parallèle de tous les fichiers de test
|
|
const loadPromises = supportedFiles.map(async (fileInfo) => {
|
|
try {
|
|
console.log(`📄 [BOOTSTRAP] Chargement de ${fileInfo.name}...`)
|
|
const file = await loadTestFile(fileInfo.name)
|
|
|
|
if (file) {
|
|
// Simuler upload local
|
|
const previewUrl = URL.createObjectURL(file)
|
|
const document: Document = {
|
|
id: `boot-${fileInfo.name}-${Date.now()}`,
|
|
name: fileInfo.name,
|
|
mimeType: fileInfo.type || 'application/octet-stream',
|
|
functionalType: undefined,
|
|
size: fileInfo.size,
|
|
uploadDate: new Date(),
|
|
status: 'completed',
|
|
previewUrl,
|
|
}
|
|
|
|
// Ajouter le document au store
|
|
dispatch(addDocuments([document]))
|
|
|
|
setProcessedCount(prev => prev + 1)
|
|
console.log(`✅ [BOOTSTRAP] ${fileInfo.name} chargé (extraction gérée par Layout)`)
|
|
return document
|
|
}
|
|
} catch (error) {
|
|
console.warn(`❌ [BOOTSTRAP] Erreur lors du chargement de ${fileInfo.name}:`, error)
|
|
setProcessedCount(prev => prev + 1)
|
|
return null
|
|
}
|
|
})
|
|
|
|
// Attendre que tous les fichiers soient chargés
|
|
const results = await Promise.all(loadPromises)
|
|
const successfulDocs = results.filter(doc => doc !== null)
|
|
|
|
if (successfulDocs.length > 0) {
|
|
console.log(`🎉 [BOOTSTRAP] ${successfulDocs.length} fichiers chargés avec succès`)
|
|
// Définir le premier document comme document courant
|
|
const firstDoc = successfulDocs[0]
|
|
if (firstDoc) {
|
|
dispatch(setCurrentDocument(firstDoc))
|
|
}
|
|
}
|
|
|
|
setIsProcessing(false)
|
|
setBootstrapped(true)
|
|
} catch (error) {
|
|
console.error('❌ [BOOTSTRAP] Erreur lors du chargement des fichiers de test:', error)
|
|
setIsProcessing(false)
|
|
setBootstrapped(true)
|
|
} finally {
|
|
setBootstrapInProgress(false)
|
|
}
|
|
}
|
|
|
|
load()
|
|
}, [dispatch, bootstrapped])
|
|
|
|
const getFileIcon = (mimeType: string) => {
|
|
if (mimeType.includes('pdf')) return <PictureAsPdf color="error" />
|
|
if (mimeType.includes('image')) return <Image color="primary" />
|
|
return <Description color="action" />
|
|
}
|
|
|
|
const progressPercentage = totalFiles > 0 ? (processedCount / totalFiles) * 100 : 0
|
|
|
|
return (
|
|
<Layout>
|
|
<Typography variant="h4" gutterBottom>
|
|
Analyse de documents 4NK IA
|
|
</Typography>
|
|
|
|
{/* Barre de progression globale */}
|
|
{isProcessing && (
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
|
<CircularProgress size={24} />
|
|
<Typography variant="h6">
|
|
Traitement en cours...
|
|
</Typography>
|
|
</Box>
|
|
<Box mb={1}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{processedCount} fichier{processedCount > 1 ? 's' : ''} sur {totalFiles} traité{totalFiles > 1 ? 's' : ''}
|
|
</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={progressPercentage}
|
|
sx={{ height: 8, borderRadius: 4 }}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Zone de drop */}
|
|
<Paper
|
|
{...getRootProps()}
|
|
sx={{
|
|
p: 4,
|
|
textAlign: 'center',
|
|
cursor: 'pointer',
|
|
border: '2px dashed',
|
|
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
|
bgcolor: isDragActive ? 'action.hover' : 'background.paper',
|
|
'&:hover': {
|
|
borderColor: 'primary.main',
|
|
bgcolor: 'action.hover',
|
|
},
|
|
}}
|
|
>
|
|
<input {...getInputProps()} />
|
|
<CloudUpload sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
|
<Typography variant="h6" gutterBottom>
|
|
{isDragActive
|
|
? 'Déposez les fichiers ici...'
|
|
: 'Glissez-déposez vos documents ou cliquez pour sélectionner'}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Formats acceptés: PDF, PNG, JPG, JPEG, TIFF
|
|
</Typography>
|
|
</Paper>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mt: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Liste des documents */}
|
|
{documents.length > 0 && (
|
|
<Box sx={{ mt: 3 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Documents analysés ({documents.length})
|
|
</Typography>
|
|
|
|
<Card>
|
|
<List>
|
|
{documents.map((doc, index) => (
|
|
<div key={`${doc.id}-${index}`}>
|
|
<ListItem>
|
|
<ListItemIcon>
|
|
{getFileIcon(doc.mimeType)}
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={
|
|
<Box display="flex" alignItems="center" gap={1}>
|
|
{getStatusIcon(doc.status)}
|
|
<Typography variant="subtitle1">
|
|
{doc.name}
|
|
</Typography>
|
|
<Chip
|
|
label={doc.status}
|
|
size="small"
|
|
color={getStatusColor(doc.status) as 'success' | 'error' | 'warning' | 'default'}
|
|
/>
|
|
</Box>
|
|
}
|
|
secondary={
|
|
<Typography variant="body2" color="text.secondary">
|
|
{doc.mimeType} • {(doc.size / 1024 / 1024).toFixed(2)} MB
|
|
</Typography>
|
|
}
|
|
/>
|
|
<Box display="flex" gap={1}>
|
|
<Button
|
|
size="small"
|
|
startIcon={<Visibility />}
|
|
onClick={() => setPreviewDocument(doc)}
|
|
disabled={doc.status !== 'completed'}
|
|
>
|
|
Aperçu
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
color="error"
|
|
onClick={() => dispatch(removeDocument(doc.id))}
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
</Box>
|
|
</ListItem>
|
|
{index < documents.length - 1 && <Divider />}
|
|
</div>
|
|
))}
|
|
</List>
|
|
</Card>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Aperçu du document */}
|
|
{previewDocument && (
|
|
<FilePreview
|
|
document={previewDocument}
|
|
onClose={() => setPreviewDocument(null)}
|
|
/>
|
|
)}
|
|
</Layout>
|
|
)
|
|
}
|