/** * 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 (
Create a persona with this avatar