Spaces:
Running
Running
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; |