"use client"; import { useEffect, useState } from "react"; import { motion, useReducedMotion } from "framer-motion"; import { Button, ProgressBar } from "@/components/ui"; import { useI18n } from "@/lib/i18n"; /** * The one loading screen for every model pipeline — deck/notes creation, * homework, and the route-level "preparing…" screens: the app icon, the * rotating stage message as the headline, the two-step progress bar, the * checklist of stages, an optional tip and Cancel. * * Extracted from the deck wizard (the look people already know) so the * homework flows and route loads read exactly the same. */ export type LoadingStage = { id: string; /** First line of the checklist (= the stage's first message). */ labelKey: string; /** Rotating messages while this stage runs. */ messageKeys: string[]; }; export function LoadingState({ stages, activeStage, message, messageKey, tipKeys, onCancel, }: { /** Pipeline stages in order. Omit for a plain "working" screen. */ stages?: LoadingStage[]; /** Which stage is running now (matches a stage id). */ activeStage?: string; /** Live message from the server; wins until the first rotation tick. */ message?: string; /** Static message for screens without stages. */ messageKey?: string; /** Pick one of these at random and show it under the stages. */ tipKeys?: string[]; onCancel?: () => void; }) { const { t } = useI18n(); const reduceMotion = useReducedMotion(); const [tick, setTick] = useState(0); const [tip, setTip] = useState(null); useEffect(() => { if (!stages) return; const timer = window.setInterval(() => setTick((value) => value + 1), 3200); return () => window.clearInterval(timer); }, [stages]); useEffect(() => { if (!tipKeys || tipKeys.length === 0) return; setTip(tipKeys[Math.floor(Math.random() * tipKeys.length)]); }, [tipKeys]); const activeIndex = stages ? Math.max(0, stages.findIndex((stage) => stage.id === activeStage)) : 0; const rotation = stages?.[activeIndex]?.messageKeys ?? []; const shown = tick === 0 && message ? message : rotation.length > 0 ? t(rotation[tick % rotation.length]) : messageKey ? t(messageKey) : ""; return (
{/* The app icon, not the bare snowflake — same logo as the header, sidebar and empty states. */} {/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
{shown} {stages && stages.length > 0 ? ( ) : null} {stages && stages.length > 1 ? (
    {stages.map((stage, index) => (
  1. {index < activeIndex ? "✅" : index === activeIndex ? "🌀" : "⬜"} {t(stage.labelKey)}
  2. ))}
) : null} {tip ?

{t(tip)}

: null} {onCancel ? ( ) : null}
); }