Hemang Thakur
deploy
d5c104e
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;