diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 9065d9c..adb3a94 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -484,6 +484,10 @@ export class PageObject { return this.page.getByText("Loading app preview..."); } + locateStartingAppPreview() { + return this.page.getByText("Starting up your app..."); + } + getPreviewIframeElement() { return this.page.getByTestId("preview-iframe-element"); } diff --git a/src/app/TitleBar.tsx b/src/app/TitleBar.tsx index ee0c348..5a9ce1c 100644 --- a/src/app/TitleBar.tsx +++ b/src/app/TitleBar.tsx @@ -1,7 +1,7 @@ import { useAtom } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useLoadApps } from "@/hooks/useLoadApps"; -import { useRouter } from "@tanstack/react-router"; +import { useRouter, useLocation } from "@tanstack/react-router"; import { useSettings } from "@/hooks/useSettings"; import { Button } from "@/components/ui/button"; // @ts-ignore @@ -20,11 +20,13 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { PreviewHeader } from "@/components/preview_panel/PreviewHeader"; export const TitleBar = () => { const [selectedAppId] = useAtom(selectedAppIdAtom); const { apps } = useLoadApps(); const { navigate } = useRouter(); + const location = useLocation(); const { settings, refreshSettings } = useSettings(); const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); const [showWindowControls, setShowWindowControls] = useState(false); @@ -90,6 +92,14 @@ export const TitleBar = () => { {displayText} {isDyadPro && } + + {/* Preview Header */} + {location.pathname === "/chat" && ( +
+ +
+ )} + {showWindowControls && } diff --git a/src/components/preview_panel/PreviewHeader.tsx b/src/components/preview_panel/PreviewHeader.tsx new file mode 100644 index 0000000..21fc738 --- /dev/null +++ b/src/components/preview_panel/PreviewHeader.tsx @@ -0,0 +1,214 @@ +import { useAtom, useAtomValue } from "jotai"; +import { previewModeAtom, selectedAppIdAtom } from "../../atoms/appAtoms"; +import { IpcClient } from "@/ipc/ipc_client"; + +import { + Eye, + Code, + MoreVertical, + Cog, + Trash2, + AlertTriangle, +} from "lucide-react"; +import { motion } from "framer-motion"; +import { useEffect, useRef, useState, useCallback } from "react"; + +import { useRunApp } from "@/hooks/useRunApp"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { showError, showSuccess } from "@/lib/toast"; +import { useMutation } from "@tanstack/react-query"; +import { useCheckProblems } from "@/hooks/useCheckProblems"; +import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; + +export type PreviewMode = "preview" | "code" | "problems"; + +// Preview Header component with preview mode toggle +export const PreviewHeader = () => { + const [previewMode, setPreviewMode] = useAtom(previewModeAtom); + const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom); + const selectedAppId = useAtomValue(selectedAppIdAtom); + const previewRef = useRef(null); + const codeRef = useRef(null); + const problemsRef = useRef(null); + const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 }); + const { problemReport } = useCheckProblems(selectedAppId); + const { restartApp, refreshAppIframe } = useRunApp(); + + const selectPanel = (panel: PreviewMode) => { + if (previewMode === panel) { + setIsPreviewOpen(!isPreviewOpen); + } else { + setPreviewMode(panel); + setIsPreviewOpen(true); + } + }; + + const onCleanRestart = useCallback(() => { + restartApp({ removeNodeModules: true }); + }, [restartApp]); + + const useClearSessionData = () => { + return useMutation({ + mutationFn: () => { + const ipcClient = IpcClient.getInstance(); + return ipcClient.clearSessionData(); + }, + onSuccess: async () => { + await refreshAppIframe(); + showSuccess("Preview data cleared"); + }, + onError: (error) => { + showError(`Error clearing preview data: ${error}`); + }, + }); + }; + + const { mutate: clearSessionData } = useClearSessionData(); + + const onClearSessionData = useCallback(() => { + clearSessionData(); + }, [clearSessionData]); + + // Get the problem count for the selected app + const problemCount = problemReport ? problemReport.problems.length : 0; + + // Format the problem count for display + const formatProblemCount = (count: number): string => { + if (count === 0) return ""; + if (count > 100) return "100+"; + return count.toString(); + }; + + const displayCount = formatProblemCount(problemCount); + + // Update indicator position when mode changes + useEffect(() => { + const updateIndicator = () => { + let targetRef: React.RefObject; + + switch (previewMode) { + case "preview": + targetRef = previewRef; + break; + case "code": + targetRef = codeRef; + break; + case "problems": + targetRef = problemsRef; + break; + default: + return; + } + + if (targetRef.current) { + const button = targetRef.current; + const container = button.parentElement; + if (container) { + const containerRect = container.getBoundingClientRect(); + const buttonRect = button.getBoundingClientRect(); + const left = buttonRect.left - containerRect.left; + const width = buttonRect.width; + + setIndicatorStyle({ left, width }); + if (!isPreviewOpen) { + setIndicatorStyle({ left: left, width: 0 }); + } + } + } + }; + + // Small delay to ensure DOM is updated + const timeoutId = setTimeout(updateIndicator, 10); + return () => clearTimeout(timeoutId); + }, [previewMode, displayCount, isPreviewOpen]); + + return ( +
+
+ + + + +
+
+ + + + + + + +
+ Rebuild + + Re-installs node_modules and restarts + +
+
+ + +
+ Clear Cache + + Clears cookies and local storage and other app cache + +
+
+
+
+
+
+ ); +}; diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index ec75848..d216383 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -18,6 +18,7 @@ import { Lightbulb, ChevronRight, MousePointerClick, + Power, } from "lucide-react"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { IpcClient } from "@/ipc/ipc_client"; @@ -38,6 +39,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useRunApp } from "@/hooks/useRunApp"; interface ErrorBannerProps { error: string | undefined; @@ -129,7 +131,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => { const [availableRoutes, setAvailableRoutes] = useState< Array<{ path: string; label: string }> >([]); - + const { restartApp } = useRunApp(); // Load router related files to extract routes const { content: routerContent } = useLoadAppFile( selectedAppId, @@ -423,6 +425,10 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => { ); } + const onRestart = () => { + restartApp(); + }; + return (
{/* Browser-style header */} @@ -519,6 +525,14 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => { {/* Action Buttons */}
+ - - -
-
- - - - - - - - -
- Rebuild - - Re-installs node_modules and restarts - -
-
- - -
- Clear Cache - - Clears cookies and local storage and other app cache - -
-
-
-
-
-
- ); -}; - // Console header component const ConsoleHeader = ({ isOpen, @@ -237,11 +47,10 @@ const ConsoleHeader = ({ // Main PreviewPanel component export function PreviewPanel() { - const [previewMode, setPreviewMode] = useAtom(previewModeAtom); + const [previewMode] = useAtom(previewModeAtom); const selectedAppId = useAtomValue(selectedAppIdAtom); const [isConsoleOpen, setIsConsoleOpen] = useState(false); - const { runApp, stopApp, restartApp, loading, app, refreshAppIframe } = - useRunApp(); + const { runApp, stopApp, loading, app } = useRunApp(); const runningAppIdRef = useRef(null); const key = useAtomValue(previewPanelKeyAtom); const appOutput = useAtomValue(appOutputAtom); @@ -250,37 +59,6 @@ export function PreviewPanel() { const latestMessage = messageCount > 0 ? appOutput[messageCount - 1]?.message : undefined; - const handleRestart = useCallback(() => { - restartApp(); - }, [restartApp]); - - const handleCleanRestart = useCallback(() => { - restartApp({ removeNodeModules: true }); - }, [restartApp]); - - const useClearSessionData = () => { - return useMutation({ - mutationFn: () => { - const ipcClient = IpcClient.getInstance(); - return ipcClient.clearSessionData(); - }, - onSuccess: async () => { - await refreshAppIframe(); - showSuccess("Preview data cleared"); - // Optionally invalidate relevant queries - }, - onError: (error) => { - showError(`Error clearing preview data: ${error}`); - }, - }); - }; - - const { mutate: clearSessionData } = useClearSessionData(); - - const handleClearSessionData = useCallback(() => { - clearSessionData(); - }, [selectedAppId, clearSessionData]); - useEffect(() => { const previousAppId = runningAppIdRef.current; @@ -327,13 +105,6 @@ export function PreviewPanel() { }, [selectedAppId, runApp, stopApp]); return (
-
diff --git a/src/hooks/useRunApp.ts b/src/hooks/useRunApp.ts index 6a59405..fcd0b92 100644 --- a/src/hooks/useRunApp.ts +++ b/src/hooks/useRunApp.ts @@ -1,4 +1,5 @@ -import { useState, useCallback } from "react"; +import { useCallback } from "react"; +import { atom } from "jotai"; import { IpcClient } from "@/ipc/ipc_client"; import { appOutputAtom, @@ -11,8 +12,10 @@ import { import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { AppOutput } from "@/ipc/ipc_types"; +const useRunAppLoadingAtom = atom(false); + export function useRunApp() { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useAtom(useRunAppLoadingAtom); const [app, setApp] = useAtom(currentAppAtom); const setAppOutput = useSetAtom(appOutputAtom); const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);