'use client' import { useState, useRef, useCallback, useEffect } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { Folder, File, Upload, Download, ChevronRight, ArrowLeft, X, CheckCircle2, AlertCircle, Loader2, } from 'lucide-react' interface FileEntry { name: string is_dir: boolean size: number path: string } interface TransferItem { id: string name: string direction: 'upload' | 'download' status: 'pending' | 'transferring' | 'done' | 'error' progress: number error?: string } interface FileTransferPanelProps { onClose: () => void sendEvent: (event: Record) => void incomingMessage: Record | null } export function FileTransferPanel({ onClose, sendEvent, incomingMessage }: FileTransferPanelProps) { const [remotePath, setRemotePath] = useState('~') const [remoteEntries, setRemoteEntries] = useState([]) const [remoteParent, setRemoteParent] = useState(null) const [transfers, setTransfers] = useState([]) const [isDragOver, setIsDragOver] = useState(false) const [isLoadingDir, setIsLoadingDir] = useState(false) const fileInputRef = useRef(null) // Download accumulation: transfer_id → {chunks: string[], filename: string, size: number} const downloadBuffers = useRef>({}) const browseRemote = useCallback((path: string) => { setIsLoadingDir(true) sendEvent({ type: 'list_files', path }) }, [sendEvent]) useEffect(() => { browseRemote('~') }, [browseRemote]) // Handle incoming relay messages useEffect(() => { if (!incomingMessage) return const msg = incomingMessage if (msg.type === 'file_list') { setIsLoadingDir(false) setRemotePath(msg.path as string) setRemoteParent((msg.parent as string) ?? null) setRemoteEntries((msg.entries as FileEntry[]) ?? []) } if (msg.type === 'file_chunk') { const tid = msg.transfer_id as string || '' const done = msg.done as boolean if (msg.upload_complete) { setTransfers(prev => prev.map(t => t.id === tid ? { ...t, status: 'done', progress: 100 } : t )) return } if (msg.error) { setTransfers(prev => prev.map(t => t.id === tid ? { ...t, status: 'error', error: msg.error as string } : t )) return } // Download chunks const filename = msg.filename as string || 'download' const totalSize = msg.size as number || 0 const chunk = msg.chunk as string || '' if (!downloadBuffers.current[tid]) { downloadBuffers.current[tid] = { chunks: [], filename, size: totalSize } } if (chunk) { downloadBuffers.current[tid].chunks.push(chunk) const received = downloadBuffers.current[tid].chunks.reduce((sum, c) => sum + c.length * 0.75, 0) const progress = totalSize > 0 ? Math.min(99, Math.round((received / totalSize) * 100)) : 50 setTransfers(prev => prev.map(t => t.id === tid ? { ...t, status: 'transferring', progress } : t )) } if (done) { const buf = downloadBuffers.current[tid] if (buf) { delete downloadBuffers.current[tid] // Decode base64 chunks and create blob const binaryChunks = buf.chunks.map(c => { const binary = atob(c) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) return bytes }) const blob = new Blob(binaryChunks) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = buf.filename a.click() URL.revokeObjectURL(url) setTransfers(prev => prev.map(t => t.id === tid ? { ...t, status: 'done', progress: 100 } : t )) } } } }, [incomingMessage]) const uploadFiles = useCallback(async (files: FileList | File[]) => { const fileArr = Array.from(files) for (const file of fileArr) { const tid = crypto.randomUUID() setTransfers(prev => [{ id: tid, name: file.name, direction: 'upload', status: 'pending', progress: 0 }, ...prev]) const destPath = remotePath === '~' ? '~/Downloads' : remotePath sendEvent({ type: 'file_upload_start', transfer_id: tid, filename: file.name, dest_path: destPath }) const CHUNK = 65536 const reader = file.stream().getReader() let seq = 0 let sent = 0 const total = file.size const pump = async () => { while (true) { const { done, value } = await reader.read() if (done) break // Convert to base64 const b64 = btoa(String.fromCharCode(...value)) sendEvent({ type: 'file_upload_chunk', transfer_id: tid, seq: seq++, chunk: b64 }) sent += value.length const progress = Math.min(99, Math.round((sent / total) * 100)) setTransfers(prev => prev.map(t => t.id === tid ? { ...t, status: 'transferring', progress } : t)) } } await pump() sendEvent({ type: 'file_upload_end', transfer_id: tid }) } }, [sendEvent, remotePath]) const downloadFile = useCallback((entry: FileEntry) => { const tid = crypto.randomUUID() setTransfers(prev => [{ id: tid, name: entry.name, direction: 'download', status: 'pending', progress: 0 }, ...prev]) downloadBuffers.current[tid] = { chunks: [], filename: entry.name, size: entry.size } sendEvent({ type: 'file_download', path: entry.path, transfer_id: tid }) }, [sendEvent]) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragOver(false) if (e.dataTransfer.files.length > 0) uploadFiles(e.dataTransfer.files) }, [uploadFiles]) const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B` if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1048576).toFixed(1)} MB` } return (
{/* Header */}
File Transfer
{/* Remote file browser */}
{remoteParent && ( )} {remotePath} {isLoadingDir && }
{remoteEntries.map((entry) => (
entry.is_dir ? browseRemote(entry.path) : undefined} > {entry.is_dir ? : } {entry.name} {!entry.is_dir && ( <> {formatSize(entry.size)} )} {entry.is_dir && }
))} {remoteEntries.length === 0 && !isLoadingDir && (

Empty directory

)}
{/* Upload drop zone */}
{ e.preventDefault(); setIsDragOver(true) }} onDragLeave={() => setIsDragOver(false)} onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} >

Drop files to upload

or click to browse

e.target.files && uploadFiles(e.target.files)} />
{/* Transfer queue */} {transfers.length > 0 && (

Transfers

{transfers.map((t) => (
{t.direction === 'upload' ? : } {t.name} {t.status === 'done' && } {t.status === 'error' && } {t.status === 'transferring' && ( {t.progress}% )} {t.status === 'pending' && }
))}
)}
) }