Download frontend/src/ui/interactive/GeneratingPanel.tsx from ruslanmv/HomePilot: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/interactive/GeneratingPanel.tsx
- Command line
-
hf download hf://spaces/ruslanmv/HomePilot/frontend/src/ui/interactive/GeneratingPanel.tsx
-
curl -L -o GeneratingPanel.tsx https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/interactive/GeneratingPanel.tsx
10.4 kB
| /** | |
| * GeneratingPanel β reusable enterprise-grade waiting overlay | |
| * used across Interactive surfaces. | |
| * | |
| * Mirrors the pattern Animate uses on its video-card processing | |
| * overlay (see Animate.tsx β bg-black/50 backdrop-blur-sm, | |
| * Loader2 size=32 animate-spin, text-white/90 sm font-medium | |
| * label, optional pill under it for extra context) so the two | |
| * features feel like siblings under the HomePilot brand. | |
| * | |
| * Two modes: | |
| * - `variant="overlay"` fills its positioned parent (use inside | |
| * a ``relative`` container; the form beneath stays in the DOM | |
| * and the overlay sits above it). | |
| * - `variant="panel"` renders a self-contained block for places | |
| * that don't have a host to overlay. | |
| * | |
| * Two progress styles (mixable): | |
| * - `steps[]` β vertical checklist; promotes each stage | |
| * "pending β running β done" as activeStep | |
| * advances. Best for a small fixed set of | |
| * phases (plan β graph β ready). | |
| * - `progress` + `progressLabel` β linear gradient progress bar | |
| * with text below. Best for long-running | |
| * batch ops (render scene 5 / 12). | |
| * | |
| * ``spinnerSize="large"`` switches the 32px Loader2 for a 64px | |
| * dual-ring spinner (outer static ring + rotating inner arc), | |
| * matching the visual weight of a full-screen creation modal. | |
| * ``iconOverride`` swaps the central Sparkles for per-phase icons | |
| * (e.g. ImageIcon while rendering stills, Film while rendering | |
| * clips) so the user sees what phase we're in at a glance. | |
| */ | |
| import React from "react"; | |
| import { Check, Loader2, Sparkles } from "lucide-react"; | |
| export interface GeneratingStep { | |
| label: string; | |
| } | |
| export interface GeneratingPanelProps { | |
| /** Big heading above the spinner, e.g. "Generating project". */ | |
| title: string; | |
| /** One-line hint below the title. Optional. */ | |
| description?: React.ReactNode; | |
| /** Variant picks full-parent overlay vs self-contained block. */ | |
| variant?: "overlay" | "panel"; | |
| /** Ordered stages the caller walks through. */ | |
| steps?: GeneratingStep[]; | |
| /** Which step is currently running. Earlier ones render as done; | |
| * later ones render as pending. Default 0. */ | |
| activeStep?: number; | |
| /** 0β100 linear progress. Renders the gradient bar + label | |
| * under the title. Omit for spinner-only indeterminate mode. */ | |
| progress?: number; | |
| /** Small text under the progress bar (e.g. "5 / 12 scenes"). */ | |
| progressLabel?: React.ReactNode; | |
| /** Tint color for the spinner + running-step indicator. Accepts | |
| * a Tailwind arbitrary-value color class (e.g. "text-[#3ea6ff]" | |
| * or "text-purple-400"). Default matches Animate (purple). */ | |
| accentClassName?: string; | |
| /** "small" = 32px Loader2 (default, legacy look). | |
| * "large" = 64px dual-ring spinner (Studio-style modal). */ | |
| spinnerSize?: "small" | "large"; | |
| /** Replace the decorative Sparkles with a phase-specific icon | |
| * (ImageIcon, Film, Gamepad2β¦). Sits centered inside the | |
| * dual-ring when spinnerSize='large', or next to the 32px | |
| * loader when 'small'. */ | |
| iconOverride?: React.ReactNode; | |
| /** Footer pill copy. Default: "AI is working β this usually | |
| * takes a few seconds." Pass null to hide the pill entirely. */ | |
| footerHint?: React.ReactNode | null; | |
| } | |
| export function GeneratingPanel({ | |
| title, | |
| description, | |
| variant = "overlay", | |
| steps, | |
| activeStep = 0, | |
| progress, | |
| progressLabel, | |
| accentClassName = "text-purple-400", | |
| spinnerSize = "small", | |
| iconOverride, | |
| footerHint, | |
| }: GeneratingPanelProps) { | |
| const clamped = typeof progress === "number" | |
| ? Math.max(0, Math.min(100, progress)) | |
| : undefined; | |
| const hasProgress = clamped !== undefined; | |
| const spinner = spinnerSize === "large" | |
| ? <LargeSpinner accentClassName={accentClassName} icon={iconOverride} /> | |
| : <SmallSpinner accentClassName={accentClassName} icon={iconOverride} />; | |
| const body = ( | |
| <div className="flex flex-col items-center justify-center max-w-md text-center px-6"> | |
| {spinner} | |
| <div className="text-white/95 text-sm font-semibold">{title}</div> | |
| {description && ( | |
| <div className="text-white/60 text-xs mt-1.5 leading-relaxed"> | |
| {description} | |
| </div> | |
| )} | |
| {hasProgress && ( | |
| <div className="mt-4 w-full max-w-xs"> | |
| <div | |
| className="h-2 bg-white/10 rounded-full overflow-hidden" | |
| role="progressbar" | |
| aria-valuenow={clamped} | |
| aria-valuemin={0} | |
| aria-valuemax={100} | |
| aria-label={typeof progressLabel === "string" ? progressLabel : "Progress"} | |
| > | |
| <div | |
| className="h-full bg-gradient-to-r from-[#3ea6ff] to-[#6366f1] transition-[width] duration-500 ease-out" | |
| style={{ width: `${clamped}%` }} | |
| /> | |
| </div> | |
| {progressLabel && ( | |
| <div className="text-[11px] text-white/50 mt-2 tabular-nums"> | |
| {progressLabel} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {steps && steps.length > 0 && ( | |
| <ol | |
| className={[ | |
| "w-full max-w-xs text-left flex flex-col gap-2", | |
| hasProgress ? "mt-4" : "mt-5", | |
| ].join(" ")} | |
| aria-label="Progress" | |
| > | |
| {steps.map((s, i) => { | |
| const done = i < activeStep; | |
| const running = i === activeStep; | |
| return ( | |
| <li | |
| key={`${i}-${s.label}`} | |
| className={[ | |
| "flex items-center gap-2.5 text-xs", | |
| done ? "text-white/90" | |
| : running ? "text-white/95" | |
| : "text-white/40", | |
| ].join(" ")} | |
| > | |
| <StepIndicator | |
| state={done ? "done" : running ? "running" : "pending"} | |
| accentClassName={accentClassName} | |
| /> | |
| <span>{s.label}</span> | |
| </li> | |
| ); | |
| })} | |
| </ol> | |
| )} | |
| {footerHint !== null && ( | |
| <div className="mt-5 px-3 py-1 rounded-full bg-black/60 border border-white/10 text-white/70 text-[11px]"> | |
| {footerHint ?? "AI is working β this usually takes a few seconds."} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| if (variant === "overlay") { | |
| return ( | |
| <div | |
| role="status" | |
| aria-live="polite" | |
| className="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm" | |
| > | |
| {body} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div | |
| role="status" | |
| aria-live="polite" | |
| className="rounded-2xl border border-[#3f3f3f] bg-[#121212] px-6 py-12 flex items-center justify-center" | |
| > | |
| {body} | |
| </div> | |
| ); | |
| } | |
| // ββ Spinner variants ββββββββββββββββββββββββββββββββββββββββββββ | |
| function SmallSpinner({ | |
| accentClassName, icon, | |
| }: { accentClassName: string; icon?: React.ReactNode }) { | |
| return ( | |
| <div className="flex items-center justify-center gap-2 mb-4"> | |
| <Loader2 size={32} className={`${accentClassName} animate-spin`} aria-hidden /> | |
| <span className={`${accentClassName} opacity-80`} aria-hidden> | |
| {icon ?? <Sparkles size={18} />} | |
| </span> | |
| </div> | |
| ); | |
| } | |
| function LargeSpinner({ | |
| accentClassName, icon, | |
| }: { accentClassName: string; icon?: React.ReactNode }) { | |
| // Studio-inspired dual-ring: a faint static outer ring plus a | |
| // bright rotating arc on top, with the phase icon planted dead | |
| // center. Keeps the visual weight enterprise-y for long waits | |
| // without depending on any Studio code. | |
| return ( | |
| <div className="relative w-20 h-20 mb-5" aria-hidden> | |
| {/* Static base ring β signals "work in progress" even when | |
| the animated layer is occluded. */} | |
| <div className={`absolute inset-0 rounded-full border-4 ${accentToBorder(accentClassName, "/20")}`} /> | |
| {/* Rotating arc. Using the built-in animate-spin on a partial | |
| border rather than a keyframe gradient so the look stays | |
| crisp at any screen DPR. */} | |
| <div | |
| className={`absolute inset-0 rounded-full border-4 border-transparent ${accentToBorder(accentClassName, "")} animate-spin`} | |
| style={{ | |
| // Show only the top-right quarter of the ring so it | |
| // reads as a clear moving arc instead of a fuzzy halo. | |
| borderRightColor: "transparent", | |
| borderBottomColor: "transparent", | |
| borderLeftColor: "transparent", | |
| }} | |
| /> | |
| {/* Center phase icon. */} | |
| <div className={`absolute inset-0 flex items-center justify-center ${accentClassName}`}> | |
| {icon ?? <Sparkles size={26} />} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /** | |
| * Derive a Tailwind border-color class from an accent text-color | |
| * class so the dual-ring picks up whatever brand tint the caller | |
| * passed. Falls back to white/20 + white for unknown prefixes so | |
| * the component still renders sensibly with custom colors. | |
| */ | |
| function accentToBorder(textClass: string, opacitySuffix: string): string { | |
| // "text-purple-400" β "border-purple-400", "text-[#3ea6ff]" β | |
| // "border-[#3ea6ff]". We keep opacity as a raw suffix so callers | |
| // can pass an empty string for the solid ring + "/20" for the | |
| // faint base. Tailwind JIT will emit both forms safely. | |
| const colorPart = textClass.replace(/^text-/, ""); | |
| if (!colorPart) return `border-white${opacitySuffix || ""}`; | |
| return `border-${colorPart}${opacitySuffix}`; | |
| } | |
| function StepIndicator({ | |
| state, accentClassName, | |
| }: { | |
| state: "pending" | "running" | "done"; | |
| accentClassName: string; | |
| }) { | |
| if (state === "done") { | |
| return ( | |
| <span className="w-4 h-4 rounded-full bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center shrink-0"> | |
| <Check className="w-2.5 h-2.5 text-emerald-400" aria-hidden /> | |
| </span> | |
| ); | |
| } | |
| if (state === "running") { | |
| return ( | |
| <span className="w-4 h-4 rounded-full border border-white/20 flex items-center justify-center shrink-0"> | |
| <Loader2 className={`w-2.5 h-2.5 ${accentClassName} animate-spin`} aria-hidden /> | |
| </span> | |
| ); | |
| } | |
| return ( | |
| <span className="w-4 h-4 rounded-full border border-white/15 shrink-0" aria-hidden /> | |
| ); | |
| } | |