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 && (
)} {alt 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 ? (