769 lines
31 KiB
TypeScript
769 lines
31 KiB
TypeScript
import { useEffect } from 'react'
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
Card,
|
|
CardContent,
|
|
Chip,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
Alert,
|
|
CircularProgress,
|
|
Button,
|
|
Tooltip,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
} from '@mui/material'
|
|
import {
|
|
Person,
|
|
LocationOn,
|
|
Business,
|
|
Description,
|
|
Language,
|
|
Verified,
|
|
ExpandMore,
|
|
AttachMoney,
|
|
CalendarToday,
|
|
Gavel,
|
|
Edit,
|
|
TextFields,
|
|
Assessment,
|
|
} from '@mui/icons-material'
|
|
import { useAppDispatch, useAppSelector } from '../store'
|
|
import { extractDocument, setCurrentDocument } from '../store/documentSlice'
|
|
import { Layout } from '../components/Layout'
|
|
|
|
export default function ExtractionView() {
|
|
const dispatch = useAppDispatch()
|
|
const { currentDocument, extractionResult, extractionById, loading, documents } = useAppSelector((state) => state.document)
|
|
|
|
useEffect(() => {
|
|
if (!currentDocument) return
|
|
const cached = extractionById[currentDocument.id]
|
|
if (!cached) dispatch(extractDocument(currentDocument.id))
|
|
}, [currentDocument, extractionById, dispatch])
|
|
|
|
const currentIndex = currentDocument ? Math.max(0, documents.findIndex(d => d.id === currentDocument.id)) : -1
|
|
const hasPrev = currentIndex > 0
|
|
const hasNext = currentIndex >= 0 && currentIndex < documents.length - 1
|
|
|
|
const gotoDoc = (index: number) => {
|
|
const doc = documents[index]
|
|
if (!doc) return
|
|
dispatch(setCurrentDocument(doc))
|
|
// Laisser l'effet décider si une nouvelle extraction est nécessaire
|
|
}
|
|
|
|
if (!currentDocument) {
|
|
return (
|
|
<Layout>
|
|
<Alert severity="info">
|
|
Veuillez d'abord téléverser et sélectionner un document.
|
|
</Alert>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Layout>
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4, alignItems: 'center', gap: 2 }}>
|
|
<CircularProgress size={24} />
|
|
<Typography>Extraction en cours...</Typography>
|
|
</Box>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
const activeResult = currentDocument ? (extractionById[currentDocument.id] || extractionResult) : extractionResult
|
|
|
|
if (!activeResult) {
|
|
return (
|
|
<Layout>
|
|
<Alert severity="warning">
|
|
Aucun résultat d'extraction disponible.
|
|
</Alert>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
// Adapter le résultat pour le nouveau format JSON standard
|
|
const getStandardResult = (result: any) => {
|
|
// Si c'est déjà le nouveau format, on le retourne tel quel
|
|
if (result.extraction && result.classification) {
|
|
return result
|
|
}
|
|
|
|
// Sinon, on adapte l'ancien format
|
|
return {
|
|
document: {
|
|
id: result.documentId,
|
|
fileName: currentDocument?.name || 'Document',
|
|
fileSize: currentDocument?.size || 0,
|
|
mimeType: currentDocument?.mimeType || 'application/octet-stream',
|
|
uploadTimestamp: new Date().toISOString()
|
|
},
|
|
classification: {
|
|
documentType: result.documentType || 'Document',
|
|
confidence: result.confidence || 0.8,
|
|
subType: result.documentType || 'Document',
|
|
language: result.language || 'fr',
|
|
pageCount: 1
|
|
},
|
|
extraction: {
|
|
text: {
|
|
raw: result.text || '',
|
|
processed: result.text || '',
|
|
wordCount: result.text ? result.text.split(/\s+/).length : 0,
|
|
characterCount: result.text ? result.text.length : 0,
|
|
confidence: result.confidence || 0.8
|
|
},
|
|
entities: {
|
|
persons: result.identities?.filter((id: any) => id.type === 'person') || [],
|
|
companies: result.identities?.filter((id: any) => id.type === 'company') || [],
|
|
addresses: result.addresses || [],
|
|
financial: { amounts: [], totals: {}, payment: {} },
|
|
dates: [],
|
|
contractual: { clauses: [], signatures: [] },
|
|
references: []
|
|
}
|
|
},
|
|
metadata: {
|
|
processing: {
|
|
engine: '4NK_IA_Backend',
|
|
version: '1.0.0',
|
|
processingTime: '0ms',
|
|
ocrEngine: 'tesseract.js',
|
|
nerEngine: 'rule-based',
|
|
preprocessing: { applied: true, reason: 'Image preprocessing applied' }
|
|
},
|
|
quality: {
|
|
globalConfidence: result.confidence || 0.8,
|
|
textExtractionConfidence: result.confidence || 0.8,
|
|
entityExtractionConfidence: 0.90,
|
|
classificationConfidence: result.confidence || 0.8
|
|
}
|
|
},
|
|
status: {
|
|
success: true,
|
|
errors: [],
|
|
warnings: [],
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
}
|
|
}
|
|
|
|
const standardResult = getStandardResult(activeResult)
|
|
|
|
return (
|
|
<Layout>
|
|
<Typography variant="h4" gutterBottom>
|
|
Extraction des données
|
|
</Typography>
|
|
|
|
{/* Navigation entre documents */}
|
|
{documents.length > 0 && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
|
<Button size="small" variant="outlined" disabled={!hasPrev} onClick={() => gotoDoc(currentIndex - 1)}>
|
|
Précédent
|
|
</Button>
|
|
<Typography variant="body2">
|
|
{currentIndex + 1} / {documents.length}
|
|
</Typography>
|
|
<Button size="small" variant="outlined" disabled={!hasNext} onClick={() => gotoDoc(currentIndex + 1)}>
|
|
Suivant
|
|
</Button>
|
|
{currentDocument && (
|
|
<Typography variant="body2" sx={{ ml: 2 }} color="text.secondary">
|
|
Document: {currentDocument.name}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
{/* Informations générales */}
|
|
<Paper sx={{ p: 2 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Informations générales
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
|
|
<Chip
|
|
icon={<Language />}
|
|
label={`Langue: ${standardResult.classification.language}`}
|
|
color="primary"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
icon={<Description />}
|
|
label={`Type: ${standardResult.classification.documentType}`}
|
|
color="secondary"
|
|
variant="outlined"
|
|
/>
|
|
{standardResult.classification.subType && (
|
|
<Chip
|
|
label={`Sous-type: ${standardResult.classification.subType}`}
|
|
color="info"
|
|
variant="outlined"
|
|
/>
|
|
)}
|
|
<Tooltip
|
|
arrow
|
|
title={`Confiance globale: ${Math.round(standardResult.metadata.quality.globalConfidence * 100)}%`}
|
|
>
|
|
<Chip
|
|
icon={<Verified />}
|
|
label={`Confiance: ${Math.round(standardResult.metadata.quality.globalConfidence * 100)}%`}
|
|
color={standardResult.metadata.quality.globalConfidence > 0.8 ? 'success' : 'warning'}
|
|
variant="outlined"
|
|
/>
|
|
</Tooltip>
|
|
</Box>
|
|
|
|
{/* Métadonnées de traitement */}
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
<Chip
|
|
icon={<Assessment />}
|
|
label={`Moteur: ${standardResult.metadata.processing.engine}`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
label={`Temps: ${standardResult.metadata.processing.processingTime}`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
label={`OCR: ${standardResult.metadata.processing.ocrEngine}`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
{/* Aperçu du document */}
|
|
{currentDocument && (
|
|
<Box>
|
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
|
Aperçu du document
|
|
</Typography>
|
|
{(() => {
|
|
const isPDF = currentDocument.mimeType.includes('pdf') || currentDocument.name.toLowerCase().endsWith('.pdf')
|
|
const isImage =
|
|
currentDocument.mimeType.startsWith('image/') ||
|
|
['.png', '.jpg', '.jpeg', '.gif', '.webp'].some((ext) => currentDocument.name.toLowerCase().endsWith(ext))
|
|
if (isImage && currentDocument.previewUrl) {
|
|
return (
|
|
<Box sx={{
|
|
border: '1px solid', borderColor: 'grey.300', borderRadius: 1, p: 1,
|
|
display: 'inline-block', maxWidth: '100%'
|
|
}}>
|
|
<img
|
|
src={currentDocument.previewUrl}
|
|
alt={currentDocument.name}
|
|
style={{ maxWidth: 200, maxHeight: 150, objectFit: 'contain' }}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
if (isPDF && currentDocument.previewUrl) {
|
|
return (
|
|
<Box sx={{
|
|
border: '1px solid', borderColor: 'grey.300', borderRadius: 1,
|
|
overflow: 'hidden', width: 200, height: 150
|
|
}}>
|
|
<iframe
|
|
src={`${currentDocument.previewUrl}#toolbar=0&navpanes=0&scrollbar=0&page=1&view=FitH`}
|
|
width="100%"
|
|
height="100%"
|
|
style={{ border: 'none' }}
|
|
title={`Aperçu rapide de ${currentDocument.name}`}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
return (
|
|
<Box sx={{
|
|
border: '1px solid', borderColor: 'grey.300', borderRadius: 1, p: 2,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
width: 200, height: 150, bgcolor: 'grey.50'
|
|
}}>
|
|
<Description color="action" />
|
|
</Box>
|
|
)
|
|
})()}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
|
|
{/* Entités extraites */}
|
|
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
|
{/* Personnes */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<Person sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Personnes ({standardResult.extraction.entities.persons?.length || 0})
|
|
</Typography>
|
|
<List dense>
|
|
{(standardResult.extraction.entities.persons || []).map((person: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={`${person.firstName || ''} ${person.lastName || ''}`.trim()}
|
|
secondaryTypographyProps={{ component: 'span' }}
|
|
secondary={
|
|
<Box component="span">
|
|
{person.role && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Rôle: {person.role}
|
|
</Typography>
|
|
)}
|
|
{person.email && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Email: {person.email}
|
|
</Typography>
|
|
)}
|
|
{person.phone && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Téléphone: {person.phone}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(person.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Sociétés */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<Business sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Sociétés ({standardResult.extraction.entities.companies?.length || 0})
|
|
</Typography>
|
|
<List dense>
|
|
{(standardResult.extraction.entities.companies || []).map((company: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={company.name}
|
|
secondaryTypographyProps={{ component: 'span' }}
|
|
secondary={
|
|
<Box component="span">
|
|
{company.legalForm && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Forme: {company.legalForm}
|
|
</Typography>
|
|
)}
|
|
{company.siret && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
SIRET: {company.siret}
|
|
</Typography>
|
|
)}
|
|
{company.tva && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
TVA: {company.tva}
|
|
</Typography>
|
|
)}
|
|
{company.role && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Rôle: {company.role}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(company.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Adresses */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Adresses ({standardResult.extraction.entities.addresses?.length || 0})
|
|
</Typography>
|
|
<List dense>
|
|
{(standardResult.extraction.entities.addresses || []).map((address: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={`${address.street}, ${address.city}`}
|
|
secondaryTypographyProps={{ component: 'span' }}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
{address.postalCode} {address.country}
|
|
</Typography>
|
|
{address.type && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {address.type}
|
|
</Typography>
|
|
)}
|
|
{address.company && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Société: {address.company}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(address.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Informations financières */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<AttachMoney sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Informations financières
|
|
</Typography>
|
|
{standardResult.extraction.entities.financial?.amounts?.length > 0 ? (
|
|
<List dense>
|
|
{standardResult.extraction.entities.financial.amounts.map((amount: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={`${amount.value} ${amount.currency}`}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {amount.type}
|
|
</Typography>
|
|
{amount.description && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
{amount.description}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(amount.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aucune information financière détectée
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Sections supplémentaires */}
|
|
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
|
{/* Dates */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<CalendarToday sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Dates ({standardResult.extraction.entities.dates?.length || 0})
|
|
</Typography>
|
|
{standardResult.extraction.entities.dates?.length > 0 ? (
|
|
<List dense>
|
|
{standardResult.extraction.entities.dates.map((date: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={date.value || date.formatted}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {date.type}
|
|
</Typography>
|
|
{date.formatted && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Formaté: {date.formatted}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(date.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aucune date détectée
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Références */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<Description sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Références ({standardResult.extraction.entities.references?.length || 0})
|
|
</Typography>
|
|
{standardResult.extraction.entities.references?.length > 0 ? (
|
|
<List dense>
|
|
{standardResult.extraction.entities.references.map((ref: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={ref.number || ref.value}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {ref.type}
|
|
</Typography>
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(ref.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aucune référence détectée
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Clauses contractuelles et signatures */}
|
|
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
|
{/* Clauses contractuelles */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<Gavel sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Clauses contractuelles ({standardResult.extraction.entities.contractual?.clauses?.length || 0})
|
|
</Typography>
|
|
{standardResult.extraction.entities.contractual?.clauses?.length > 0 ? (
|
|
<List dense>
|
|
{standardResult.extraction.entities.contractual.clauses.map((clause: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={clause.content}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {clause.type}
|
|
</Typography>
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(clause.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aucune clause contractuelle détectée
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
|
|
{/* Signatures */}
|
|
<Box sx={{ flex: '1 1 300px' }}>
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<Edit sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Signatures ({standardResult.extraction.entities.contractual?.signatures?.length || 0})
|
|
</Typography>
|
|
{standardResult.extraction.entities.contractual?.signatures?.length > 0 ? (
|
|
<List dense>
|
|
{standardResult.extraction.entities.contractual.signatures.map((signature: any, index: number) => (
|
|
<ListItem key={index}>
|
|
<ListItemText
|
|
primary={signature.signatory || 'Signature détectée'}
|
|
secondary={
|
|
<Box component="span">
|
|
<Typography variant="caption" display="block" component="span">
|
|
Type: {signature.type}
|
|
</Typography>
|
|
<Typography variant="caption" display="block" component="span">
|
|
Présente: {signature.present ? 'Oui' : 'Non'}
|
|
</Typography>
|
|
{signature.date && (
|
|
<Typography variant="caption" display="block" component="span">
|
|
Date: {signature.date}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="caption" display="block" component="span">
|
|
Confiance: {(signature.confidence * 100).toFixed(1)}%
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
) : (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Aucune signature détectée
|
|
</Typography>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Texte extrait */}
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
<TextFields sx={{ mr: 1, verticalAlign: 'middle' }} />
|
|
Texte extrait
|
|
</Typography>
|
|
<Box sx={{ mb: 2 }}>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
<Chip
|
|
label={`${standardResult.extraction.text.wordCount} mots`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
label={`${standardResult.extraction.text.characterCount} caractères`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
label={`Confiance: ${Math.round(standardResult.extraction.text.confidence * 100)}%`}
|
|
size="small"
|
|
color={standardResult.extraction.text.confidence > 0.8 ? 'success' : 'warning'}
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
|
<Typography variant="subtitle1">Texte brut</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<Paper
|
|
sx={{
|
|
p: 2,
|
|
bgcolor: 'grey.50',
|
|
maxHeight: 300,
|
|
overflow: 'auto',
|
|
}}
|
|
>
|
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
{standardResult.extraction.text.raw}
|
|
</Typography>
|
|
</Paper>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
{standardResult.extraction.text.processed !== standardResult.extraction.text.raw && (
|
|
<Accordion>
|
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
|
<Typography variant="subtitle1">Texte traité</Typography>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<Paper
|
|
sx={{
|
|
p: 2,
|
|
bgcolor: 'grey.50',
|
|
maxHeight: 300,
|
|
overflow: 'auto',
|
|
}}
|
|
>
|
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
{standardResult.extraction.text.processed}
|
|
</Typography>
|
|
</Paper>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Statut et métadonnées */}
|
|
{standardResult.status && (
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom>
|
|
Statut du traitement
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
|
|
<Chip
|
|
label={standardResult.status.success ? 'Succès' : 'Échec'}
|
|
color={standardResult.status.success ? 'success' : 'error'}
|
|
variant="outlined"
|
|
/>
|
|
<Chip
|
|
label={`Traité le: ${new Date(standardResult.status.timestamp).toLocaleString()}`}
|
|
size="small"
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
{standardResult.status.warnings?.length > 0 && (
|
|
<Alert severity="warning" sx={{ mb: 1 }}>
|
|
<Typography variant="subtitle2">Avertissements:</Typography>
|
|
<ul>
|
|
{standardResult.status.warnings.map((warning: string, index: number) => (
|
|
<li key={index}>{warning}</li>
|
|
))}
|
|
</ul>
|
|
</Alert>
|
|
)}
|
|
{standardResult.status.errors?.length > 0 && (
|
|
<Alert severity="error">
|
|
<Typography variant="subtitle2">Erreurs:</Typography>
|
|
<ul>
|
|
{standardResult.status.errors.map((error: string, index: number) => (
|
|
<li key={index}>{error}</li>
|
|
))}
|
|
</ul>
|
|
</Alert>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</Box>
|
|
</Layout>
|
|
)
|
|
} |