webapp: image ref in promptbox click to preview (#1532)

feature: add high-res image preview modal to prompt box

- Added click-to-preview functionality for reference and end frame
images in `ImagePromptRow`.
- Updated `RefImage` interface to include `fullUrl` for fetching
high-resolution library images.
- Populated `fullUrl` for library selections in `create-image` and
`create-video` pages.
- Configured the preview modal to tightly wrap the image so the close
button anchors correctly to the corner.
- Updated the image uploading indicator to use the standardized
`spinner-third` icon.
This commit is contained in:
BFlat authored and GitHub committed 2026-05-17 12:39:53 +07:00
1 parent 36df1f780e
commit fa4776bb21
9 files changed
+95 -44

No files matched your search

@@ -9,13 +9,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faImages,
faPlus,
faSpinner,
faSpinnerThird,
faXmark,
} from "@fortawesome/pro-solid-svg-icons";
import { faImage } from "@fortawesome/pro-regular-svg-icons";
import { Button } from "@storyteller/ui-button";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Modal } from "@storyteller/ui-modal";
import { twMerge } from "tailwind-merge";
import { UploaderStates } from "@storyteller/common";
import {
@@ -75,6 +75,7 @@ export const ImagePromptRow = ({
const [uploadingImages, setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
const [previewImage, setPreviewImage] = useState<RefImage | null>(null);
const referenceImagesRef = useRef(referenceImages);
referenceImagesRef.current = referenceImages;
@@ -273,6 +274,7 @@ export const ImagePromptRow = ({
image={image}
allowReorder={allowReorder}
onRemove={handleRemoveReference}
onPreview={(img) => setPreviewImage(img)}
/>
))}
</SortableContext>
@@ -285,6 +287,7 @@ export const ImagePromptRow = ({
key={image.id}
image={image}
onRemove={handleRemoveReference}
onPreview={(img) => setPreviewImage(img)}
/>
))
)}
@@ -371,14 +374,20 @@ export const ImagePromptRow = ({
</div>
<div className="flex items-center gap-2">
{endFrameImage ? (
<div className="group relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all hover:border-white/80">
<div
className="group relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all cursor-pointer hover:border-white/80"
onClick={() => setPreviewImage(endFrameImage)}
>
<img
src={endFrameImage.url}
alt="End frame"
className="h-full w-full object-cover"
/>
<button
onClick={() => setEndFrameImage?.(undefined)}
onClick={(e) => {
e.stopPropagation();
setEndFrameImage?.(undefined);
}}
className="absolute right-[2px] top-[2px] flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/50 text-white sm:opacity-0 backdrop-blur-md transition-colors hover:bg-black sm:group-hover:opacity-100"
>
<FontAwesomeIcon icon={faXmark} className="h-2.5 w-2.5" />
@@ -389,7 +398,7 @@ export const ImagePromptRow = ({
<FontAwesomeIcon
icon={faSpinnerThird}
spin
className="h-6 w-6 text-white"
className="h-5 w-5 text-white"
/>
</div>
) : onPickEndFrameFromLibrary ? (
@@ -445,6 +454,25 @@ export const ImagePromptRow = ({
</div>
)}
</div>
<Modal
isOpen={!!previewImage}
onClose={() => setPreviewImage(null)}
className="w-fit h-fit max-w-none bg-transparent border-0 p-0 shadow-none overflow-visible"
backdropClassName="!bg-black/80"
showClose={true}
closeOnOutsideClick={true}
>
{previewImage && (
<div className="relative flex items-center justify-center">
<img
src={previewImage.fullUrl || previewImage.url}
alt="Preview"
className="max-w-[90vw] max-h-[90vh] object-contain drop-shadow-2xl rounded-lg"
/>
</div>
)}
</Modal>
</>
);
};
@@ -454,11 +482,16 @@ export const ImagePromptRow = ({
const ImageThumbnail = ({
image,
onRemove,
onPreview,
}: {
image: RefImage;
onRemove: (id: string) => void;
onPreview?: (image: RefImage) => void;
}) => (
<div className="group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all hover:border-white/80">
<div
className="group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all cursor-pointer hover:border-white/80"
onClick={() => onPreview?.(image)}
>
<img
src={image.url}
alt="Reference"
@@ -480,10 +513,12 @@ const SortableImage = ({
image,
allowReorder,
onRemove,
onPreview,
}: {
image: RefImage;
allowReorder: boolean;
onRemove: (id: string) => void;
onPreview?: (image: RefImage) => void;
}) => {
const {
attributes,
@@ -505,6 +540,7 @@ const SortableImage = ({
ref={setNodeRef}
style={style}
{...(allowReorder ? { ...attributes, ...listeners } : {})}
onClick={() => onPreview?.(image)}
className={twMerge(
"group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-opacity",
allowReorder
@@ -546,7 +582,7 @@ const UploadingThumbnail = ({ file }: { file: File }) => {
/>
</div>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<FontAwesomeIcon icon={faSpinner} spin className="h-6 w-6 text-white" />
<FontAwesomeIcon icon={faSpinnerThird} spin className="h-6 w-6 text-white" />
</div>
</div>
);
@@ -1,8 +1,10 @@
export interface RefImage {
id: string;
url: string;
file: File;
mediaToken: string;
fullUrl?: string;
file?: File;
mediaToken?: string;
isCustom?: boolean;
}
export interface RefVideo {
@@ -268,6 +268,7 @@ export default function CreateImage() {
.map((item) => ({
id: Math.random().toString(36).substring(7),
url: item.thumbnail || item.fullImage || "",
fullUrl: item.fullImage || undefined,
file: new File([], "library-image"),
mediaToken: item.id,
}));
@@ -295,7 +296,7 @@ export default function CreateImage() {
const imageMediaTokens = selectedModel.image_refs_supported
? referenceImages
.map((img) => img.mediaToken)
.filter((t) => t.length > 0)
.filter((t): t is string => typeof t === "string" && t.length > 0)
: undefined;
const result = await enqueueImageGeneration({
@@ -683,6 +683,7 @@ export default function CreateVideo() {
.map((item) => ({
id: Math.random().toString(36).substring(7),
url: item.thumbnail || item.fullImage || "",
fullUrl: item.fullImage || undefined,
file: new File([], "library-image"),
mediaToken: item.id,
}));
@@ -698,6 +699,7 @@ export default function CreateVideo() {
setEndFrameImage({
id: Math.random().toString(36).substring(7),
url: item.thumbnail || item.fullImage || "",
fullUrl: item.fullImage || undefined,
file: new File([], "library-image"),
mediaToken: item.id,
});
@@ -744,15 +746,15 @@ export default function CreateVideo() {
isReferenceMode && referenceImages.length > 0
? referenceImages
.map((img) => img.mediaToken)
.filter((t) => t.length > 0)
.filter((t): t is string => typeof t === "string" && t.length > 0)
: undefined;
const referenceVideoTokens =
isReferenceMode && referenceVideos.length > 0
? referenceVideos.map((v) => v.mediaToken).filter((t) => t.length > 0)
? referenceVideos.map((v) => v.mediaToken).filter((t): t is string => typeof t === "string" && t.length > 0)
: undefined;
const referenceAudioTokens =
isReferenceMode && referenceAudios.length > 0
? referenceAudios.map((a) => a.mediaToken).filter((t) => t.length > 0)
? referenceAudios.map((a) => a.mediaToken).filter((t): t is string => typeof t === "string" && t.length > 0)
: undefined;
// Extract character tokens from @-mentions in the prompt. Match longest
@@ -40,9 +40,6 @@
{
"path": "../../libs/components/upload-modal/tsconfig.lib.json"
},
{
"path": "../../libs/components/pagescene/tsconfig.lib.json"
},
{
"path": "../../libs/components/slider-v2/tsconfig.lib.json"
},
@@ -52,9 +49,21 @@
{
"path": "../../libs/components/gallery-modal/tsconfig.lib.json"
},
{
"path": "../../libs/components/pricing-modal/tsconfig.lib.json"
},
{
"path": "../../libs/components/promptbox/tsconfig.lib.json"
},
{
"path": "../../libs/components/model-selector/tsconfig.lib.json"
},
{
"path": "../../libs/components/close-button/tsconfig.lib.json"
},
{
"path": "../../libs/components/pagescene/tsconfig.lib.json"
},
{
"path": "../../libs/components/switch/tsconfig.lib.json"
},
@@ -82,9 +91,6 @@
{
"path": "../../libs/components/tooltip/tsconfig.lib.json"
},
{
"path": "../../libs/common/tsconfig.lib.json"
},
{
"path": "../../libs/components/popover/tsconfig.lib.json"
},
@@ -97,6 +103,9 @@
{
"path": "../../libs/components/button/tsconfig.lib.json"
},
{
"path": "../../libs/common/tsconfig.lib.json"
},
{
"path": "../../libs/api/tsconfig.lib.json"
}
+15 -6
View File
@@ -5,9 +5,6 @@
{
"path": "../../libs/components/upload-modal"
},
{
"path": "../../libs/components/pagescene"
},
{
"path": "../../libs/components/slider-v2"
},
@@ -17,9 +14,21 @@
{
"path": "../../libs/components/gallery-modal"
},
{
"path": "../../libs/components/pricing-modal"
},
{
"path": "../../libs/components/promptbox"
},
{
"path": "../../libs/components/model-selector"
},
{
"path": "../../libs/components/close-button"
},
{
"path": "../../libs/components/pagescene"
},
{
"path": "../../libs/components/switch"
},
@@ -47,9 +56,6 @@
{
"path": "../../libs/components/tooltip"
},
{
"path": "../../libs/common"
},
{
"path": "../../libs/components/popover"
},
@@ -62,6 +68,9 @@
{
"path": "../../libs/components/button"
},
{
"path": "../../libs/common"
},
{
"path": "../../libs/api"
},
@@ -1,3 +1 @@
export * from "./UploadModal3D";
export * from "./UploadModalMedia";
export * from "./UploadModalSplat";
@@ -11,9 +11,6 @@
{
"path": "../label"
},
{
"path": "../settings-modal"
},
{
"path": "../button-icon-select"
},
@@ -32,18 +29,12 @@
{
"path": "../checkbox"
},
{
"path": "../tooltip"
},
{
"path": "../button"
},
{
"path": "../button-dropdown"
},
{
"path": "../popover"
},
{
"path": "../promptbox"
},
@@ -62,6 +53,12 @@
{
"path": "../../model-list"
},
{
"path": "../tooltip"
},
{
"path": "../popover"
},
{
"path": "../model-selector"
},
@@ -59,9 +59,6 @@
{
"path": "../label/tsconfig.lib.json"
},
{
"path": "../settings-modal/tsconfig.lib.json"
},
{
"path": "../button-icon-select/tsconfig.lib.json"
},
@@ -80,18 +77,12 @@
{
"path": "../checkbox/tsconfig.lib.json"
},
{
"path": "../tooltip/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../button-dropdown/tsconfig.lib.json"
},
{
"path": "../popover/tsconfig.lib.json"
},
{
"path": "../promptbox/tsconfig.lib.json"
},
@@ -110,6 +101,12 @@
{
"path": "../../model-list/tsconfig.lib.json"
},
{
"path": "../tooltip/tsconfig.lib.json"
},
{
"path": "../popover/tsconfig.lib.json"
},
{
"path": "../model-selector/tsconfig.lib.json"
},