From 43ebc94b5bc60b9e0c6e0958e91089d2a2cd666b Mon Sep 17 00:00:00 2001 From: 4NK IA Date: Thu, 18 Sep 2025 13:53:56 +0000 Subject: [PATCH] =?UTF-8?q?feat(entities):=20suppression=20unitaire=20+=20?= =?UTF-8?q?=C3=A9dition=20inline=20(personnes/adresses/entreprises)=20avec?= =?UTF-8?q?=20persistance=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 42 +++++++++++++++++ ecosystem.config.cjs | 1 + src/components/FilePreview.tsx | 82 +++++++++++++++++++++++++++++++--- src/services/folderApi.ts | 21 +++++++++ 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/backend/server.js b/backend/server.js index f906ce8..b338b0c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2267,6 +2267,48 @@ app.post('/api/folders/:folderHash/files/:fileHash/entities/delete', express.jso } }) +// Mettre à jour une entité (personne/adresse/entreprise) dans le cache JSON +// Body: { kind: 'person'|'address'|'company', id?: string, index?: number, patch: object } +app.post('/api/folders/:folderHash/files/:fileHash/entities/update', express.json(), (req, res) => { + try { + const { folderHash, fileHash } = req.params + const { kind, id, index, patch } = req.body || {} + if (!['person', 'address', 'company'].includes(kind)) { + return res.status(400).json({ success: false, error: 'Paramètre kind invalide' }) + } + if (!patch || typeof patch !== 'object') { + return res.status(400).json({ success: false, error: 'Patch invalide' }) + } + const jsonPath = path.join('cache', folderHash, `${fileHash}.json`) + if (!fs.existsSync(jsonPath)) { + return res.status(404).json({ success: false, error: 'Résultat non trouvé' }) + } + const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) + const ents = (((data || {}).extraction || {}).entities || {}) + const map = { person: 'persons', address: 'addresses', company: 'companies' } + const key = map[kind] + if (!Array.isArray(ents[key])) ents[key] = [] + + let targetIndex = -1 + if (typeof index === 'number' && index >= 0 && index < ents[key].length) { + targetIndex = index + } else if (typeof id === 'string' && id.length > 0) { + targetIndex = ents[key].findIndex((e) => e && e.id === id) + } + if (targetIndex < 0) { + return res.status(404).json({ success: false, error: 'Entité non trouvée' }) + } + const before = ents[key][targetIndex] || {} + const updated = { ...before, ...patch } + ents[key][targetIndex] = updated + // Sauvegarder + fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2)) + return res.json({ success: true, kind, index: targetIndex, entity: updated }) + } catch (e) { + return res.status(500).json({ success: false, error: e?.message || String(e) }) + } +}) + // Suppression d'un fichier d'un dossier (uploads + cache) app.delete('/api/folders/:folderHash/files/:fileHash', (req, res) => { try { diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index f8db949..4e0699d 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -7,6 +7,7 @@ module.exports = { env: { NODE_ENV: 'production', PORT: 3001, + OLLAMA_MIN_REVIEW_MS: process.env.OLLAMA_MIN_REVIEW_MS || 0, }, instances: 1, exec_mode: 'fork', diff --git a/src/components/FilePreview.tsx b/src/components/FilePreview.tsx index d1e3553..4287879 100644 --- a/src/components/FilePreview.tsx +++ b/src/components/FilePreview.tsx @@ -23,7 +23,7 @@ import { import type { Document } from '../types' import { useAppDispatch, useAppSelector } from '../store' import { loadFolderResults } from '../store/documentSlice' -import { deleteEntity } from '../services/folderApi' +import { deleteEntity, updateEntity } from '../services/folderApi' interface FilePreviewProps { document: Document @@ -39,6 +39,7 @@ export const FilePreview: React.FC = ({ document, onClose }) = const [page, setPage] = useState(1) const [scale, setScale] = useState(1.0) const [numPages, setNumPages] = useState(0) + const [savingKey, setSavingKey] = useState(null) useEffect(() => { setLoading(true) @@ -323,8 +324,29 @@ export const FilePreview: React.FC = ({ document, onClose }) = {Array.isArray((fullResult?.extraction?.entities?.persons)) && fullResult.extraction.entities.persons.length > 0 ? ( fullResult.extraction.entities.persons.map((p: any, i: number) => ( - - {p.firstName} {p.lastName} + + + (p.firstName = e.target.value)} /> + (p.lastName = e.target.value)} /> + + + + )) ) : ( @@ -354,8 +377,31 @@ export const FilePreview: React.FC = ({ document, onClose }) = {Array.isArray((fullResult?.extraction?.entities?.addresses)) && fullResult.extraction.entities.addresses.length > 0 ? ( fullResult.extraction.entities.addresses.map((a: any, i: number) => ( - - {a.street}, {a.postalCode} {a.city} {a.country || ''} + + + (a.street = e.target.value)} /> + (a.postalCode = e.target.value)} /> + (a.city = e.target.value)} /> + (a.country = e.target.value)} /> + + + + )) ) : ( @@ -384,8 +431,28 @@ export const FilePreview: React.FC = ({ document, onClose }) = {Array.isArray((fullResult?.extraction?.entities?.companies)) && fullResult.extraction.entities.companies.length > 0 ? ( fullResult.extraction.entities.companies.map((c: any, i: number) => ( - - {c.name} + + + (c.name = e.target.value)} /> + + + + )) ) : ( diff --git a/src/services/folderApi.ts b/src/services/folderApi.ts index 027365d..ab1e3e5 100644 --- a/src/services/folderApi.ts +++ b/src/services/folderApi.ts @@ -364,3 +364,24 @@ export async function deleteEntity( } return response.json() } + +// Mettre à jour une entité dans le cache (merge partiel) +export async function updateEntity( + folderHash: string, + fileHash: string, + kind: 'person' | 'address' | 'company', + payload: { id?: string; index?: number; patch: Record }, +): Promise<{ success: boolean }>{ + const response = await fetch( + `${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/entities/update`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ kind, ...payload }), + }, + ) + if (!response.ok) { + throw new Error(`Erreur mise à jour entité: ${response.statusText}`) + } + return response.json() +}