4NK_IA_front/src/services/folderApi.ts

388 lines
12 KiB
TypeScript

/**
* API pour la gestion des dossiers par hash
*/
function getApiBaseUrl(): string {
const env: any = (import.meta as any)?.env || {}
// En prod, si le site n'est pas servi depuis localhost, forcer la même origine
if (env.PROD && typeof window !== 'undefined' && window.location.hostname !== 'localhost') {
return '/api'
}
// Dev/local: privilégier VITE_API_BASE puis VITE_BACKEND_URL
if (env.VITE_API_BASE) return env.VITE_API_BASE
if (env.VITE_BACKEND_URL) return `${env.VITE_BACKEND_URL}/api`
return '/api'
}
const API_BASE_URL = getApiBaseUrl()
// Gestion simple d'ETag et de cache local (par dossier)
const ETAG_PREFIX = '4nk:etag:'
const RESULT_PREFIX = '4nk:results:'
function getStoredEtag(folderHash: string): string | null {
try {
return localStorage.getItem(ETAG_PREFIX + folderHash)
} catch {
return null
}
}
function setStoredEtag(folderHash: string, etag: string | null) {
try {
if (etag) localStorage.setItem(ETAG_PREFIX + folderHash, etag)
} catch {}
}
function getStoredResults(folderHash: string): FolderResponse | null {
try {
const raw = localStorage.getItem(RESULT_PREFIX + folderHash)
return raw ? (JSON.parse(raw) as FolderResponse) : null
} catch {
return null
}
}
function setStoredResults(folderHash: string, data: FolderResponse) {
try {
localStorage.setItem(RESULT_PREFIX + folderHash, JSON.stringify(data))
} catch {}
}
export interface FolderResult {
fileHash: string
document: {
id: string
fileName: string
mimeType: string
fileSize: number
uploadTimestamp: number
}
classification: {
documentType: string
language: string
}
extraction: {
text: {
raw: string
processed: string
}
entities: {
persons: string[]
addresses: string[]
companies: string[]
}
}
metadata: {
quality: {
globalConfidence: number
}
}
status: {
timestamp: number
}
}
export interface FolderResponse {
success: boolean
folderHash: string
folderName?: string | null
results: FolderResult[]
pending: Array<{
fileHash: string
folderHash: string
timestamp: string
status: string
}>
hasPending: boolean
count: number
}
export interface CreateFolderResponse {
success: boolean
folderHash: string
message: string
name?: string | null
description?: string | null
}
// Créer un nouveau dossier
export async function createFolder(name?: string, description?: string): Promise<CreateFolderResponse> {
const response = await fetch(`${API_BASE_URL}/folders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: name || 'Nouveau dossier', description: description || '' }),
})
if (!response.ok) {
throw new Error(`Erreur lors de la création du dossier: ${response.statusText}`)
}
return response.json()
}
// Créer le dossier par défaut avec les fichiers de test
export async function createDefaultFolder(): Promise<CreateFolderResponse> {
const response = await fetch(`${API_BASE_URL}/folders/default`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`Erreur lors de la création du dossier par défaut: ${response.statusText}`)
}
return response.json()
}
// Utiliser le dossier par défaut existant (sans créer de nouveau dossier)
export async function getDefaultFolder(): Promise<CreateFolderResponse> {
// Délègue au backend la création/récupération du dossier par défaut
const response = await fetch(`${API_BASE_URL}/folders/default`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`Erreur lors de la récupération du dossier par défaut: ${response.statusText}`)
}
return response.json()
}
// Récupérer les résultats d'un dossier (avec ETag)
export async function getFolderResults(folderHash: string): Promise<FolderResponse> {
console.log(`[API] Appel getFolderResults pour le dossier: ${folderHash}`)
console.log(`[API] API_BASE_URL: ${API_BASE_URL}`)
console.log(`[API] URL complète: ${API_BASE_URL}/folders/${folderHash}/results`)
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => {
console.log(`[API] Timeout après 10 secondes`)
controller.abort()
}, 10000)
const url = `${API_BASE_URL}/folders/${folderHash}/results?t=${Date.now()}`
console.log(`[API] URL finale: ${url}`)
const etag = getStoredEtag(folderHash)
const response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(etag ? { 'If-None-Match': etag } : {}),
},
})
clearTimeout(timeoutId)
console.log(`[API] Réponse reçue:`, response.status, response.statusText)
console.log(`[API] Headers:`, Object.fromEntries(response.headers.entries()))
// Gestion 304: retourner le cache local si disponible
if (response.status === 304) {
const cached = getStoredResults(folderHash)
if (cached) {
console.log('[API] 304 Not Modified - utilisation du cache local')
return cached
}
// Aucun cache disponible: tomber en repli en forçant une nouvelle requête sans ETag
console.warn('[API] 304 sans cache: nouvelle requête sans If-None-Match')
const fallback = await fetch(`${API_BASE_URL}/folders/${folderHash}/results`, {
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
})
if (!fallback.ok) throw new Error(`Erreur backend: ${fallback.statusText}`)
const data = (await fallback.json()) as FolderResponse
const newEtag = fallback.headers.get('ETag')
if (newEtag) setStoredEtag(folderHash, newEtag)
setStoredResults(folderHash, data)
return data
}
if (!response.ok) {
console.error(`[API] Erreur HTTP:`, response.status, response.statusText)
throw new Error(
`Erreur lors de la récupération des résultats du dossier: ${response.statusText}`,
)
}
const data = (await response.json()) as FolderResponse
const newEtag = response.headers.get('ETag')
if (newEtag) setStoredEtag(folderHash, newEtag)
setStoredResults(folderHash, data)
return data
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.error(`[API] Requête annulée (timeout)`)
throw new Error('Timeout: La requête a pris trop de temps')
}
console.error(`[API] Erreur lors de l'appel API:`, error)
throw error
}
}
// Ajoute: vider le cache d'un dossier
export async function clearFolderCache(folderHash: string): Promise<{ success: boolean; removed: number }>{
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/cache`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
},
})
if (!response.ok) {
throw new Error(`Erreur lors du vidage du cache du dossier: ${response.statusText}`)
}
return response.json()
}
// Re-traiter un dossier existant basé sur uploads/<hash>
export async function reprocessFolder(folderHash: string): Promise<{ success: boolean; scheduled: number }>{
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/reprocess`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
throw new Error(`Erreur lors du re-traitement du dossier: ${response.statusText}`)
}
return response.json()
}
export async function getFolderMeta(folderHash: string): Promise<{ success: boolean; folderHash: string; name: string | null }>{
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/meta`)
if (!response.ok) {
throw new Error(`Erreur lors de la récupération des métadonnées du dossier: ${response.statusText}`)
}
return response.json()
}
// Récupérer un fichier original depuis un dossier
export async function getFolderFile(folderHash: string, fileHash: string): Promise<Blob> {
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}`)
if (!response.ok) {
throw new Error(`Erreur lors de la récupération du fichier: ${response.statusText}`)
}
return response.blob()
}
// Uploader un fichier dans un dossier
export async function uploadFileToFolder(file: File, folderHash: string): Promise<FolderResult> {
const formData = new FormData()
formData.append('document', file)
formData.append('folderHash', folderHash)
const response = await fetch(`${API_BASE_URL}/extract`, {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error(`Erreur lors de l'upload: ${response.statusText}`)
}
return response.json()
}
// Supprimer un fichier (uploads + cache)
export async function deleteFolderFile(folderHash: string, fileHash: string): Promise<{ success: boolean }>{
const response = await fetch(`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}`, {
method: 'DELETE',
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error(`Erreur lors de la suppression du fichier: ${response.statusText}`)
}
return response.json()
}
// Confirmer/corriger l'adresse détectée
export async function confirmDetectedAddress(
folderHash: string,
fileHash: string,
address: { street: string; city: string; postalCode: string; country?: string },
): Promise<{ success: boolean }>{
const response = await fetch(
`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/confirm-address`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirmed: true, address }),
},
)
if (!response.ok) {
throw new Error(`Erreur lors de la confirmation d'adresse: ${response.statusText}`)
}
return response.json()
}
// Demander une révision IA (Ollama) d'un résultat existant
export async function reviewFileWithAI(
folderHash: string,
fileHash: string,
): Promise<{ success: boolean; review?: { score: number | null; corrections: any[]; avis: string } }>{
const response = await fetch(
`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/review`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
},
)
if (!response.ok) {
throw new Error(`Erreur révision IA: ${response.statusText}`)
}
return response.json()
}
// Supprimer une entité du cache (person/address/company)
export async function deleteEntity(
folderHash: string,
fileHash: string,
kind: 'person' | 'address' | 'company',
payload: { id?: string; index?: number },
): Promise<{ success: boolean }>{
const response = await fetch(
`${API_BASE_URL}/folders/${folderHash}/files/${fileHash}/entities/delete`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind, ...payload }),
},
)
if (!response.ok) {
throw new Error(`Erreur suppression entité: ${response.statusText}`)
}
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<string, any> },
): 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()
}