"use client"; import React, { useCallback, useMemo } from "react"; import { Save, RefreshCw, Download, Hash, AlertTriangle, CheckCircle2, UploadCloud, Wand2, Sparkles, GitBranch, Keyboard, } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Separator } from "~/components/ui/separator"; import { cn } from "~/lib/utils"; import { useDesignerStore } from "../state/store"; /** * BottomStatusBar * * Compact, persistent status + quick-action bar for the Experiment Designer. * Shows: * - Validation / drift / unsaved state * - Short design hash & version * - Aggregate counts (steps / actions) * - Last persisted hash (if available) * - Quick actions (Save, Validate, Export, Command Palette) * * The bar is intentionally UI-only: callback props are used so that higher-level * orchestration (e.g. DesignerRoot / Shell) controls actual side effects. */ export interface BottomStatusBarProps { onSave?: () => void; onValidate?: () => void; onExport?: () => void; onOpenCommandPalette?: () => void; onToggleVersionStrategy?: () => void; className?: string; saving?: boolean; validating?: boolean; exporting?: boolean; /** * Optional externally supplied last saved Date for relative display. */ lastSavedAt?: Date; } export function BottomStatusBar({ onSave, onValidate, onExport, onOpenCommandPalette, onToggleVersionStrategy, className, saving, validating, exporting, lastSavedAt, }: BottomStatusBarProps) { /* ------------------------------------------------------------------------ */ /* Store Selectors */ /* ------------------------------------------------------------------------ */ const steps = useDesignerStore((s) => s.steps); const lastPersistedHash = useDesignerStore((s) => s.lastPersistedHash); const currentDesignHash = useDesignerStore((s) => s.currentDesignHash); const lastValidatedHash = useDesignerStore((s) => s.lastValidatedHash); const pendingSave = useDesignerStore((s) => s.pendingSave); const versionStrategy = useDesignerStore((s) => s.versionStrategy); const autoSaveEnabled = useDesignerStore((s) => s.autoSaveEnabled); const actionCount = useMemo( () => steps.reduce((sum, st) => sum + st.actions.length, 0), [steps], ); const hasUnsaved = useMemo( () => Boolean(currentDesignHash) && currentDesignHash !== lastPersistedHash && !pendingSave, [currentDesignHash, lastPersistedHash, pendingSave], ); const validationStatus = useMemo<"unvalidated" | "valid" | "drift">(() => { if (!currentDesignHash || !lastValidatedHash) return "unvalidated"; if (currentDesignHash !== lastValidatedHash) return "drift"; return "valid"; }, [currentDesignHash, lastValidatedHash]); const shortHash = useMemo( () => (currentDesignHash ? currentDesignHash.slice(0, 8) : "—"), [currentDesignHash], ); const lastPersistedShort = useMemo( () => (lastPersistedHash ? lastPersistedHash.slice(0, 8) : null), [lastPersistedHash], ); /* ------------------------------------------------------------------------ */ /* Derived Display Helpers */ /* ------------------------------------------------------------------------ */ function formatRelative(date?: Date): string { if (!date) return "—"; const now = Date.now(); const diffMs = now - date.getTime(); if (diffMs < 30_000) return "just now"; const mins = Math.floor(diffMs / 60_000); if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); return `${days}d ago`; } const relSaved = formatRelative(lastSavedAt); const validationBadge = (() => { switch (validationStatus) { case "valid": return ( Validated ); case "drift": return ( Drift ); default: return ( Unvalidated ); } })(); const unsavedBadge = hasUnsaved && !pendingSave ? ( Unsaved ) : null; const savingIndicator = pendingSave || saving ? ( Saving… ) : null; /* ------------------------------------------------------------------------ */ /* Handlers */ /* ------------------------------------------------------------------------ */ const handleSave = useCallback(() => { if (onSave) onSave(); }, [onSave]); const handleValidate = useCallback(() => { if (onValidate) onValidate(); }, [onValidate]); const handleExport = useCallback(() => { if (onExport) onExport(); }, [onExport]); const handlePalette = useCallback(() => { if (onOpenCommandPalette) onOpenCommandPalette(); }, [onOpenCommandPalette]); const handleToggleVersionStrategy = useCallback(() => { if (onToggleVersionStrategy) onToggleVersionStrategy(); }, [onToggleVersionStrategy]); /* ------------------------------------------------------------------------ */ /* Render */ /* ------------------------------------------------------------------------ */ return (
{/* Left Cluster: Validation & Hash */}
{validationBadge} {unsavedBadge} {savingIndicator}
{shortHash} {lastPersistedShort && lastPersistedShort !== shortHash && ( / {lastPersistedShort} )}
{/* Middle Cluster: Aggregate Counts */}
{steps.length} steps
{actionCount} actions
{autoSaveEnabled ? "auto-save on" : "auto-save off"}
{versionStrategy.replace(/_/g, " ")}
Saved {relSaved}
{/* Flexible Spacer */}
{/* Right Cluster: Quick Actions */}
); } export default BottomStatusBar;