ui-nyla/src/hooks/useFiles.ts

586 lines
No EOL
20 KiB
TypeScript

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<UserFile[]>([]);
const { request, isLoading: loading, error } = useApiRequest<null, FileInfo[]>();
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<Set<string>>(new Set());
const [deletingFiles, setDeletingFiles] = useState<Set<string>>(new Set());
const [uploadingFile, setUploadingFile] = useState(false);
const { request, isLoading } = useApiRequest();
const [downloadError, setDownloadError] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [uploadError, setUploadError] = useState<string | null>(null);
const [previewingFiles, setPreviewingFiles] = useState<Set<string>>(new Set());
const [previewError, setPreviewError] = useState<string | null>(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
};
}