"use client"; import { useRef, useState, useEffect } from "react"; import { useUpdateEffect } from "react-use"; import classNames from "classnames"; import { cn } from "@/lib/utils"; import { GridPattern } from "@/components/magic-ui/grid-pattern"; import { useEditor } from "@/hooks/useEditor"; import { useAi } from "@/hooks/useAi"; import { htmlTagToText } from "@/lib/html-tag-to-text"; import { AnimatedBlobs } from "@/components/animated-blobs"; import { AiLoading } from "../ask-ai/loading"; import { defaultHTML } from "@/lib/consts"; import { Button } from "@/components/ui/button"; import { LivePreview } from "../live-preview"; import { HistoryNotification } from "../history-notification"; import { AlertCircle } from "lucide-react"; import { api } from "@/lib/api"; import { toast } from "sonner"; import Loading from "@/components/loading"; export const Preview = ({ isNew }: { isNew: boolean }) => { const { project, device, isLoadingProject, currentTab, currentCommit, setCurrentCommit, currentPageData, pages, setCurrentPage, } = useEditor(); const { isEditableModeEnabled, setSelectedElement, isAiWorking, globalAiLoading, setIsEditableModeEnabled, } = useAi(); const iframeRef = useRef(null); // Inject event handling script const injectInteractivityScript = (html: string) => { const interactivityScript = ` `; // Inject the script before closing body tag, or at the end if no body tag if (html.includes("")) { return html.replace("", `${interactivityScript}`); } else { return html + interactivityScript; } }; const [hoveredElement, setHoveredElement] = useState<{ tagName: string; rect: { top: number; left: number; width: number; height: number }; } | null>(null); const [isPromotingVersion, setIsPromotingVersion] = useState(false); const [stableHtml, setStableHtml] = useState(""); // Handle PostMessage communication with iframe useEffect(() => { const handleMessage = (event: MessageEvent) => { // Verify origin for security if (!event.origin.includes(window.location.origin)) { return; } const { type, data } = event.data; switch (type) { case "IFRAME_SCRIPT_READY": if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: isEditableModeEnabled ? "ENABLE_EDIT_MODE" : "DISABLE_EDIT_MODE", }, "*" ); } break; case "ELEMENT_HOVERED": if (isEditableModeEnabled) { setHoveredElement(data); } break; case "ELEMENT_MOUSE_OUT": if (isEditableModeEnabled) { setHoveredElement(null); } break; case "ELEMENT_CLICKED": if (isEditableModeEnabled) { const mockElement = { tagName: data.tagName, getBoundingClientRect: () => data.rect, outerHTML: data.element, }; setSelectedElement(mockElement as any); setIsEditableModeEnabled(false); } break; case "NAVIGATE_TO_PAGE": // Handle navigation between pages by updating currentPageData if (data.targetPath) { // Find the page in the pages array const targetPage = pages.find( (page) => page.path === data.targetPath ); if (targetPage) { setCurrentPage(data.targetPath); } else { // If page doesn't exist, you might want to create it or show an error console.warn(`Page not found: ${data.targetPath}`); toast.error(`Page not found: ${data.targetPath}`); } } break; } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [setSelectedElement, isEditableModeEnabled, pages, setCurrentPage]); // Send edit mode state to iframe and clear hover state when disabled useUpdateEffect(() => { if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: isEditableModeEnabled ? "ENABLE_EDIT_MODE" : "DISABLE_EDIT_MODE", }, "*" ); } // Clear hover state when edit mode is disabled if (!isEditableModeEnabled) { setHoveredElement(null); } }, [isEditableModeEnabled, stableHtml]); // Update stable HTML only when AI finishes working to prevent blinking useEffect(() => { if (!isAiWorking && !globalAiLoading && currentPageData?.html) { setStableHtml(currentPageData.html); } }, [isAiWorking, globalAiLoading, currentPageData?.html]); // Initialize stable HTML when component first loads useEffect(() => { if ( currentPageData?.html && !stableHtml && !isAiWorking && !globalAiLoading ) { setStableHtml(currentPageData.html); } }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]); const promoteVersion = async () => { setIsPromotingVersion(true); await api .post( `/me/projects/${project?.space_id}/commits/${currentCommit}/promote` ) .then((res) => { if (res.data.ok) { setCurrentCommit(null); toast.success("Version promoted successfully"); } }) .catch((err) => { toast.error(err.response.data.error); }); setIsPromotingVersion(false); }; return (
{!isAiWorking && hoveredElement && isEditableModeEnabled && (
{htmlTagToText(hoveredElement.tagName.toLowerCase())}
)} {isNew && !isLoadingProject ? (