import { useState, useEffect, useRef } from 'react'; import * as sourcesApi from '../api/sources.js'; const ALLOWED_EXTENSIONS = ['.pdf', '.txt', '.md', '.docx', '.mp3', '.wav', '.m4a', '.ogg', '.flac', '.webm']; const FILE_ACCEPT = ALLOWED_EXTENSIONS.join(','); function isFileAllowed(file) { const ext = file.name.slice(file.name.lastIndexOf('.')).toLowerCase(); return ALLOWED_EXTENSIONS.includes(ext); } function SourcePanel({ sourceCorpusId, hoveredSourceIndex, onSourceHover, onSourcesChange, children }) { const [sources, setSources] = useState([]); const [uploading, setUploading] = useState(false); const [isDragging, setIsDragging] = useState(false); const [urlInput, setUrlInput] = useState(''); const [addingUrl, setAddingUrl] = useState(false); const [error, setError] = useState(null); const fileRef = useRef(null); const dragCounter = useRef(0); const errorTimer = useRef(null); const showError = (msg) => { setError(msg); clearTimeout(errorTimer.current); errorTimer.current = setTimeout(() => setError(null), 8000); }; useEffect(() => { setSources([]); onSourcesChange?.(0); sourcesApi.listSources(sourceCorpusId).then((s) => { setSources(s); onSourcesChange?.(s.length); }).catch((err) => showError(err.message || 'Failed to load sources')); }, [sourceCorpusId, onSourcesChange]); useEffect(() => () => clearTimeout(errorTimer.current), []); const uploadFile = async (file) => { if (!file) return; if (!isFileAllowed(file)) { showError(`Unsupported file type. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`); return; } setError(null); setUploading(true); try { const src = await sourcesApi.uploadSource(sourceCorpusId, file); setSources((prev) => { const next = [...prev, src]; onSourcesChange?.(next.length); return next; }); } catch (err) { showError(err.message || 'Upload failed'); } finally { setUploading(false); if (fileRef.current) fileRef.current.value = ''; } }; const handleUpload = (e) => { uploadFile(e.target.files?.[0]); }; const handleAddUrl = async (e) => { e.preventDefault(); const url = urlInput.trim(); if (!url || addingUrl) return; setError(null); setAddingUrl(true); try { const src = await sourcesApi.addUrlSource(sourceCorpusId, url); if (src.error) { showError(src.error); } else { setSources((prev) => { const next = [...prev, src]; onSourcesChange?.(next.length); return next; }); setUrlInput(''); } } catch (err) { showError(err.message || 'Failed to add URL'); } finally { setAddingUrl(false); } }; const handleDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); dragCounter.current++; setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); e.stopPropagation(); dragCounter.current--; if (dragCounter.current === 0) setIsDragging(false); }; const handleDragOver = (e) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); dragCounter.current = 0; if (uploading) return; const file = e.dataTransfer.files?.[0]; uploadFile(file); }; const busy = uploading || addingUrl; return (

Sources

[ or drag and drop ]
setUrlInput(e.target.value)} disabled={busy} />
{error && (
{error}
)} {children}
); } export default SourcePanel;