|
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; |
|
const ALLOWED_EXTENSIONS = new Set([ |
|
|
|
'.pdf', '.doc', '.docx', '.odt', '.txt', '.rtf', '.md', |
|
|
|
'.csv', '.xls', '.xlsx', |
|
|
|
'.ppt', '.pptx', |
|
|
|
'.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); |
|
|
|
|
|
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)) { |
|
|
|
if (files.some(existing => existing.file.name === file.name && existing.file.size === file.size)) { |
|
continue; |
|
} |
|
|
|
|
|
const fileExtension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase(); |
|
if (!ALLOWED_EXTENSIONS.has(fileExtension)) { |
|
openSnackbar(`File type not supported: ${file.name}`, 'error', 5000); |
|
continue; |
|
} |
|
|
|
|
|
if (currentTotalSize + file.size > MAX_TOTAL_SIZE) { |
|
openSnackbar('Total file size cannot exceed 10 MB', 'error', 5000); |
|
break; |
|
} |
|
|
|
currentTotalSize += file.size; |
|
validFiles.push({ |
|
id: window.crypto.randomUUID(), |
|
file: file, |
|
progress: 0, |
|
}); |
|
} |
|
|
|
if (validFiles.length > 0) { |
|
setFiles(prevFiles => [...prevFiles, ...validFiles]); |
|
} |
|
} |
|
}, [files, openSnackbar]); |
|
|
|
|
|
const handleRemoveFile = useCallback((fileId) => { |
|
setFiles(prevFiles => prevFiles.filter(f => f.id !== fileId)); |
|
}, []); |
|
|
|
|
|
if (!isOpen) { |
|
return null; |
|
} |
|
|
|
|
|
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]; |
|
}; |
|
|
|
|
|
const handleDragOver = (e) => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
setIsDragging(true); |
|
}; |
|
|
|
|
|
const handleDragLeave = (e) => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
setIsDragging(false); |
|
}; |
|
|
|
|
|
const handleDrop = (e) => { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
setIsDragging(false); |
|
handleFiles(e.dataTransfer.files); |
|
}; |
|
|
|
|
|
const handleFileSelect = (e) => { |
|
handleFiles(e.target.files); |
|
|
|
e.target.value = null; |
|
}; |
|
|
|
|
|
const handleBoxClick = () => { |
|
fileInputRef.current.click(); |
|
}; |
|
|
|
|
|
const handleReset = () => { |
|
setFiles([]); |
|
setUrlInput(""); |
|
}; |
|
|
|
|
|
const handleAdd = () => { |
|
setIsUploading(true); |
|
|
|
|
|
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/; |
|
const urls = urlInput.split('\n').map(url => url.trim()).filter(url => url); |
|
|
|
|
|
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); |
|
return; |
|
} |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
xhr.upload.onprogress = (event) => { |
|
if (event.lengthComputable) { |
|
const percentage = Math.round((event.loaded / event.total) * 100); |
|
setFiles(prevFiles => |
|
prevFiles.map(f => ({ ...f, progress: percentage })) |
|
); |
|
} |
|
}; |
|
|
|
|
|
xhr.onload = () => { |
|
if (xhr.status === 200) { |
|
|
|
|
|
|
|
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); |
|
} else { |
|
const errorResult = JSON.parse(xhr.responseText); |
|
openSnackbar(errorResult.detail || 'Failed to add content.', 'error', 5000); |
|
setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); |
|
setIsUploading(false); |
|
} |
|
}; |
|
|
|
|
|
xhr.onerror = () => { |
|
openSnackbar('An error occurred during the upload. Please check your network.', 'error', 5000); |
|
setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); |
|
}; |
|
|
|
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; |