feat: complete UI implementation with Material-UI and full functionality
- Add complete TypeScript types for all document entities - Implement Redux store with document slice and async thunks - Create comprehensive API services layer with external integrations - Build complete UI views with Material-UI components: * UploadView: drag&drop with file preview and status * ExtractionView: structured data display (identities, addresses, properties, contracts) * AnalyseView: CNI verification, credibility scoring, recommendations * ContexteView: external data sources (Cadastre, Géorisques, BODACC, etc.) * ConseilView: LLM analysis with risks and next steps - Add Layout component with navigation tabs - Configure environment variables for backend integration - Fix all TypeScript compilation errors - Replace Grid with Box for better compatibility - Add comprehensive error handling and loading states
This commit is contained in:
parent
c2eba34f57
commit
0bb4ea6678
13
.env
Normal file
13
.env
Normal file
@ -0,0 +1,13 @@
|
||||
# Configuration API Backend
|
||||
VITE_API_URL=http://localhost:8000
|
||||
|
||||
# Configuration pour le développement
|
||||
VITE_APP_NAME=4NK IA Front Notarial
|
||||
VITE_APP_VERSION=0.1.0
|
||||
|
||||
# Configuration des services externes (optionnel)
|
||||
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
||||
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
||||
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
13
.env.exemple
Normal file
13
.env.exemple
Normal file
@ -0,0 +1,13 @@
|
||||
# Configuration API Backend
|
||||
VITE_API_URL=http://localhost:8000
|
||||
|
||||
# Configuration pour le développement
|
||||
VITE_APP_NAME=4NK IA Front Notarial
|
||||
VITE_APP_VERSION=0.1.0
|
||||
|
||||
# Configuration des services externes (optionnel)
|
||||
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
||||
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
||||
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
785
package-lock.json
generated
785
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,10 +15,15 @@
|
||||
"test:ui": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"axios": "^1.11.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.8.2"
|
||||
},
|
||||
|
36
src/components/Layout.tsx
Normal file
36
src/components/Layout.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { AppBar, Toolbar, Typography, Container, Box } from '@mui/material'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { NavigationTabs } from './NavigationTabs'
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, cursor: 'pointer' }}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
4NK IA - Front Notarial
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<NavigationTabs currentPath={location.pathname} />
|
||||
|
||||
<Container maxWidth="xl" sx={{ mt: 3, mb: 3 }}>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
41
src/components/NavigationTabs.tsx
Normal file
41
src/components/NavigationTabs.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { Tabs, Tab, Box } from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
interface NavigationTabsProps {
|
||||
currentPath: string
|
||||
}
|
||||
|
||||
export const NavigationTabs: React.FC<NavigationTabsProps> = ({ currentPath }) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const tabs = [
|
||||
{ label: 'Téléversement', path: '/' },
|
||||
{ label: 'Extraction', path: '/extraction' },
|
||||
{ label: 'Analyse', path: '/analyse' },
|
||||
{ label: 'Contexte', path: '/contexte' },
|
||||
{ label: 'Conseil', path: '/conseil' },
|
||||
]
|
||||
|
||||
const currentTabIndex = tabs.findIndex(tab => tab.path === currentPath)
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
navigate(tabs[newValue].path)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={currentTabIndex >= 0 ? currentTabIndex : 0}
|
||||
onChange={handleTabChange}
|
||||
aria-label="navigation tabs"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab key={index} label={tab.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||
import { Box, CircularProgress, Typography } from '@mui/material'
|
||||
|
||||
const UploadView = lazy(() => import('../views/UploadView'))
|
||||
const ExtractionView = lazy(() => import('../views/ExtractionView'))
|
||||
@ -7,12 +8,19 @@ const AnalyseView = lazy(() => import('../views/AnalyseView'))
|
||||
const ContexteView = lazy(() => import('../views/ContexteView'))
|
||||
const ConseilView = lazy(() => import('../views/ConseilView'))
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Chargement...</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: '/', element: <Suspense fallback={<div>Chargement…</div>}><UploadView /></Suspense> },
|
||||
{ path: '/extraction', element: <Suspense fallback={<div>Chargement…</div>}><ExtractionView /></Suspense> },
|
||||
{ path: '/analyse', element: <Suspense fallback={<div>Chargement…</div>}><AnalyseView /></Suspense> },
|
||||
{ path: '/contexte', element: <Suspense fallback={<div>Chargement…</div>}><ContexteView /></Suspense> },
|
||||
{ path: '/conseil', element: <Suspense fallback={<div>Chargement…</div>}><ConseilView /></Suspense> },
|
||||
{ path: '/', element: <Suspense fallback={<LoadingFallback />}><UploadView /></Suspense> },
|
||||
{ path: '/extraction', element: <Suspense fallback={<LoadingFallback />}><ExtractionView /></Suspense> },
|
||||
{ path: '/analyse', element: <Suspense fallback={<LoadingFallback />}><AnalyseView /></Suspense> },
|
||||
{ path: '/contexte', element: <Suspense fallback={<LoadingFallback />}><ContexteView /></Suspense> },
|
||||
{ path: '/conseil', element: <Suspense fallback={<LoadingFallback />}><ConseilView /></Suspense> },
|
||||
])
|
||||
|
||||
export const AppRouter = () => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import type { Document, ExtractionResult, AnalysisResult, ContextResult, ConseilResult } from '../types'
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
|
||||
@ -7,13 +8,101 @@ export const apiClient = axios.create({
|
||||
timeout: 60000,
|
||||
})
|
||||
|
||||
export type DetectionResult = { type: string; confidence: number }
|
||||
// Intercepteur pour les erreurs
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const detectDocumentType = async (file: File) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const { data } = await apiClient.post<DetectionResult>('/api/ocr/detect', form, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return data
|
||||
// Services API pour les documents
|
||||
export const documentApi = {
|
||||
// Téléversement de document
|
||||
upload: async (file: File): Promise<Document> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const { data } = await apiClient.post<Document>('/api/documents/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
// Extraction des données
|
||||
extract: async (documentId: string): Promise<ExtractionResult> => {
|
||||
const { data } = await apiClient.get<ExtractionResult>(`/api/documents/${documentId}/extract`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Analyse du document
|
||||
analyze: async (documentId: string): Promise<AnalysisResult> => {
|
||||
const { data } = await apiClient.get<AnalysisResult>(`/api/documents/${documentId}/analyze`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Données contextuelles
|
||||
getContext: async (documentId: string): Promise<ContextResult> => {
|
||||
const { data } = await apiClient.get<ContextResult>(`/api/documents/${documentId}/context`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Conseil LLM
|
||||
getConseil: async (documentId: string): Promise<ConseilResult> => {
|
||||
const { data } = await apiClient.get<ConseilResult>(`/api/documents/${documentId}/conseil`)
|
||||
return data
|
||||
},
|
||||
|
||||
// Détection du type de document
|
||||
detectType: async (file: File): Promise<{ type: string; confidence: number }> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const { data } = await apiClient.post('/api/ocr/detect', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
||||
// Services API pour les données externes
|
||||
export const externalApi = {
|
||||
// Cadastre
|
||||
cadastre: async (address: string) => {
|
||||
const cadastreUrl = import.meta.env.VITE_CADASTRE_API_URL
|
||||
if (!cadastreUrl) throw new Error('Cadastre API URL not configured')
|
||||
const { data } = await axios.get(`${cadastreUrl}/parcelle`, { params: { q: address } })
|
||||
return data
|
||||
},
|
||||
|
||||
// Géorisques
|
||||
georisques: async (coordinates: { lat: number; lng: number }) => {
|
||||
const georisquesUrl = import.meta.env.VITE_GEORISQUES_API_URL
|
||||
if (!georisquesUrl) throw new Error('Géorisques API URL not configured')
|
||||
const { data } = await axios.get(`${georisquesUrl}/risques`, { params: coordinates })
|
||||
return data
|
||||
},
|
||||
|
||||
// Géofoncier
|
||||
geofoncier: async (address: string) => {
|
||||
const geofoncierUrl = import.meta.env.VITE_GEOFONCIER_API_URL
|
||||
if (!geofoncierUrl) throw new Error('Géofoncier API URL not configured')
|
||||
const { data } = await axios.get(`${geofoncierUrl}/dossiers`, { params: { address } })
|
||||
return data
|
||||
},
|
||||
|
||||
// BODACC
|
||||
bodacc: async (companyName: string) => {
|
||||
const bodaccUrl = import.meta.env.VITE_BODACC_API_URL
|
||||
if (!bodaccUrl) throw new Error('BODACC API URL not configured')
|
||||
const { data } = await axios.get(`${bodaccUrl}/annonces`, { params: { q: companyName } })
|
||||
return data
|
||||
},
|
||||
|
||||
// Infogreffe
|
||||
infogreffe: async (siren: string) => {
|
||||
const infogreffeUrl = import.meta.env.VITE_INFOGREFFE_API_URL
|
||||
if (!infogreffeUrl) throw new Error('Infogreffe API URL not configured')
|
||||
const { data } = await axios.get(`${infogreffeUrl}/infogreffe/rcs/extrait`, { params: { siren } })
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
108
src/store/documentSlice.ts
Normal file
108
src/store/documentSlice.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { Document, ExtractionResult, AnalysisResult, ContextResult, ConseilResult } from '../types'
|
||||
import { documentApi } from '../services/api'
|
||||
|
||||
interface DocumentState {
|
||||
documents: Document[]
|
||||
currentDocument: Document | null
|
||||
extractionResult: ExtractionResult | null
|
||||
analysisResult: AnalysisResult | null
|
||||
contextResult: ContextResult | null
|
||||
conseilResult: ConseilResult | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const initialState: DocumentState = {
|
||||
documents: [],
|
||||
currentDocument: null,
|
||||
extractionResult: null,
|
||||
analysisResult: null,
|
||||
contextResult: null,
|
||||
conseilResult: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
export const uploadDocument = createAsyncThunk(
|
||||
'document/upload',
|
||||
async (file: File) => {
|
||||
return await documentApi.upload(file)
|
||||
}
|
||||
)
|
||||
|
||||
export const extractDocument = createAsyncThunk(
|
||||
'document/extract',
|
||||
async (documentId: string) => {
|
||||
return await documentApi.extract(documentId)
|
||||
}
|
||||
)
|
||||
|
||||
export const analyzeDocument = createAsyncThunk(
|
||||
'document/analyze',
|
||||
async (documentId: string) => {
|
||||
return await documentApi.analyze(documentId)
|
||||
}
|
||||
)
|
||||
|
||||
export const getContextData = createAsyncThunk(
|
||||
'document/context',
|
||||
async (documentId: string) => {
|
||||
return await documentApi.getContext(documentId)
|
||||
}
|
||||
)
|
||||
|
||||
export const getConseil = createAsyncThunk(
|
||||
'document/conseil',
|
||||
async (documentId: string) => {
|
||||
return await documentApi.getConseil(documentId)
|
||||
}
|
||||
)
|
||||
|
||||
const documentSlice = createSlice({
|
||||
name: 'document',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentDocument: (state, action: PayloadAction<Document | null>) => {
|
||||
state.currentDocument = action.payload
|
||||
},
|
||||
clearResults: (state) => {
|
||||
state.extractionResult = null
|
||||
state.analysisResult = null
|
||||
state.contextResult = null
|
||||
state.conseilResult = null
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(uploadDocument.pending, (state) => {
|
||||
state.loading = true
|
||||
state.error = null
|
||||
})
|
||||
.addCase(uploadDocument.fulfilled, (state, action) => {
|
||||
state.loading = false
|
||||
state.documents.push(action.payload)
|
||||
state.currentDocument = action.payload
|
||||
})
|
||||
.addCase(uploadDocument.rejected, (state, action) => {
|
||||
state.loading = false
|
||||
state.error = action.error.message || 'Erreur lors du téléversement'
|
||||
})
|
||||
.addCase(extractDocument.fulfilled, (state, action) => {
|
||||
state.extractionResult = action.payload
|
||||
})
|
||||
.addCase(analyzeDocument.fulfilled, (state, action) => {
|
||||
state.analysisResult = action.payload
|
||||
})
|
||||
.addCase(getContextData.fulfilled, (state, action) => {
|
||||
state.contextResult = action.payload
|
||||
})
|
||||
.addCase(getConseil.fulfilled, (state, action) => {
|
||||
state.conseilResult = action.payload
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { setCurrentDocument, clearResults } = documentSlice.actions
|
||||
export const documentReducer = documentSlice.reducer
|
@ -2,11 +2,12 @@ import { configureStore } from '@reduxjs/toolkit'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { TypedUseSelectorHook } from 'react-redux'
|
||||
import { appReducer } from './appSlice'
|
||||
import { documentReducer } from './documentSlice'
|
||||
|
||||
// Root placeholder slice will be added later as features grow
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
app: appReducer,
|
||||
document: documentReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
||||
serializableCheck: false,
|
||||
|
94
src/types/index.ts
Normal file
94
src/types/index.ts
Normal file
@ -0,0 +1,94 @@
|
||||
export interface Document {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
uploadDate: Date
|
||||
status: 'uploading' | 'processing' | 'completed' | 'error'
|
||||
}
|
||||
|
||||
export interface Identity {
|
||||
id: string
|
||||
type: 'person' | 'company'
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
companyName?: string
|
||||
birthDate?: string
|
||||
nationality?: string
|
||||
address?: Address
|
||||
confidence: number
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
street: string
|
||||
city: string
|
||||
postalCode: string
|
||||
country: string
|
||||
coordinates?: { lat: number; lng: number }
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
id: string
|
||||
type: 'house' | 'apartment' | 'land' | 'commercial'
|
||||
address: Address
|
||||
surface?: number
|
||||
cadastralReference?: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export interface Contract {
|
||||
id: string
|
||||
type: 'sale' | 'rent' | 'inheritance' | 'donation'
|
||||
parties: Identity[]
|
||||
property?: Property
|
||||
amount?: number
|
||||
date?: string
|
||||
clauses: string[]
|
||||
}
|
||||
|
||||
export interface ExtractionResult {
|
||||
documentId: string
|
||||
text: string
|
||||
language: string
|
||||
documentType: string
|
||||
identities: Identity[]
|
||||
addresses: Address[]
|
||||
properties: Property[]
|
||||
contracts: Contract[]
|
||||
signatures: string[]
|
||||
confidence: number
|
||||
}
|
||||
|
||||
export interface AnalysisResult {
|
||||
documentId: string
|
||||
documentType: string
|
||||
isCNI: boolean
|
||||
country?: string
|
||||
verificationResult?: {
|
||||
numberValid: boolean
|
||||
formatValid: boolean
|
||||
checksumValid: boolean
|
||||
}
|
||||
credibilityScore: number
|
||||
summary: string
|
||||
recommendations: string[]
|
||||
}
|
||||
|
||||
export interface ContextResult {
|
||||
documentId: string
|
||||
cadastreData?: any
|
||||
georisquesData?: any
|
||||
geofoncierData?: any
|
||||
bodaccData?: any
|
||||
infogreffeData?: any
|
||||
lastUpdated: Date
|
||||
}
|
||||
|
||||
export interface ConseilResult {
|
||||
documentId: string
|
||||
analysis: string
|
||||
recommendations: string[]
|
||||
risks: string[]
|
||||
nextSteps: string[]
|
||||
generatedAt: Date
|
||||
}
|
@ -1,8 +1,254 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Alert,
|
||||
LinearProgress,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
CheckCircle,
|
||||
Error,
|
||||
Warning,
|
||||
Flag,
|
||||
Security,
|
||||
Assessment,
|
||||
Info,
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { analyzeDocument } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function AnalyseView() {
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1>Analyse</h1>
|
||||
<p>Cas CNI: pays, vérification numéro, score de vraisemblance, avis. Autres: synthèse et score.</p>
|
||||
</div>
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentDocument, analysisResult, loading } = useAppSelector(
|
||||
(state) => state.document
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument && !analysisResult) {
|
||||
dispatch(analyzeDocument(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, analysisResult, dispatch])
|
||||
|
||||
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', flexDirection: 'column', alignItems: 'center', mt: 4 }}>
|
||||
<LinearProgress sx={{ width: '100%', mb: 2 }} />
|
||||
<Typography>Analyse en cours...</Typography>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!analysisResult) {
|
||||
return (
|
||||
<Layout>
|
||||
<Alert severity="warning">
|
||||
Aucun résultat d'analyse disponible.
|
||||
</Alert>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 0.8) return 'success'
|
||||
if (score >= 0.6) return 'warning'
|
||||
return 'error'
|
||||
}
|
||||
|
||||
const getScoreIcon = (score: number) => {
|
||||
if (score >= 0.8) return <CheckCircle color="success" />
|
||||
if (score >= 0.6) return <Warning color="warning" />
|
||||
return <Error color="error" />
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Analyse du document
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Résumé général */}
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Résumé de l'analyse
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Chip
|
||||
icon={<Assessment />}
|
||||
label={`Score de vraisemblance: ${(analysisResult.credibilityScore * 100).toFixed(1)}%`}
|
||||
color={getScoreColor(analysisResult.credibilityScore) as any}
|
||||
variant="filled"
|
||||
/>
|
||||
<Chip
|
||||
icon={<Info />}
|
||||
label={`Type: ${analysisResult.documentType}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
{analysisResult.isCNI && (
|
||||
<Chip
|
||||
icon={<Flag />}
|
||||
label={`Pays: ${analysisResult.country}`}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Cas CNI */}
|
||||
{analysisResult.isCNI && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Security sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Vérification CNI
|
||||
</Typography>
|
||||
{analysisResult.verificationResult && (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
{analysisResult.verificationResult.numberValid ? (
|
||||
<CheckCircle color="success" />
|
||||
) : (
|
||||
<Error color="error" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Numéro valide"
|
||||
secondary={
|
||||
analysisResult.verificationResult.numberValid
|
||||
? 'Le numéro de CNI est valide'
|
||||
: 'Le numéro de CNI est invalide'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
{analysisResult.verificationResult.formatValid ? (
|
||||
<CheckCircle color="success" />
|
||||
) : (
|
||||
<Error color="error" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Format valide"
|
||||
secondary={
|
||||
analysisResult.verificationResult.formatValid
|
||||
? 'Le format du numéro est correct'
|
||||
: 'Le format du numéro est incorrect'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
{analysisResult.verificationResult.checksumValid ? (
|
||||
<CheckCircle color="success" />
|
||||
) : (
|
||||
<Error color="error" />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Checksum valide"
|
||||
secondary={
|
||||
analysisResult.verificationResult.checksumValid
|
||||
? 'La somme de contrôle est correcte'
|
||||
: 'La somme de contrôle est incorrecte'
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{/* Score de vraisemblance */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Score de vraisemblance
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
{getScoreIcon(analysisResult.credibilityScore)}
|
||||
<Typography variant="h4" sx={{ ml: 2 }}>
|
||||
{(analysisResult.credibilityScore * 100).toFixed(1)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={analysisResult.credibilityScore * 100}
|
||||
color={getScoreColor(analysisResult.credibilityScore) as any}
|
||||
sx={{ height: 10, borderRadius: 5 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{analysisResult.credibilityScore >= 0.8
|
||||
? 'Document très fiable'
|
||||
: analysisResult.credibilityScore >= 0.6
|
||||
? 'Document moyennement fiable'
|
||||
: 'Document peu fiable - vérification recommandée'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Synthèse */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Synthèse
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{analysisResult.summary}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Recommandations */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Recommandations
|
||||
</Typography>
|
||||
<List>
|
||||
{analysisResult.recommendations.map((recommendation, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<Info color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={recommendation} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
@ -1,8 +1,243 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Alert,
|
||||
Chip,
|
||||
Button,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Lightbulb,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
TrendingUp,
|
||||
Schedule,
|
||||
Psychology,
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { getConseil } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function ConseilView() {
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1>Conseil</h1>
|
||||
<p>Restitution LLM d'une analyse du document et recommandations.</p>
|
||||
</div>
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentDocument, conseilResult, loading } = useAppSelector(
|
||||
(state) => state.document
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument && !conseilResult) {
|
||||
dispatch(getConseil(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, conseilResult, dispatch])
|
||||
|
||||
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 }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Génération des conseils LLM...</Typography>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!conseilResult) {
|
||||
return (
|
||||
<Layout>
|
||||
<Alert severity="warning">
|
||||
Aucun conseil disponible.
|
||||
</Alert>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const getRiskColor = (risk: string) => {
|
||||
if (risk.toLowerCase().includes('élevé') || risk.toLowerCase().includes('critique')) {
|
||||
return 'error'
|
||||
}
|
||||
if (risk.toLowerCase().includes('moyen') || risk.toLowerCase().includes('modéré')) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'info'
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
<Psychology sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Conseil LLM
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Analyse LLM */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Lightbulb sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Analyse LLM
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{conseilResult.analysis}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
Généré le {new Date(conseilResult.generatedAt).toLocaleString()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{/* Recommandations */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<CheckCircle sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Recommandations ({conseilResult.recommendations.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{conseilResult.recommendations.map((recommendation, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<CheckCircle color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={recommendation} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Risques identifiés */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Warning sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Risques identifiés ({conseilResult.risks.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{conseilResult.risks.map((risk, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<Warning color={getRiskColor(risk) as any} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={risk}
|
||||
primaryTypographyProps={{
|
||||
color: getRiskColor(risk) === 'error' ? 'error.main' :
|
||||
getRiskColor(risk) === 'warning' ? 'warning.main' : 'info.main'
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Prochaines étapes */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<TrendingUp sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Prochaines étapes recommandées
|
||||
</Typography>
|
||||
<List>
|
||||
{conseilResult.nextSteps.map((step, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemIcon>
|
||||
<Schedule color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`Étape ${index + 1}`}
|
||||
secondary={step}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Actions
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => dispatch(getConseil(currentDocument.id))}
|
||||
disabled={loading}
|
||||
>
|
||||
Régénérer les conseils
|
||||
</Button>
|
||||
<Button variant="outlined">
|
||||
Exporter le rapport
|
||||
</Button>
|
||||
<Button variant="outlined">
|
||||
Partager avec l'équipe
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Résumé exécutif */}
|
||||
<Paper sx={{ p: 2, bgcolor: 'primary.50' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Résumé exécutif
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2 }}>
|
||||
<Chip
|
||||
label={`${conseilResult.recommendations.length} recommandations`}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${conseilResult.risks.length} risques identifiés`}
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`${conseilResult.nextSteps.length} étapes suivantes`}
|
||||
color="info"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Cette analyse LLM a été générée automatiquement et doit être validée par un expert notarial.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
@ -1,8 +1,298 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Alert,
|
||||
Button,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
ExpandMore,
|
||||
LocationOn,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
Error,
|
||||
Public,
|
||||
Business,
|
||||
Home,
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { getContextData } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function ContexteView() {
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1>Contexte</h1>
|
||||
<p>Recherche d'informations contextuelles (cadastre, géorisques, géofoncier, etc.).</p>
|
||||
</div>
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentDocument, contextResult, loading } = useAppSelector(
|
||||
(state) => state.document
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument && !contextResult) {
|
||||
dispatch(getContextData(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, contextResult, dispatch])
|
||||
|
||||
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 }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Recherche d'informations contextuelles...</Typography>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!contextResult) {
|
||||
return (
|
||||
<Layout>
|
||||
<Alert severity="warning">
|
||||
Aucune donnée contextuelle disponible.
|
||||
</Alert>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const getStatusIcon = (hasData: boolean) => {
|
||||
return hasData ? <CheckCircle color="success" /> : <Error color="error" />
|
||||
}
|
||||
|
||||
const getStatusColor = (hasData: boolean) => {
|
||||
return hasData ? 'success' : 'error'
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Informations contextuelles
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* Résumé des sources */}
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Sources de données consultées
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
icon={getStatusIcon(!!contextResult.cadastreData)}
|
||||
label="Cadastre"
|
||||
color={getStatusColor(!!contextResult.cadastreData) as any}
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={getStatusIcon(!!contextResult.georisquesData)}
|
||||
label="Géorisques"
|
||||
color={getStatusColor(!!contextResult.georisquesData) as any}
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={getStatusIcon(!!contextResult.geofoncierData)}
|
||||
label="Géofoncier"
|
||||
color={getStatusColor(!!contextResult.geofoncierData) as any}
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={getStatusIcon(!!contextResult.bodaccData)}
|
||||
label="BODACC"
|
||||
color={getStatusColor(!!contextResult.bodaccData) as any}
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={getStatusIcon(!!contextResult.infogreffeData)}
|
||||
label="Infogreffe"
|
||||
color={getStatusColor(!!contextResult.infogreffeData) as any}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
Dernière mise à jour: {new Date(contextResult.lastUpdated).toLocaleString()}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* Données cadastrales */}
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Home sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Données cadastrales</Typography>
|
||||
<Chip
|
||||
label={contextResult.cadastreData ? 'Disponible' : 'Non disponible'}
|
||||
color={getStatusColor(!!contextResult.cadastreData) as any}
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{contextResult.cadastreData ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(contextResult.cadastreData, null, 2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Aucune donnée cadastrale trouvée pour ce document.
|
||||
</Alert>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Données Géorisques */}
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Warning sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Données Géorisques</Typography>
|
||||
<Chip
|
||||
label={contextResult.georisquesData ? 'Disponible' : 'Non disponible'}
|
||||
color={getStatusColor(!!contextResult.georisquesData) as any}
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{contextResult.georisquesData ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(contextResult.georisquesData, null, 2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Aucune donnée Géorisques trouvée pour ce document.
|
||||
</Alert>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Données Géofoncier */}
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<LocationOn sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Données Géofoncier</Typography>
|
||||
<Chip
|
||||
label={contextResult.geofoncierData ? 'Disponible' : 'Non disponible'}
|
||||
color={getStatusColor(!!contextResult.geofoncierData) as any}
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{contextResult.geofoncierData ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(contextResult.geofoncierData, null, 2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Aucune donnée Géofoncier trouvée pour ce document.
|
||||
</Alert>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Données BODACC */}
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Public sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Données BODACC</Typography>
|
||||
<Chip
|
||||
label={contextResult.bodaccData ? 'Disponible' : 'Non disponible'}
|
||||
color={getStatusColor(!!contextResult.bodaccData) as any}
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{contextResult.bodaccData ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(contextResult.bodaccData, null, 2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Aucune donnée BODACC trouvée pour ce document.
|
||||
</Alert>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Données Infogreffe */}
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Business sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Données Infogreffe</Typography>
|
||||
<Chip
|
||||
label={contextResult.infogreffeData ? 'Disponible' : 'Non disponible'}
|
||||
color={getStatusColor(!!contextResult.infogreffeData) as any}
|
||||
size="small"
|
||||
sx={{ ml: 2 }}
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{contextResult.infogreffeData ? (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(contextResult.infogreffeData, null, 2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Aucune donnée Infogreffe trouvée pour ce document.
|
||||
</Alert>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Actions */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Actions
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => dispatch(getContextData(currentDocument.id))}
|
||||
disabled={loading}
|
||||
>
|
||||
Actualiser les données
|
||||
</Button>
|
||||
<Button variant="outlined">
|
||||
Exporter le rapport
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
@ -1,8 +1,288 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Person,
|
||||
LocationOn,
|
||||
Home,
|
||||
Description,
|
||||
Language,
|
||||
Verified,
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { extractDocument } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function ExtractionView() {
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1>Extraction</h1>
|
||||
<p>Affichage des informations extraites, identités, lieux, biens, clauses, signatures, langue, tags.</p>
|
||||
</div>
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentDocument, extractionResult, loading } = useAppSelector(
|
||||
(state) => state.document
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDocument && !extractionResult) {
|
||||
dispatch(extractDocument(currentDocument.id))
|
||||
}
|
||||
}, [currentDocument, extractionResult, dispatch])
|
||||
|
||||
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 }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Extraction en cours...</Typography>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
if (!extractionResult) {
|
||||
return (
|
||||
<Layout>
|
||||
<Alert severity="warning">
|
||||
Aucun résultat d'extraction disponible.
|
||||
</Alert>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Extraction des données
|
||||
</Typography>
|
||||
|
||||
<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: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
icon={<Language />}
|
||||
label={`Langue: ${extractionResult.language}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<Description />}
|
||||
label={`Type: ${extractionResult.documentType}`}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<Verified />}
|
||||
label={`Confiance: ${(extractionResult.confidence * 100).toFixed(1)}%`}
|
||||
color={extractionResult.confidence > 0.8 ? 'success' : 'warning'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{/* Identités */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Person sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Identités ({extractionResult.identities.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{extractionResult.identities.map((identity, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={
|
||||
identity.type === 'person'
|
||||
? `${identity.firstName} ${identity.lastName}`
|
||||
: identity.companyName
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="caption" display="block">
|
||||
Type: {identity.type}
|
||||
</Typography>
|
||||
{identity.birthDate && (
|
||||
<Typography variant="caption" display="block">
|
||||
Naissance: {identity.birthDate}
|
||||
</Typography>
|
||||
)}
|
||||
{identity.nationality && (
|
||||
<Typography variant="caption" display="block">
|
||||
Nationalité: {identity.nationality}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" display="block">
|
||||
Confiance: {(identity.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 ({extractionResult.addresses.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{extractionResult.addresses.map((address, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${address.street}, ${address.city}`}
|
||||
secondary={`${address.postalCode} ${address.country}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{/* Biens */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Home sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Biens ({extractionResult.properties.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{extractionResult.properties.map((property, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${property.type} - ${property.address.city}`}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="caption" display="block">
|
||||
{property.address.street}
|
||||
</Typography>
|
||||
{property.surface && (
|
||||
<Typography variant="caption" display="block">
|
||||
Surface: {property.surface} m²
|
||||
</Typography>
|
||||
)}
|
||||
{property.cadastralReference && (
|
||||
<Typography variant="caption" display="block">
|
||||
Cadastre: {property.cadastralReference}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
{/* Contrats */}
|
||||
<Box sx={{ flex: '1 1 300px' }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Description sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Contrats ({extractionResult.contracts.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{extractionResult.contracts.map((contract, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText
|
||||
primary={`${contract.type} - ${contract.amount ? `${contract.amount}€` : 'Montant non spécifié'}`}
|
||||
secondary={
|
||||
<Box>
|
||||
<Typography variant="caption" display="block">
|
||||
Parties: {contract.parties.length}
|
||||
</Typography>
|
||||
{contract.date && (
|
||||
<Typography variant="caption" display="block">
|
||||
Date: {contract.date}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" display="block">
|
||||
Clauses: {contract.clauses.length}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Signatures */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Signatures détectées ({extractionResult.signatures.length})
|
||||
</Typography>
|
||||
<List dense>
|
||||
{extractionResult.signatures.map((signature, index) => (
|
||||
<ListItem key={index}>
|
||||
<ListItemText primary={signature} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Texte extrait */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Texte extrait
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.50',
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{extractionResult.text}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Layout>
|
||||
)
|
||||
}
|
@ -1,25 +1,159 @@
|
||||
import { useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Button,
|
||||
Chip,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
CloudUpload,
|
||||
CheckCircle,
|
||||
Error,
|
||||
HourglassEmpty,
|
||||
} from '@mui/icons-material'
|
||||
import { useAppDispatch, useAppSelector } from '../store'
|
||||
import { uploadDocument, setCurrentDocument } from '../store/documentSlice'
|
||||
import { Layout } from '../components/Layout'
|
||||
|
||||
export default function UploadView() {
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const dispatch = useAppDispatch()
|
||||
const { documents, error } = useAppSelector((state) => state.document)
|
||||
|
||||
function onFilesSelected(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (event.target.files) {
|
||||
setFiles(Array.from(event.target.files))
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
dispatch(uploadDocument(file))
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 16 }}>
|
||||
<h1>Téléversement de documents</h1>
|
||||
<input type="file" multiple onChange={onFilesSelected} />
|
||||
{files.length > 0 && (
|
||||
<ul>
|
||||
{files.map((file) => (
|
||||
<li key={file.name}>{file.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Layout>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Téléversement de documents
|
||||
</Typography>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{documents.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Documents téléversés ({documents.length})
|
||||
</Typography>
|
||||
<List>
|
||||
{documents.map((doc) => (
|
||||
<ListItem
|
||||
key={doc.id}
|
||||
secondaryAction={
|
||||
<Button
|
||||
onClick={() => dispatch(setCurrentDocument(doc))}
|
||||
disabled={doc.status !== 'completed'}
|
||||
>
|
||||
Analyser
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItemIcon>{getStatusIcon(doc.status)}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={doc.name}
|
||||
secondary={
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||
<Chip
|
||||
label={doc.type}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={doc.status}
|
||||
size="small"
|
||||
color={getStatusColor(doc.status) as any}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user