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' import { openaiDocumentApi } from '../services/openai' import { backendDocumentApi, checkBackendHealth } from '../services/backendApi' import { createDefaultFolder, getDefaultFolder, getFolderResults, uploadFileToFolder, type FolderResult } from '../services/folderApi' interface DocumentState { documents: Document[] currentDocument: Document | null extractionResult: ExtractionResult | null extractionById: Record fileById: Record analysisResult: AnalysisResult | null contextResult: ContextResult | null conseilResult: ConseilResult | null loading: boolean error: string | null progressById: Record bootstrapped: boolean // Flag pour indiquer si le bootstrap a été effectué // Nouvelles propriétés pour les dossiers currentFolderHash: string | null folderResults: FolderResult[] currentResultIndex: number // Propriétés pour le système de pending pendingFiles: Array<{ fileHash: string folderHash: string timestamp: string status: string }> hasPending: boolean pollingInterval: NodeJS.Timeout | null } // Fonction pour charger l'état depuis localStorage const loadStateFromStorage = (): Partial => { try { const savedState = localStorage.getItem('4nk-ia-documents') if (savedState) { const parsed = JSON.parse(savedState) console.log('💾 [STORE] État chargé depuis localStorage:', { documentsCount: parsed.documents?.length || 0, extractionsCount: Object.keys(parsed.extractionById || {}).length }) return parsed } } catch (error) { console.warn('⚠️ [STORE] Erreur lors du chargement depuis localStorage:', error) } return {} } // Fonction pour sauvegarder l'état dans localStorage const saveStateToStorage = (state: DocumentState) => { try { const stateToSave = { documents: state.documents, extractionById: state.extractionById, currentDocument: state.currentDocument } localStorage.setItem('4nk-ia-documents', JSON.stringify(stateToSave)) } catch (error) { console.warn('⚠️ [STORE] Erreur lors de la sauvegarde dans localStorage:', error) } } const initialState: DocumentState = { documents: [], currentDocument: null, extractionResult: null, extractionById: {}, fileById: {}, analysisResult: null, contextResult: null, conseilResult: null, loading: false, error: null, progressById: {}, bootstrapped: false, // Nouvelles propriétés pour les dossiers currentFolderHash: null, folderResults: [], currentResultIndex: 0, // Propriétés pour le système de pending pendingFiles: [], hasPending: false, pollingInterval: null, ...loadStateFromStorage() } export const uploadDocument = createAsyncThunk( 'document/upload', async (file: File) => { return await documentApi.upload(file) } ) export const extractDocument = createAsyncThunk( 'document/extract', async (documentId: string, thunkAPI) => { console.log(`🚀 [STORE] Extraction du document: ${documentId}`) const state = thunkAPI.getState() as { document: DocumentState } const doc = state.document.documents.find((d) => d.id === documentId) if (!doc) { throw new Error(`Document ${documentId} non trouvé`) } // Vérifier si une extraction est déjà en cours ou terminée if (state.document.extractionById[documentId]) { console.log(`⚠️ [STORE] Extraction déjà en cours/terminée pour ${documentId}`) return state.document.extractionById[documentId] } // Vérifier si le backend est disponible const backendAvailable = await checkBackendHealth() if (backendAvailable) { console.log('🚀 [STORE] Utilisation du backend pour l\'extraction') // Pas de hooks de progression pour le backend - traitement direct if (doc?.previewUrl) { try { const res = await fetch(doc.previewUrl) const blob = await res.blob() const file = new File([blob], doc.name, { type: doc.mimeType }) return await backendDocumentApi.extract(documentId, file) } catch (error) { console.error('❌ [STORE] Erreur backend:', error) throw error } } return await backendDocumentApi.extract(documentId, undefined) } else { console.log('⚠️ [STORE] Backend non disponible, utilisation du mode local') // Fallback vers le mode local (OpenAI ou règles) const useOpenAI = import.meta.env.VITE_USE_OPENAI === 'true' if (useOpenAI) { const state = thunkAPI.getState() as { document: DocumentState } const doc = state.document.documents.find((d) => d.id === documentId) // Pas de hooks de progression pour le mode local if (doc?.previewUrl) { try { const res = await fetch(doc.previewUrl) const blob = await res.blob() const file = new File([blob], doc.name, { type: doc.mimeType }) return await openaiDocumentApi.extract(documentId, file) } catch { return await openaiDocumentApi.extract(documentId, undefined) } } return await openaiDocumentApi.extract(documentId, undefined) } 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) } ) // Thunks pour la gestion des dossiers export const createDefaultFolderThunk = createAsyncThunk( 'document/createDefaultFolder', async () => { return await getDefaultFolder() } ) export const loadFolderResults = createAsyncThunk( 'document/loadFolderResults', async (folderHash: string) => { return await getFolderResults(folderHash) } ) export const uploadFileToFolderThunk = createAsyncThunk( 'document/uploadFileToFolder', async ({ file, folderHash }: { file: File; folderHash: string }) => { return await uploadFileToFolder(file, folderHash) } ) const documentSlice = createSlice({ name: 'document', initialState, reducers: { setCurrentDocument: (state, action: PayloadAction) => { state.currentDocument = action.payload }, clearResults: (state) => { state.extractionResult = null // Ne pas effacer extractionById pour conserver les résultats par document state.analysisResult = null state.contextResult = null state.conseilResult = null }, addDocuments: (state, action: PayloadAction) => { const incoming = action.payload // Évite les doublons par (name,size) pour les bootstraps répétés en dev const seenKey = new Set(state.documents.map((d) => `${d.name}::${d.size}`)) const merged = [...state.documents] incoming.forEach((d) => { const key = `${d.name}::${d.size}` if (!seenKey.has(key)) { seenKey.add(key) merged.push(d) } }) state.documents = merged }, removeDocument: (state, action: PayloadAction) => { const idToRemove = action.payload state.documents = state.documents.filter((d) => d.id !== idToRemove) if (state.currentDocument && state.currentDocument.id === idToRemove) { state.currentDocument = null state.extractionResult = null state.analysisResult = null state.contextResult = null state.conseilResult = null } delete state.progressById[idToRemove] }, setOcrProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => { const { id, progress } = action.payload state.progressById[id] = { ocr: Math.max(0, Math.min(100, Math.round(progress * 100))), llm: state.progressById[id]?.llm || 0 } }, setLlmProgress: (state, action: PayloadAction<{ id: string; progress: number }>) => { const { id, progress } = action.payload state.progressById[id] = { ocr: state.progressById[id]?.ocr || 0, llm: Math.max(0, Math.min(100, Math.round(progress * 100))) } }, setBootstrapped: (state, action: PayloadAction) => { state.bootstrapped = action.payload }, // Nouveaux reducers pour les dossiers setCurrentFolderHash: (state, action: PayloadAction) => { state.currentFolderHash = action.payload }, setCurrentResultIndex: (state, action: PayloadAction) => { state.currentResultIndex = action.payload }, clearFolderResults: (state) => { state.folderResults = [] state.currentResultIndex = 0 }, // Reducers pour le système de pending setPendingFiles: (state, action: PayloadAction>) => { state.pendingFiles = action.payload state.hasPending = action.payload.length > 0 }, setPollingInterval: (state, action: PayloadAction) => { state.pollingInterval = action.payload }, stopPolling: (state) => { if (state.pollingInterval) { clearInterval(state.pollingInterval) state.pollingInterval = null } state.hasPending = false state.pendingFiles = [] }, }, extraReducers: (builder) => { builder .addCase(uploadDocument.pending, (state) => { state.loading = true state.error = null }) .addCase(uploadDocument.fulfilled, (state, action) => { state.loading = false const { document, extraction } = action.payload console.log('📤 [STORE] Upload fulfilled:', { documentId: document.id, documentName: document.name, hasExtraction: !!extraction, extractionDocumentId: extraction?.documentId }) state.documents.push(document) state.currentDocument = document // Stocker le résultat d'extraction si disponible if (extraction) { state.extractionResult = extraction state.extractionById[document.id] = extraction console.log('✅ [STORE] Extraction stored for document:', document.id) } // Capture le File depuis l'URL blob si disponible if (document.previewUrl?.startsWith('blob:')) { // On ne peut pas récupérer l'objet File initial ici sans passer par onDrop; // il est reconstruit lors de l'extraction via fetch blob. } }) .addCase(uploadDocument.rejected, (state, action) => { state.loading = false state.error = action.error.message || 'Erreur lors du téléversement' }) .addCase(extractDocument.pending, (state) => { state.loading = true state.error = null }) .addCase(extractDocument.fulfilled, (state, action) => { state.loading = false state.extractionResult = action.payload state.extractionById[action.payload.documentId] = action.payload // Mettre à jour le statut du document courant if (state.currentDocument && state.currentDocument.id === action.payload.documentId) { state.currentDocument.status = 'completed' } }) .addCase(extractDocument.rejected, (state, action) => { state.loading = false state.error = action.error.message || 'Erreur lors de l\'extraction' // Mettre à jour le statut du document courant en cas d'erreur if (state.currentDocument) { state.currentDocument.status = 'error' } }) .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 }) // ExtraReducers pour les dossiers .addCase(createDefaultFolderThunk.fulfilled, (state, action) => { state.currentFolderHash = action.payload.folderHash state.loading = false }) .addCase(createDefaultFolderThunk.pending, (state) => { state.loading = true }) .addCase(createDefaultFolderThunk.rejected, (state, action) => { state.loading = false state.error = action.error.message || 'Erreur lors de la création du dossier par défaut' }) .addCase(loadFolderResults.fulfilled, (state, action) => { console.log(`[STORE] loadFolderResults.fulfilled appelé avec:`, action.payload) console.log(`[STORE] Nombre de résultats reçus:`, action.payload.results?.length || 0) state.folderResults = action.payload.results state.currentFolderHash = action.payload.folderHash state.loading = false // Gérer les fichiers pending state.pendingFiles = action.payload.pending || [] state.hasPending = action.payload.hasPending || false // Convertir les résultats en documents pour la compatibilité if (action.payload.results && action.payload.results.length > 0) { state.documents = action.payload.results.map((result, index) => { console.log(`[STORE] Mapping résultat ${index}:`, { fileHash: result.fileHash, fileName: result.document?.fileName, mimeType: result.document?.mimeType }) return { id: result.fileHash, name: result.document.fileName, mimeType: result.document.mimeType, size: 0, // Taille non disponible dans la structure actuelle uploadDate: new Date(result.document.uploadTimestamp), status: 'completed' as const, previewUrl: `blob:folder-${result.fileHash}` } }) } else { console.log(`[STORE] Aucun résultat à mapper`) state.documents = [] } console.log(`[STORE] Dossier chargé: ${action.payload.results.length} résultats, ${action.payload.pending?.length || 0} pending`) console.log(`[STORE] Documents finaux:`, state.documents.length) console.log(`[STORE] Documents mappés:`, state.documents.map(d => ({ id: d.id, name: d.name, status: d.status }))) }) .addCase(loadFolderResults.pending, (state) => { // Ne pas afficher la barre de progression pour le chargement initial des résultats // state.loading = true }) .addCase(loadFolderResults.rejected, (state, action) => { state.loading = false state.error = action.error.message || 'Erreur lors du chargement des résultats du dossier' }) .addCase(uploadFileToFolderThunk.fulfilled, (state, action) => { // Recharger les résultats du dossier après upload state.loading = false }) .addCase(uploadFileToFolderThunk.pending, (state) => { state.loading = true }) .addCase(uploadFileToFolderThunk.rejected, (state, action) => { state.loading = false state.error = action.error.message || 'Erreur lors de l\'upload du fichier' }) }, }) export const { setCurrentDocument, clearResults, addDocuments, removeDocument, setOcrProgress, setLlmProgress, setBootstrapped, setCurrentFolderHash, setCurrentResultIndex, clearFolderResults, setPendingFiles, setPollingInterval, stopPolling } = documentSlice.actions export const documentReducer = documentSlice.reducer