"use client"; import { Ban, Check, Circle, CircleAlert, Loader2, RefreshCw, X, } from "lucide-react"; import { memo, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { BatchItemStatus, VerificationSummary } from "./studio-types"; const statusLabels: Record = { pending: "Waiting", started: "Starting", downloading: "Downloading assets", converting: "Preparing assets", writing_db: "Saving", done: "Imported", failed: "Failed", }; function formatDuration(ms: number): string { if (!Number.isFinite(ms) || ms < 0) return "0s"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); const rest = s % 60; return rest > 0 ? `${m}m ${rest}s` : `${m}m`; } function formatRate(completed: number, ms: number): string { if (ms < 1500) return ""; const perMinute = completed / (ms / 60000); if (perMinute < 1) return `${(completed / (ms / 1000)).toFixed(1)}/s`; return `${Math.round(perMinute)}/min`; } export const BatchProgress = memo(function BatchProgress({ progress, done, succeeded, failed, cancelling, verification, failedClassnames, startTime, onCancel, onDismiss, onRetry, }: { progress: Map; done: boolean; succeeded: number; failed: number; cancelling?: boolean; verification?: VerificationSummary | null; startTime?: number; failedClassnames?: string[]; onCancel?: () => void; onDismiss?: () => void; onRetry?: (classnames: string[]) => void; }) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (done) return; const id = setInterval(() => setNow(Date.now()), 500); return () => clearInterval(id); }, [done]); // Relevance-first list: newest activity on top, capped so high-volume // imports don't render hundreds of DOM rows on every progress event. const VISIBLE_ITEMS = 60; const entries = useMemo( () => [...progress.values()].slice().reverse(), [progress], ); const completed = useMemo( () => entries.filter((i) => i.status === "done" || i.status === "failed") .length, [entries], ); const total = entries.length; const percent = total ? Math.round((completed / total) * 100) : 0; const imported = done ? succeeded : entries.filter((item) => item.status === "done").length; const errors = done ? failed : entries.filter((item) => item.status === "failed").length; const unfinished = Math.max(0, total - completed); const elapsed = startTime ? now - startTime : 0; const rate = formatRate(completed, elapsed); const heading = !done ? cancelling ? "Stopping import…" : "Importing furniture…" : unfinished ? "Import stopped" : errors ? "Import completed with errors" : "Import completed"; const issues = verification ? verification.furniDataMissing + verification.furniDataConflicts : 0; return (

{heading}

{imported} {" "} imported · 0 && "text-[var(--admin-error)]", )} > {errors} {" "} failed {unfinished > 0 && ( <> · {unfinished} {done ? "not completed" : "remaining"} )} {!done && startTime ? ( <> · {formatDuration(elapsed)} {rate && ( {rate} )} ) : null}

{done ? ( <> {!!failedClassnames?.length && onRetry && ( )} ) : ( )}
{done && verification && (

{issues ? `${issues} catalog data issue(s) still need attention. Open Catalog audit to review them.` : "Catalog data verification completed."}

)}
View item details ({total})
{entries.length > VISIBLE_ITEMS && (

Showing {VISIBLE_ITEMS} of {entries.length} items

)} {entries.slice(0, VISIBLE_ITEMS).map((item) => (
{item.status === "done" ? ( ) : item.status === "failed" ? ( ) : done || item.status === "pending" ? ( ) : ( )} {item.classname} {done && item.status !== "done" && item.status !== "failed" ? "Not completed" : statusLabels[item.status]} {item.message && ( {item.message} )} {[...new Set(item.warnings ?? [])].map((warning) => ( {warning} ))}
))}
); }); function NodeChip({ count, label }: { count: number; label: string }) { if (!count) return null; return ( {count} {label} ); }