From 9d8eedc77a0fdbd5d1651bec4ff8a06398715e3e Mon Sep 17 00:00:00 2001 From: BFlat Date: Fri, 12 Sep 2025 14:12:35 +0700 Subject: [PATCH] Feature/dnd image prompts (#823) - hide/show end frame input depending on the model selected - image prompt drag and drop reordering if there's more than one image added --- .../promptbox/src/lib/ImagePromptRow.tsx | 189 +++++++++++++++--- .../promptbox/src/lib/PromptBoxVideo.tsx | 10 +- 2 files changed, 171 insertions(+), 28 deletions(-) diff --git a/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx b/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx index 0881748237..4283180942 100644 --- a/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx +++ b/frontend/libs/components/promptbox/src/lib/ImagePromptRow.tsx @@ -1,4 +1,10 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, +} from "react"; import { Button } from "@storyteller/ui-button"; import { Tooltip } from "@storyteller/ui-tooltip"; import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal"; @@ -15,6 +21,23 @@ import { faImage } from "@fortawesome/pro-regular-svg-icons"; import { RefImage } from "./promptStore"; import { twMerge } from "tailwind-merge"; import { UploaderState, UploaderStates } from "@storyteller/common"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + horizontalListSortingStrategy, + useSortable, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; export type UploadImageFn = ({ title, @@ -40,6 +63,7 @@ interface ImagePromptRowProps { endFrameImage?: RefImage; setEndFrameImage?: (image?: RefImage) => void; allowUploadEnd?: boolean; + showEndFrameSection?: boolean; } export const ImagePromptRow = ({ @@ -56,6 +80,7 @@ export const ImagePromptRow = ({ endFrameImage, setEndFrameImage, allowUploadEnd, + showEndFrameSection = true, }: ImagePromptRowProps) => { const fileInputRef = useRef(null); const [uploadingImages, setUploadingImages] = useState< @@ -72,6 +97,89 @@ export const ImagePromptRow = ({ file: File; } | null>(null); + const referenceImagesRef = useRef(referenceImages); + useEffect(() => { + referenceImagesRef.current = referenceImages; + }, [referenceImages]); + + const allowReorder = useMemo( + () => maxImagePromptCount > 1 && referenceImages.length > 1, + [maxImagePromptCount, referenceImages.length] + ); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 6 }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const SortableImage = ({ image }: { image: RefImage }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: image.id }); + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + zIndex: isDragging ? 9999 : undefined, + }; + + return ( +
onImageClick?.(image)} + > + Reference + +
+ ); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + const oldIndex = referenceImages.findIndex((img) => img.id === active.id); + const newIndex = referenceImages.findIndex((img) => img.id === over.id); + if (oldIndex === -1 || newIndex === -1) return; + const newOrder = arrayMove(referenceImages, oldIndex, newIndex); + setReferenceImages(newOrder); + }; + const usedSlotsRender = useMemo( () => Math.min( @@ -167,7 +275,10 @@ export const ImagePromptRow = ({ if (uploadTarget === "end") { setEndFrameImage?.(referenceImage); } else { - setReferenceImages([...referenceImages, referenceImage]); + setReferenceImages([ + ...referenceImagesRef.current, + referenceImage, + ]); } } else if ( newState.status === UploaderStates.assetError || @@ -200,7 +311,7 @@ export const ImagePromptRow = ({ if (uploadTarget === "end") { setEndFrameImage?.(referenceImage); } else { - setReferenceImages([...referenceImages, referenceImage]); + setReferenceImages([...referenceImagesRef.current, referenceImage]); } } @@ -286,7 +397,7 @@ export const ImagePromptRow = ({
@@ -308,30 +419,54 @@ export const ImagePromptRow = ({
- {referenceImages - .slice(0, Math.max(0, maxImagePromptCount)) - .map((image) => ( -
onImageClick?.(image)} + {allowReorder ? ( + + img.id)} + strategy={horizontalListSortingStrategy} > - Reference - -
- ))} + Reference + +
+ )) + )} {uploadingImages .slice( 0, @@ -414,7 +549,7 @@ export const ImagePromptRow = ({ )}
- {isVideo && ( + {isVideo && showEndFrameSection && (
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx index c3dacdec48..e397918a14 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxVideo.tsx @@ -222,6 +222,13 @@ export const PromptBoxVideo = ({ const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); + // Hide/clear ending frame if model doesn't support it + useEffect(() => { + if (selectedModel && !selectedModel.endFrame && endFrameImage) { + setEndFrameImage(undefined); + } + }, [selectedModel, endFrameImage, setEndFrameImage]); + return ( <> )}