chore: clean up usePromptFileHandling

This commit is contained in:
Dustin Healy 2025-09-06 00:36:36 -07:00
parent 607a5a2fcf
commit 69772317b2

View file

@ -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<string, TFile> = {};
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<ExtendedFile[]>(() => {
return params?.initialFiles || [];
});
const [filesLoading, setFilesLoading] = useState(false);
const [, setFilesLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(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<HTMLInputElement>, 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,