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)}>
{/* 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 && ()}
)}
);
}