design
This commit is contained in:
parent
0f0a26ed46
commit
5b2a5782be
2
.env
2
.env
@ -4,6 +4,8 @@ VITE_API_URL=http://localhost:18000
|
||||
# Configuration pour le développement
|
||||
VITE_APP_NAME=4NK IA Lecoffre.io
|
||||
VITE_APP_VERSION=0.1.0
|
||||
VITE_USE_RULE_NER=true
|
||||
VITE_LLM_CLASSIFY_ONLY=true
|
||||
|
||||
# Configuration des services externes (optionnel)
|
||||
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
||||
|
||||
@ -79,6 +79,7 @@ VITE_USE_OPENAI=false
|
||||
VITE_OPENAI_API_KEY=
|
||||
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
VITE_OPENAI_MODEL=gpt-4o-mini
|
||||
VITE_USE_RULE_NER=true
|
||||
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>4NK IA - Lecoffre.io</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import React from 'react'
|
||||
import { AppBar, Toolbar, Typography, Container, Box } from '@mui/material'
|
||||
import React, { useEffect } from 'react'
|
||||
import { AppBar, Toolbar, Typography, Container, Box, LinearProgress } from '@mui/material'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { NavigationTabs } from './NavigationTabs'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { extractDocument, analyzeDocument, getContextData, getConseil } from '../store/documentSlice'
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
@ -10,6 +12,24 @@ interface LayoutProps {
|
||||
export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { documents, extractionById, loading, currentDocument, contextResult, conseilResult, analysisResult } = useAppSelector((s) => s.document)
|
||||
|
||||
// Au chargement/nav: lancer OCR+classification pour tous les documents sans résultat
|
||||
useEffect(() => {
|
||||
documents.forEach((doc) => {
|
||||
if (!extractionById[doc.id]) dispatch(extractDocument(doc.id))
|
||||
})
|
||||
}, [documents, extractionById, dispatch])
|
||||
|
||||
// Déclencher contexte et conseil globaux une fois qu'un document courant existe
|
||||
useEffect(() => {
|
||||
if (currentDocument) {
|
||||
if (!analysisResult) dispatch(analyzeDocument(currentDocument.id))
|
||||
if (!contextResult) dispatch(getContextData(currentDocument.id))
|
||||
if (!conseilResult) dispatch(getConseil(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, analysisResult, contextResult, conseilResult, dispatch])
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
@ -28,6 +48,12 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
|
||||
<NavigationTabs currentPath={location.pathname} />
|
||||
|
||||
{loading && (
|
||||
<Box sx={{ px: 2, pt: 1 }}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Container maxWidth="xl" sx={{ mt: 3, mb: 3 }}>
|
||||
{children}
|
||||
</Container>
|
||||
|
||||
@ -18,10 +18,20 @@ async function getPdfJs() {
|
||||
export async function extractTextFromFile(file: File): Promise<string> {
|
||||
const mime = file.type || ''
|
||||
if (mime.includes('pdf') || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
return extractFromPdf(file)
|
||||
const pdfText = await extractFromPdf(file)
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[OCR][PDF]', file.name, 'len=', pdfText.length, 'peek=', pdfText.slice(0, 200))
|
||||
}
|
||||
return pdfText
|
||||
}
|
||||
if (mime.startsWith('image/') || ['.png', '.jpg', '.jpeg'].some((ext) => file.name.toLowerCase().endsWith(ext))) {
|
||||
return extractFromImage(file)
|
||||
const imgText = await extractFromImage(file)
|
||||
if (import.meta.env.DEV) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[OCR][IMG]', file.name, 'len=', imgText.length, 'peek=', imgText.slice(0, 200))
|
||||
}
|
||||
return imgText
|
||||
}
|
||||
// Fallback: lecture texte brut
|
||||
try {
|
||||
@ -40,8 +50,23 @@ async function extractFromPdf(file: File): Promise<string> {
|
||||
const numPages = Math.min(pdf.numPages, 50)
|
||||
for (let i = 1; i <= numPages; i += 1) {
|
||||
const page = await pdf.getPage(i)
|
||||
const content = await page.getTextContent()
|
||||
const pageText = content.items.map((it: any) => (it.str ? it.str : '')).join(' ')
|
||||
const content = await page.getTextContent().catch(() => null)
|
||||
let pageText = ''
|
||||
if (content) {
|
||||
pageText = content.items.map((it: any) => (it.str ? it.str : '')).join(' ')
|
||||
}
|
||||
// Fallback OCR si pas de texte exploitable
|
||||
if (!pageText || pageText.replace(/\s+/g, '').length < 30) {
|
||||
const viewport = page.getViewport({ scale: 2 })
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
const ctx = canvas.getContext('2d') as any
|
||||
await page.render({ canvasContext: ctx, viewport }).promise
|
||||
const blob: Blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b as Blob), 'image/png'))
|
||||
const ocrText = await extractFromImage(new File([blob], `${file.name}-p${i}.png`, { type: 'image/png' }))
|
||||
pageText = ocrText
|
||||
}
|
||||
if (pageText.trim()) texts.push(pageText)
|
||||
}
|
||||
return texts.join('\n')
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
ConseilResult,
|
||||
} from '../types'
|
||||
import { extractTextFromFile } from './fileExtract'
|
||||
import { runRuleNER } from './ruleNer'
|
||||
|
||||
const OPENAI_API_KEY = import.meta.env.VITE_OPENAI_API_KEY
|
||||
const OPENAI_BASE_URL = import.meta.env.VITE_OPENAI_BASE_URL || 'https://api.openai.com/v1'
|
||||
@ -83,28 +84,105 @@ export const openaiDocumentApi = {
|
||||
localText = ''
|
||||
}
|
||||
}
|
||||
// Flags de mode
|
||||
const useRuleNer = import.meta.env.VITE_USE_RULE_NER === 'true'
|
||||
const classifyOnly = import.meta.env.VITE_LLM_CLASSIFY_ONLY === 'true'
|
||||
|
||||
// Si NER local actif, on l'utilise pour tout (identités/adresses/...) puis, si demandé,
|
||||
// on peut consulter le LLM uniquement pour classifier le type de document
|
||||
if (useRuleNer) {
|
||||
let res = runRuleNER(documentId, localText)
|
||||
if (classifyOnly && OPENAI_API_KEY && localText) {
|
||||
try {
|
||||
hooks?.onLlmProgress?.(0)
|
||||
const cls = await callOpenAIChat([
|
||||
{ role: 'system', content: 'Tu es un classifieur. Retourne uniquement un JSON strict.' },
|
||||
{ role: 'user', content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}` },
|
||||
])
|
||||
const parsed = JSON.parse(cls)
|
||||
if (parsed && typeof parsed.documentType === 'string') {
|
||||
res = { ...res, documentType: parsed.documentType }
|
||||
res.confidenceReasons = [...(res.confidenceReasons || []), 'Classification LLM limitée au documentType']
|
||||
}
|
||||
hooks?.onLlmProgress?.(1)
|
||||
} catch {
|
||||
// ignore échec de classification
|
||||
hooks?.onLlmProgress?.(1)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
hooks?.onLlmProgress?.(0)
|
||||
// Si on demande uniquement la classification par LLM, ne demander que le type;
|
||||
// sinon on demande la structuration complète (mode précédent)
|
||||
if (classifyOnly) {
|
||||
try {
|
||||
const cls = await callOpenAIChat([
|
||||
{ role: 'system', content: 'Tu es un classifieur. Retourne uniquement un JSON strict.' },
|
||||
{ role: 'user', content: `Classifie ce texte en une des catégories suivantes: [CNI, Facture, Attestation, Document]. Réponds strictement sous la forme {"documentType":"..."}.\nTexte:\n${localText.slice(0, 8000)}` },
|
||||
])
|
||||
const parsed = JSON.parse(cls)
|
||||
hooks?.onLlmProgress?.(1)
|
||||
return {
|
||||
documentId,
|
||||
text: localText || '',
|
||||
language: 'fr',
|
||||
documentType: (parsed && parsed.documentType) || 'Document',
|
||||
identities: [],
|
||||
addresses: [],
|
||||
properties: [],
|
||||
contracts: [],
|
||||
signatures: [],
|
||||
confidence: 0.6,
|
||||
confidenceReasons: ['Classification LLM sans contexte, pas d\'extraction d\'identités'],
|
||||
}
|
||||
} catch {
|
||||
hooks?.onLlmProgress?.(1)
|
||||
return {
|
||||
documentId,
|
||||
text: localText || '',
|
||||
language: 'fr',
|
||||
documentType: 'Document',
|
||||
identities: [],
|
||||
addresses: [],
|
||||
properties: [],
|
||||
contracts: [],
|
||||
signatures: [],
|
||||
confidence: 0.6,
|
||||
confidenceReasons: ['Classification LLM échouée, valeur par défaut'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = await callOpenAIChat([
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'Tu es un assistant qui extrait des informations structurées (identités, adresses, biens, contrats) à partir de documents. Réponds en JSON strict, sans texte autour.',
|
||||
'Tu extrais uniquement les informations présentes dans le texte OCR. Interdiction d\'inventer. Interdiction d\'utiliser le nom du fichier comme identité. Réponds en JSON strict, sans texte autour.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Document ID: ${documentId}. Texte: ${localText.slice(0, 8000)}\nRetourne un JSON avec la forme suivante: {"language":"fr","documentType":"...","identities":[{"id":"id-1","type":"person","firstName":"...","lastName":"...","confidence":0.9}],"addresses":[{"street":"...","city":"...","postalCode":"...","country":"..."}],"properties":[{"id":"prop-1","type":"apartment","address":{"street":"...","city":"...","postalCode":"...","country":"..."},"surface":75}],"contracts":[{"id":"contract-1","type":"sale","parties":[],"amount":0,"date":"YYYY-MM-DD","clauses":["..."]}],"signatures":[],"confidence":0.7,"confidenceReasons":["..."]}`,
|
||||
content: `Document ID: ${documentId}. Texte OCR (tronqué): ${localText.slice(0, 8000)}\nRègles: 1) ne pas inventer, 2) si incertitude, laisser vide, 3) ne JAMAIS utiliser le nom du fichier comme identité. Schéma JSON: {"language":"fr","documentType":"...","identities":[{"id":"id-1","type":"person","firstName":"...","lastName":"...","confidence":0.9}],"addresses":[{"street":"...","city":"...","postalCode":"...","country":"..."}],"properties":[{"id":"prop-1","type":"apartment","address":{"street":"...","city":"...","postalCode":"...","country":"..."},"surface":75}],"contracts":[{"id":"contract-1","type":"sale","parties":[],"amount":0,"date":"YYYY-MM-DD","clauses":["..."]}],"signatures":[],"confidence":0.7,"confidenceReasons":["sources présentes dans le texte"]}`,
|
||||
},
|
||||
])
|
||||
// Essaye d'analyser le JSON, sinon fallback heuristique
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
hooks?.onLlmProgress?.(1)
|
||||
// Post-traitement: filtrage des identités qui ressemblent au nom de fichier
|
||||
const docBase = (file?.name || '').toLowerCase().replace(/\.[a-z0-9]+$/, '')
|
||||
const safeIdentities = (parsed.identities || []).filter((it: any) => {
|
||||
const full = `${it.firstName || ''} ${it.lastName || ''}`.trim().toLowerCase()
|
||||
return full && !docBase || (full && !docBase.includes(full) && !full.includes(docBase))
|
||||
})
|
||||
|
||||
return {
|
||||
documentId,
|
||||
text: localText || '',
|
||||
language: parsed.language || 'fr',
|
||||
documentType: parsed.documentType || 'Document',
|
||||
identities: parsed.identities || [],
|
||||
identities: safeIdentities,
|
||||
addresses: parsed.addresses || [],
|
||||
properties: parsed.properties || [],
|
||||
contracts: parsed.contracts || [],
|
||||
|
||||
140
src/services/ruleNer.ts
Normal file
140
src/services/ruleNer.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type { ExtractionResult, Identity, Address, Property, Contract } from '../types'
|
||||
|
||||
function toTitleCase(input: string): string {
|
||||
return input
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function extractMRZ(text: string): { firstName?: string; lastName?: string } | null {
|
||||
// Cherche MRZ (deux lignes, < comme séparateur). Stricte A-Z0-9<
|
||||
const lines = text.split(/\n|\r/).map((l) => l.trim().toUpperCase())
|
||||
for (let i = 0; i < lines.length - 1; i += 1) {
|
||||
const a = lines[i].replace(/[^A-Z0-9<]/g, '')
|
||||
const b = lines[i + 1].replace(/[^A-Z0-9<]/g, '')
|
||||
if (a.includes('<<') || b.includes('<<')) {
|
||||
const target = a.length >= b.length ? a : b
|
||||
const parts = target.split('<<')
|
||||
if (parts.length >= 2) {
|
||||
const rawLast = parts[0].replace(/<+/g, ' ').trim()
|
||||
const rawFirst = parts[1].replace(/<+/g, ' ').trim()
|
||||
if (rawLast && rawFirst) return { firstName: toTitleCase(rawFirst), lastName: rawLast.replace(/\s+/g, ' ') }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function extractDates(text: string): string[] {
|
||||
const results = new Set<string>()
|
||||
const patterns = [
|
||||
/(\b\d{2}[\/\-]\d{2}[\/\-]\d{4}\b)/g, // JJ/MM/AAAA ou JJ-MM-AAAA
|
||||
/(\b\d{4}[\/\-]\d{2}[\/\-]\d{2}\b)/g, // AAAA/MM/JJ
|
||||
]
|
||||
for (const re of patterns) {
|
||||
for (const m of text.matchAll(re)) results.add(m[1])
|
||||
}
|
||||
return Array.from(results)
|
||||
}
|
||||
|
||||
function extractCniNumbers(text: string): string[] {
|
||||
const results = new Set<string>()
|
||||
const re = /\b[A-Z0-9]{12,15}\b/g
|
||||
for (const m of text.toUpperCase().matchAll(re)) results.add(m[0])
|
||||
return Array.from(results)
|
||||
}
|
||||
|
||||
function extractAddresses(text: string): Address[] {
|
||||
const items: Address[] = []
|
||||
const typeVoie = '(rue|avenue|av\.?|bd\.?|boulevard|impasse|chemin|all(é|e)e|route|place|quai|passage|square|voie|faubourg|fg\.?|cours|sentier|residence|résidence)'
|
||||
const re = new RegExp(`(\\b\\d{1,4})\\s+([A-Za-zÀ-ÖØ-öø-ÿ\\-']{2,})\\s+${typeVoie}\\s+([A-Za-zÀ-ÖØ-öø-ÿ\\-']{2,})(?:\\s+|,)+(\\b\\d{5}\\b)\\s+([A-Za-zÀ-ÖØ-öø-ÿ\\-']{2,})`, 'gi')
|
||||
for (const m of text.matchAll(re)) {
|
||||
const street = `${m[1]} ${toTitleCase(`${m[2]} ${m[3]} ${m[4]}`)}`.trim()
|
||||
const postalCode = m[5]
|
||||
const city = toTitleCase(m[6])
|
||||
items.push({ street, city, postalCode, country: 'France' })
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
function extractNames(text: string): Identity[] {
|
||||
const identities: Identity[] = []
|
||||
// Heuristique: lignes en MAJUSCULES pour NOM; prénoms capitalisés à proximité
|
||||
const lines = text.split(/\n|\r/).map((l) => l.trim()).filter(Boolean)
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i]
|
||||
if (/^[A-ZÀ-ÖØ-Þ\-\s]{3,}$/.test(line) && line.length <= 40) {
|
||||
const lastName = line.replace(/\s+/g, ' ').trim()
|
||||
// Cherche un prénom sur la ligne suivante ou la même ligne
|
||||
const cand = (lines[i + 1] || '').trim()
|
||||
const firstNameMatch = cand.match(/^[A-Z][a-zà-öø-ÿ'\-]{1,}(?:\s+[A-Z][a-zà-öø-ÿ'\-]{1,})?$/)
|
||||
const firstName = firstNameMatch ? cand : undefined
|
||||
if (lastName && (!firstName || firstName.length <= 40)) {
|
||||
identities.push({
|
||||
id: `id-${i}`,
|
||||
type: 'person',
|
||||
firstName: firstName ? toTitleCase(firstName) : undefined,
|
||||
lastName,
|
||||
confidence: firstName ? 0.85 : 0.7,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
||||
export function runRuleNER(documentId: string, text: string): ExtractionResult {
|
||||
const identitiesFromMRZ = extractMRZ(text)
|
||||
const identities = identitiesFromMRZ
|
||||
? [
|
||||
{
|
||||
id: 'mrz-1',
|
||||
type: 'person',
|
||||
firstName: identitiesFromMRZ.firstName,
|
||||
lastName: identitiesFromMRZ.lastName!,
|
||||
confidence: 0.9,
|
||||
} as Identity,
|
||||
]
|
||||
: extractNames(text)
|
||||
|
||||
const addresses = extractAddresses(text)
|
||||
const cniNumbers = extractCniNumbers(text)
|
||||
const dates = extractDates(text)
|
||||
|
||||
const contracts: Contract[] = []
|
||||
const properties: Property[] = []
|
||||
|
||||
const reasons: string[] = []
|
||||
if (identities.length) reasons.push('Identités détectées par règles')
|
||||
if (addresses.length) reasons.push('Adresse(s) détectée(s) par motifs')
|
||||
if (cniNumbers.length) reasons.push('Numéro CNI plausible détecté')
|
||||
if (dates.length) reasons.push('Dates détectées')
|
||||
|
||||
let documentType = 'Document'
|
||||
if (/carte\s+nationale\s+d'identité|cni|mrz|identite/i.test(text)) documentType = 'CNI'
|
||||
else if (/facture|tva|siren|montant/i.test(text)) documentType = 'Facture'
|
||||
else if (/attestation|certificat/i.test(text)) documentType = 'Attestation'
|
||||
|
||||
// Confiance: base 0.6 + bonus par signal
|
||||
let confidence = 0.6
|
||||
if (identities.length) confidence += 0.15
|
||||
if (cniNumbers.length) confidence += 0.15
|
||||
if (addresses.length) confidence += 0.05
|
||||
confidence = Math.max(0, Math.min(1, confidence))
|
||||
|
||||
return {
|
||||
documentId,
|
||||
text,
|
||||
language: 'fr',
|
||||
documentType,
|
||||
identities,
|
||||
addresses,
|
||||
properties,
|
||||
contracts,
|
||||
signatures: [],
|
||||
confidence,
|
||||
confidenceReasons: reasons,
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ interface DocumentState {
|
||||
currentDocument: Document | null
|
||||
extractionResult: ExtractionResult | null
|
||||
extractionById: Record<string, ExtractionResult>
|
||||
fileById: Record<string, File>
|
||||
analysisResult: AnalysisResult | null
|
||||
contextResult: ContextResult | null
|
||||
conseilResult: ConseilResult | null
|
||||
@ -22,6 +23,7 @@ const initialState: DocumentState = {
|
||||
currentDocument: null,
|
||||
extractionResult: null,
|
||||
extractionById: {},
|
||||
fileById: {},
|
||||
analysisResult: null,
|
||||
contextResult: null,
|
||||
conseilResult: null,
|
||||
@ -148,6 +150,11 @@ const documentSlice = createSlice({
|
||||
state.loading = false
|
||||
state.documents.push(action.payload)
|
||||
state.currentDocument = action.payload
|
||||
// Capture le File depuis l'URL blob si disponible
|
||||
if (action.payload.previewUrl?.startsWith('blob:')) {
|
||||
// On ne peut pas récupérer l'objet File initial ici sans passer par onDrop;
|
||||
// il est reconstruit lors de l'extraction via fetch blob.
|
||||
}
|
||||
})
|
||||
.addCase(uploadDocument.rejected, (state, action) => {
|
||||
state.loading = false
|
||||
|
||||
@ -24,20 +24,19 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import type { ChipProps, LinearProgressProps } from '@mui/material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { analyzeDocument } from '../store/documentSlice'
|
||||
import { analyzeDocument, getConseil, getContextData } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function AnalyseView() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentDocument, analysisResult, loading } = useAppSelector(
|
||||
(state) => state.document
|
||||
)
|
||||
const { currentDocument, analysisResult, loading, conseilResult, contextResult } = useAppSelector((state) => state.document)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument && !analysisResult) {
|
||||
dispatch(analyzeDocument(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, analysisResult, dispatch])
|
||||
if (!currentDocument) return
|
||||
if (!analysisResult) dispatch(analyzeDocument(currentDocument.id))
|
||||
if (!conseilResult) dispatch(getConseil(currentDocument.id))
|
||||
if (!contextResult) dispatch(getContextData(currentDocument.id))
|
||||
}, [currentDocument, analysisResult, conseilResult, contextResult, dispatch])
|
||||
|
||||
if (!currentDocument) {
|
||||
return (
|
||||
@ -97,16 +96,10 @@ export default function AnalyseView() {
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Chip
|
||||
icon={<Assessment />}
|
||||
label={`Score de vraisemblance: ${(analysisResult.credibilityScore * 100).toFixed(1)}%`}
|
||||
label={`Avancement: ${Math.round(analysisResult.credibilityScore * 100)}%`}
|
||||
color={getScoreColor(analysisResult.credibilityScore)}
|
||||
variant="filled"
|
||||
/>
|
||||
<Chip
|
||||
icon={<Info />}
|
||||
label={`Type: ${analysisResult.documentType}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
{analysisResult.isCNI && (
|
||||
<Chip
|
||||
icon={<Flag />}
|
||||
|
||||
@ -68,7 +68,9 @@ export default function ExtractionView() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!extractionResult) {
|
||||
const activeResult = currentDocument ? (extractionById[currentDocument.id] || extractionResult) : extractionResult
|
||||
|
||||
if (!activeResult) {
|
||||
return (
|
||||
<Layout>
|
||||
<Alert severity="warning">
|
||||
@ -113,28 +115,28 @@ export default function ExtractionView() {
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
icon={<Language />}
|
||||
label={`Langue: ${ (extractionById[currentDocument!.id] || extractionResult)!.language }`}
|
||||
label={`Langue: ${ activeResult.language }`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<Description />}
|
||||
label={`Type: ${ (extractionById[currentDocument!.id] || extractionResult)!.documentType }`}
|
||||
label={`Type: ${ activeResult.documentType }`}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Tooltip
|
||||
arrow
|
||||
title={
|
||||
(() => { const r = (extractionById[currentDocument!.id] || extractionResult)!; return (r.confidenceReasons && r.confidenceReasons.length > 0)
|
||||
? r.confidenceReasons.join(' • ')
|
||||
: `Évaluation automatique basée sur le contenu et le type (${r.documentType}).` })()
|
||||
(activeResult.confidenceReasons && activeResult.confidenceReasons.length > 0)
|
||||
? activeResult.confidenceReasons.join(' • ')
|
||||
: `Évaluation automatique basée sur le contenu et le type (${activeResult.documentType}).`
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
icon={<Verified />}
|
||||
label={`Confiance: ${(() => { const r = (extractionById[currentDocument!.id] || extractionResult)!; return Math.round(r.confidence * 100)})()}%`}
|
||||
color={(() => { const r = (extractionById[currentDocument!.id] || extractionResult)!; return r.confidence > 0.8 ? 'success' : 'warning' })()}
|
||||
label={`Confiance: ${Math.round(activeResult.confidence * 100)}%`}
|
||||
color={activeResult.confidence > 0.8 ? 'success' : 'warning'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Tooltip>
|
||||
@ -210,10 +212,10 @@ export default function ExtractionView() {
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Person sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Identités ({extractionResult.identities?.length || 0})
|
||||
Identités ({activeResult.identities?.length || 0})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{(extractionResult.identities || []).map((identity, index) => (
|
||||
{(activeResult.identities || []).map((identity, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={
|
||||
@ -256,10 +258,10 @@ export default function ExtractionView() {
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Adresses ({extractionResult.addresses?.length || 0})
|
||||
Adresses ({activeResult.addresses?.length || 0})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{(extractionResult.addresses || []).map((address, index) => (
|
||||
{(activeResult.addresses || []).map((address, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${address.street}, ${address.city}`}
|
||||
@ -280,10 +282,10 @@ export default function ExtractionView() {
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Home sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Biens ({extractionResult.properties?.length || 0})
|
||||
Biens ({activeResult.properties?.length || 0})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{(extractionResult.properties || []).map((property, index) => (
|
||||
{(activeResult.properties || []).map((property, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${property.type} - ${property.address.city}`}
|
||||
@ -319,10 +321,10 @@ export default function ExtractionView() {
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Description sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Contrats ({extractionResult.contracts?.length || 0})
|
||||
Contrats ({activeResult.contracts?.length || 0})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{(extractionResult.contracts || []).map((contract, index) => (
|
||||
{(activeResult.contracts || []).map((contract, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${contract.type} - ${contract.amount ? `${contract.amount}€` : 'Montant non spécifié'}`}
|
||||
@ -355,10 +357,10 @@ export default function ExtractionView() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Signatures détectées ({extractionResult.signatures?.length || 0})
|
||||
Signatures détectées ({activeResult.signatures?.length || 0})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{(extractionResult.signatures || []).map((signature: any, index: number) => {
|
||||
{(activeResult.signatures || []).map((signature: any, index: number) => {
|
||||
const label = typeof signature === 'string'
|
||||
? signature
|
||||
: signature?.name || signature?.title || signature?.date || JSON.stringify(signature)
|
||||
@ -387,7 +389,7 @@ export default function ExtractionView() {
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{extractionResult.text}
|
||||
{activeResult.text}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</CardContent>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user