|
|
@ -20,6 +20,7 @@ import { getPopiImageAssetPreview, resolvePopiImageAsset } from "@/lib/popiAsset |
|
|
type ImageInputNodeType = Node<ImageInputNodeData, "imageInput">; |
|
|
type ImageInputNodeType = Node<ImageInputNodeData, "imageInput">; |
|
|
const IMAGE_INPUT_DIMENSIONS = defaultNodeDimensions.imageInput; |
|
|
const IMAGE_INPUT_DIMENSIONS = defaultNodeDimensions.imageInput; |
|
|
const IMAGE_DIMENSION_TIMEOUT_MS = 10_000; |
|
|
const IMAGE_DIMENSION_TIMEOUT_MS = 10_000; |
|
|
|
|
|
const IMAGE_LOAD_RETRY_DELAYS_MS = [600, 1600]; |
|
|
|
|
|
|
|
|
export interface ImageInputNodeViewProps { |
|
|
export interface ImageInputNodeViewProps { |
|
|
id: string; |
|
|
id: string; |
|
|
@ -91,7 +92,7 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal |
|
|
const { t } = useI18n(); |
|
|
const { t } = useI18n(); |
|
|
const nodeData = data; |
|
|
const nodeData = data; |
|
|
const adaptiveImage = useAdaptiveImageSrc(nodeData.image, id); |
|
|
const adaptiveImage = useAdaptiveImageSrc(nodeData.image, id); |
|
|
const displayImage = nodeData.previewImage || adaptiveImage || nodeData.image; |
|
|
const preferredDisplayImage = nodeData.previewImage || adaptiveImage || nodeData.image; |
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
const updateNodeData = useWorkflowStore((state) => state.updateNodeData); |
|
|
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); |
|
|
const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); |
|
|
const { setNodes } = useReactFlow(); |
|
|
const { setNodes } = useReactFlow(); |
|
|
@ -99,12 +100,23 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal |
|
|
const prevImageRef = useRef<string | null>(null); |
|
|
const prevImageRef = useRef<string | null>(null); |
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false); |
|
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false); |
|
|
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); |
|
|
const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); |
|
|
|
|
|
const [imageRetryNonce, setImageRetryNonce] = useState(0); |
|
|
|
|
|
const [displayFallbackImage, setDisplayFallbackImage] = useState<string | null>(null); |
|
|
const [imageLoadFailed, setImageLoadFailed] = useState(false); |
|
|
const [imageLoadFailed, setImageLoadFailed] = useState(false); |
|
|
const isUploading = nodeData.uploadStatus === "loading"; |
|
|
const isUploading = nodeData.uploadStatus === "loading"; |
|
|
|
|
|
const displayImage = displayFallbackImage || preferredDisplayImage; |
|
|
|
|
|
|
|
|
|
|
|
const imageSrc = displayImage |
|
|
|
|
|
? imageRetryNonce > 0 && /^https?:\/\//i.test(displayImage) && !displayImage.includes("?") |
|
|
|
|
|
? `${displayImage}${displayImage.includes("?") ? "&" : "?"}canvasRetry=${imageRetryNonce}` |
|
|
|
|
|
: displayImage |
|
|
|
|
|
: null; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
|
|
|
setDisplayFallbackImage(null); |
|
|
setImageLoadFailed(false); |
|
|
setImageLoadFailed(false); |
|
|
}, [displayImage]); |
|
|
setImageRetryNonce(0); |
|
|
|
|
|
}, [preferredDisplayImage]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
if (!nodeData.image || !nodeData.dimensions || nodeData.image === prevImageRef.current) { |
|
|
if (!nodeData.image || !nodeData.dimensions || nodeData.image === prevImageRef.current) { |
|
|
@ -180,7 +192,7 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal |
|
|
}) |
|
|
}) |
|
|
.catch((error) => { |
|
|
.catch((error) => { |
|
|
const message = error instanceof Error ? error.message : "Media upload failed"; |
|
|
const message = error instanceof Error ? error.message : "Media upload failed"; |
|
|
updateNodeData(id, { uploadStatus: "error", uploadError: message }); |
|
|
updateNodeData(id, { uploadStatus: "idle", uploadError: message }); |
|
|
alert(message); |
|
|
alert(message); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
@ -467,10 +479,29 @@ export function ImageInputNodeView({ id, data, selected, renderInputHandle = fal |
|
|
className="relative group h-full w-full cursor-zoom-in overflow-clip rounded-lg bg-neutral-800" |
|
|
className="relative group h-full w-full cursor-zoom-in overflow-clip rounded-lg bg-neutral-800" |
|
|
> |
|
|
> |
|
|
<img |
|
|
<img |
|
|
src={displayImage ?? undefined} |
|
|
key={`${displayImage ?? "empty"}-${imageRetryNonce}`} |
|
|
|
|
|
src={imageSrc ?? undefined} |
|
|
alt={nodeData.filename || t("imageInput.uploadedAlt")} |
|
|
alt={nodeData.filename || t("imageInput.uploadedAlt")} |
|
|
onLoad={() => setImageLoadFailed(false)} |
|
|
onLoad={() => setImageLoadFailed(false)} |
|
|
onError={() => setImageLoadFailed(true)} |
|
|
onError={() => { |
|
|
|
|
|
if (nodeData.previewImage && displayImage === nodeData.previewImage && nodeData.image && nodeData.image !== nodeData.previewImage) { |
|
|
|
|
|
setDisplayFallbackImage(nodeData.image); |
|
|
|
|
|
setImageLoadFailed(false); |
|
|
|
|
|
setImageRetryNonce(0); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const retryIndex = imageRetryNonce; |
|
|
|
|
|
const retryDelay = IMAGE_LOAD_RETRY_DELAYS_MS[retryIndex]; |
|
|
|
|
|
if (retryDelay !== undefined && displayImage && /^https?:\/\//i.test(displayImage)) { |
|
|
|
|
|
window.setTimeout(() => { |
|
|
|
|
|
setImageRetryNonce((current) => current === retryIndex ? current + 1 : current); |
|
|
|
|
|
}, retryDelay); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setImageLoadFailed(true); |
|
|
|
|
|
}} |
|
|
className="h-full w-full rounded-lg object-contain" |
|
|
className="h-full w-full rounded-lg object-contain" |
|
|
/> |
|
|
/> |
|
|
{imageLoadFailed && ( |
|
|
{imageLoadFailed && ( |
|
|
|