| // src/components/multimodal/FilePreviewCard.tsx | |
| import type { ProcessedFile } from "@/types/multimodal"; | |
| import { | |
| FileImage, FileSpreadsheet, FileText, FileCode, File, | |
| FlaskConical, X, Loader2, | |
| } from "lucide-react"; | |
| interface FilePreviewCardProps { | |
| file: ProcessedFile; | |
| onRemove: (id: string) => void; | |
| onAnalyze?: (file: ProcessedFile) => void; | |
| } | |
| const CATEGORY_ICONS: Record<string, typeof File> = { | |
| image: FileImage, | |
| csv: FileSpreadsheet, | |
| pdf: FileText, | |
| code: FileCode, | |
| latex: FileCode, | |
| chemical: FlaskConical, | |
| document: FileText, | |
| audio: File, | |
| video: FileImage, | |
| unknown: File, | |
| }; | |
| function formatSize(bytes: number): string { | |
| if (bytes < 1024) return `${bytes} B`; | |
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; | |
| return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | |
| } | |
| export function FilePreviewCard({ file, onRemove, onAnalyze }: FilePreviewCardProps) { | |
| const Icon = CATEGORY_ICONS[file.category] ?? File; | |
| return ( | |
| <div className="relative group bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden shadow-sm hover:shadow-md transition"> | |
| {/* Thumbnail area */} | |
| {file.category === "image" && file.thumbnailUrl ? ( | |
| <div className="w-full h-24 bg-gray-100 dark:bg-gray-700"> | |
| <img | |
| src={file.thumbnailUrl} | |
| alt={file.name} | |
| className="w-full h-full object-cover" | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="w-full h-24 bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 flex items-center justify-center"> | |
| <Icon className="w-10 h-10 text-gray-400" /> | |
| </div> | |
| )} | |
| {/* Info bar */} | |
| <div className="p-2"> | |
| <p className="text-xs font-medium text-gray-700 dark:text-gray-300 truncate"> | |
| {file.name} | |
| </p> | |
| <p className="text-[10px] text-gray-400">{formatSize(file.size)}</p> | |
| </div> | |
| {/* Progress bar */} | |
| {file.uploadStatus === "uploading" && ( | |
| <div className="absolute inset-x-0 bottom-0 h-1 bg-gray-200 dark:bg-gray-700"> | |
| <div | |
| className="h-full bg-primary transition-all" | |
| style={{ width: `${file.uploadProgress ?? 0}%` }} | |
| /> | |
| </div> | |
| )} | |
| {/* Remove button */} | |
| <button | |
| onClick={() => onRemove(file.id)} | |
| className="absolute top-1 right-1 w-5 h-5 bg-white dark:bg-gray-900 rounded-full flex items-center justify-center shadow opacity-0 group-hover:opacity-100 transition" | |
| > | |
| <X className="w-3 h-3 text-gray-500" /> | |
| </button> | |
| {/* AI analyze button */} | |
| {onAnalyze && file.uploadStatus === "done" && ( | |
| <button | |
| onClick={() => onAnalyze(file)} | |
| className="absolute bottom-8 right-1 text-[10px] bg-primary text-white px-2 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition" | |
| > | |
| Ask AI | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 3 kB
- Xet hash:
- 233e96ffb51a715abc5a1a29098ebb19ac85251f94e933fcebd01db4d74aab1f
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.