Spaces:
Paused
Paused
| 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 ( | |
| <div className="add-files-dialog" onClick={isUploading ? null : onClose}> | |
| <div className="add-files-dialog-inner" onClick={(e) => e.stopPropagation()}> | |
| <label className="dialog-title">Add Files and Links</label> | |
| <button className="close-btn" onClick={onClose} disabled={isUploading}> | |
| <FaTimes /> | |
| </button> | |
| <div className="dialog-content-area"> | |
| <div className="url-input-container"> | |
| <textarea | |
| id="url-input" | |
| className="url-input-textarea" | |
| placeholder="Enter one URL per line" | |
| value={urlInput} | |
| onChange={(e) => setUrlInput(e.target.value)} | |
| /> | |
| </div> | |
| <div | |
| className={`file-drop-zone ${isDragging ? 'dragging' : ''}`} | |
| onClick={handleBoxClick} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| > | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| onChange={handleFileSelect} | |
| style={{ display: 'none' }} | |
| multiple | |
| /> | |
| <FaFileUpload className="upload-icon" /> | |
| <p>Drag and drop files here, or click to select files</p> | |
| </div> | |
| {files.length > 0 && ( | |
| <div className="file-list"> | |
| {files.map(fileWrapper => ( | |
| <div key={fileWrapper.id} className="file-item"> | |
| <FaFileAlt className="file-icon" /> | |
| <div className="file-info"> | |
| <span className="file-name">{fileWrapper.file.name}</span> | |
| <span className="file-size">{formatFileSize(fileWrapper.file.size)}</span> | |
| </div> | |
| {isUploading && ( | |
| <div className="progress-bar-container"> | |
| <div className="progress-bar" style={{ width: `${fileWrapper.progress}%` }}></div> | |
| </div> | |
| )} | |
| <button className="cancel-file-btn" onClick={() => handleRemoveFile(fileWrapper.id)} disabled={isUploading}> | |
| <FaTimes /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="dialog-actions"> | |
| <Button | |
| disabled={isUploading} | |
| onClick={handleReset} | |
| sx={{ color: "#2196f3" }} | |
| > | |
| Reset | |
| </Button> | |
| <Button | |
| disabled={isUploading} | |
| onClick={handleAdd} | |
| variant="contained" | |
| color="success" | |
| > | |
| Add | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default AddFilesDialog; |