import React, { useEffect, useRef, useState } from "react";
import { AspectRatio } from "./ui/aspect-ratio";
import { Globe, FileText, Image as ImageIcon, Copy, Check, Download } from "lucide-react";
import { toast } from "sonner";
import { IFRAME_ALLOW, IFRAME_SANDBOX } from "../lib/iframePreview";
import { copyImageFromSrc, downloadImageFromSrc } from "../lib/imageExport";
import { isImagePath, previewTextSnippet } from "../lib/previewHelpers";
/** Try JSON base64 fallback when /generated-image URL 404s on HF workers. */
async function fetchGeneratedImageB64(url) {
if (!url || typeof url !== "string") return null;
const m = url.match(/\/generated-image\/([^/?]+)(?:\?t=([^&]+))?/);
if (!m) return null;
const base = url.split("/generated-image/")[0];
const token = m[2] || (url.includes("?t=") ? url.split("?t=")[1]?.split("&")[0] : "");
const b64Url = `${base}/generated-image/${m[1]}/b64${token ? `?t=${token}` : ""}`;
const resp = await fetch(b64Url, { mode: "cors", credentials: "omit" });
if (!resp.ok) return null;
const data = await resp.json();
const b64 = data?.image_base64;
if (!b64 || b64.startsWith("[")) return null;
return `data:${data.mime || "image/png"};base64,${b64}`;
}
async function loadRemoteImageSrc(url) {
const resp = await fetch(url, { mode: "cors", credentials: "omit" });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const blob = await resp.blob();
if (!blob.type.startsWith("image/") && blob.size < 500) {
throw new Error("Not an image");
}
return URL.createObjectURL(blob);
}
/**
* Cadre carré réutilisable pour aperçus site / fichier.
* kind: "iframe" | "image" | "text" | "empty"
*/
function ImagePreviewActions({ src }) {
const [copied, setCopied] = useState(false);
const [busy, setBusy] = useState(false);
const handleCopy = async (e) => {
e.stopPropagation();
if (busy || !src) return;
setBusy(true);
try {
await copyImageFromSrc(src);
setCopied(true);
toast.success("Image copiée");
setTimeout(() => setCopied(false), 1500);
} catch {
toast.error("Copie impossible");
} finally {
setBusy(false);
}
};
const handleSave = async (e) => {
e.stopPropagation();
if (busy || !src) return;
setBusy(true);
try {
await downloadImageFromSrc(src);
toast.success("Image enregistrée");
} catch {
toast.error("Enregistrement impossible");
} finally {
setBusy(false);
}
};
return (
);
}
function PreviewImage({ src, alt, fallbackSrc, className, style, showActions = false }) {
const [current, setCurrent] = useState(null);
const [failed, setFailed] = useState(false);
const [loaded, setLoaded] = useState(false);
const loadedRef = useRef(false);
useEffect(() => {
loadedRef.current = loaded;
}, [loaded]);
useEffect(() => {
let objectUrl = null;
let cancelled = false;
const applySrc = (next, immediate = false) => {
if (cancelled || !next) return;
setCurrent(next);
setFailed(false);
const isReady = immediate || next.startsWith("data:") || next.startsWith("blob:");
setLoaded(isReady);
loadedRef.current = isReady;
};
const fail = () => {
if (cancelled) return;
setFailed(true);
setLoaded(false);
};
const tryFallbacks = async (primaryUrl) => {
if (fallbackSrc && fallbackSrc !== primaryUrl) {
applySrc(fallbackSrc, fallbackSrc.startsWith("data:"));
return true;
}
if (primaryUrl?.includes("/generated-image/")) {
try {
const dataUrl = await fetchGeneratedImageB64(primaryUrl);
if (dataUrl && !cancelled) {
applySrc(dataUrl, true);
return true;
}
} catch {
/* ignore */
}
}
return false;
};
if (!src || src.includes("[image:") || src.endsWith("base64,")) {
fail();
return () => {};
}
if (src.startsWith("data:") || src.startsWith("blob:")) {
applySrc(src, true);
return () => {
cancelled = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}
if (src.startsWith("http://") || src.startsWith("https://")) {
(async () => {
try {
objectUrl = await loadRemoteImageSrc(src);
if (cancelled) return;
applySrc(objectUrl, true);
} catch {
if (cancelled) return;
const ok = await tryFallbacks(src);
if (!ok) fail();
}
})();
} else {
applySrc(src);
}
const timer = setTimeout(async () => {
if (cancelled || loadedRef.current) return;
const ok = await tryFallbacks(src);
if (!ok) fail();
}, 8000);
return () => {
cancelled = true;
clearTimeout(timer);
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [src, fallbackSrc]);
if (failed) {
return (
Image indisponible
);
}
if (!current) {
return (
);
}
return (
<>
{!loaded && (
)}
setLoaded(true)}
onError={async () => {
if (fallbackSrc && current !== fallbackSrc) {
setCurrent(fallbackSrc);
setLoaded(fallbackSrc.startsWith("data:"));
return;
}
if (current?.includes("/generated-image/")) {
try {
const dataUrl = await fetchGeneratedImageB64(current);
if (dataUrl) {
setCurrent(dataUrl);
setLoaded(true);
return;
}
} catch {
/* ignore */
}
}
setFailed(true);
}}
/>
{showActions && loaded && current && (
)}
>
);
}
export default function SquarePreviewFrame({
kind = "empty",
url,
src,
imageFallback,
title,
subtitle,
text,
emptyLabel = "Aperçu",
className = "",
testId = "square-preview",
showImageActions = false,
}) {
return (
{kind === "iframe" && url ? (
) : kind === "image" && src ? (
) : kind === "text" && text ? (
{text}
) : (
{kind === "iframe" ? : null}
{kind === "text" ? : null}
{kind === "image" ? : null}
{emptyLabel}
)}
{(title || subtitle) && (
{title &&
{title}
}
{subtitle &&
{subtitle}
}
)}
);
}
export { isImagePath, previewTextSnippet } from "../lib/previewHelpers";