| "use client"; |
|
|
| import { useRef } from "react"; |
| import type { Artifact } from "@/lib/types"; |
|
|
| export interface PendingFile { |
| |
| tempId: string; |
| name: string; |
| size: number; |
| status: "uploading" | "done" | "error"; |
| artifact?: Artifact; |
| error?: string; |
| } |
|
|
| interface FileUploadButtonProps { |
| taskId: string | null; |
| onFileStart: (file: PendingFile) => void; |
| onFileComplete: (tempId: string, artifact: Artifact) => void; |
| onFileError: (tempId: string, error: string) => void; |
| disabled?: boolean; |
| } |
|
|
| const ACCEPTED = ".fasta,.pdb,.csv,.nwk"; |
|
|
| let tempCounter = 0; |
|
|
| 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 { formatSize }; |
|
|
| export default function FileUploadButton({ |
| taskId, |
| onFileStart, |
| onFileComplete, |
| onFileError, |
| disabled, |
| }: FileUploadButtonProps) { |
| const inputRef = useRef<HTMLInputElement>(null); |
|
|
| const handleClick = () => { |
| if (disabled || !taskId) return; |
| inputRef.current?.click(); |
| }; |
|
|
| const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { |
| const files = e.target.files; |
| if (!files || !taskId) return; |
|
|
| for (const file of Array.from(files)) { |
| const tempId = `upload_${++tempCounter}_${Date.now()}`; |
| const pending: PendingFile = { |
| tempId, |
| name: file.name, |
| size: file.size, |
| status: "uploading", |
| }; |
| onFileStart(pending); |
|
|
| try { |
| const formData = new FormData(); |
| formData.append("file", file); |
| formData.append("taskId", taskId); |
|
|
| const res = await fetch("/api/upload", { |
| method: "POST", |
| body: formData, |
| }); |
|
|
| if (!res.ok) { |
| const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` })); |
| throw new Error(body.error || `Upload failed (${res.status})`); |
| } |
|
|
| const { artifact } = (await res.json()) as { artifact: Artifact }; |
| onFileComplete(tempId, artifact); |
| } catch (err) { |
| const msg = err instanceof Error ? err.message : "Upload failed"; |
| onFileError(tempId, msg); |
| } |
| } |
|
|
| |
| if (inputRef.current) inputRef.current.value = ""; |
| }; |
|
|
| return ( |
| <> |
| <input |
| ref={inputRef} |
| type="file" |
| accept={ACCEPTED} |
| multiple |
| className="hidden" |
| onChange={handleChange} |
| /> |
| <button |
| onClick={handleClick} |
| disabled={disabled || !taskId} |
| title="Attach file (.fasta, .pdb, .csv, .nwk)" |
| className="p-2.5 rounded-xl text-muted-fg hover:text-foreground hover:bg-muted transition-colors disabled:opacity-30" |
| > |
| <svg |
| className="w-4 h-4" |
| viewBox="0 0 24 24" |
| fill="none" |
| stroke="currentColor" |
| strokeWidth={2} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| > |
| <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" /> |
| </svg> |
| </button> |
| </> |
| ); |
| } |
|
|