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).
{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 ? (
) : 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 ? (
) : (
)}
);
}