Tip:
{isDockerError
? "Make sure Docker Desktop is running and try restarting the app."
: error.source === "dyad-app"
? "Try restarting the Dyad app or restarting your computer to see if that fixes the error."
: "Check if restarting the app fixes the error."}
{/* Action buttons at the bottom */}
{!isDockerError && error.source === "preview-app" && (
)}
);
};
// 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 { routes: availableRoutes } = useParseRouter(selectedAppId);
const { restartApp } = useRunApp();
const { userBudget } = useUserBudgetInfo();
const isProMode = !!userBudget;
// 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 setSelectedComponentsPreview = useSetAtom(
selectedComponentsPreviewAtom,
);
const [visualEditingSelectedComponent, setVisualEditingSelectedComponent] =
useAtom(visualEditingSelectedComponentAtom);
const setCurrentComponentCoordinates = useSetAtom(
currentComponentCoordinatesAtom,
);
const setPreviewIframeRef = useSetAtom(previewIframeRefAtom);
const iframeRef = useRef(null);
const [isPicking, setIsPicking] = useState(false);
const [annotatorMode, setAnnotatorMode] = useAtom(annotatorModeAtom);
const [screenshotDataUrl, setScreenshotDataUrl] = useAtom(
screenshotDataUrlAtom,
);
const { addAttachments } = useAttachments();
const setPendingChanges = useSetAtom(pendingVisualChangesAtom);
// AST Analysis State
const [isDynamicComponent, setIsDynamicComponent] = useState(false);
const [hasStaticText, setHasStaticText] = useState(false);
// Device mode state
type DeviceMode = "desktop" | "tablet" | "mobile";
const [deviceMode, setDeviceMode] = useState("desktop");
const [isDevicePopoverOpen, setIsDevicePopoverOpen] = useState(false);
// Device configurations
const deviceWidthConfig = {
tablet: 768,
mobile: 375,
};
//detect if the user is using Mac
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const analyzeComponent = async (componentId: string) => {
if (!componentId || !selectedAppId) return;
try {
const result = await IpcClient.getInstance().analyzeComponent({
appId: selectedAppId,
componentId,
});
setIsDynamicComponent(result.isDynamic);
setHasStaticText(result.hasStaticText);
// Automatically enable text editing if component has static text
if (result.hasStaticText && iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{
type: "enable-dyad-text-editing",
data: {
componentId: componentId,
runtimeId: visualEditingSelectedComponent?.runtimeId,
},
},
"*",
);
}
} catch (err) {
console.error("Failed to analyze component", err);
setIsDynamicComponent(false);
setHasStaticText(false);
}
};
const handleTextUpdated = async (data: any) => {
const { componentId, text } = data;
if (!componentId || !selectedAppId) return;
// Parse componentId to extract file path and line number
const [filePath, lineStr] = componentId.split(":");
const lineNumber = parseInt(lineStr, 10);
if (!filePath || isNaN(lineNumber)) {
console.error("Invalid componentId format:", componentId);
return;
}
// Store text change in pending changes
setPendingChanges((prev) => {
const updated = new Map(prev);
const existing = updated.get(componentId);
updated.set(componentId, {
componentId: componentId,
componentName:
existing?.componentName || visualEditingSelectedComponent?.name || "",
relativePath: filePath,
lineNumber: lineNumber,
styles: existing?.styles || {},
textContent: text,
});
return updated;
});
};
// Function to get current styles from selected element
const getCurrentElementStyles = () => {
if (!iframeRef.current?.contentWindow || !visualEditingSelectedComponent)
return;
try {
// Send message to iframe to get current styles
iframeRef.current.contentWindow.postMessage(
{
type: "get-dyad-component-styles",
data: {
elementId: visualEditingSelectedComponent.id,
runtimeId: visualEditingSelectedComponent.runtimeId,
},
},
"*",
);
} catch (error) {
console.error("Failed to get element styles:", error);
}
};
useEffect(() => {
setAnnotatorMode(false);
}, []);
// Reset visual editing state when app changes or component unmounts
useEffect(() => {
return () => {
// Cleanup on unmount or when app changes
setVisualEditingSelectedComponent(null);
setPendingChanges(new Map());
setCurrentComponentCoordinates(null);
};
}, [selectedAppId]);
// Update iframe ref atom
useEffect(() => {
setPreviewIframeRef(iframeRef.current);
}, [iframeRef.current, setPreviewIframeRef]);
// Send pro mode status to iframe
useEffect(() => {
if (iframeRef.current?.contentWindow && isComponentSelectorInitialized) {
iframeRef.current.contentWindow.postMessage(
{ type: "dyad-pro-mode", enabled: isProMode },
"*",
);
}
}, [isProMode, isComponentSelectorInitialized]);
// 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);
iframeRef.current?.contentWindow?.postMessage(
{ type: "dyad-pro-mode", enabled: isProMode },
"*",
);
return;
}
if (event.data?.type === "dyad-text-updated") {
handleTextUpdated(event.data);
return;
}
if (event.data?.type === "dyad-text-finalized") {
handleTextUpdated(event.data);
return;
}
if (event.data?.type === "dyad-component-selected") {
console.log("Component picked:", event.data);
const component = parseComponentSelection(event.data);
if (!component) return;
// Store the coordinates
if (event.data.coordinates && isProMode) {
setCurrentComponentCoordinates(event.data.coordinates);
}
// Add to selected components if not already there
setSelectedComponentsPreview((prev) => {
const exists = prev.some((c) => {
// Check by runtimeId if available otherwise by id
// Stored components may have lost their runtimeId after re-renders or reloading the page
if (component.runtimeId && c.runtimeId) {
return c.runtimeId === component.runtimeId;
}
return c.id === component.id;
});
if (exists) {
return prev;
}
return [...prev, component];
});
if (isProMode) {
// Set as the highlighted component for visual editing
setVisualEditingSelectedComponent(component);
// Trigger AST analysis
analyzeComponent(component.id);
}
return;
}
if (event.data?.type === "dyad-component-deselected") {
const componentId = event.data.componentId;
if (componentId) {
// Disable text editing for the deselected component
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{
type: "disable-dyad-text-editing",
data: { componentId },
},
"*",
);
}
setSelectedComponentsPreview((prev) =>
prev.filter((c) => c.id !== componentId),
);
setVisualEditingSelectedComponent((prev) => {
const shouldClear = prev?.id === componentId;
if (shouldClear) {
setCurrentComponentCoordinates(null);
}
return shouldClear ? null : prev;
});
}
return;
}
if (event.data?.type === "dyad-component-coordinates-updated") {
if (event.data.coordinates) {
setCurrentComponentCoordinates(event.data.coordinates);
}
return;
}
if (event.data?.type === "dyad-screenshot-response") {
if (event.data.success && event.data.dataUrl) {
setScreenshotDataUrl(event.data.dataUrl);
setAnnotatorMode(true);
} else {
showError(event.data.error);
}
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({ message: errorMessage, source: "preview-app" });
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({ message: errorMessage, source: "preview-app" });
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,
setSelectedComponentsPreview,
setVisualEditingSelectedComponent,
]);
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]);
// Get current styles when component is selected for visual editing
useEffect(() => {
if (visualEditingSelectedComponent) {
getCurrentElementStyles();
}
}, [visualEditingSelectedComponent]);
// Function to activate component selector in the iframe
const handleActivateComponentSelector = () => {
if (iframeRef.current?.contentWindow) {
const newIsPicking = !isPicking;
if (!newIsPicking) {
// Clean up any text editing states when deactivating
iframeRef.current.contentWindow.postMessage(
{ type: "cleanup-all-text-editing" },
"*",
);
}
setIsPicking(newIsPicking);
setVisualEditingSelectedComponent(null);
iframeRef.current.contentWindow.postMessage(
{
type: newIsPicking
? "activate-dyad-component-selector"
: "deactivate-dyad-component-selector",
},
"*",
);
}
};
// Function to handle annotator button click
const handleAnnotatorClick = () => {
if (annotatorMode) {
setAnnotatorMode(false);
return;
}
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage(
{
type: "dyad-take-screenshot",
},
"*",
);
}
};
// Activate component selector using a shortcut
useShortcut(
"c",
{ shift: true, ctrl: !isMac, meta: isMac },
handleActivateComponentSelector,
isComponentSelectorInitialized,
iframeRef,
);
// 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);
// Reset visual editing state
setVisualEditingSelectedComponent(null);
setPendingChanges(new Map());
setCurrentComponentCoordinates(null);
// 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 (
Preparing app preview...
);
}
// Display message if no app is selected
if (selectedAppId === null) {
return (