import React, { useState } from 'react'; import { Check, ChevronDown, Clipboard, Info, Loader2, Orbit, Sparkles, Trash2, X } from 'lucide-react'; import { VIEW_ANGLE_OPTIONS } from './viewPack'; const SOURCE_OPTIONS = [ { id: 'anchor', label: 'Anchor' }, { id: 'latest', label: 'Latest Outfit' }, { id: 'equipped', label: 'Equipped' }, ]; /** Format a unix timestamp as a short relative string. */ function timeAgo(ts) { const seconds = Math.floor((Date.now() - ts) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } /** Inline prompt detail popover for a generated angle. */ function PromptDetailPopover({ result, label, onClose }) { const [copied, setCopied] = useState(null); const prompt = result.metadata?.view_prompt || result.metadata?.prompt || ''; const negative = result.metadata?.view_negative || ''; const copyText = async (text, which) => { try { await navigator.clipboard.writeText(text); setCopied(which); setTimeout(() => setCopied(null), 1500); } catch { /* clipboard not available */ } }; return (
e.stopPropagation()}> {/* Header */}
{label} Prompt
{/* Scrollable body */}
{prompt && (
Positive
{prompt}
)} {negative && (
Negative
{negative}
)} {!prompt && !negative && (
No prompt metadata stored for this view.
)}
); } export function AvatarViewPackPanel({ open, source, previews, results = {}, timestamps = {}, loadingAngles, busy = false, disableLatest = false, disableEquipped = false, onToggle, onSourceChange, onGenerateAngle, onOpenAngle, onDeleteAngle, onGenerateMissing, onClearAll, hasAnyResults = false, }) { const readyCount = VIEW_ANGLE_OPTIONS.filter((a) => previews[a.id]).length; const [promptDetail, setPromptDetail] = useState(null); return (
{open && (
Source
{SOURCE_OPTIONS.map((option) => { const disabled = (option.id === 'latest' && disableLatest) || (option.id === 'equipped' && disableEquipped); return (); })}
Angles
{VIEW_ANGLE_OPTIONS.map((angle) => { const previewUrl = previews[angle.id]; const ready = Boolean(previewUrl); const loading = Boolean(loadingAngles[angle.id]); const ts = timestamps[angle.id]; const result = results[angle.id]; const showingPrompt = promptDetail === angle.id; return (
{/* Thumbnail preview when ready — click to open in main stage */} {ready && previewUrl ? (
onOpenAngle(angle.id)}> {angle.label}
{/* Bottom info bar — label + timestamp */}
{angle.label}
{ts && (
{timeAgo(ts)}
)}
) : ( /* Default empty tile — click to generate */ )} {/* Prompt info button — top-right corner, visible on hover */} {ready && !loading && result && ()} {/* Delete button — top-left corner, visible on hover */} {ready && !loading && ()} {/* Prompt detail popover overlay */} {showingPrompt && result && ( setPromptDetail(null)}/>)} {/* Loading overlay */} {loading && ready && (
)}
); })}
{hasAnyResults && onClearAll && ()}
)}
); }