import { useState, useEffect } from 'react'; import { useApiRequest } from './useApi'; // File interfaces - exactly matching backend FileItem model export interface FileInfo { id: string; mandateId: string; // Required in backend fileName: string; // Required in backend mimeType: string; fileHash: string; fileSize: number; creationDate: number; // Backend uses float for UTC timestamp in seconds } export interface UserFile { id: string; file_name: string; mime_type?: string; action: string; created_at: number |string; size?: number; source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me' } // Files list hook export function useUserFiles() { const [files, setFiles] = useState([]); const { request, isLoading: loading, error } = useApiRequest(); const fetchFiles = async () => { try { console.log('🔍 Fetching files from API...'); const data = await request({ url: '/api/files/list', method: 'get' }); console.log('📥 Raw API response:', data); // Ensure data is an array, handle null/undefined responses const fileList = Array.isArray(data) ? data : []; console.log(`📋 Processing ${fileList.length} files from API`); // Filter out invalid files and map API response to our frontend model const validFiles = fileList.filter((apiFile: any): boolean => { // Skip files with invalid data that would cause backend validation errors // Backend FileItem requires: id, mandateId, fileName, mimeType, fileHash, fileSize, creationDate const isValid = !!( apiFile && typeof apiFile === 'object' && apiFile.id && typeof apiFile.id === 'string' && apiFile.mandateId && typeof apiFile.mandateId === 'string' && apiFile.fileName && typeof apiFile.fileName === 'string' && apiFile.fileName.trim() !== '' && apiFile.mimeType && typeof apiFile.mimeType === 'string' && apiFile.fileHash && typeof apiFile.fileHash === 'string' && typeof apiFile.fileSize === 'number' && apiFile.fileSize >= 0 && typeof apiFile.creationDate === 'number' && apiFile.creationDate > 0 ); if (!isValid) { console.warn('❌ Filtering out invalid file record:', { id: apiFile?.id, mandateId: apiFile?.mandateId, fileName: apiFile?.fileName, mimeType: apiFile?.mimeType, fileHash: apiFile?.fileHash, fileSize: apiFile?.fileSize, creationDate: apiFile?.creationDate, creationDateType: typeof apiFile?.creationDate, fullObject: apiFile }); } else { console.log('✅ Valid file:', { id: apiFile.id, fileName: apiFile.fileName, creationDate: apiFile.creationDate }); } return isValid; }); console.log(`✨ Filtered to ${validFiles.length} valid files`); if (validFiles.length !== fileList.length) { console.warn(`⚠️ Filtered out ${fileList.length - validFiles.length} invalid files`); } const mappedFiles = validFiles.map((apiFile: any): UserFile => { // Derive a simplified action from the MIME type let action = 'Document'; if (apiFile.mimeType) { const mimePrefix = apiFile.mimeType.split('/')[0]; switch (mimePrefix) { case 'image': action = 'Bild'; break; case 'application': if (apiFile.mimeType.includes('pdf')) { action = 'PDF'; } else if (apiFile.mimeType.includes('word') || apiFile.mimeType.includes('office')) { action = 'Dokument'; } else if (apiFile.mimeType.includes('excel') || apiFile.mimeType.includes('spreadsheet')) { action = 'Tabelle'; } else { action = 'Datei'; } break; case 'text': action = 'Text'; break; case 'video': action = 'Video'; break; case 'audio': action = 'Audio'; break; default: action = 'Datei'; } } // Convert creationDate from backend float (seconds since epoch) to ISO string // Backend always sends creationDate as float seconds since epoch (UTC) const createdAt = new Date(apiFile.creationDate * 1000).toISOString(); return { id: apiFile.id, file_name: apiFile.fileName, // Required field from backend mime_type: apiFile.mimeType, // Required field from backend action: action, created_at: createdAt, size: apiFile.fileSize, // Required field from backend source: 'user_uploaded' // Default source since workflowId is not part of FileItem model }; }); console.log(`🎉 Successfully processed ${mappedFiles.length} files for display`); setFiles(mappedFiles); } catch (error) { // Error is already handled by useApiRequest console.error('❌ Error fetching files:', error); // Set empty array on error to prevent UI issues setFiles([]); } }; // Optimistically remove a file from the local state const removeFileOptimistically = (fileId: string) => { setFiles(prevFiles => prevFiles.filter(file => file.id !== fileId)); }; // Add a file to the local state (for when upload completes) const addFileOptimistically = (newFile: UserFile) => { setFiles(prevFiles => [newFile, ...prevFiles]); }; useEffect(() => { fetchFiles(); }, []); return { files, loading, error, refetch: fetchFiles, removeFileOptimistically, addFileOptimistically }; } // File operations hook export function useFileOperations() { const [downloadingFiles, setDownloadingFiles] = useState>(new Set()); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [uploadingFile, setUploadingFile] = useState(false); const { request, isLoading } = useApiRequest(); const [downloadError, setDownloadError] = useState(null); const [deleteError, setDeleteError] = useState(null); const [uploadError, setUploadError] = useState(null); const [previewingFiles, setPreviewingFiles] = useState>(new Set()); const [previewError, setPreviewError] = useState(null); const handleFileDownload = async (fileId: string, fileName: string) => { setDownloadError(null); setDownloadingFiles(prev => new Set(prev).add(fileId)); try { console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`); // Try to get the file download const blob = await request({ url: `/api/files/${fileId}/download`, method: 'get', // Override axios config for blob response additionalConfig: { responseType: 'blob', // Better error handling for blob responses validateStatus: function (status: number) { return status >= 200 && status < 300; // default } } }); console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type }); // Create a download link and trigger the download const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); return true; } catch (error: any) { console.error(`❌ Download failed for ${fileName}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File "${fileName}" not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to download "${fileName}".`; } setDownloadError(errorMessage); return false; } finally { setDownloadingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; const handleFileDelete = async (fileId: string, onOptimisticDelete?: () => void) => { setDeleteError(null); setDeletingFiles(prev => new Set(prev).add(fileId)); // Optimistically remove from UI if callback provided if (onOptimisticDelete) { onOptimisticDelete(); } try { console.log(`🗑️ Starting delete for file ID: ${fileId}`); await request({ url: `/api/files/${fileId}`, method: 'delete' }); console.log(`✅ Delete successful for file ID: ${fileId}`); // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { console.error(`❌ Delete failed for file ID ${fileId}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File not found or has already been deleted.`; // If file doesn't exist, consider it successfully "deleted" return true; } else if (error.response?.status === 403) { errorMessage = `No permission to delete this file.`; } setDeleteError(errorMessage); // If deletion failed and we optimistically removed it, we should refetch to restore the file return false; } finally { setDeletingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; /** * File upload function - backend bug has been fixed! * * BACKEND FIXES APPLIED: * - Fixed file.fileName → file.filename in routeDataFiles.py * - Removed workflowId from FileItem creation in interfaceComponentObjects.py * - Upload should now work correctly */ const handleFileUpload = async (file: globalThis.File, workflowId?: string) => { setUploadError(null); setUploadingFile(true); try { console.log('📤 Starting file upload...', { fileName: file.name, fileSize: file.size, fileType: file.type, workflowId: workflowId }); // Validate file before upload if (!file || !file.name || file.name.trim() === '') { throw new Error('Invalid file: File must have a valid name'); } if (file.size === 0) { throw new Error('Invalid file: File cannot be empty'); } const formData = new FormData(); formData.append('file', file); if (workflowId) { formData.append('workflowId', workflowId); } // FormData is now correctly configured for backend console.log('📋 FormData prepared:', { hasFile: formData.has('file'), hasWorkflowId: formData.has('workflowId'), workflowId: workflowId }); // Log the actual file object being sent console.log('📁 File object details:', { name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, constructor: file.constructor.name }); console.log('🚀 Sending upload request...'); const fileData = await request({ url: '/api/files/upload', method: 'post', data: formData, // Override axios config for form data additionalConfig: { headers: { 'Content-Type': 'multipart/form-data', } } }); console.log('✅ Upload successful:', fileData); return { success: true, fileData }; } catch (error: any) { console.error('❌ Upload failed:', { error: error, message: error.message, response: error.response, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data }); // Provide detailed error analysis for backend team let errorMessage = error.message; if (error.response?.status === 500) { if (errorMessage.includes('validation')) { errorMessage = 'File validation failed. Please ensure the file is valid and try again.'; } else { errorMessage = 'Server error during upload. Please try again later.'; } } console.error('❌ Upload error details:', error); setUploadError(errorMessage); return { success: false, error: errorMessage }; } finally { setUploadingFile(false); } }; const handleFileUpdate = async (fileId: string, updateData: Partial<{ fileName: string }>) => { setUploadError(null); // Reuse upload error state for update operations try { console.log(`✏️ Starting update for file ID: ${fileId}`, updateData); const updatedFile = await request({ url: `/api/files/${fileId}`, method: 'put', data: updateData }); console.log(`✅ Update successful for file ID: ${fileId}`); return { success: true, fileData: updatedFile }; } catch (error: any) { console.error(`❌ Update failed for file ID ${fileId}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to update this file.`; } else if (error.response?.status === 400) { errorMessage = `Invalid file update data.`; } setUploadError(errorMessage); return { success: false, error: errorMessage }; } }; const handleFilePreview = async (fileId: string, fileName: string) => { setPreviewError(null); setPreviewingFiles(prev => new Set(prev).add(fileId)); try { console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`); // First try to get JSON response (for text-based files) try { const jsonResponse = await request({ url: `/api/files/${fileId}/preview`, method: 'get', additionalConfig: { responseType: 'json', validateStatus: function (status: number) { return status >= 200 && status < 300; } } }); console.log(`✅ JSON preview successful for: ${fileName}`, jsonResponse); // Check if response has content field (structured response) if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { const content = jsonResponse.content; const mimeType = jsonResponse.mimeType || 'text/plain'; console.log('📄 Structured JSON response detected:', { hasContent: !!content, mimeType: mimeType, contentLength: content?.length, contentPreview: content?.substring(0, 100) + '...' }); // Check if content is base64 encoded (common pattern) let decodedContent = content; try { // Try to decode as base64 if it looks like base64 if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { console.log('📄 Content appears to be base64 encoded, attempting decode...'); decodedContent = atob(content); console.log('📄 Base64 decode successful:', { originalLength: content.length, decodedLength: decodedContent.length, decodedPreview: decodedContent.substring(0, 200) + '...' }); } } catch (decodeError) { console.log('📄 Base64 decode failed, using original content:', decodeError); decodedContent = content; } // Create a blob from the (possibly decoded) content const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); console.log('🔗 Created blob URL:', url); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) { // Handle base64 encoded content in 'result' field console.log('📄 Base64 encoded content detected in result field'); try { // Decode base64 content const decodedContent = atob(jsonResponse.result); const mimeType = jsonResponse.mimeType || 'application/json'; console.log('📄 Decoded content:', { length: decodedContent.length, preview: decodedContent.substring(0, 200) + '...', mimeType: mimeType, originalResult: jsonResponse.result.substring(0, 100) + '...', decodedFirstChars: decodedContent.substring(0, 50) }); // Create a blob from the decoded content const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); console.log('🔗 Created blob URL for decoded content:', url); console.log('🔍 Blob details:', { size: blob.size, type: blob.type, url: url }); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } catch (decodeError) { console.error('❌ Failed to decode base64 content:', decodeError); // Fallback to treating as raw JSON const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } else { console.log('📄 Raw JSON response, treating as content'); // If it's not structured JSON, treat as raw content const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } catch (jsonError) { console.log('JSON preview failed, trying blob response...', jsonError); // Fallback to blob response for binary files const previewData = await request({ url: `/api/files/${fileId}/preview`, method: 'get', additionalConfig: { responseType: 'blob', validateStatus: function (status: number) { return status >= 200 && status < 300; } } }); console.log(`✅ Blob preview successful for: ${fileName}`, { size: previewData.size, type: previewData.type }); // Create a blob URL for preview const url = window.URL.createObjectURL(previewData); return { success: true, previewUrl: url, blob: previewData, isJsonContent: false }; } } catch (error: any) { console.error(`❌ Preview failed for ${fileName}:`, error); let errorMessage = error.message; if (error.response?.status === 404) { errorMessage = `File "${fileName}" not found or has been deleted.`; } else if (error.response?.status === 403) { errorMessage = `No permission to preview "${fileName}".`; } else if (error.response?.status === 415) { errorMessage = `File type "${fileName}" is not supported for preview.`; } setPreviewError(errorMessage); return { success: false, error: errorMessage }; } finally { setPreviewingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileId); return newSet; }); } }; return { downloadingFiles, deletingFiles, uploadingFile, downloadError, deleteError, uploadError, previewingFiles, previewError, handleFileDownload, handleFileDelete, handleFileUpload, handleFileUpdate, handleFilePreview, isLoading }; }