Exched/DeepSeek_Coder
0
1"use client";2 3import { useRef, useState, useEffect } from "react";4import { useUpdateEffect } from "react-use";5import classNames from "classnames";6 7import { cn } from "@/lib/utils";8import { GridPattern } from "@/components/magic-ui/grid-pattern";9import { useEditor } from "@/hooks/useEditor";10import { useAi } from "@/hooks/useAi";11import { htmlTagToText } from "@/lib/html-tag-to-text";12import { AnimatedBlobs } from "@/components/animated-blobs";13import { AiLoading } from "../ask-ai/loading";14import { defaultHTML } from "@/lib/consts";15import { HistoryNotification } from "../history-notification";16import { api } from "@/lib/api";17import { toast } from "sonner";18 19export const Preview = ({ isNew }: { isNew: boolean }) => {20 const {21 project,22 device,23 isLoadingProject,24 currentTab,25 currentCommit,26 setCurrentCommit,27 currentPageData,28 pages,29 setPages,30 setCurrentPage,31 isSameHtml,32 } = useEditor();33 const {34 isEditableModeEnabled,35 setSelectedElement,36 isAiWorking,37 globalAiLoading,38 } = useAi();39 40 const iframeRef = useRef<HTMLIFrameElement>(null);41 42 const [hoveredElement, setHoveredElement] = useState<{43 tagName: string;44 rect: { top: number; left: number; width: number; height: number };45 } | null>(null);46 const [isPromotingVersion, setIsPromotingVersion] = useState(false);47 const [stableHtml, setStableHtml] = useState<string>("");48 const [throttledHtml, setThrottledHtml] = useState<string>("");49 const lastUpdateTimeRef = useRef<number>(0);50 51 // For new projects, throttle HTML updates to every 3 seconds52 useEffect(() => {53 if (isNew && currentPageData?.html) {54 const now = Date.now();55 const timeSinceLastUpdate = now - lastUpdateTimeRef.current;56 57 // If this is the first update or 3 seconds have passed, update immediately58 if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {59 setThrottledHtml(currentPageData.html);60 lastUpdateTimeRef.current = now;61 } else {62 // Otherwise, schedule an update for when 3 seconds will have passed63 const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;64 const timer = setTimeout(() => {65 setThrottledHtml(currentPageData.html);66 lastUpdateTimeRef.current = Date.now();67 }, timeUntilNextUpdate);68 return () => clearTimeout(timer);69 }70 }71 }, [isNew, currentPageData?.html]);72 73 useEffect(() => {74 if (!isAiWorking && !globalAiLoading && currentPageData?.html) {75 setStableHtml(currentPageData.html);76 }77 }, [isAiWorking, globalAiLoading, currentPageData?.html]);78 79 useEffect(() => {80 if (81 currentPageData?.html &&82 !stableHtml &&83 !isAiWorking &&84 !globalAiLoading85 ) {86 setStableHtml(currentPageData.html);87 }88 }, [currentPageData?.html, stableHtml, isAiWorking, globalAiLoading]);89 90 useUpdateEffect(() => {91 const cleanupListeners = () => {92 if (iframeRef?.current?.contentDocument) {93 const iframeDocument = iframeRef.current.contentDocument;94 iframeDocument.removeEventListener("mouseover", handleMouseOver);95 iframeDocument.removeEventListener("mouseout", handleMouseOut);96 iframeDocument.removeEventListener("click", handleClick);97 }98 };99 100 if (iframeRef?.current) {101 const iframeDocument = iframeRef.current.contentDocument;102 if (iframeDocument) {103 cleanupListeners();104 105 if (isEditableModeEnabled) {106 iframeDocument.addEventListener("mouseover", handleMouseOver);107 iframeDocument.addEventListener("mouseout", handleMouseOut);108 iframeDocument.addEventListener("click", handleClick);109 }110 }111 }112 113 return cleanupListeners;114 }, [iframeRef, isEditableModeEnabled]);115 116 const promoteVersion = async () => {117 setIsPromotingVersion(true);118 await api119 .post(120 `/me/projects/${project?.space_id}/commits/${currentCommit}/promote`121 )122 .then((res) => {123 if (res.data.ok) {124 setCurrentCommit(null);125 setPages(res.data.pages);126 setCurrentPage(res.data.pages[0].path);127 toast.success("Version promoted successfully");128 }129 })130 .catch((err) => {131 toast.error(err.response.data.error);132 });133 setIsPromotingVersion(false);134 };135 136 const handleMouseOver = (event: MouseEvent) => {137 if (iframeRef?.current) {138 const iframeDocument = iframeRef.current.contentDocument;139 if (iframeDocument) {140 const targetElement = event.target as HTMLElement;141 if (142 hoveredElement?.tagName !== targetElement.tagName ||143 hoveredElement?.rect.top !==144 targetElement.getBoundingClientRect().top ||145 hoveredElement?.rect.left !==146 targetElement.getBoundingClientRect().left ||147 hoveredElement?.rect.width !==148 targetElement.getBoundingClientRect().width ||149 hoveredElement?.rect.height !==150 targetElement.getBoundingClientRect().height151 ) {152 if (targetElement !== iframeDocument.body) {153 const rect = targetElement.getBoundingClientRect();154 setHoveredElement({155 tagName: targetElement.tagName,156 rect: {157 top: rect.top,158 left: rect.left,159 width: rect.width,160 height: rect.height,161 },162 });163 targetElement.classList.add("hovered-element");164 } else {165 return setHoveredElement(null);166 }167 }168 }169 }170 };171 const handleMouseOut = () => {172 setHoveredElement(null);173 };174 const handleClick = (event: MouseEvent) => {175 if (iframeRef?.current) {176 const iframeDocument = iframeRef.current.contentDocument;177 if (iframeDocument) {178 const targetElement = event.target as HTMLElement;179 if (targetElement !== iframeDocument.body) {180 setSelectedElement(targetElement);181 }182 }183 }184 };185 186 const handleCustomNavigation = (event: MouseEvent) => {187 if (iframeRef?.current) {188 const iframeDocument = iframeRef.current.contentDocument;189 if (iframeDocument) {190 const findClosestAnchor = (191 element: HTMLElement192 ): HTMLAnchorElement | null => {193 let current = element;194 while (current && current !== iframeDocument.body) {195 if (current.tagName === "A") {196 return current as HTMLAnchorElement;197 }198 current = current.parentElement as HTMLElement;199 }200 return null;201 };202 203 const anchorElement = findClosestAnchor(event.target as HTMLElement);204 if (anchorElement) {205 let href = anchorElement.getAttribute("href");206 if (href) {207 event.stopPropagation();208 event.preventDefault();209 210 if (href.includes("#") && !href.includes(".html")) {211 const targetElement = iframeDocument.querySelector(href);212 if (targetElement) {213 targetElement.scrollIntoView({ behavior: "smooth" });214 }215 return;216 }217 218 href = href.split(".html")[0] + ".html";219 const isPageExist = pages.some((page) => page.path === href);220 if (isPageExist) {221 setCurrentPage(href);222 }223 }224 }225 }226 }227 };228 229 return (230 <div231 className={classNames(232 "bg-neutral-900/30 w-full h-[calc(100dvh-57px)] flex flex-col items-center justify-center relative z-1 lg:border-l border-neutral-800",233 {234 "max-lg:h-0 overflow-hidden": currentTab === "chat",235 "max-lg:h-full": currentTab === "preview",236 }237 )}238 >239 <GridPattern240 x={-1}241 y={-1}242 strokeDasharray={"4 2"}243 className={cn(244 "[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"245 )}246 />247 {!isAiWorking && hoveredElement && isEditableModeEnabled && (248 <div249 className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"250 style={{251 top: hoveredElement.rect.top,252 left: hoveredElement.rect.left,253 width: hoveredElement.rect.width,254 height: hoveredElement.rect.height,255 }}256 >257 <span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0">258 {htmlTagToText(hoveredElement.tagName.toLowerCase())}259 </span>260 </div>261 )}262 {isLoadingProject ? (263 <div className="w-full h-full flex items-center justify-center relative">264 <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">265 <AiLoading text="Fetching your project..." className="flex-col" />266 <AnimatedBlobs />267 <AnimatedBlobs />268 </div>269 </div>270 ) : (271 <>272 <iframe273 id="preview-iframe"274 ref={iframeRef}275 className={classNames(276 "w-full select-none transition-all duration-200 bg-black h-full",277 {278 "lg:max-w-md lg:mx-auto lg:!rounded-[42px] lg:border-[8px] lg:border-neutral-700 lg:shadow-2xl lg:h-[80dvh] lg:max-h-[996px]":279 device === "mobile",280 }281 )}282 src={283 currentCommit284 ? `https://${project?.space_id?.replaceAll(285 "/",286 "-"287 )}--rev-${currentCommit.slice(0, 7)}.static.hf.space`288 : undefined289 }290 srcDoc={291 !currentCommit292 ? isNew293 ? throttledHtml || defaultHTML294 : stableHtml295 : undefined296 }297 onLoad={298 !currentCommit299 ? () => {300 if (iframeRef?.current?.contentWindow?.document?.body) {301 iframeRef.current.contentWindow.document.body.scrollIntoView(302 {303 block: isAiWorking ? "end" : "start",304 inline: "nearest",305 behavior: isAiWorking ? "instant" : "smooth",306 }307 );308 }309 // add event listener to all links in the iframe to handle navigation310 if (iframeRef?.current?.contentWindow?.document) {311 const links =312 iframeRef.current.contentWindow.document.querySelectorAll(313 "a"314 );315 links.forEach((link) => {316 link.addEventListener("click", handleCustomNavigation);317 });318 }319 }320 : undefined321 }322 sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"323 allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"324 />325 {!isNew && (326 <>327 <div328 className={classNames(329 "w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",330 {331 "opacity-0 pointer-events-none": !globalAiLoading,332 }333 )}334 >335 <div className="py-10 w-full relative z-1 max-w-3xl mx-auto text-center">336 <AiLoading337 text={338 isLoadingProject ? "Fetching your project..." : undefined339 }340 className="flex-col"341 />342 <AnimatedBlobs />343 <AnimatedBlobs />344 </div>345 </div>346 <HistoryNotification347 isVisible={!!currentCommit}348 isPromotingVersion={isPromotingVersion}349 onPromoteVersion={promoteVersion}350 onGoBackToCurrent={() => setCurrentCommit(null)}351 />352 </>353 )}354 </>355 )}356 </div>357 );358};359 