import React, { useState, useRef, useCallback } from 'react'; import { FaTimes, FaFileUpload, FaFileAlt } from 'react-icons/fa'; import Button from '@mui/material/Button'; import './AddFilesDialog.css'; const MAX_TOTAL_SIZE = 10 * 1024 * 1024; // 10 MB const ALLOWED_EXTENSIONS = new Set([ // Documents '.pdf', '.doc', '.docx', '.odt', '.txt', '.rtf', '.md', // Spreadsheets '.csv', '.xls', '.xlsx', // Presentations '.ppt', '.pptx', // Code files '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp', '.h', '.cs', '.html', '.css', '.scss', '.json', '.xml', '.sql', '.sh', '.rb', '.php', '.go' ]); function AddFilesDialog({ isOpen, onClose, openSnackbar, setSessionContent }) { const [isUploading, setIsUploading] = useState(false); const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState([]); const [urlInput, setUrlInput] = useState(""); const fileInputRef = useRef(null); // Function to handle files dropped or selected const handleFiles = useCallback((incomingFiles) => { if (incomingFiles && incomingFiles.length > 0) { let currentTotalSize = files.reduce((acc, f) => acc + f.file.size, 0); const validFiles = []; for (const file of Array.from(incomingFiles)) { // 1. Check for duplicates if (files.some(existing => existing.file.name === file.name && existing.file.size === file.size)) { continue; // Skip duplicate file } // 2. Check file type const fileExtension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase(); if (!ALLOWED_EXTENSIONS.has(fileExtension)) { openSnackbar(`File type not supported: ${file.name}`, 'error', 5000); continue; // Skip unsupported file type } // 3. Check total size limit if (currentTotalSize + file.size > MAX_TOTAL_SIZE) { openSnackbar('Total file size cannot exceed 10 MB', 'error', 5000); break; // Stop processing further files as limit is reached } currentTotalSize += file.size; validFiles.push({ id: window.crypto.randomUUID(), file: file, progress: 0, }); } if (validFiles.length > 0) { setFiles(prevFiles => [...prevFiles, ...validFiles]); } } }, [files, openSnackbar]); // Function to handle file removal const handleRemoveFile = useCallback((fileId) => { setFiles(prevFiles => prevFiles.filter(f => f.id !== fileId)); }, []); // Ensure that the component does not render if isOpen is false if (!isOpen) { return null; } // Function to format file size in a human-readable format const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Handlers for drag and drop events const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; // Handler for when the drag leaves the drop zone const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; // Handler for when files are dropped into the drop zone const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); handleFiles(e.dataTransfer.files); }; // Handler for when files are selected via the file input const handleFileSelect = (e) => { handleFiles(e.target.files); // Reset input value to allow selecting the same file again e.target.value = null; }; // Handler for clicking the drop zone to open the file dialog const handleBoxClick = () => { fileInputRef.current.click(); }; // Handler for resetting the file list const handleReset = () => { setFiles([]); setUrlInput(""); }; // Handler for adding files const handleAdd = () => { setIsUploading(true); // Start upload state, disable buttons // Regex to validate URL format const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/; const urls = urlInput.split('\n').map(url => url.trim()).filter(url => url); // 1. Validate URLs before proceeding if (files.length === 0 && urls.length === 0) { openSnackbar("Please add files or URLs before submitting.", "error", 5000); return; } for (const url of urls) { if (!urlRegex.test(url)) { openSnackbar(`Invalid URL format: ${url}`, 'error', 5000); setIsUploading(false); // Reset upload state on validation error return; // Stop the process if an invalid URL is found } } // 2. If all URLs are valid, proceed with logging/uploading const formData = new FormData(); if (files.length > 0) { files.forEach(fileWrapper => { formData.append('files', fileWrapper.file, fileWrapper.file.name); }); } formData.append('urls', JSON.stringify(urls)); const xhr = new XMLHttpRequest(); xhr.open('POST', '/add-content', true); // Track upload progress xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentage = Math.round((event.loaded / event.total) * 100); setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: percentage })) ); } }; // Handle completion xhr.onload = () => { if (xhr.status === 200) { // --- ARTIFICIAL DELAY FOR LOCAL DEVELOPMENT --- // This timeout ensures the 100% progress bar is visible before the dialog closes. // This can be removed for production. setTimeout(() => { const result = JSON.parse(xhr.responseText); openSnackbar('Content added successfully!', 'success'); setSessionContent(prev => ({ files: [...prev.files, ...result.files_added], links: [...prev.links, ...result.links_added], })); handleReset(); onClose(); }, 500); // 0.5-second delay } else { const errorResult = JSON.parse(xhr.responseText); openSnackbar(errorResult.detail || 'Failed to add content.', 'error', 5000); setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error setIsUploading(false); // End upload state } }; // Handle network errors xhr.onerror = () => { openSnackbar('An error occurred during the upload. Please check your network.', 'error', 5000); setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error }; xhr.send(formData); }; return (
e.stopPropagation()}>