'use client'; import React, { forwardRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Paperclip, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { createClient } from '@/lib/supabase/client'; import { useQueryClient } from '@tanstack/react-query'; import { fileQueryKeys } from '@/hooks/react-query/files/use-file-queries'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { UploadedFile } from './chat-input'; import { normalizeFilenameToNFC } from '@/lib/utils/unicode'; const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; const handleLocalFiles = ( files: File[], setPendingFiles: React.Dispatch>, setUploadedFiles: React.Dispatch>, ) => { const filteredFiles = files.filter((file) => { if (file.size > 50 * 1024 * 1024) { toast.error(`File size exceeds 50MB limit: ${file.name}`); return false; } return true; }); setPendingFiles((prevFiles) => [...prevFiles, ...filteredFiles]); const newUploadedFiles: UploadedFile[] = filteredFiles.map((file) => { // Normalize filename to NFC const normalizedName = normalizeFilenameToNFC(file.name); return { name: normalizedName, path: `/workspace/${normalizedName}`, size: file.size, type: file.type || 'application/octet-stream', localUrl: URL.createObjectURL(file) }; }); setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); filteredFiles.forEach((file) => { const normalizedName = normalizeFilenameToNFC(file.name); toast.success(`File attached: ${normalizedName}`); }); }; const uploadFiles = async ( files: File[], sandboxId: string, setUploadedFiles: React.Dispatch>, setIsUploading: React.Dispatch>, messages: any[] = [], // Add messages parameter to check for existing files queryClient?: any, // Add queryClient parameter for cache invalidation ) => { try { setIsUploading(true); const newUploadedFiles: UploadedFile[] = []; for (const file of files) { if (file.size > 50 * 1024 * 1024) { toast.error(`File size exceeds 50MB limit: ${file.name}`); continue; } // Normalize filename to NFC const normalizedName = normalizeFilenameToNFC(file.name); const uploadPath = `/workspace/${normalizedName}`; // Check if this filename already exists in chat messages const isFileInChat = messages.some(message => { const content = typeof message.content === 'string' ? message.content : ''; return content.includes(`[Uploaded File: ${uploadPath}]`); }); const formData = new FormData(); // If the filename was normalized, append with the normalized name in the field name // The server will use the path parameter for the actual filename formData.append('file', file, normalizedName); formData.append('path', uploadPath); const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new Error('No access token available'); } const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, }, body: formData, }); if (!response.ok) { throw new Error(`Upload failed: ${response.statusText}`); } // If file was already in chat and we have queryClient, invalidate its cache if (isFileInChat && queryClient) { console.log(`Invalidating cache for existing file: ${uploadPath}`); // Invalidate all content types for this file ['text', 'blob', 'json'].forEach(contentType => { const queryKey = fileQueryKeys.content(sandboxId, uploadPath, contentType); queryClient.removeQueries({ queryKey }); }); // Also invalidate directory listing const directoryPath = uploadPath.substring(0, uploadPath.lastIndexOf('/')); queryClient.invalidateQueries({ queryKey: fileQueryKeys.directory(sandboxId, directoryPath), }); } newUploadedFiles.push({ name: normalizedName, path: uploadPath, size: file.size, type: file.type || 'application/octet-stream', }); toast.success(`File uploaded: ${normalizedName}`); } setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); } catch (error) { console.error('File upload failed:', error); toast.error( typeof error === 'string' ? error : error instanceof Error ? error.message : 'Failed to upload file', ); } finally { setIsUploading(false); } }; const handleFiles = async ( files: File[], sandboxId: string | undefined, setPendingFiles: React.Dispatch>, setUploadedFiles: React.Dispatch>, setIsUploading: React.Dispatch>, messages: any[] = [], // Add messages parameter queryClient?: any, // Add queryClient parameter ) => { if (sandboxId) { // If we have a sandboxId, upload files directly await uploadFiles(files, sandboxId, setUploadedFiles, setIsUploading, messages, queryClient); } else { // Otherwise, store files locally handleLocalFiles(files, setPendingFiles, setUploadedFiles); } }; interface FileUploadHandlerProps { loading: boolean; disabled: boolean; isAgentRunning: boolean; isUploading: boolean; sandboxId?: string; setPendingFiles: React.Dispatch>; setUploadedFiles: React.Dispatch>; setIsUploading: React.Dispatch>; messages?: any[]; // Add messages prop isLoggedIn?: boolean; } export const FileUploadHandler = forwardRef< HTMLInputElement, FileUploadHandlerProps >( ( { loading, disabled, isAgentRunning, isUploading, sandboxId, setPendingFiles, setUploadedFiles, setIsUploading, messages = [], isLoggedIn = true, }, ref, ) => { const queryClient = useQueryClient(); // Clean up object URLs when component unmounts useEffect(() => { return () => { // Clean up any object URLs to avoid memory leaks setUploadedFiles(prev => { prev.forEach(file => { if (file.localUrl) { URL.revokeObjectURL(file.localUrl); } }); return prev; }); }; }, []); const handleFileUpload = () => { if (ref && 'current' in ref && ref.current) { ref.current.click(); } }; const processFileUpload = async ( event: React.ChangeEvent, ) => { if (!event.target.files || event.target.files.length === 0) return; const files = Array.from(event.target.files); // Use the helper function instead of the static method handleFiles( files, sandboxId, setPendingFiles, setUploadedFiles, setIsUploading, messages, queryClient, ); event.target.value = ''; }; return ( <>

{isLoggedIn ? 'Attach files' : 'Please login to attach files'}

); }, ); FileUploadHandler.displayName = 'FileUploadHandler'; export { handleFiles, handleLocalFiles, uploadFiles };