/** * SaveAsPersonaModal — lightweight modal for saving an avatar as a Persona. * * Additive component — does not modify PersonaWizard or any existing code. * * Two paths: * 1. "Quick Create" — creates a Custom persona project immediately * 2. "Open in Wizard" — opens PersonaWizard with pre-filled draft (full customization) */ import React, { useState, useCallback, useMemo } from 'react'; import { X, Sparkles, User, Loader2, ChevronRight, Shirt, Camera, RotateCcw } from 'lucide-react'; import { draftFromGalleryItem, getVisibleBlueprints, professionToPersonaClass } from './personaBridge'; import { createPersonaProject } from '../personaApi'; import { resolveFileUrl } from '../resolveFileUrl'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function resolveUrl(url, backendUrl) { return resolveFileUrl(url, backendUrl); } function readNsfwMode() { try { return localStorage.getItem('homepilot_nsfw_mode') === 'true'; } catch { return false; } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function SaveAsPersonaModal({ item, outfitItems, batchSiblings, backendUrl, apiKey, onClose, onOpenWizard, onCreated, }) { const [name, setName] = useState(''); // Auto-select persona class from wizard profession if available const [classId, setClassId] = useState(() => item.wizardMeta?.professionId ? professionToPersonaClass(item.wizardMeta.professionId) : 'custom'); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const isSpicy = readNsfwMode(); const blueprints = useMemo(() => getVisibleBlueprints(isSpicy), [isSpicy]); const imgUrl = resolveUrl(item.url, backendUrl); // Count outfits that have 3D angle views (view_pack with at least 2 angles) const outfits3dCount = useMemo(() => { if (!outfitItems) return 0; return outfitItems.filter((oi) => { // Check GalleryItem.view_pack field if (oi.view_pack && Object.keys(oi.view_pack).length >= 2) return true; // Fallback: check localStorage cache try { const raw = localStorage.getItem(`hp_viewpack_${oi.id}`); if (raw) { const parsed = JSON.parse(raw); const results = parsed?.results ?? parsed; if (results && typeof results === 'object') { const count = ['front', 'left', 'right', 'back'].filter((a) => results[a]?.url).length; return count >= 2; } } } catch { /* ignore */ } return false; }).length; }, [outfitItems]); const handleOpenWizard = useCallback(() => { const draft = draftFromGalleryItem(item, name.trim() || 'My Persona', classId, outfitItems, batchSiblings); onOpenWizard(draft); }, [item, name, classId, outfitItems, batchSiblings, onOpenWizard]); const handleQuickCreate = useCallback(async () => { if (!name.trim()) return; setSaving(true); setError(null); try { const draft = draftFromGalleryItem(item, name.trim(), classId, outfitItems, batchSiblings); const description = draft.persona_agent.role || item.wizardMeta?.professionDescription || ''; const result = await createPersonaProject({ backendUrl, apiKey, name: name.trim(), description, persona_agent: { ...draft.persona_agent, persona_class: draft.persona_class, memory_mode: draft.memory_mode, }, persona_appearance: draft.persona_appearance, agentic: draft.agentic, }); onCreated?.(result.project || result); onClose(); } catch (e) { setError(e instanceof Error ? e.message : 'Failed to create persona'); } finally { setSaving(false); } }, [item, name, classId, outfitItems, batchSiblings, backendUrl, apiKey, onCreated, onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }}>
{/* Header */}

Export to Persona

Create a persona with this avatar

{/* Body */}
{/* Avatar preview */}
Avatar preview
{item.seed !== undefined &&
Seed: {item.seed}
} {item.prompt &&
{item.prompt}
}
Mode: {item.mode}
{batchSiblings && batchSiblings.length > 0 && (
{batchSiblings.length + 1} portrait{batchSiblings.length > 0 ? 's' : ''} included
)} {outfitItems && outfitItems.length > 0 && (
{outfitItems.length} outfit{outfitItems.length !== 1 ? 's' : ''} included {outfits3dCount > 0 && ( {outfits3dCount} 360° )}
)}
{/* Wizard profession info (if available) */} {item.wizardMeta?.professionLabel && (
From Avatar Wizard
{item.wizardMeta.professionLabel}
{item.wizardMeta.professionDescription && (
{item.wizardMeta.professionDescription}
)} {item.wizardMeta.tone && (
Tone: {item.wizardMeta.tone}
)}
)} {/* Name input */}
setName(e.target.value)} placeholder="e.g. Elena, Assistant, Maya..." autoFocus className="w-full px-3 py-2.5 rounded-xl bg-white/5 border border-white/10 text-white text-sm placeholder:text-white/25 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/20 transition-all" onKeyDown={(e) => { if (e.key === 'Enter' && name.trim()) handleQuickCreate(); }}/>
{/* Class selector */}
{blueprints.map((bp) => ())}
{/* Error */} {error && (
{error}
)}
{/* Footer */}
); }