4NK_IA_front/src/views/UploadView.tsx
Nicolas Cantu a3f2ecf6ae backend
2025-09-16 01:51:36 +02:00

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>
)
}