diff --git a/client/src/hooks/Prompts/usePromptFileHandling.ts b/client/src/hooks/Prompts/usePromptFileHandling.ts index 82f15efdac..0dcdb0890f 100644 --- a/client/src/hooks/Prompts/usePromptFileHandling.ts +++ b/client/src/hooks/Prompts/usePromptFileHandling.ts @@ -1,11 +1,10 @@ -import React, { useState, useCallback, useMemo, useRef } from 'react'; import { v4 } from 'uuid'; import { useToastContext } from '@librechat/client'; +import { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { EModelEndpoint, EToolResources, FileSources } from 'librechat-data-provider'; import type { AgentToolResources, TFile } from 'librechat-data-provider'; import type { ExtendedFile } from '~/common'; import { useUploadFileMutation, useGetFiles } from '~/data-provider'; -import { useAuthContext } from '~/hooks'; import { logger } from '~/utils'; interface UsePromptFileHandling { @@ -14,28 +13,25 @@ interface UsePromptFileHandling { onFileChange?: (updatedFiles: ExtendedFile[]) => void; // Callback when files are added/removed } -/** - * Simplified file handling hook for prompts that doesn't depend on ChatContext - */ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { const { showToast } = useToastContext(); - const { user } = useAuthContext(); const { data: allFiles = [] } = useGetFiles(); - // Create a fileMap for quick lookup const fileMap = useMemo(() => { const map: Record = {}; - allFiles.forEach((file) => { - if (file.file_id) { - map[file.file_id] = file; - } - }); + if (Array.isArray(allFiles)) { + allFiles.forEach((file) => { + if (file.file_id) { + map[file.file_id] = file; + } + }); + } return map; }, [allFiles]); const [files, setFiles] = useState(() => { return params?.initialFiles || []; }); - const [filesLoading, setFilesLoading] = useState(false); + const [, setFilesLoading] = useState(false); const abortControllerRef = useRef(null); const uploadFile = useUploadFileMutation({ @@ -70,7 +66,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { status: 'success', }); - // Call the onFileChange callback to trigger save with updated files const updatedFiles = files.map((file) => { if (file.temp_file_id === data.temp_file_id) { return { @@ -96,29 +91,26 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { logger.error('File upload error:', error); setFilesLoading(false); - // Remove the failed file from the UI const file_id = body.get('file_id'); if (file_id) { setFiles((prev) => { return prev.filter((file) => { if (file.file_id === file_id || file.temp_file_id === file_id) { - // Clean up blob URL if it exists if (file.preview && file.preview.startsWith('blob:')) { URL.revokeObjectURL(file.preview); } - return false; // Remove this file + return false; } - return true; // Keep this file + return true; }); }); } - // Show specific error message let errorMessage = 'Failed to upload file'; - if (error?.response?.data?.message) { - errorMessage = error.response.data.message; - } else if (error?.message) { - errorMessage = error.message; + if ((error as any)?.response?.data?.message) { + errorMessage = (error as any).response.data.message; + } else if ((error as any)?.message) { + errorMessage = (error as any).message; } showToast({ @@ -128,22 +120,18 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { }, }); - // Files are already an array, no conversion needed const promptFiles = files; - // Call fileSetter when files change - React.useEffect(() => { + useEffect(() => { if (params?.fileSetter) { params.fileSetter(files); } - }, [files, params?.fileSetter]); + }, [files, params]); - // Load image and extract dimensions (like useFileHandling does) const loadImage = useCallback( (extendedFile: ExtendedFile, preview: string) => { const img = new Image(); img.onload = async () => { - // Update the file with dimensions extendedFile.width = img.width; extendedFile.height = img.height; extendedFile.progress = 0.6; @@ -156,14 +144,16 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { prev.map((file) => (file.file_id === extendedFile.file_id ? updatedFile : file)), ); - // Create form data for upload const formData = new FormData(); formData.append('endpoint', EModelEndpoint.agents); - formData.append('file', extendedFile.file!, encodeURIComponent(extendedFile.filename)); + formData.append( + 'file', + extendedFile.file!, + encodeURIComponent(extendedFile.filename || ''), + ); formData.append('file_id', extendedFile.file_id); formData.append('message_file', 'true'); // For prompts, treat as message attachment - // Include dimensions for image recognition formData.append('width', img.width.toString()); formData.append('height', img.height.toString()); @@ -171,7 +161,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { formData.append('tool_resource', extendedFile.tool_resource.toString()); } - // Upload the file with dimensions uploadFile.mutate(formData); }; img.src = preview; @@ -179,7 +168,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { [uploadFile], ); - // Handle file uploads const handleFileChange = useCallback( (event: React.ChangeEvent, toolResource?: EToolResources | string) => { event.stopPropagation(); @@ -190,9 +178,8 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { fileList.forEach(async (file) => { const file_id = v4(); - const temp_file_id = file_id; // Use same ID initially, backend will reassign + const temp_file_id = file_id; - // Add file to state immediately with progress indicator const extendedFile: ExtendedFile = { file_id, temp_file_id, @@ -211,16 +198,14 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { setFiles((prev) => [...prev, extendedFile]); - // For images, load and extract dimensions before upload if (file.type.startsWith('image/') && extendedFile.preview) { loadImage(extendedFile, extendedFile.preview); } else { - // For non-images, upload immediately const formData = new FormData(); formData.append('endpoint', EModelEndpoint.agents); formData.append('file', file, encodeURIComponent(file.name)); formData.append('file_id', file_id); - formData.append('message_file', 'true'); // For prompts, treat as message attachment + formData.append('message_file', 'true'); if (toolResource) { formData.append('tool_resource', toolResource.toString()); @@ -230,65 +215,52 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { } }); - // Reset input event.target.value = ''; }, [uploadFile, loadImage], ); - // Handle file removal const handleFileRemove = useCallback( (fileId: string) => { - // For prompts, we only remove the file from the current prompt's tool_resources - // We don't delete the file from the database to preserve previous versions - setFiles((prev) => { return prev.filter((file) => { if (file.file_id === fileId || file.temp_file_id === fileId) { - // Clean up blob URL if it exists if (file.preview && file.preview.startsWith('blob:')) { URL.revokeObjectURL(file.preview); } - return false; // Remove this file + return false; } - return true; // Keep this file + return true; }); }); - // Call the onFileChange callback to trigger prompt version update with updated files const updatedFiles = files.filter((file) => { if (file.file_id === fileId || file.temp_file_id === fileId) { - return false; // Remove this file + return false; } - return true; // Keep this file + return true; }); params?.onFileChange?.(updatedFiles); }, - [params?.onFileChange], + [files, params], ); - // Sync with external fileSetter when files change - React.useEffect(() => { + useEffect(() => { if (params?.fileSetter) { params.fileSetter(promptFiles); } - }, [promptFiles, params?.fileSetter]); + }, [promptFiles, params]); - // Cleanup blob URLs on unmount - React.useEffect(() => { + useEffect(() => { return () => { - // Clean up all blob URLs when component unmounts files.forEach((file) => { if (file.preview && file.preview.startsWith('blob:')) { URL.revokeObjectURL(file.preview); } }); }; - }, []); + }, [files]); - /** - * Convert current files to tool_resources format for API submission - */ const getToolResources = useCallback((): AgentToolResources | undefined => { if (promptFiles.length === 0) { return undefined; @@ -297,38 +269,20 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { const toolResources: AgentToolResources = {}; promptFiles.forEach((file) => { - if (!file.file_id) return; // Skip files that haven't been uploaded yet + if (!file.file_id || !file.tool_resource) return; - // Determine tool resource type based on file type or explicit tool_resource - let toolResource: EToolResources; - - if (file.tool_resource) { - toolResource = file.tool_resource as EToolResources; - } else if (file.type?.startsWith('image/')) { - toolResource = EToolResources.image_edit; - } else if (file.type === 'application/pdf' || file.type?.includes('text')) { - toolResource = EToolResources.file_search; - } else { - toolResource = EToolResources.file_search; // Default fallback + if (!toolResources[file.tool_resource]) { + toolResources[file.tool_resource] = { file_ids: [] }; } - // Initialize the tool resource if it doesn't exist - if (!toolResources[toolResource]) { - toolResources[toolResource] = { file_ids: [] }; - } - - // Add file_id to the appropriate tool resource - if (!toolResources[toolResource]!.file_ids!.includes(file.file_id)) { - toolResources[toolResource]!.file_ids!.push(file.file_id); + if (!toolResources[file.tool_resource]!.file_ids!.includes(file.file_id)) { + toolResources[file.tool_resource]!.file_ids!.push(file.file_id); } }); return Object.keys(toolResources).length > 0 ? toolResources : undefined; }, [promptFiles]); - /** - * Load files from tool_resources format (for editing existing prompts) - */ const loadFromToolResources = useCallback( async (toolResources?: AgentToolResources) => { if (!toolResources) { @@ -338,7 +292,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { const filesArray: ExtendedFile[] = []; - // Process all files and create blob URLs for images for (const [toolResource, resource] of Object.entries(toolResources)) { if (resource?.file_ids) { for (const fileId of resource.file_ids) { @@ -351,7 +304,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { let file: ExtendedFile; if (dbFile) { - // Use real file metadata from database file = { file_id: dbFile.file_id, temp_file_id: dbFile.file_id, @@ -359,7 +311,7 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { filename: dbFile.filename, filepath: dbFile.filepath, progress: 1, - preview: dbFile.filepath, // Use filepath as preview for existing files + preview: dbFile.filepath, size: dbFile.bytes || 0, width: dbFile.width, height: dbFile.height, @@ -369,7 +321,6 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { source, }; } else { - // Fallback to placeholder if file not found in database file = { file_id: fileId, temp_file_id: fileId, @@ -394,19 +345,13 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { setFiles(filesArray); }, - [fileMap, user?.id], + [fileMap], ); - /** - * Check if all files have been uploaded successfully - */ const areFilesReady = useMemo(() => { return promptFiles.every((file) => file.file_id && file.progress === 1); }, [promptFiles]); - /** - * Get count of files by type - */ const fileStats = useMemo(() => { const stats = { total: promptFiles.length, @@ -437,16 +382,11 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => { }, []); return { - // File handling functions handleFileChange, abortUpload, - - // File state files, setFiles, promptFiles, - - // Utility functions getToolResources, loadFromToolResources, areFilesReady,