TwoTh / frontend /src /hooks /useUpload.ts
Aravindselvan
Migrate TwoTh to Hugging Face Space pipeline
d325ede
Raw
History Blame Contribute Delete
3.1 kB
/**
* Hook for managing local file staging before sending to Hugging Face Space.
*/
import { useState, useCallback } from 'react';
import type { GenerationRouteState, ImageFile, ViewLabel } from '../types';
const VIEW_LABELS: ViewLabel[] = ['Front', 'Back', 'Left', 'Right', 'Top', 'Bottom'];
const MAX_FILES = VIEW_LABELS.length;
const REQUIRED_FILES = 4;
interface UseUploadReturn {
files: ImageFile[];
addFiles: (newFiles: File[]) => void;
removeFile: (id: string) => void;
reorderFiles: (fromIndex: number, toIndex: number) => void;
beginSession: () => GenerationRouteState | null;
error: string | null;
clearError: () => void;
}
function relabelFiles(files: ImageFile[]): ImageFile[] {
return files.map((image, index) => ({
...image,
label: VIEW_LABELS[index] ?? `View ${index + 1}`,
}));
}
export function useUpload(): UseUploadReturn {
const [files, setFiles] = useState<ImageFile[]>([]);
const [error, setError] = useState<string | null>(null);
const generateThumbnail = useCallback((file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.readAsDataURL(file);
});
}, []);
const createImageFile = useCallback(async (file: File, index: number): Promise<ImageFile> => {
const thumbnail = await generateThumbnail(file);
return {
id: `img-${Date.now()}-${index}`,
file,
thumbnail,
filename: file.name,
size: file.size,
label: VIEW_LABELS[index] ?? `View ${index + 1}`,
};
}, [generateThumbnail]);
const addFiles = useCallback(async (newFiles: File[]) => {
setError(null);
const currentCount = files.length;
const remainingSlots = MAX_FILES - currentCount;
if (remainingSlots <= 0) return;
const filesToAdd = newFiles.slice(0, remainingSlots);
const newImageFiles = await Promise.all(
filesToAdd.map((file, i) => createImageFile(file, currentCount + i))
);
setFiles((prev) => relabelFiles([...prev, ...newImageFiles]));
}, [files.length, createImageFile]);
const removeFile = useCallback((id: string) => {
setFiles((prev) => relabelFiles(prev.filter((f) => f.id !== id)));
}, []);
const reorderFiles = useCallback((fromIndex: number, toIndex: number) => {
setFiles((prev) => {
const newFiles = [...prev];
const [removed] = newFiles.splice(fromIndex, 1);
newFiles.splice(toIndex, 0, removed);
return relabelFiles(newFiles);
});
}, []);
const beginSession = useCallback((): GenerationRouteState | null => {
if (files.length < REQUIRED_FILES) {
setError('Please provide at least Front, Back, Left, and Right views (4 images minimum).');
return null;
}
return {
sourceImages: files.map((item) => item.file),
};
}, [files]);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
files,
addFiles,
removeFile,
reorderFiles,
beginSession,
error,
clearError,
};
}