feat(ui): refonte complete interface extraction moderne
This commit is contained in:
parent
0f9e50df71
commit
9af63f22fe
@ -95,7 +95,7 @@ export default function App() {
|
|||||||
const hiddenDelay = 20000
|
const hiddenDelay = 20000
|
||||||
console.log('⏸️ [APP] Onglet caché, report du polling de', hiddenDelay, 'ms')
|
console.log('⏸️ [APP] Onglet caché, report du polling de', hiddenDelay, 'ms')
|
||||||
const t = setTimeout(tick, hiddenDelay)
|
const t = setTimeout(tick, hiddenDelay)
|
||||||
dispatch(setPollingInterval(t as unknown as number))
|
dispatch(setPollingInterval(t as any))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,11 +108,11 @@ export default function App() {
|
|||||||
const factor = Math.min(4, Math.pow(2, Math.floor(pollCount / 5)))
|
const factor = Math.min(4, Math.pow(2, Math.floor(pollCount / 5)))
|
||||||
const delay = base * factor
|
const delay = base * factor
|
||||||
const t = setTimeout(tick, delay)
|
const t = setTimeout(tick, delay)
|
||||||
dispatch(setPollingInterval(t as unknown as number))
|
dispatch(setPollingInterval(t as any))
|
||||||
}
|
}
|
||||||
|
|
||||||
const t0 = setTimeout(tick, 0)
|
const t0 = setTimeout(tick, 0)
|
||||||
dispatch(setPollingInterval(t0 as unknown as number))
|
dispatch(setPollingInterval(t0 as any))
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useCallback } from 'react'
|
||||||
import { Box, Typography, Paper, Card, CardContent, Chip, Button, List, ListItem, ListItemText, ListItemButton, Tooltip, Alert, Accordion, AccordionSummary, AccordionDetails, CircularProgress } from '@mui/material'
|
import {
|
||||||
import { Person, LocationOn, Business, Description, Language, Verified, ExpandMore, TextFields, Assessment } from '@mui/icons-material'
|
Box, Typography, Paper, Card, CardContent, Chip, Button, List, ListItemText, ListItemButton,
|
||||||
|
Tooltip, Alert, Accordion, AccordionSummary, AccordionDetails, CircularProgress, TextField,
|
||||||
|
Grid, Divider, Badge, Stack, Avatar, CardHeader, Fade,
|
||||||
|
Snackbar, Alert as MuiAlert
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Person, LocationOn, Business, Description, Language, Verified, ExpandMore, TextFields, Assessment,
|
||||||
|
Save, Delete, Search, Visibility, Check, Refresh,
|
||||||
|
FileDownload, FileOpen, AutoAwesome, Security
|
||||||
|
} from '@mui/icons-material'
|
||||||
import { useAppDispatch, useAppSelector } from '../store'
|
import { useAppDispatch, useAppSelector } from '../store'
|
||||||
import { setCurrentResultIndex } from '../store/documentSlice'
|
import { setCurrentResultIndex } from '../store/documentSlice'
|
||||||
import { clearFolderCache, reprocessFolder, startEnrichment, getEnrichmentStatus } from '../services/folderApi'
|
import { clearFolderCache, reprocessFolder, startEnrichment, getEnrichmentStatus } from '../services/folderApi'
|
||||||
@ -13,14 +22,20 @@ export default function ExtractionView() {
|
|||||||
const { currentFolderHash } = useAppSelector((state) => state.document)
|
const { currentFolderHash } = useAppSelector((state) => state.document)
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(currentResultIndex)
|
const [currentIndex, setCurrentIndex] = useState(currentResultIndex)
|
||||||
|
|
||||||
// Utiliser les résultats du dossier pour la navigation
|
|
||||||
const currentResult = folderResults[currentIndex]
|
|
||||||
const [savingKey, setSavingKey] = useState<string | null>(null)
|
const [savingKey, setSavingKey] = useState<string | null>(null)
|
||||||
const [personsDraft, setPersonsDraft] = useState<any[]>([])
|
const [personsDraft, setPersonsDraft] = useState<any[]>([])
|
||||||
const [addressesDraft, setAddressesDraft] = useState<any[]>([])
|
const [addressesDraft, setAddressesDraft] = useState<any[]>([])
|
||||||
const [companiesDraft, setCompaniesDraft] = useState<any[]>([])
|
const [companiesDraft, setCompaniesDraft] = useState<any[]>([])
|
||||||
const [enriching, setEnriching] = useState<Record<string, string>>({})
|
const [enriching, setEnriching] = useState<Record<string, string>>({})
|
||||||
|
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' | 'info' }>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
severity: 'info'
|
||||||
|
})
|
||||||
|
const [showTextExtract, setShowTextExtract] = useState(false)
|
||||||
|
|
||||||
|
// Utiliser les résultats du dossier pour la navigation
|
||||||
|
const currentResult = folderResults[currentIndex]
|
||||||
|
|
||||||
// Initialiser les brouillons à chaque changement de résultat courant
|
// Initialiser les brouillons à chaque changement de résultat courant
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -39,12 +54,76 @@ export default function ExtractionView() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentResult?.fileHash])
|
}, [currentResult?.fileHash])
|
||||||
|
|
||||||
const gotoResult = (index: number) => {
|
const gotoResult = useCallback((index: number) => {
|
||||||
if (index >= 0 && index < folderResults.length) {
|
if (index >= 0 && index < folderResults.length) {
|
||||||
setCurrentIndex(index)
|
setCurrentIndex(index)
|
||||||
dispatch(setCurrentResultIndex(index))
|
dispatch(setCurrentResultIndex(index))
|
||||||
}
|
}
|
||||||
|
}, [dispatch, folderResults.length])
|
||||||
|
|
||||||
|
const showSnackbar = useCallback((message: string, severity: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
setSnackbar({ open: true, message, severity })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSaveEntity = useCallback(async (type: 'person' | 'address' | 'company', index: number, entity: any) => {
|
||||||
|
if (!currentFolderHash || !currentResult) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSavingKey(`${type}-${index}`)
|
||||||
|
await updateEntity(currentFolderHash, currentResult.fileHash, type, {
|
||||||
|
index,
|
||||||
|
id: entity.id,
|
||||||
|
patch: entity
|
||||||
|
})
|
||||||
|
showSnackbar(`${type === 'person' ? 'Personne' : type === 'address' ? 'Adresse' : 'Entreprise'} sauvegardée`, 'success')
|
||||||
|
} catch (error: any) {
|
||||||
|
showSnackbar(`Erreur lors de la sauvegarde: ${error?.message || error}`, 'error')
|
||||||
|
} finally {
|
||||||
|
setSavingKey(null)
|
||||||
}
|
}
|
||||||
|
}, [currentFolderHash, currentResult, showSnackbar])
|
||||||
|
|
||||||
|
const handleDeleteEntity = useCallback(async (type: 'person' | 'address' | 'company', index: number, entity: any) => {
|
||||||
|
if (!currentFolderHash || !currentResult) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteEntity(currentFolderHash, currentResult.fileHash, type, { index, id: entity.id })
|
||||||
|
showSnackbar(`${type === 'person' ? 'Personne' : type === 'address' ? 'Adresse' : 'Entreprise'} supprimée`, 'success')
|
||||||
|
|
||||||
|
// Mettre à jour les brouillons locaux
|
||||||
|
if (type === 'person') {
|
||||||
|
setPersonsDraft(prev => prev.filter((_, i) => i !== index))
|
||||||
|
} else if (type === 'address') {
|
||||||
|
setAddressesDraft(prev => prev.filter((_, i) => i !== index))
|
||||||
|
} else {
|
||||||
|
setCompaniesDraft(prev => prev.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showSnackbar(`Erreur lors de la suppression: ${error?.message || error}`, 'error')
|
||||||
|
}
|
||||||
|
}, [currentFolderHash, currentResult, showSnackbar])
|
||||||
|
|
||||||
|
const handleEnrichment = useCallback(async (type: 'person' | 'address' | 'company', index: number) => {
|
||||||
|
if (!currentFolderHash || !currentResult) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setEnriching(prev => ({ ...prev, [`${type}-${index}`]: 'running' }))
|
||||||
|
await startEnrichment(currentFolderHash, currentResult.fileHash, type)
|
||||||
|
showSnackbar(`Enrichissement ${type} démarré`, 'info')
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const status = await getEnrichmentStatus(currentFolderHash, currentResult.fileHash, type)
|
||||||
|
setEnriching(prev => ({ ...prev, [`${type}-${index}`]: status.state || 'idle' }))
|
||||||
|
} catch (error) {
|
||||||
|
setEnriching(prev => ({ ...prev, [`${type}-${index}`]: 'error' }))
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
} catch (error: any) {
|
||||||
|
showSnackbar(`Erreur lors de l'enrichissement: ${error?.message || error}`, 'error')
|
||||||
|
setEnriching(prev => ({ ...prev, [`${type}-${index}`]: 'error' }))
|
||||||
|
}
|
||||||
|
}, [currentFolderHash, currentResult, showSnackbar])
|
||||||
|
|
||||||
// Navigation supprimée
|
// Navigation supprimée
|
||||||
|
|
||||||
@ -82,93 +161,123 @@ export default function ExtractionView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Box sx={{ mb: 3 }}>
|
{/* Header moderne avec actions */}
|
||||||
<Typography variant="h4" gutterBottom>
|
<Box sx={{ mb: 4 }}>
|
||||||
Résultats d'extraction
|
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 600, color: 'primary.main' }}>
|
||||||
|
Extraction & Analyse
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Gestion et enrichissement des entités extraites
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Actions de dossier */}
|
<Stack direction="row" spacing={2}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
<Tooltip title="Re-traiter tous les documents du dossier">
|
||||||
<Tooltip title="Re-traiter le dossier: vide le cache puis relance l'analyse de tous les fichiers présents dans uploads/<hash>.">
|
|
||||||
<span>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="outlined"
|
||||||
color="primary"
|
startIcon={<Refresh />}
|
||||||
disabled={!currentFolderHash}
|
disabled={!currentFolderHash}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!currentFolderHash) return
|
if (!currentFolderHash) return
|
||||||
try {
|
try {
|
||||||
const cleared = await clearFolderCache(currentFolderHash)
|
const cleared = await clearFolderCache(currentFolderHash)
|
||||||
const repro = await reprocessFolder(currentFolderHash)
|
const repro = await reprocessFolder(currentFolderHash)
|
||||||
// eslint-disable-next-line no-alert
|
showSnackbar(
|
||||||
alert(
|
`Cache vidé (${cleared.removed} éléments). Re-traitement lancé (${repro.scheduled} fichiers).`,
|
||||||
`Cache vidé (${cleared.removed} éléments). Re-traitement lancé (${repro.scheduled} fichiers).`
|
'success'
|
||||||
)
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// eslint-disable-next-line no-alert
|
showSnackbar(`Erreur lors du re-traitement: ${e?.message || e}`, 'error')
|
||||||
alert(`Erreur lors du re-traitement: ${e?.message || e}`)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Re-traiter le dossier
|
Re-traiter
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Navigation supprimée */}
|
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', md: 'row' } }}>
|
||||||
</Box>
|
{/* Sidebar de navigation moderne */}
|
||||||
|
<Box sx={{ flex: '0 0 300px', minWidth: 0 }}>
|
||||||
<Box sx={{ display: 'flex', gap: 3 }}>
|
<Card sx={{ height: 'fit-content', position: 'sticky', top: 20 }}>
|
||||||
{/* Liste latérale de navigation avec ellipsis */}
|
<CardHeader
|
||||||
<Card sx={{ flex: '0 0 320px', maxHeight: '70vh', overflow: 'auto' }}>
|
title="Documents"
|
||||||
<CardContent sx={{ p: 0 }}>
|
subheader={`${folderResults.length} fichier${folderResults.length > 1 ? 's' : ''}`}
|
||||||
|
avatar={<Avatar sx={{ bgcolor: 'primary.main' }}><Description /></Avatar>}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
<List dense disablePadding>
|
<List dense disablePadding>
|
||||||
{folderResults.map((result, index) => (
|
{folderResults.map((result, index) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={result.fileHash}
|
key={result.fileHash}
|
||||||
selected={index === currentIndex}
|
selected={index === currentIndex}
|
||||||
onClick={() => gotoResult(index)}
|
onClick={() => gotoResult(index)}
|
||||||
|
sx={{
|
||||||
|
borderLeft: index === currentIndex ? 3 : 0,
|
||||||
|
borderLeftColor: 'primary.main',
|
||||||
|
bgcolor: index === currentIndex ? 'primary.50' : 'transparent'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={result.document.fileName} placement="right">
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primaryTypographyProps={{
|
primary={
|
||||||
sx: {
|
<Typography
|
||||||
display: 'block',
|
variant="body2"
|
||||||
maxWidth: 260,
|
sx={{
|
||||||
whiteSpace: 'nowrap',
|
fontWeight: index === currentIndex ? 600 : 400,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
},
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
primary={result.document.fileName}
|
>
|
||||||
secondary={new Date(
|
{result.document.fileName}
|
||||||
result.document.uploadTimestamp as unknown as string,
|
</Typography>
|
||||||
).toLocaleString()}
|
}
|
||||||
|
secondary={
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{new Date(result.document.uploadTimestamp as unknown as string).toLocaleDateString()}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
{index === currentIndex && (
|
||||||
|
<Check color="primary" sx={{ ml: 1 }} />
|
||||||
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
<Box sx={{ flex: '1 1 auto', minWidth: 0 }}>
|
<Box sx={{ flex: '1 1 auto', minWidth: 0 }}>
|
||||||
{/* Informations du document courant */}
|
{/* Header du document avec métadonnées */}
|
||||||
<Card sx={{ mb: 3 }}>
|
<Card sx={{ mb: 3 }}>
|
||||||
<CardContent>
|
<CardHeader
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
avatar={<Avatar sx={{ bgcolor: 'primary.main' }}><FileOpen /></Avatar>}
|
||||||
<Description color="primary" />
|
title={extraction.document.fileName}
|
||||||
<Typography variant="h6">{extraction.document.fileName}</Typography>
|
subheader={`Téléversé le ${new Date(extraction.document.uploadTimestamp as unknown as string).toLocaleString()}`}
|
||||||
<Chip label={extraction.document.mimeType} size="small" variant="outlined" />
|
action={
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<Chip
|
||||||
|
label={extraction.document.mimeType}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
label={`${(extraction.document.fileSize / 1024 / 1024).toFixed(2)} MB`}
|
label={`${(extraction.document.fileSize / 1024 / 1024).toFixed(2)} MB`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
color="default"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Stack>
|
||||||
|
}
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<Stack direction="row" spacing={2} flexWrap="wrap" useFlexGap>
|
||||||
<Chip
|
<Chip
|
||||||
icon={<Language />}
|
icon={<Language />}
|
||||||
label={`Langue: ${extraction.classification.language}`}
|
label={`Langue: ${extraction.classification.language}`}
|
||||||
@ -187,211 +296,481 @@ export default function ExtractionView() {
|
|||||||
color={extraction.metadata.quality.globalConfidence > 0.8 ? 'success' : 'warning'}
|
color={extraction.metadata.quality.globalConfidence > 0.8 ? 'success' : 'warning'}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Box>
|
{(extraction.metadata.quality as any).ollamaScore && (
|
||||||
|
<Chip
|
||||||
|
icon={<AutoAwesome />}
|
||||||
|
label={`IA: ${((extraction.metadata.quality as any).ollamaScore * 100).toFixed(1)}%`}
|
||||||
|
color="secondary"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Texte extrait */}
|
{/* Texte extrait avec toggle */}
|
||||||
<Card sx={{ mb: 3 }}>
|
<Card sx={{ mb: 3 }}>
|
||||||
<CardContent>
|
<CardHeader
|
||||||
<Typography variant="h6" gutterBottom>
|
title="Texte extrait"
|
||||||
<TextFields sx={{ mr: 1, verticalAlign: 'middle' }} />
|
avatar={<Avatar sx={{ bgcolor: 'info.main' }}><TextFields /></Avatar>}
|
||||||
Texte extrait
|
action={
|
||||||
</Typography>
|
<Button
|
||||||
<Paper sx={{ p: 2, bgcolor: 'grey.50', maxHeight: 300, overflow: 'auto' }}>
|
size="small"
|
||||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
startIcon={showTextExtract ? <Visibility /> : <Visibility />}
|
||||||
|
onClick={() => setShowTextExtract(!showTextExtract)}
|
||||||
|
>
|
||||||
|
{showTextExtract ? 'Masquer' : 'Afficher'}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Fade in={showTextExtract}>
|
||||||
|
<CardContent sx={{ pt: 0 }}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
bgcolor: 'grey.50',
|
||||||
|
maxHeight: 300,
|
||||||
|
overflow: 'auto',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'grey.200'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
||||||
{extraction.extraction.text.raw}
|
{extraction.extraction.text.raw}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
</Fade>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Entités extraites */}
|
{/* Entités extraites avec design moderne */}
|
||||||
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||||
{/* Personnes */}
|
{/* Personnes */}
|
||||||
{personsDraft.length > 0 && (
|
{personsDraft.length > 0 && (
|
||||||
<Card sx={{ flex: '1 1 300px' }}>
|
<Box sx={{ flex: '1 1 300px', minWidth: 0 }}>
|
||||||
<CardContent>
|
<Card sx={{ height: '100%' }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<CardHeader
|
||||||
<Person sx={{ mr: 1, verticalAlign: 'middle' }} />
|
title="Personnes"
|
||||||
Personnes ({personsDraft.length})
|
subheader={`${personsDraft.length} entité${personsDraft.length > 1 ? 's' : ''} détectée${personsDraft.length > 1 ? 's' : ''}`}
|
||||||
</Typography>
|
avatar={<Avatar sx={{ bgcolor: 'success.main' }}><Person /></Avatar>}
|
||||||
<Box sx={{ mb: 1, color: 'text.secondary' }}>Au clic sur "Collecter", la collecte externe démarre si nécessaire puis génère un PDF dans le dossier.</Box>
|
action={
|
||||||
<List dense>
|
<Badge badgeContent={personsDraft.length} color="primary">
|
||||||
|
<Person color="action" />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
{personsDraft.map((p: any, i: number) => (
|
{personsDraft.map((p: any, i: number) => (
|
||||||
<ListItem key={`p-${i}`} disableGutters sx={{ py: 0.5 }}>
|
<Paper key={`p-${i}`} sx={{ p: 2, border: '1px solid', borderColor: 'grey.200' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, width: '100%', flexWrap: 'wrap' }}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: '1 1 auto', minWidth: 0, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<input style={{ padding: 4, width: 140 }} placeholder="Prénom" value={p.firstName} onChange={(e)=> setPersonsDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], firstName:e.target.value}; return c})} />
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<input style={{ padding: 4, width: 160 }} placeholder="Nom" value={p.lastName} onChange={(e)=> setPersonsDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], lastName:e.target.value}; return c})} />
|
<TextField
|
||||||
<input style={{ padding: 4, width: 260 }} placeholder="Description" value={p.description} onChange={(e)=> setPersonsDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], description:e.target.value}; return c})} />
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="Prénom"
|
||||||
|
value={p.firstName}
|
||||||
|
onChange={(e) => setPersonsDraft((prev) => {
|
||||||
|
const c = [...prev]
|
||||||
|
c[i] = { ...c[i], firstName: e.target.value }
|
||||||
|
return c
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="Nom"
|
||||||
|
value={p.lastName}
|
||||||
|
onChange={(e) => setPersonsDraft((prev) => {
|
||||||
|
const c = [...prev]
|
||||||
|
c[i] = { ...c[i], lastName: e.target.value }
|
||||||
|
return c
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, whiteSpace: 'nowrap' }}>
|
<TextField
|
||||||
<Button size="small" variant="outlined"
|
size="small"
|
||||||
onClick={async ()=>{
|
fullWidth
|
||||||
setEnriching((s)=>({ ...s, [`p-${i}`]: 'running' }))
|
label="Description"
|
||||||
await startEnrichment(currentFolderHash!, extraction.fileHash, 'person')
|
multiline
|
||||||
// sondage court
|
rows={2}
|
||||||
setTimeout(async ()=>{
|
value={p.description}
|
||||||
const st = await getEnrichmentStatus(currentFolderHash!, extraction.fileHash, 'person')
|
onChange={(e) => setPersonsDraft((prev) => {
|
||||||
setEnriching((s)=>({ ...s, [`p-${i}`]: st.state || 'idle' }))
|
const c = [...prev]
|
||||||
}, 1800)
|
c[i] = { ...c[i], description: e.target.value }
|
||||||
}}
|
return c
|
||||||
>{enriching[`p-${i}`]==='running' ? 'Collecte…' : 'Collecter'}</Button>
|
})}
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/person/pdf`} target="_blank" rel="noopener noreferrer">Voir PDF</Button>
|
/>
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/person/status`} target="_blank" rel="noopener noreferrer">Voir JSON</Button>
|
|
||||||
<Button size="small" variant="outlined" disabled={savingKey===`p-${i}`}
|
|
||||||
onClick={async ()=>{
|
|
||||||
try{
|
|
||||||
setSavingKey(`p-${i}`)
|
|
||||||
await updateEntity(currentFolderHash!, extraction.fileHash, 'person', { index: i, id: p.id, patch: { firstName: p.firstName, lastName: p.lastName, description: p.description || '' } })
|
|
||||||
} finally { setSavingKey(null) }
|
|
||||||
}}>Enregistrer</Button>
|
|
||||||
<Button size="small" color="error"
|
|
||||||
onClick={async ()=>{
|
|
||||||
await deleteEntity(currentFolderHash!, extraction.fileHash, 'person', { index: i, id: p.id })
|
|
||||||
}}>Supprimer</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
</ListItem>
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={enriching[`person-${i}`] === 'running' ? <CircularProgress size={16} /> : <Search />}
|
||||||
|
disabled={enriching[`person-${i}`] === 'running'}
|
||||||
|
onClick={() => handleEnrichment('person', i)}
|
||||||
|
>
|
||||||
|
{enriching[`person-${i}`] === 'running' ? 'Collecte...' : 'Enrichir'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownload />}
|
||||||
|
component="a"
|
||||||
|
href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/person/pdf`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={savingKey === `person-${i}` ? <CircularProgress size={16} /> : <Save />}
|
||||||
|
disabled={savingKey === `person-${i}`}
|
||||||
|
onClick={() => handleSaveEntity('person', i, p)}
|
||||||
|
>
|
||||||
|
Sauver
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() => handleDeleteEntity('person', i, p)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</List>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Adresses */}
|
{/* Adresses */}
|
||||||
{addressesDraft.length > 0 && (
|
{addressesDraft.length > 0 && (
|
||||||
<Card sx={{ flex: '1 1 300px' }}>
|
<Grid item xs={12} lg={4}>
|
||||||
<CardContent>
|
<Card sx={{ height: '100%' }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<CardHeader
|
||||||
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
title="Adresses"
|
||||||
Adresses ({addressesDraft.length})
|
subheader={`${addressesDraft.length} entité${addressesDraft.length > 1 ? 's' : ''} détectée${addressesDraft.length > 1 ? 's' : ''}`}
|
||||||
</Typography>
|
avatar={<Avatar sx={{ bgcolor: 'warning.main' }}><LocationOn /></Avatar>}
|
||||||
<List dense>
|
action={
|
||||||
|
<Badge badgeContent={addressesDraft.length} color="primary">
|
||||||
|
<LocationOn color="action" />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
{addressesDraft.map((a: any, i: number) => (
|
{addressesDraft.map((a: any, i: number) => (
|
||||||
<ListItem key={`a-${i}`} disableGutters sx={{ py: 0.5 }}>
|
<Paper key={`a-${i}`} sx={{ p: 2, border: '1px solid', borderColor: 'grey.200' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, width: '100%', flexWrap: 'wrap' }}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: '1 1 auto', minWidth: 0, flexWrap: 'wrap' }}>
|
<Grid container spacing={1}>
|
||||||
<input style={{ padding: 4, width: 240 }} placeholder="Rue" value={a.street} onChange={(e)=> setAddressesDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], street:e.target.value}; return c})} />
|
<Grid item xs={12}>
|
||||||
<input style={{ padding: 4, width: 100 }} placeholder="CP" value={a.postalCode} onChange={(e)=> setAddressesDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], postalCode:e.target.value}; return c})} />
|
<TextField
|
||||||
<input style={{ padding: 4, width: 180 }} placeholder="Ville" value={a.city} onChange={(e)=> setAddressesDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], city:e.target.value}; return c})} />
|
size="small"
|
||||||
<input style={{ padding: 4, width: 140 }} placeholder="Pays" value={a.country} onChange={(e)=> setAddressesDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], country:e.target.value}; return c})} />
|
fullWidth
|
||||||
<input style={{ padding: 4, width: 260 }} placeholder="Description" value={a.description} onChange={(e)=> setAddressesDraft((prev)=>{ const c=[...prev]; c[i]={...c[i], description:e.target.value}; return c})} />
|
label="Rue"
|
||||||
</Box>
|
value={a.street}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, whiteSpace: 'nowrap' }}>
|
onChange={(e) => setAddressesDraft((prev) => {
|
||||||
<Button size="small" variant="outlined"
|
const c = [...prev]
|
||||||
onClick={async ()=>{
|
c[i] = { ...c[i], street: e.target.value }
|
||||||
setEnriching((s)=>({ ...s, [`a-${i}`]: 'running' }))
|
return c
|
||||||
await startEnrichment(currentFolderHash!, extraction.fileHash, 'address')
|
})}
|
||||||
setTimeout(async ()=>{
|
/>
|
||||||
const st = await getEnrichmentStatus(currentFolderHash!, extraction.fileHash, 'address')
|
</Grid>
|
||||||
setEnriching((s)=>({ ...s, [`a-${i}`]: st.state || 'idle' }))
|
<Grid item xs={4}>
|
||||||
}, 1800)
|
<TextField
|
||||||
}}
|
size="small"
|
||||||
>{enriching[`a-${i}`]==='running' ? 'Collecte…' : 'Collecter'}</Button>
|
fullWidth
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/address/pdf`} target="_blank" rel="noopener noreferrer">Voir PDF</Button>
|
label="Code postal"
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/address/status`} target="_blank" rel="noopener noreferrer">Voir JSON</Button>
|
value={a.postalCode}
|
||||||
<Button size="small" variant="outlined" disabled={savingKey===`a-${i}`}
|
onChange={(e) => setAddressesDraft((prev) => {
|
||||||
onClick={async ()=>{
|
const c = [...prev]
|
||||||
try{
|
c[i] = { ...c[i], postalCode: e.target.value }
|
||||||
setSavingKey(`a-${i}`)
|
return c
|
||||||
await updateEntity(currentFolderHash!, extraction.fileHash, 'address', { index: i, id: a.id, patch: { street: a.street, city: a.city, postalCode: a.postalCode, country: a.country, description: a.description || '' } })
|
})}
|
||||||
} finally { setSavingKey(null) }
|
/>
|
||||||
}}>Enregistrer</Button>
|
</Grid>
|
||||||
<Button size="small" color="error"
|
<Grid item xs={8}>
|
||||||
onClick={async ()=>{
|
<TextField
|
||||||
await deleteEntity(currentFolderHash!, extraction.fileHash, 'address', { index: i, id: a.id })
|
size="small"
|
||||||
}}>Supprimer</Button>
|
fullWidth
|
||||||
</Box>
|
label="Ville"
|
||||||
</Box>
|
value={a.city}
|
||||||
</ListItem>
|
onChange={(e) => setAddressesDraft((prev) => {
|
||||||
|
const c = [...prev]
|
||||||
|
c[i] = { ...c[i], city: e.target.value }
|
||||||
|
return c
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="Pays"
|
||||||
|
value={a.country}
|
||||||
|
onChange={(e) => setAddressesDraft((prev) => {
|
||||||
|
const c = [...prev]
|
||||||
|
c[i] = { ...c[i], country: e.target.value }
|
||||||
|
return c
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="Description"
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
value={a.description}
|
||||||
|
onChange={(e) => setAddressesDraft((prev) => {
|
||||||
|
const c = [...prev]
|
||||||
|
c[i] = { ...c[i], description: e.target.value }
|
||||||
|
return c
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={enriching[`address-${i}`] === 'running' ? <CircularProgress size={16} /> : <Search />}
|
||||||
|
disabled={enriching[`address-${i}`] === 'running'}
|
||||||
|
onClick={() => handleEnrichment('address', i)}
|
||||||
|
>
|
||||||
|
{enriching[`address-${i}`] === 'running' ? 'Collecte...' : 'Enrichir'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownload />}
|
||||||
|
component="a"
|
||||||
|
href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/address/pdf`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={savingKey === `address-${i}` ? <CircularProgress size={16} /> : <Save />}
|
||||||
|
disabled={savingKey === `address-${i}`}
|
||||||
|
onClick={() => handleSaveEntity('address', i, a)}
|
||||||
|
>
|
||||||
|
Sauver
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() => handleDeleteEntity('address', i, a)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</List>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Entreprises */}
|
{/* Entreprises */}
|
||||||
{companiesDraft.length > 0 && (
|
{companiesDraft.length > 0 && (
|
||||||
<Card sx={{ flex: '1 1 300px' }}>
|
<Grid item xs={12} lg={4}>
|
||||||
<CardContent>
|
<Card sx={{ height: '100%' }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<CardHeader
|
||||||
<Business sx={{ mr: 1, verticalAlign: 'middle' }} />
|
title="Entreprises"
|
||||||
Entreprises ({companiesDraft.length})
|
subheader={`${companiesDraft.length} entité${companiesDraft.length > 1 ? 's' : ''} détectée${companiesDraft.length > 1 ? 's' : ''}`}
|
||||||
</Typography>
|
avatar={<Avatar sx={{ bgcolor: 'info.main' }}><Business /></Avatar>}
|
||||||
<List dense>
|
action={
|
||||||
|
<Badge badgeContent={companiesDraft.length} color="primary">
|
||||||
|
<Business color="action" />
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
{companiesDraft.map((c: any, i: number) => (
|
{companiesDraft.map((c: any, i: number) => (
|
||||||
<ListItem key={`c-${i}`} disableGutters sx={{ py: 0.5 }}>
|
<Paper key={`c-${i}`} sx={{ p: 2, border: '1px solid', borderColor: 'grey.200' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, width: '100%', flexWrap: 'wrap' }}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: '1 1 auto', minWidth: 0, flexWrap: 'wrap' }}>
|
<Grid container spacing={1}>
|
||||||
<input style={{ padding: 4, width: 300 }} placeholder="Raison sociale" value={c.name} onChange={(e)=> setCompaniesDraft((prev)=>{ const x=[...prev]; x[i]={...x[i], name:e.target.value}; return x})} />
|
<Grid item xs={12}>
|
||||||
<input style={{ padding: 4, width: 260 }} placeholder="Description" value={c.description} onChange={(e)=> setCompaniesDraft((prev)=>{ const x=[...prev]; x[i]={...x[i], description:e.target.value}; return x})} />
|
<TextField
|
||||||
</Box>
|
size="small"
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, whiteSpace: 'nowrap' }}>
|
fullWidth
|
||||||
<Button size="small" variant="outlined"
|
label="Raison sociale"
|
||||||
onClick={async ()=>{
|
value={c.name}
|
||||||
setEnriching((s)=>({ ...s, [`c-${i}`]: 'running' }))
|
onChange={(e) => setCompaniesDraft((prev) => {
|
||||||
await startEnrichment(currentFolderHash!, extraction.fileHash, 'company')
|
const x = [...prev]
|
||||||
setTimeout(async ()=>{
|
x[i] = { ...x[i], name: e.target.value }
|
||||||
const st = await getEnrichmentStatus(currentFolderHash!, extraction.fileHash, 'company')
|
return x
|
||||||
setEnriching((s)=>({ ...s, [`c-${i}`]: st.state || 'idle' }))
|
})}
|
||||||
}, 1800)
|
/>
|
||||||
}}
|
</Grid>
|
||||||
>{enriching[`c-${i}`]==='running' ? 'Collecte…' : 'Collecter'}</Button>
|
<Grid item xs={12}>
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/company/pdf`} target="_blank" rel="noopener noreferrer">Voir PDF</Button>
|
<TextField
|
||||||
<Button size="small" component="a" href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/company/status`} target="_blank" rel="noopener noreferrer">Voir JSON</Button>
|
size="small"
|
||||||
<Button size="small" variant="outlined" disabled={savingKey===`c-${i}`}
|
fullWidth
|
||||||
onClick={async ()=>{
|
label="Description"
|
||||||
try{
|
multiline
|
||||||
setSavingKey(`c-${i}`)
|
rows={3}
|
||||||
await updateEntity(currentFolderHash!, extraction.fileHash, 'company', { index: i, id: c.id, patch: { name: c.name, description: c.description || '' } })
|
value={c.description}
|
||||||
} finally { setSavingKey(null) }
|
onChange={(e) => setCompaniesDraft((prev) => {
|
||||||
}}>Enregistrer</Button>
|
const x = [...prev]
|
||||||
<Button size="small" color="error"
|
x[i] = { ...x[i], description: e.target.value }
|
||||||
onClick={async ()=>{
|
return x
|
||||||
await deleteEntity(currentFolderHash!, extraction.fileHash, 'company', { index: i, id: c.id })
|
})}
|
||||||
}}>Supprimer</Button>
|
/>
|
||||||
</Box>
|
</Grid>
|
||||||
</Box>
|
</Grid>
|
||||||
</ListItem>
|
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={enriching[`company-${i}`] === 'running' ? <CircularProgress size={16} /> : <Search />}
|
||||||
|
disabled={enriching[`company-${i}`] === 'running'}
|
||||||
|
onClick={() => handleEnrichment('company', i)}
|
||||||
|
>
|
||||||
|
{enriching[`company-${i}`] === 'running' ? 'Collecte...' : 'Enrichir'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<FileDownload />}
|
||||||
|
component="a"
|
||||||
|
href={`/api/folders/${currentFolderHash}/files/${extraction.fileHash}/enrich/company/pdf`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
PDF
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={savingKey === `company-${i}` ? <CircularProgress size={16} /> : <Save />}
|
||||||
|
disabled={savingKey === `company-${i}`}
|
||||||
|
onClick={() => handleSaveEntity('company', i, c)}
|
||||||
|
>
|
||||||
|
Sauver
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() => handleDeleteEntity('company', i, c)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</List>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Grid>
|
||||||
|
|
||||||
{/* Métadonnées détaillées */}
|
{/* Métadonnées détaillées */}
|
||||||
<Card sx={{ mt: 3 }}>
|
<Card sx={{ mt: 3 }}>
|
||||||
|
<CardHeader
|
||||||
|
title="Métadonnées techniques"
|
||||||
|
avatar={<Avatar sx={{ bgcolor: 'grey.600' }}><Security /></Avatar>}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Métadonnées détaillées
|
|
||||||
</Typography>
|
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
<Typography>Informations de traitement</Typography>
|
<Typography>Informations de traitement</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Grid container spacing={2}>
|
||||||
<Typography variant="body2">
|
<Grid item xs={12} sm={6}>
|
||||||
<strong>Hash du fichier:</strong> {extraction.fileHash}
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Hash du fichier
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>
|
||||||
|
{extraction.fileHash}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Traitement effectué
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<strong>Timestamp:</strong>{' '}
|
|
||||||
{new Date(extraction.status.timestamp).toLocaleString()}
|
{new Date(extraction.status.timestamp).toLocaleString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Confiance globale
|
||||||
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
<strong>Confiance globale:</strong>{' '}
|
|
||||||
{(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}%
|
{(extraction.metadata.quality.globalConfidence * 100).toFixed(1)}%
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Grid>
|
||||||
|
{(extraction.metadata.quality as any).ollamaScore && (
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Score IA (Ollama)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{((extraction.metadata.quality as any).ollamaScore * 100).toFixed(1)}%
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Grid>
|
||||||
</Box>
|
</Grid>
|
||||||
|
|
||||||
|
{/* Snackbar pour les notifications */}
|
||||||
|
<Snackbar
|
||||||
|
open={snackbar.open}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
|
>
|
||||||
|
<MuiAlert
|
||||||
|
onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
|
||||||
|
severity={snackbar.severity}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{snackbar.message}
|
||||||
|
</MuiAlert>
|
||||||
|
</Snackbar>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user