import { selectedAppIdAtom, appUrlAtom, appOutputAtom, previewErrorMessageAtom, } from "@/atoms/appAtoms"; import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { useEffect, useRef, useState } from "react"; import { ArrowLeft, ArrowRight, RefreshCw, ExternalLink, Loader2, X, Sparkles, ChevronDown, Lightbulb, ChevronRight, MousePointerClick, } from "lucide-react"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { IpcClient } from "@/ipc/ipc_client"; import { useLoadAppFile } from "@/hooks/useLoadAppFile"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useStreamChat } from "@/hooks/useStreamChat"; import { selectedComponentPreviewAtom } from "@/atoms/previewAtoms"; import { ComponentSelection } from "@/ipc/ipc_types"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; interface ErrorBannerProps { error: string | undefined; onDismiss: () => void; onAIFix: () => void; } const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => { const [isCollapsed, setIsCollapsed] = useState(true); const { isStreaming } = useStreamChat(); if (!error) return null; const getTruncatedError = () => { const firstLine = error.split("\n")[0]; const snippetLength = 200; const snippet = error.substring(0, snippetLength); return firstLine.length < snippet.length ? firstLine : snippet + (snippet.length === snippetLength ? "..." : ""); }; return (
{/* Close button in top left */} {/* Error message in the middle */}
setIsCollapsed(!isCollapsed)} > {isCollapsed ? getTruncatedError() : error}
{/* Tip message */}
Tip: Check if restarting the app fixes the error.
{/* AI Fix button at the bottom */}
); }; // Preview iframe component export const PreviewIframe = ({ loading }: { loading: boolean }) => { const selectedAppId = useAtomValue(selectedAppIdAtom); const { appUrl, originalUrl } = useAtomValue(appUrlAtom); const setAppOutput = useSetAtom(appOutputAtom); // State to trigger iframe reload const [reloadKey, setReloadKey] = useState(0); const [errorMessage, setErrorMessage] = useAtom(previewErrorMessageAtom); const selectedChatId = useAtomValue(selectedChatIdAtom); const { streamMessage } = useStreamChat(); const [availableRoutes, setAvailableRoutes] = useState< Array<{ path: string; label: string }> >([]); // Load router related files to extract routes const { content: routerContent } = useLoadAppFile( selectedAppId, "src/App.tsx", ); // Effect to parse routes from the router file useEffect(() => { if (routerContent) { try { const routes: Array<{ path: string; label: string }> = []; // Extract route imports and paths using regex for React Router syntax // Match const routePathsRegex = /]*\s+)?path=["']([^"']+)["']/g; let match; // Find all route paths in the router content while ((match = routePathsRegex.exec(routerContent)) !== null) { const path = match[1]; // Create a readable label from the path const label = path === "/" ? "Home" : path .split("/") .filter((segment) => segment && !segment.startsWith(":")) .pop() ?.replace(/[-_]/g, " ") .replace(/^\w/, (c) => c.toUpperCase()) || path; if (!routes.some((r) => r.path === path)) { routes.push({ path, label }); } } setAvailableRoutes(routes); } catch (e) { console.error("Error parsing router file:", e); } } }, [routerContent]); // Navigation state const [isComponentSelectorInitialized, setIsComponentSelectorInitialized] = useState(false); const [canGoBack, setCanGoBack] = useState(false); const [canGoForward, setCanGoForward] = useState(false); const [navigationHistory, setNavigationHistory] = useState([]); const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0); const [selectedComponentPreview, setSelectedComponentPreview] = useAtom( selectedComponentPreviewAtom, ); const iframeRef = useRef(null); const [isPicking, setIsPicking] = useState(false); // Deactivate component selector when selection is cleared useEffect(() => { if (!selectedComponentPreview) { if (iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: "deactivate-dyad-component-selector" }, "*", ); } setIsPicking(false); } }, [selectedComponentPreview]); // Add message listener for iframe errors and navigation events useEffect(() => { const handleMessage = (event: MessageEvent) => { // Only handle messages from our iframe if (event.source !== iframeRef.current?.contentWindow) { return; } if (event.data?.type === "dyad-component-selector-initialized") { setIsComponentSelectorInitialized(true); return; } if (event.data?.type === "dyad-component-selected") { console.log("Component picked:", event.data); setSelectedComponentPreview(parseComponentSelection(event.data)); setIsPicking(false); return; } const { type, payload } = event.data as { type: | "window-error" | "unhandled-rejection" | "iframe-sourcemapped-error" | "build-error-report" | "pushState" | "replaceState"; payload?: { message?: string; stack?: string; reason?: string; newUrl?: string; file?: string; frame?: string; }; }; if ( type === "window-error" || type === "unhandled-rejection" || type === "iframe-sourcemapped-error" ) { const stack = type === "iframe-sourcemapped-error" ? payload?.stack?.split("\n").slice(0, 1).join("\n") : payload?.stack; const errorMessage = `Error ${ payload?.message || payload?.reason }\nStack trace: ${stack}`; console.error("Iframe error:", errorMessage); setErrorMessage(errorMessage); setAppOutput((prev) => [ ...prev, { message: `Iframe error: ${errorMessage}`, type: "client-error", appId: selectedAppId!, timestamp: Date.now(), }, ]); } else if (type === "build-error-report") { console.debug(`Build error report: ${payload}`); const errorMessage = `${payload?.message} from file ${payload?.file}.\n\nSource code:\n${payload?.frame}`; setErrorMessage(errorMessage); setAppOutput((prev) => [ ...prev, { message: `Build error report: ${JSON.stringify(payload)}`, type: "client-error", appId: selectedAppId!, timestamp: Date.now(), }, ]); } else if (type === "pushState" || type === "replaceState") { console.debug(`Navigation event: ${type}`, payload); // Update navigation history based on the type of state change if (type === "pushState" && payload?.newUrl) { // For pushState, we trim any forward history and add the new URL const newHistory = [ ...navigationHistory.slice(0, currentHistoryPosition + 1), payload.newUrl, ]; setNavigationHistory(newHistory); setCurrentHistoryPosition(newHistory.length - 1); } else if (type === "replaceState" && payload?.newUrl) { // For replaceState, we replace the current URL const newHistory = [...navigationHistory]; newHistory[currentHistoryPosition] = payload.newUrl; setNavigationHistory(newHistory); } } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [ navigationHistory, currentHistoryPosition, selectedAppId, errorMessage, setErrorMessage, setIsComponentSelectorInitialized, setSelectedComponentPreview, ]); useEffect(() => { // Update navigation buttons state setCanGoBack(currentHistoryPosition > 0); setCanGoForward(currentHistoryPosition < navigationHistory.length - 1); }, [navigationHistory, currentHistoryPosition]); // Initialize navigation history when iframe loads useEffect(() => { if (appUrl) { setNavigationHistory([appUrl]); setCurrentHistoryPosition(0); setCanGoBack(false); setCanGoForward(false); } }, [appUrl]); // Function to activate component selector in the iframe const handleActivateComponentSelector = () => { if (iframeRef.current?.contentWindow) { const newIsPicking = !isPicking; setIsPicking(newIsPicking); iframeRef.current.contentWindow.postMessage( { type: newIsPicking ? "activate-dyad-component-selector" : "deactivate-dyad-component-selector", }, "*", ); } }; // Function to navigate back const handleNavigateBack = () => { if (canGoBack && iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: "navigate", payload: { direction: "backward" }, }, "*", ); // Update our local state setCurrentHistoryPosition((prev) => prev - 1); setCanGoBack(currentHistoryPosition - 1 > 0); setCanGoForward(true); } }; // Function to navigate forward const handleNavigateForward = () => { if (canGoForward && iframeRef.current?.contentWindow) { iframeRef.current.contentWindow.postMessage( { type: "navigate", payload: { direction: "forward" }, }, "*", ); // Update our local state setCurrentHistoryPosition((prev) => prev + 1); setCanGoBack(true); setCanGoForward( currentHistoryPosition + 1 < navigationHistory.length - 1, ); } }; // Function to handle reload const handleReload = () => { setReloadKey((prevKey) => prevKey + 1); setErrorMessage(undefined); // Optionally, add logic here if you need to explicitly stop/start the app again // For now, just changing the key should remount the iframe console.debug("Reloading iframe preview for app", selectedAppId); }; // Function to navigate to a specific route const navigateToRoute = (path: string) => { if (iframeRef.current?.contentWindow && appUrl) { // Create the full URL by combining the base URL with the path const baseUrl = new URL(appUrl).origin; const newUrl = `${baseUrl}${path}`; // Navigate to the URL iframeRef.current.contentWindow.location.href = newUrl; // iframeRef.current.src = newUrl; // Update navigation history const newHistory = [ ...navigationHistory.slice(0, currentHistoryPosition + 1), newUrl, ]; setNavigationHistory(newHistory); setCurrentHistoryPosition(newHistory.length - 1); setCanGoBack(true); setCanGoForward(false); } }; // Display loading state if (loading) { return
Loading app preview...
; } // Display message if no app is selected if (selectedAppId === null) { return (
Select an app to see the preview.
); } return (
{/* Browser-style header */}
{/* Navigation Buttons */}

{isPicking ? "Deactivate component selector" : "Select component"}

{/* Address Bar with Routes Dropdown - using shadcn/ui dropdown-menu */}
{navigationHistory[currentHistoryPosition] ? new URL(navigationHistory[currentHistoryPosition]) .pathname : "/"}
{availableRoutes.length > 0 ? ( availableRoutes.map((route) => ( navigateToRoute(route.path)} className="flex justify-between" > {route.label} {route.path} )) ) : ( Loading routes... )}
{/* Action Buttons */}
setErrorMessage(undefined)} onAIFix={() => { if (selectedChatId) { streamMessage({ prompt: `Fix error: ${errorMessage}`, chatId: selectedChatId, }); } }} /> {!appUrl ? (

Starting up your app...

) : (