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"
|
"test:ui": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.8.2"
|
"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 { lazy, Suspense } from 'react'
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
|
import { Box, CircularProgress, Typography } from '@mui/material'
|
||||||
|
|
||||||
const UploadView = lazy(() => import('../views/UploadView'))
|
const UploadView = lazy(() => import('../views/UploadView'))
|
||||||
const ExtractionView = lazy(() => import('../views/ExtractionView'))
|
const ExtractionView = lazy(() => import('../views/ExtractionView'))
|
||||||
@ -7,12 +8,19 @@ const AnalyseView = lazy(() => import('../views/AnalyseView'))
|
|||||||
const ContexteView = lazy(() => import('../views/ContexteView'))
|
const ContexteView = lazy(() => import('../views/ContexteView'))
|
||||||
const ConseilView = lazy(() => import('../views/ConseilView'))
|
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([
|
const router = createBrowserRouter([
|
||||||
{ path: '/', element: <Suspense fallback={<div>Chargement…</div>}><UploadView /></Suspense> },
|
{ path: '/', element: <Suspense fallback={<LoadingFallback />}><UploadView /></Suspense> },
|
||||||
{ path: '/extraction', element: <Suspense fallback={<div>Chargement…</div>}><ExtractionView /></Suspense> },
|
{ path: '/extraction', element: <Suspense fallback={<LoadingFallback />}><ExtractionView /></Suspense> },
|
||||||
{ path: '/analyse', element: <Suspense fallback={<div>Chargement…</div>}><AnalyseView /></Suspense> },
|
{ path: '/analyse', element: <Suspense fallback={<LoadingFallback />}><AnalyseView /></Suspense> },
|
||||||
{ path: '/contexte', element: <Suspense fallback={<div>Chargement…</div>}><ContexteView /></Suspense> },
|
{ path: '/contexte', element: <Suspense fallback={<LoadingFallback />}><ContexteView /></Suspense> },
|
||||||
{ path: '/conseil', element: <Suspense fallback={<div>Chargement…</div>}><ConseilView /></Suspense> },
|
{ path: '/conseil', element: <Suspense fallback={<LoadingFallback />}><ConseilView /></Suspense> },
|
||||||
])
|
])
|
||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
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'
|
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
@ -7,13 +8,101 @@ export const apiClient = axios.create({
|
|||||||
timeout: 60000,
|
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) => {
|
// Services API pour les documents
|
||||||
const form = new FormData()
|
export const documentApi = {
|
||||||
form.append('file', file)
|
// Téléversement de document
|
||||||
const { data } = await apiClient.post<DetectionResult>('/api/ocr/detect', form, {
|
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' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
})
|
})
|
||||||
return 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 { useDispatch, useSelector } from 'react-redux'
|
||||||
import type { TypedUseSelectorHook } from 'react-redux'
|
import type { TypedUseSelectorHook } from 'react-redux'
|
||||||
import { appReducer } from './appSlice'
|
import { appReducer } from './appSlice'
|
||||||
|
import { documentReducer } from './documentSlice'
|
||||||
|
|
||||||
// Root placeholder slice will be added later as features grow
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
app: appReducer,
|
app: appReducer,
|
||||||
|
document: documentReducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
||||||
serializableCheck: false,
|
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() {
|
export default function AnalyseView() {
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<Layout>
|
||||||
<h1>Analyse</h1>
|
<Alert severity="info">
|
||||||
<p>Cas CNI: pays, vérification numéro, score de vraisemblance, avis. Autres: synthèse et score.</p>
|
Veuillez d'abord téléverser et sélectionner un document.
|
||||||
</div>
|
</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() {
|
export default function ConseilView() {
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<Layout>
|
||||||
<h1>Conseil</h1>
|
<Alert severity="info">
|
||||||
<p>Restitution LLM d'une analyse du document et recommandations.</p>
|
Veuillez d'abord téléverser et sélectionner un document.
|
||||||
</div>
|
</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() {
|
export default function ContexteView() {
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<Layout>
|
||||||
<h1>Contexte</h1>
|
<Alert severity="info">
|
||||||
<p>Recherche d'informations contextuelles (cadastre, géorisques, géofoncier, etc.).</p>
|
Veuillez d'abord téléverser et sélectionner un document.
|
||||||
</div>
|
</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() {
|
export default function ExtractionView() {
|
||||||
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<Layout>
|
||||||
<h1>Extraction</h1>
|
<Alert severity="info">
|
||||||
<p>Affichage des informations extraites, identités, lieux, biens, clauses, signatures, langue, tags.</p>
|
Veuillez d'abord téléverser et sélectionner un document.
|
||||||
</div>
|
</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() {
|
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>) {
|
const onDrop = useCallback(
|
||||||
if (event.target.files) {
|
(acceptedFiles: File[]) => {
|
||||||
setFiles(Array.from(event.target.files))
|
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 (
|
return (
|
||||||
<div style={{ padding: 16 }}>
|
<Layout>
|
||||||
<h1>Téléversement de documents</h1>
|
<Typography variant="h4" gutterBottom>
|
||||||
<input type="file" multiple onChange={onFilesSelected} />
|
Téléversement de documents
|
||||||
{files.length > 0 && (
|
</Typography>
|
||||||
<ul>
|
|
||||||
{files.map((file) => (
|
<Paper
|
||||||
<li key={file.name}>{file.name}</li>
|
{...getRootProps()}
|
||||||
))}
|
sx={{
|
||||||
</ul>
|
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