llama1's picture
Upload 781 files
5da4770 verified
'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<React.SetStateAction<File[]>>,
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>,
) => {
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<React.SetStateAction<UploadedFile[]>>,
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>,
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<React.SetStateAction<File[]>>,
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>,
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>,
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<React.SetStateAction<File[]>>;
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>;
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>;
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<HTMLInputElement>,
) => {
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 (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
type="button"
onClick={handleFileUpload}
variant="outline"
size="sm"
className="h-8 px-3 py-2 bg-transparent border border-border rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2"
disabled={
!isLoggedIn || loading || (disabled && !isAgentRunning) || isUploading
}
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Paperclip className="h-4 w-4" />
)}
<span className="text-sm">Attach</span>
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="top">
<p>{isLoggedIn ? 'Attach files' : 'Please login to attach files'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<input
type="file"
ref={ref}
className="hidden"
onChange={processFileUpload}
multiple
/>
</>
);
},
);
FileUploadHandler.displayName = 'FileUploadHandler';
export { handleFiles, handleLocalFiles, uploadFiles };