Download frontend/src/ui/interactive/WizardProgressOverlay.tsx from ruslanmv/HomePilot: direct link, hf CLI and curl.
- Browser
- Download file 6.17 kB
-
https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/interactive/WizardProgressOverlay.tsx
- Command line
-
hf download hf://spaces/ruslanmv/HomePilot/frontend/src/ui/interactive/WizardProgressOverlay.tsx
-
curl -L -o WizardProgressOverlay.tsx https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/interactive/WizardProgressOverlay.tsx
6.17 kB
| /** | |
| * Global wizard-progress overlay. | |
| * | |
| * Mounted once at App level so the "Generating scenes…" modal | |
| * persists across tab switches. WizardAutoPreview used to render | |
| * this inline, which meant clicking Chat / Imagine / Voice mid- | |
| * generation unmounted the wizard and the modal vanished even | |
| * though the SSE stream was still running on the backend. | |
| * | |
| * State lives in ``wizardProgressStore`` (module-level singleton); | |
| * this component is a pure consumer. | |
| * | |
| * Renders nothing when ``state.active === false``. When active, it | |
| * portals a fixed-position overlay over the entire viewport so it | |
| * stays on top of whatever route the user has navigated to. | |
| */ | |
| import React, { useMemo } from "react"; | |
| import { createPortal } from "react-dom"; | |
| import { ExpertLogPanel, useExpertMode } from "./ExpertLogPanel"; | |
| import { GeneratingPanel } from "./GeneratingPanel"; | |
| import { | |
| useWizardProgress, | |
| dismissOverlay, | |
| type WizardProgressState, | |
| } from "./wizardProgressStore"; | |
| const GENERATE_STEPS = [ | |
| { label: "Saving your project" }, | |
| { label: "Drafting the scene graph" }, | |
| { label: "Writing dialogue + choices" }, | |
| { label: "Rendering scenes" }, | |
| { label: "Opening the editor" }, | |
| ] as const; | |
| function _panelTitle(s: WizardProgressState, renderEnabled: boolean): string { | |
| if (s.error) return "Generation failed"; | |
| if (s.renderTotal > 0 && s.renderDone < s.renderTotal) { | |
| return `Rendering scenes · ${s.renderDone + 1} of ${s.renderTotal}`; | |
| } | |
| if (s.libraryTotal > 0 && s.libraryDone < s.libraryTotal) { | |
| return `Building persona library · ${s.libraryDone + 1} of ${s.libraryTotal}`; | |
| } | |
| if (s.genStep >= GENERATE_STEPS.length - 1) return "Opening the editor"; | |
| return GENERATE_STEPS[Math.max(0, Math.min(s.genStep, GENERATE_STEPS.length - 1))].label; | |
| } | |
| function _panelDescription(s: WizardProgressState): string { | |
| if (s.error) return s.error; | |
| if (s.currentSceneTitle && s.renderTotal > 0 && s.renderDone < s.renderTotal) { | |
| return `Now rendering: ${s.currentSceneTitle}`; | |
| } | |
| if (s.libraryTotal > 0 && s.libraryDone < s.libraryTotal) { | |
| return "Pre-generating the persona's reaction pack — this is a one-time pass per persona."; | |
| } | |
| if (s.genStep === GENERATE_STEPS.length - 1) { | |
| return "Almost ready — opening the editor."; | |
| } | |
| return "AI is working — this usually takes a few seconds."; | |
| } | |
| export function WizardProgressOverlay(): React.ReactElement | null { | |
| const state = useWizardProgress(); | |
| const [expertMode] = useExpertMode(); | |
| const showProgressBar = useMemo( | |
| () => state.renderTotal > 0, | |
| [state.renderTotal], | |
| ); | |
| const progressPct = useMemo(() => { | |
| if (!showProgressBar) return undefined; | |
| return Math.round((state.renderDone / Math.max(1, state.renderTotal)) * 100); | |
| }, [showProgressBar, state.renderDone, state.renderTotal]); | |
| const progressLabel = useMemo(() => { | |
| if (!showProgressBar) return undefined; | |
| return `${state.renderDone} / ${state.renderTotal} scenes${ | |
| state.renderSkipped > 0 ? ` · ${state.renderSkipped} skipped` : "" | |
| }`; | |
| }, [showProgressBar, state.renderDone, state.renderTotal, state.renderSkipped]); | |
| // Hide when nothing's running. Errors are shown for ~5 s after the | |
| // run fails so the user has a moment to read them before the | |
| // overlay clears (ushered out by the wizard's inline retry button). | |
| if (!state.active && !state.error) { | |
| return null; | |
| } | |
| // The wizard route renders its own GeneratingPanel inline. We don't | |
| // want two stacked modals when the user IS on the wizard route — | |
| // detect that by looking for the wizard's container in the DOM. | |
| // Cheap, race-free: we render at App level only when no inline | |
| // overlay is mounted by WizardAutoPreview. | |
| // | |
| // Implementation: WizardAutoPreview no longer renders its own panel | |
| // (the inline render is now this component), so always show the | |
| // overlay when the store says we're active. | |
| const node = ( | |
| <div | |
| role="dialog" | |
| aria-modal="true" | |
| aria-label="Project generation progress" | |
| style={{ | |
| position: "fixed", | |
| inset: 0, | |
| zIndex: 9000, | |
| background: "rgba(0,0,0,0.55)", | |
| backdropFilter: "blur(4px)", | |
| WebkitBackdropFilter: "blur(4px)", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| padding: "1rem", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| maxWidth: expertMode ? 920 : 480, | |
| width: "100%", | |
| display: "flex", | |
| flexDirection: expertMode ? "row" : "column", | |
| gap: expertMode ? "0.75rem" : 0, | |
| alignItems: "stretch", | |
| }} | |
| > | |
| <div style={{ flex: expertMode ? "0 0 420px" : "1 1 auto", minWidth: 0 }}> | |
| <GeneratingPanel | |
| title={_panelTitle(state, true)} | |
| description={_panelDescription(state)} | |
| steps={GENERATE_STEPS as unknown as Array<{ label: string }>} | |
| activeStep={state.genStep} | |
| accentClassName="text-[#c4b5fd]" | |
| spinnerSize="large" | |
| progress={progressPct} | |
| progressLabel={progressLabel} | |
| /> | |
| </div> | |
| {expertMode && ( | |
| <ExpertLogPanel | |
| className="flex-1 min-w-0 max-h-[28rem]" | |
| /> | |
| )} | |
| {state.error && ( | |
| <div | |
| style={{ | |
| marginTop: "0.75rem", | |
| display: "flex", | |
| justifyContent: "flex-end", | |
| }} | |
| > | |
| <button | |
| type="button" | |
| onClick={() => dismissOverlay()} | |
| style={{ | |
| padding: "0.5rem 1rem", | |
| background: "#1f2937", | |
| color: "#f1f1f1", | |
| border: "1px solid #374151", | |
| borderRadius: 6, | |
| cursor: "pointer", | |
| }} | |
| > | |
| Dismiss | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| // Portal to body so this floats above the entire app shell | |
| // regardless of which route is active when the user navigates. | |
| return typeof document !== "undefined" | |
| ? createPortal(node, document.body) | |
| : null; | |
| } | |