cc-rag / frontend /src /components /UploadPanel.tsx
matsuap's picture
feat(ui): add drag-and-drop file upload to UploadPanel
596b0e3
Raw
History Blame Contribute Delete
6.25 kB
import { useState, useRef, useCallback } from 'react'
import { Upload, FileText, Zap, CheckCircle, AlertTriangle } from 'lucide-react'
import { api } from '../api'
import type { UploadResponse, BuildGraphResponse, AppSettings } from '../types'
interface Props {
settings: AppSettings
onDocumentReady: (id: string) => void
}
export function UploadPanel({ settings, onDocumentReady }: Props) {
const [uploadResult, setUploadResult] = useState<UploadResponse | null>(null)
const [buildResult, setBuildResult] = useState<BuildGraphResponse | null>(null)
const [uploading, setUploading] = useState(false)
const [building, setBuilding] = useState(false)
const [error, setError] = useState<string | null>(null)
const [dragging, setDragging] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const handleFile = async (file: File) => {
setError(null)
setUploading(true)
try {
const res = await api.upload(file)
setUploadResult(res)
setBuildResult(null)
} catch (e: any) {
setError(String(e.message))
} finally {
setUploading(false)
}
}
const handleSample = async () => {
setError(null)
setUploading(true)
try {
const res = await api.loadSample()
setUploadResult(res)
setBuildResult(null)
} catch (e: any) {
setError(String(e.message))
} finally {
setUploading(false)
}
}
const handleBuild = async () => {
if (!uploadResult) return
setError(null)
setBuilding(true)
try {
const res = await api.buildGraph(uploadResult.document_id, settings)
setBuildResult(res)
onDocumentReady(uploadResult.document_id)
} catch (e: any) {
setError(String(e.message))
} finally {
setBuilding(false)
}
}
const onDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragging(true)
}, [])
const onDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
}, [])
const onDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
const file = e.dataTransfer.files[0]
if (file) handleFile(file)
}, [])
return (
<div style={{ maxWidth: 640, display: 'flex', flexDirection: 'column', gap: 16 }}>
<div
className="card"
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
style={{
border: dragging ? '2px dashed var(--accent)' : undefined,
transition: 'border 0.15s',
}}
>
<div className="label">Document</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<button className="btn-primary" onClick={() => inputRef.current?.click()} disabled={uploading}>
<Upload size={14} style={{ display: 'inline', marginRight: 6 }} />
Upload File
</button>
<button className="btn-secondary" onClick={handleSample} disabled={uploading}>
<Zap size={14} style={{ display: 'inline', marginRight: 6 }} />
Load Sample
</button>
<input
ref={inputRef} type="file" accept=".txt,.md,.pdf"
style={{ display: 'none' }}
onChange={e => e.target.files?.[0] && handleFile(e.target.files[0])}
/>
</div>
<p style={{ fontSize: 12, color: dragging ? 'var(--accent)' : 'var(--muted)' }}>
{dragging ? 'Drop to upload' : 'Accepts .txt, .md, .pdf — or drag & drop here'}
</p>
{uploading && <div style={{ marginTop: 12 }}><span className="spinner" /></div>}
</div>
{error && <div className="error-msg">{error}</div>}
{uploadResult && (
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FileText size={16} color="var(--accent2)" />
<span style={{ fontWeight: 600 }}>{uploadResult.filename}</span>
<span style={{ color: 'var(--muted)', fontSize: 12 }}>{uploadResult.char_count.toLocaleString()} chars</span>
</div>
<div style={{
background: 'var(--bg3)', border: '1px solid var(--border)',
borderRadius: 'var(--radius)', padding: 12,
fontSize: 12, color: 'var(--muted)', fontFamily: 'monospace',
whiteSpace: 'pre-wrap', maxHeight: 200, overflowY: 'auto',
marginBottom: 12,
}}>
{uploadResult.text_preview}
</div>
<button className="btn-primary" onClick={handleBuild} disabled={building}>
{building ? <><span className="spinner" style={{ width: 14, height: 14, marginRight: 6 }} />Building...</> : 'Build Causal Graph'}
</button>
</div>
)}
{buildResult && (
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<CheckCircle size={16} color="var(--success)" />
<span style={{ fontWeight: 600, color: 'var(--success)' }}>Graph Built</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 12 }}>
{[
['Chunks', buildResult.chunks],
['Triples', buildResult.triples],
['Nodes', buildResult.nodes],
['Edges', buildResult.edges],
['Cycles', buildResult.cycles.length],
].map(([k, v]) => (
<div key={String(k)} style={{ textAlign: 'center', background: 'var(--bg3)', borderRadius: 'var(--radius)', padding: '8px 0' }}>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--accent2)' }}>{v}</div>
<div style={{ fontSize: 11, color: 'var(--muted)' }}>{k}</div>
</div>
))}
</div>
{buildResult.warnings.map((w, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 6, fontSize: 12, color: 'var(--warning)', marginBottom: 4 }}>
<AlertTriangle size={14} style={{ flexShrink: 0, marginTop: 1 }} />
{w}
</div>
))}
</div>
)}
</div>
)
}