| 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> |
| ) |
| } |
|
|