"use client"; import { useRef, useState } from "react"; import { toast } from "sonner"; import { editor } from "monaco-editor"; import Editor from "@monaco-editor/react"; import { CopyIcon } from "lucide-react"; import { useCopyToClipboard, useEvent, useLocalStorage, useMount, useUnmount, useUpdateEffect, } from "react-use"; import classNames from "classnames"; import { useRouter, useSearchParams } from "next/navigation"; import { Header } from "@/components/editor/header"; import { Footer } from "@/components/editor/footer"; import { defaultHTML } from "@/lib/consts"; import { Preview } from "@/components/editor/preview"; import { useEditor } from "@/hooks/useEditor"; import { AskAI } from "@/components/editor/ask-ai"; import { DeployButton } from "./deploy-button"; import { Project } from "@/types"; import { SaveButton } from "./save-button"; import { LoadProject } from "../my-projects/load-project"; import { isTheSameHtml } from "@/lib/compare-html-diff"; export const AppEditor = ({ project }: { project?: Project | null }) => { const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content"); const [, copyToClipboard] = useCopyToClipboard(); const { html, setHtml, htmlHistory, setHtmlHistory, prompts, setPrompts } = useEditor(project?.html ?? (htmlStorage as string) ?? defaultHTML); // get query params from URL const searchParams = useSearchParams(); const router = useRouter(); const deploy = searchParams.get("deploy") === "true"; const iframeRef = useRef(null); const preview = useRef(null); const editor = useRef(null); const editorRef = useRef(null); const resizer = useRef(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const monacoRef = useRef(null); const [currentTab, setCurrentTab] = useState("chat"); const [device, setDevice] = useState<"desktop" | "mobile">("desktop"); const [isResizing, setIsResizing] = useState(false); const [isAiWorking, setIsAiWorking] = useState(false); const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false); const [selectedElement, setSelectedElement] = useState( null ); /** * Resets the layout based on screen size * - For desktop: Sets editor to 1/3 width and preview to 2/3 * - For mobile: Removes inline styles to let CSS handle it */ const resetLayout = () => { if (!editor.current || !preview.current) return; // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults if (window.innerWidth >= 1024) { // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px const availableWidth = window.innerWidth - resizerWidth; const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3 editor.current.style.width = `${initialEditorWidth}px`; preview.current.style.width = `${initialPreviewWidth}px`; } else { // Remove inline styles for smaller screens, let CSS flex-col handle it editor.current.style.width = ""; preview.current.style.width = ""; } }; /** * Handles resizing when the user drags the resizer * Ensures minimum widths are maintained for both panels */ const handleResize = (e: MouseEvent) => { if (!editor.current || !preview.current || !resizer.current) return; const resizerWidth = resizer.current.offsetWidth; const minWidth = 100; // Minimum width for editor/preview const maxWidth = window.innerWidth - resizerWidth - minWidth; const editorWidth = e.clientX; const clampedEditorWidth = Math.max( minWidth, Math.min(editorWidth, maxWidth) ); const calculatedPreviewWidth = window.innerWidth - clampedEditorWidth - resizerWidth; editor.current.style.width = `${clampedEditorWidth}px`; preview.current.style.width = `${calculatedPreviewWidth}px`; }; const handleMouseDown = () => { setIsResizing(true); document.addEventListener("mousemove", handleResize); document.addEventListener("mouseup", handleMouseUp); }; const handleMouseUp = () => { setIsResizing(false); document.removeEventListener("mousemove", handleResize); document.removeEventListener("mouseup", handleMouseUp); }; useMount(() => { if (deploy && project?._id) { toast.success("Your project is deployed! 🎉", { action: { label: "See Project", onClick: () => { window.open( `https://huggingface.co/spaces/${project?.space_id}`, "_blank" ); }, }, }); router.replace(`/projects/${project?.space_id}`); } if (htmlStorage) { removeHtmlStorage(); toast.warning("Previous HTML content restored from local storage."); } resetLayout(); if (!resizer.current) return; resizer.current.addEventListener("mousedown", handleMouseDown); window.addEventListener("resize", resetLayout); }); useUnmount(() => { document.removeEventListener("mousemove", handleResize); document.removeEventListener("mouseup", handleMouseUp); if (resizer.current) { resizer.current.removeEventListener("mousedown", handleMouseDown); } window.removeEventListener("resize", resetLayout); }); // Prevent accidental navigation away when AI is working or content has changed useEvent("beforeunload", (e) => { if (isAiWorking || !isTheSameHtml(html)) { e.preventDefault(); return ""; } }); useUpdateEffect(() => { if (currentTab === "chat") { // Reset editor width when switching to reasoning tab resetLayout(); // re-add the event listener for resizing if (resizer.current) { resizer.current.addEventListener("mousedown", handleMouseDown); } } else { if (preview.current) { // Reset preview width when switching to preview tab preview.current.style.width = "100%"; } } }, [currentTab]); const handleEditorValidation = (markers: editor.IMarker[]) => { console.log("Editor validation markers:", markers); }; return (
{ router.push(`/projects/${project.space_id}`); }} /> {project?._id ? ( ) : ( )}
{currentTab === "chat" && ( <>
{ copyToClipboard(html); toast.success("HTML copied to clipboard!"); }} /> { const newValue = value ?? ""; setHtml(newValue); }} onMount={(editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; }} onValidate={handleEditorValidation} /> { setHtml(newHtml); }} htmlHistory={htmlHistory} onSuccess={( finalHtml: string, p: string, updatedLines?: number[][] ) => { const currentHistory = [...htmlHistory]; currentHistory.unshift({ html: finalHtml, createdAt: new Date(), prompt: p, }); setHtmlHistory(currentHistory); setSelectedElement(null); // if xs or sm if (window.innerWidth <= 1024) { setCurrentTab("preview"); } if (updatedLines && updatedLines?.length > 0) { const decorations = updatedLines.map((line) => ({ range: new monacoRef.current.Range( line[0], 1, line[1], 1 ), options: { inlineClassName: "matched-line", }, })); setTimeout(() => { editorRef?.current ?.getModel() ?.deltaDecorations([], decorations); editorRef.current?.revealLine(updatedLines[0][0]); }, 100); } }} isAiWorking={isAiWorking} setisAiWorking={setIsAiWorking} onNewPrompt={(prompt: string) => { setPrompts((prev) => [...prev, prompt]); }} onScrollToBottom={() => { editorRef.current?.revealLine( editorRef.current?.getModel()?.getLineCount() ?? 0 ); }} isEditableModeEnabled={isEditableModeEnabled} setIsEditableModeEnabled={setIsEditableModeEnabled} selectedElement={selectedElement} setSelectedElement={setSelectedElement} />
)} { setIsEditableModeEnabled(false); setSelectedElement(element); setCurrentTab("chat"); }} />
); };