| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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]); |
|
|
| |
| |
| |
| if (!state.active && !state.error) { |
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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> |
| ); |
|
|
| |
| |
| return typeof document !== "undefined" |
| ? createPortal(node, document.body) |
| : null; |
| } |
|
|