mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 23:47:51 +00:00
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:
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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user