import { useState, useEffect, useRef, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useAuth } from '@clerk/clerk-react'
import { API_BASE } from '../config.js'
function DocumentRow({ doc, index, onDelete }) {
const [deleting, setDeleting] = useState(false)
const handleDelete = async (e) => {
e.stopPropagation()
if (!confirm(`Delete "${doc.name}" and all its indexed chunks?`)) return
setDeleting(true)
await onDelete(doc.name)
setDeleting(false)
}
return (
{doc.chunks} chunk{doc.chunks !== 1 ? 's' : ''}
{deleting ? (
) : (
)}
)
}
function UploadResultBadge({ result }) {
const styles = {
ok: 'bg-emerald-500/15 border-emerald-500/30 text-emerald-400',
queued: 'bg-accent/10 border-accent/25 text-accent/80',
warning: 'bg-yellow-500/15 border-yellow-500/30 text-yellow-400',
error: 'bg-red-500/15 border-red-500/30 text-red-400',
skipped: 'bg-white/5 border-white/10 text-muted',
}
const icons = {
ok: ,
queued: ,
warning: <>>,
error: <>>,
skipped: <>>,
}
const style = styles[result.status] || styles.skipped
return (
{result.name}
{result.status === 'ok' && {result.chunks} chunk{result.chunks !== 1 ? 's' : ''} from {result.pages} page{result.pages !== 1 ? 's' : ''}}
{result.message && {result.message}}
)
}
export default function UploadPanel({ onUploaded }) {
const { getToken } = useAuth()
const [isOpen, setIsOpen] = useState(false)
const [isDragOver, setIsDragOver] = useState(false)
const [pendingFiles, setPendingFiles] = useState([])
const [uploadStatus, setUploadStatus] = useState('idle')
const [uploadResults, setUploadResults] = useState([])
const [documents, setDocuments] = useState([])
const [totalChunks, setTotalChunks] = useState(0)
const [loadingDocs, setLoadingDocs] = useState(false)
const fileInputRef = useRef(null)
const authHeaders = useCallback(async () => {
const token = await getToken()
return token ? { Authorization: `Bearer ${token}` } : {}
}, [getToken])
const fetchDocuments = useCallback(async () => {
setLoadingDocs(true)
try {
const res = await fetch(`${API_BASE}/documents`, { headers: await authHeaders() })
const data = await res.json()
setDocuments(data.documents || [])
setTotalChunks(data.total_chunks || 0)
} catch {
// backend not running — silently ignore
} finally {
setLoadingDocs(false)
}
}, [authHeaders])
useEffect(() => {
if (isOpen) fetchDocuments()
}, [isOpen, fetchDocuments])
const addFiles = useCallback((fileList) => {
const pdfs = Array.from(fileList).filter(f => f.name.toLowerCase().endsWith('.pdf'))
if (!pdfs.length) return
setPendingFiles(prev => {
const existing = new Set(prev.map(f => f.name))
return [...prev, ...pdfs.filter(f => !existing.has(f.name))]
})
}, [])
const handleDragOver = (e) => { e.preventDefault(); setIsDragOver(true) }
const handleDragLeave = (e) => { if (!e.currentTarget.contains(e.relatedTarget)) setIsDragOver(false) }
const handleDrop = (e) => { e.preventDefault(); setIsDragOver(false); addFiles(e.dataTransfer.files) }
const handleFileInput = (e) => { addFiles(e.target.files); e.target.value = '' }
const removeFile = (name) => setPendingFiles(prev => prev.filter(f => f.name !== name))
const handleUpload = async () => {
if (!pendingFiles.length || uploadStatus === 'uploading') return
setUploadStatus('uploading')
setUploadResults([])
const formData = new FormData()
pendingFiles.forEach(f => formData.append('files', f))
try {
const res = await fetch(`${API_BASE}/upload`, {
method: 'POST',
headers: await authHeaders(),
body: formData,
})
if (!res.ok) {
let msg = `Server error ${res.status}`
try { const e = await res.json(); if (e.detail) msg = `${res.status}: ${e.detail}` } catch {}
throw new Error(msg)
}
const data = await res.json()
const results = data.results || []
setUploadResults(results)
setTotalChunks(data.total_chunks || totalChunks)
setPendingFiles([])
setUploadStatus('done')
fetchDocuments()
onUploaded?.()
// If any files are queued for background indexing, poll until they appear
if (results.some(r => r.status === 'queued')) {
let attempts = 0
const poll = setInterval(async () => {
attempts++
await fetchDocuments()
if (attempts >= 12) clearInterval(poll) // stop after 2 min
}, 10000) // every 10 s
}
} catch (err) {
setUploadResults([{ name: 'Upload failed', status: 'error', message: err.message }])
setUploadStatus('error')
}
}
const handleDelete = async (name) => {
try {
const res = await fetch(`${API_BASE}/document/${encodeURIComponent(name)}`, {
method: 'DELETE',
headers: await authHeaders(),
})
if (!res.ok) throw new Error(`Server error ${res.status}`)
await fetchDocuments()
onUploaded?.()
} catch (err) {
alert(`Delete failed: ${err.message}`)
}
}
const successCount = uploadResults.filter(r => r.status === 'ok').length
return (
{isOpen && (
fileInputRef.current?.click()}
className="relative rounded-xl p-6 flex flex-col items-center gap-3 cursor-pointer select-none"
style={{ border: '1.5px dashed rgba(99,102,241,0.3)' }}
>
{isDragOver ? 'Drop PDFs here' : 'Drag & drop PDFs here'}
or click to browse · PDF only
{pendingFiles.length > 0 && (
Ready to upload ({pendingFiles.length})
{pendingFiles.map(file => (
{file.name}
{(file.size / 1024).toFixed(0)} KB
))}
{uploadStatus === 'uploading' ? (<>Uploading & indexing…>) : (<>Upload {pendingFiles.length} file{pendingFiles.length !== 1 ? 's' : ''}>)}
)}
{uploadResults.length > 0 && (
{successCount > 0 && {successCount} file{successCount !== 1 ? 's' : ''} indexed successfully
}
{uploadResults.map((r, i) => )}
)}
{documents.length === 0 && !loadingDocs &&
No documents indexed yet
}
{documents.map((doc, i) => )}
{documents.length > 0 &&
{documents.length} documents · {totalChunks} total chunks
}
)}
)
}