import { useRef, useState } from "react"; import { BoxIcon, ImageIcon, LoaderCircleIcon, XIcon } from "lucide-react"; import { DynamicIcon } from "@storyteller/icons"; import { AddButton, type RefImage } from "../../../components/prompt-box"; import { MESH_FILE_ACCEPT, uploadMeshFile, uploadViewImage, } from "./mesh-upload"; // Multi-view side inputs (front/back/left/right) and the mesh-to-mesh input // file — the mobile form band. The desktop prompt box renders the same store // slice as reference-deck cards via useMeshDeckRefs. export type MultiViewSlot = "front" | "back" | "left" | "right"; interface MeshInputsRowProps { showMultiView: boolean; showMeshInput: boolean; frontImage?: RefImage; backImage?: RefImage; leftImage?: RefImage; rightImage?: RefImage; inputMesh?: RefImage; onChange: (patch: { frontImage?: RefImage; backImage?: RefImage; leftImage?: RefImage; rightImage?: RefImage; inputMesh?: RefImage; }) => void; // Opens the library picker targeting a specific multi-view slot. onPickSlotFromLibrary?: (slot: MultiViewSlot) => void; } export function MeshInputsRow({ showMultiView, showMeshInput, frontImage, backImage, leftImage, rightImage, inputMesh, onChange, onPickSlotFromLibrary, }: MeshInputsRowProps) { if (!showMultiView && !showMeshInput) return null; // Mesh models are mutually exclusive here (multi-view vs mesh-input), so a // single title reflects whichever slots are shown. const title = showMultiView ? "Multi-view" : "Input mesh"; const subtitle = showMultiView ? "Reference angles (optional)" : "Mesh file to process"; const titleIcon = showMultiView ? ImageIcon : BoxIcon; return ( // Title on the left, upload slots right-aligned + top-aligned (matches // ImagePromptRow).
{title}
{subtitle}
{showMultiView && ( <> onChange({ frontImage: img })} onPickFromLibrary={ onPickSlotFromLibrary ? () => onPickSlotFromLibrary("front") : undefined } /> onChange({ backImage: img })} onPickFromLibrary={ onPickSlotFromLibrary ? () => onPickSlotFromLibrary("back") : undefined } /> onChange({ leftImage: img })} onPickFromLibrary={ onPickSlotFromLibrary ? () => onPickSlotFromLibrary("left") : undefined } /> onChange({ rightImage: img })} onPickFromLibrary={ onPickSlotFromLibrary ? () => onPickSlotFromLibrary("right") : undefined } /> )} {showMeshInput && ( onChange({ inputMesh: img })} /> )}
); } // ── Slots ────────────────────────────────────────────────────────────────── const SLOT_CLASS = "flex aspect-square w-14 items-center justify-center overflow-hidden rounded-[3px] border border-dashed border-white/25 bg-white/5 transition-all hover:border-white/40 hover:bg-white/10"; function SlotShell({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{children} {label}
); } function ImageSlot({ label, image, onChange, onPickFromLibrary, }: { label: string; image?: RefImage; onChange: (img?: RefImage) => void; onPickFromLibrary?: () => void; }) { const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const handleUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (inputRef.current) inputRef.current.value = ""; if (!file) return; setUploading(true); const image = await uploadViewImage(label, file); setUploading(false); if (image) onChange(image); }; return ( {image ? (
{`${label}
) : uploading ? (
) : ( inputRef.current?.click()} onPickFromLibrary={onPickFromLibrary} /> )}
); } function MeshFileSlot({ mesh, onChange, }: { mesh?: RefImage; onChange: (img?: RefImage) => void; }) { const inputRef = useRef(null); const [uploading, setUploading] = useState(false); const handleUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (inputRef.current) inputRef.current.value = ""; if (!file) return; setUploading(true); const mesh = await uploadMeshFile(file); setUploading(false); if (mesh) onChange(mesh); }; return ( {mesh ? (
) : uploading ? (
) : ( )}
); }