/** * PersonaImportExport — Phase 4 (Enterprise MCP Auto-Install) * * Enhanced modal for importing .hpersona packages with MCP server auto-install: * Step 0: Upload (drag-drop or file picker) * Step 1: Preview + dependency check * Step 2: MCP Server Installation (if external servers needed) * Step 3: Install complete * * When a shared persona requires an external MCP server (e.g. hp-news): * 1. Dependency checker detects the missing server * 2. User sees a confirmation prompt: "This persona needs mcp-news. Install it?" * 3. On "Yes": clones from git → installs deps → starts server → syncs to Forge * 4. Real-time progress bars show each installation phase * 5. Once all servers are ready, persona project is created * * Also provides an export button component for project cards. */ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { X, Upload, Package, Check, AlertTriangle, Download, ChevronLeft, ChevronRight, Wrench, Server, Bot, Image as ImageIcon, Shield, Cpu, HelpCircle, GitBranch, Loader2, ExternalLink, CheckCircle, XCircle, SkipForward, RefreshCw, } from 'lucide-react'; import { previewPersonaPackage, importPersonaPackage, importPersonaAtomic, exportPersona, resolvePersonaDeps, installPersonaDeps, } from './personaPortability'; // --------------------------------------------------------------------------- // Status icon helper // --------------------------------------------------------------------------- function StatusIcon({ status }) { if (status === 'available') return
; if (status === 'installable') return
; if (status === 'downloadable') return
; if (status === 'missing') return
; if (status === 'degraded') return
; return
; } function DependencyRow({ item }) { return (
{item.name}
{item.detail || item.description}
{item.source_type === 'builtin' && ( Built-in )} {item.source_type === 'community_bundle' && ( {item.status === 'available' ? 'Installed' : item.status === 'installable' ? 'Ready' : 'Download'} )} {item.source_type === 'registry' && ( {item.status === 'available' ? 'Installed' : 'Discover'} )} {item.source_type === 'external' && ( {item.status === 'available' ? 'Installed' : item.status === 'downloadable' ? 'Auto-install' : 'Missing'} )}
); } // --------------------------------------------------------------------------- // MCP Server Install Status Card // --------------------------------------------------------------------------- const PHASE_LABELS = { analyzing: 'Analyzing', cloning: 'Cloning from Git', registering: 'Registering', starting: 'Starting Server', discovering: 'Discovering Tools', syncing: 'Syncing to Forge', complete: 'Complete', failed: 'Failed', skipped: 'Already Installed', }; function McpInstallCard({ status, backendUrl, apiKey, }) { const isComplete = status.phase === 'complete' || status.phase === 'skipped'; const isFailed = status.phase === 'failed'; const isActive = !isComplete && !isFailed; return (
{/* Header */}
{isComplete ? : isFailed ? : }
{status.server_name}
{PHASE_LABELS[status.phase] || status.phase} {status.port && ` \u2022 Port ${status.port}`}
{status.source_type && ( {status.source_type} )}
{/* Progress bar */} {isActive && (
)} {/* Status message */}
{isFailed ? status.error : status.message}
{/* Git URL */} {status.git_url && (
{status.git_url}
)} {/* Tools discovered */} {isComplete && status.tools_discovered > 0 && (
{status.tools_discovered} tools discovered {status.tools_registered} registered in Forge {status.elapsed_ms > 0 && {(status.elapsed_ms / 1000).toFixed(1)}s}
)} {/* Install logs — show for failed installs so user can debug */} {isFailed && backendUrl && ()}
); } function InstallLogViewer({ backendUrl, apiKey, serverName, active, }) { const [logs, setLogs] = useState([]); const [expanded, setExpanded] = useState(false); const sinceRef = useRef(0); const scrollRef = useRef(null); useEffect(() => { let cancelled = false; const fetchLogs = async () => { try { const headers = {}; if (apiKey) headers['x-api-key'] = apiKey; const res = await fetch(`${backendUrl}/v1/agentic/servers/install-logs?server=${encodeURIComponent(serverName)}&since=${sinceRef.current}`, { headers }); if (res.ok) { const data = await res.json(); const newLogs = data.logs || []; if (newLogs.length > 0) { sinceRef.current += newLogs.length; setLogs((prev) => [...prev, ...newLogs].slice(-100)); requestAnimationFrame(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }); }); } } } catch { // non-critical } // If active, keep polling; otherwise one-time fetch if (!cancelled && active) { setTimeout(fetchLogs, 1500); } }; fetchLogs(); return () => { cancelled = true; }; }, [active, backendUrl, apiKey, serverName]); if (logs.length === 0 && !active) return null; const visibleLogs = expanded ? logs : logs.slice(-5); const levelColor = (level) => { if (level === 'error') return 'text-red-400'; if (level === 'warning') return 'text-amber-400'; if (level === 'debug') return 'text-white/20'; return 'text-white/40'; }; return (
{(expanded || logs.length <= 5) && (
{visibleLogs.map((log, i) => (
{log.phase} {log.message}
))} {logs.length === 0 && active && (
Waiting for install logs...
)}
)}
); } // --------------------------------------------------------------------------- // Step breadcrumb // --------------------------------------------------------------------------- function StepBreadcrumb({ step, totalSteps }) { const labels = totalSteps === 4 ? ['Upload', 'Preview', 'MCP Servers', 'Install'] : ['Upload', 'Preview', 'Install']; return (
{labels.map((label, i) => ( {i > 0 && } {i < step ? '\u2713 ' : ''}{label} ))}
); } // --------------------------------------------------------------------------- // Import Modal // --------------------------------------------------------------------------- export function PersonaImportModal({ onClose, onImported, backendUrl, apiKey, }) { const [step, setStep] = useState(0); const [file, setFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [preview, setPreview] = useState(null); const [importing, setImporting] = useState(false); const [importedProject, setImportedProject] = useState(null); const [memoryMode, setMemoryMode] = useState('adaptive'); const fileInputRef = useRef(null); // MCP install state const [mcpPlan, setMcpPlan] = useState(null); const [mcpInstalling, setMcpInstalling] = useState(false); const [mcpResult, setMcpResult] = useState(null); const [forceReinstall, setForceReinstall] = useState(false); // Do we need MCP server installation? (installable, downloadable, or external missing) const needsMcpInstall = preview?.dependency_check?.mcp_servers?.some((s) => s.status === 'installable' || s.status === 'downloadable' || (s.status === 'missing' && s.source_type === 'external')) ?? false; // Alias for backwards compat in the component const hasMissingMcpServers = needsMcpInstall; // Are there MCP servers that are already installed? (user may want to reinstall) const hasAlreadyInstalledServers = preview?.dependency_check?.mcp_servers?.some((s) => s.status === 'available' && s.source_type === 'external') ?? false; // Total steps: 4 if MCP install needed, 3 otherwise const totalSteps = hasMissingMcpServers ? 4 : 3; // Map step numbers: 0=Upload, 1=Preview, 2=MCP (if needed), last=Complete const STEP_UPLOAD = 0; const STEP_PREVIEW = 1; const STEP_MCP = hasMissingMcpServers ? 2 : -1; const STEP_COMPLETE = hasMissingMcpServers ? 3 : 2; // --- Drag & drop --- const handleDragOver = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const f = e.dataTransfer.files?.[0]; if (f) handleFileSelected(f); }, []); const handleFileChange = useCallback((e) => { const f = e.target.files?.[0]; if (f) handleFileSelected(f); }, []); const handleFileSelected = async (f) => { setFile(f); setError(null); setLoading(true); try { const result = await previewPersonaPackage({ backendUrl, apiKey, file: f }); setPreview(result); const mm = result.manifest?.memory_mode || result.persona_agent?.memory_mode || 'adaptive'; setMemoryMode(mm === 'basic' ? 'basic' : 'adaptive'); setStep(STEP_PREVIEW); } catch (err) { setError(err.message || 'Failed to parse package'); } finally { setLoading(false); } }; // Resolve MCP deps (check what needs install) const handleResolveDeps = async () => { if (!file) return; setError(null); setLoading(true); try { const plan = await resolvePersonaDeps({ backendUrl, apiKey, file }); setMcpPlan(plan); setStep(STEP_MCP); } catch (err) { setError(err.message || 'Failed to check MCP dependencies'); } finally { setLoading(false); } }; // Install missing MCP servers const handleInstallMcp = async () => { if (!file) return; setMcpInstalling(true); setError(null); try { const result = await installPersonaDeps({ backendUrl, apiKey, file }); setMcpResult(result); // After MCP install, proceed to import the persona if (result.all_satisfied) { await doImport(); } } catch (err) { setError(err.message || 'MCP server installation failed'); } finally { setMcpInstalling(false); } }; // Import the persona project const doImport = async () => { if (!file) return; setImporting(true); setError(null); try { const result = await importPersonaPackage({ backendUrl, apiKey, file }); setImportedProject(result.project); setStep(STEP_COMPLETE); try { const engineValue = memoryMode === 'basic' ? 'v1' : 'v2'; localStorage.setItem('homepilot_memory_engine', engineValue); } catch { /* non-critical */ } onImported(result.project); } catch (err) { setError(err.message || 'Import failed'); } finally { setImporting(false); } }; // Atomic import: install MCP servers + create persona in one call const handleAtomicImport = async () => { if (!file) return; setImporting(true); setError(null); try { const result = await importPersonaAtomic({ backendUrl, apiKey, file, autoInstallServers: true, forceReinstall, }); setImportedProject(result.project); if (result.install_plan) { setMcpResult(result.install_plan); } setStep(STEP_COMPLETE); try { const engineValue = memoryMode === 'basic' ? 'v1' : 'v2'; localStorage.setItem('homepilot_memory_engine', engineValue); } catch { /* non-critical */ } onImported(result.project); } catch (err) { setError(err.message || 'Atomic import failed'); } finally { setImporting(false); } }; const handleImport = async () => { if (needsMcpInstall) { // Use atomic import for one-click install await handleAtomicImport(); } else { await doImport(); } }; // Extract preview data const agentLabel = preview?.persona_agent?.label || preview?.persona_agent?.id || 'Unknown'; const agentRole = preview?.persona_agent?.role || ''; const tone = preview?.persona_agent?.response_style?.tone || preview?.persona_agent?.tone || ''; const contentRating = preview?.manifest?.content_rating || 'sfw'; const schemaVer = preview?.manifest?.schema_version || 1; const pkgVer = preview?.manifest?.package_version || 1; const depCheck = preview?.dependency_check; const tools = preview?.persona_agent?.allowed_tools || []; const contents = preview?.manifest?.contents; return (
{/* Header */}

Import Persona

{/* Content */}
{/* ── Step 0: Upload ── */} {step === STEP_UPLOAD && (

{loading ? 'Analyzing package...' : 'Drop .hpersona file here'}

{loading ? 'Checking dependencies and validating contents' : 'or click to browse'}

{!loading && ()} {loading && (
Parsing package...
)}

.hpersona files contain persona configurations, avatar images, and model references. No executable code is included. Tool and server dependencies are checked before installation.

)} {/* ── Step 1: Preview ── */} {step === STEP_PREVIEW && preview && (
{/* Persona card */}
{preview.avatar_preview_data_url ? ({agentLabel}) : preview.has_avatar ? () : ()}

{agentLabel}

{agentRole && {agentRole}} {tone && {tone}} {contentRating === 'nsfw' && (NSFW)}
{tools.length > 0 && {tools.length} tools} {contents?.has_avatar && Avatar included} {(contents?.outfit_count || 0) > 0 && {contents.outfit_count} outfits} v{pkgVer} / schema {schemaVer}
{/* Memory Mode */}
Memory Mode
{memoryMode === 'adaptive' ? 'Learns over time, forgets irrelevant details.' : 'Only remembers what is explicitly saved.'}
{['adaptive', 'basic'].map((mode) => ())}
{/* System prompt preview */} {preview.persona_agent?.system_prompt && (
System Prompt
{preview.persona_agent.system_prompt.slice(0, 300)} {preview.persona_agent.system_prompt.length > 300 ? '...' : ''}
)} {/* Dependencies */} {depCheck && (
Dependencies
{depCheck.summary}
{/* Models */} {depCheck.models.length > 0 && (
Image Models
{depCheck.models.map((m, i) => )}
)} {/* Tools */} {depCheck.tools.length > 0 && (
Tools ({depCheck.tools.length})
{depCheck.tools.map((t, i) => )}
)} {/* MCP Servers */} {depCheck.mcp_servers.length > 0 && (
MCP Servers ({depCheck.mcp_servers.length})
{depCheck.mcp_servers.map((s, i) => )}
)} {/* A2A Agents */} {depCheck.a2a_agents.length > 0 && (
A2A Agents ({depCheck.a2a_agents.length})
{depCheck.a2a_agents.map((a, i) => )}
)} {/* No dependencies */} {depCheck.models.length === 0 && depCheck.tools.length === 0 && depCheck.mcp_servers.length === 0 && depCheck.a2a_agents.length === 0 && (
No external dependencies required
)}
)} {/* MCP Server Install Prompt */} {needsMcpInstall && (

MCP Server Setup Required

This persona requires MCP server(s) that will be automatically installed. Click Install Persona + Server to set everything up in one step.

{depCheck?.mcp_servers .filter(s => s.status === 'installable' || s.status === 'downloadable' || (s.status === 'missing' && s.source_type === 'external')) .map((s, i) => (
{s.status === 'installable' ? (<> {s.name} Ready to install{s.port ? ` on port ${s.port}` : ''} ) : (<> {s.name} {s.url && ({s.url})} )}
))}
)} {/* Reinstall option — shown when servers are already installed */} {hasAlreadyInstalledServers && (

MCP Server Already Installed

The required MCP server(s) are already running. Choose whether to reuse them or reinstall from scratch.

{depCheck?.mcp_servers .filter(s => s.status === 'available' && s.source_type === 'external') .map((s, i) => (
{s.name} running{s.port ? ` on port ${s.port}` : ''}
))}
)}
)} {/* ── Step 2: MCP Server Installation ── */} {step === STEP_MCP && (
{/* Header */}

MCP Server Installation

{mcpInstalling ? 'Installing required MCP servers...' : mcpResult ? mcpResult.all_satisfied ? 'All servers installed successfully!' : 'Some servers could not be installed' : `${mcpPlan?.servers_to_install.length || 0} server(s) need to be installed`}

{/* Install confirmation (before install) */} {!mcpInstalling && !mcpResult && mcpPlan && (
{/* Already available */} {mcpPlan.servers_already_available.length > 0 && (
Available
{mcpPlan.servers_already_available.map((s, i) => (
{s.name || 'unknown'} {s.description || ''}
))}
)} {/* To install */} {mcpPlan.servers_to_install.length > 0 && (
Will be installed
{mcpPlan.servers_to_install.map((s, i) => (
{s.name}
{s.description}
{s.git_url && (
{s.git_url}
)} {s.tools_provided.length > 0 && (
{s.tools_provided.length} tools: {s.tools_provided.join(', ')}
)}
))}
)} {/* Install button */}
)} {/* Installing progress — live log viewer */} {mcpInstalling && (
Installing MCP servers... This may take a moment.
{/* Live install logs for each server being installed */} {mcpPlan?.servers_to_install?.map((srv, i) => (
{srv.name}
))}
)} {/* Install results */} {mcpResult && (
{mcpResult.install_statuses.map((s, i) => ())} {/* Summary */}
{mcpResult.summary}
)}
)} {/* ── Step 3 (or 2): Complete ── */} {step === STEP_COMPLETE && importedProject && (

{importedProject.name || 'Persona'} installed!

The persona project has been created and is ready to use.

{preview?.has_avatar && (
Avatar committed to project storage
)} {(preview?.manifest?.contents?.outfit_count || 0) > 0 && (
{preview.manifest.contents.outfit_count} outfits imported
)} {(preview?.persona_agent?.allowed_tools?.length || 0) > 0 && (
{preview.persona_agent.allowed_tools.length} tools configured
)} {mcpResult && mcpResult.install_statuses.length > 0 && (
{mcpResult.install_statuses.filter(s => s.phase === 'complete' || s.phase === 'skipped').length} MCP server(s) installed and synced
)} {(preview?.dependency_check?.mcp_servers?.length || 0) > 0 && !mcpResult && (
{preview.dependency_check.mcp_servers.length} MCP server(s) referenced
)}
Memory: {memoryMode === 'adaptive' ? 'Adaptive Memory' : 'Basic Memory'}
)} {/* Error display */} {error && (
{error}
)}
{/* Footer */}
{step === STEP_UPLOAD && (<>
)} {step === STEP_PREVIEW && (<> )} {step === STEP_MCP && (<> {mcpResult?.all_satisfied && !importedProject && ()} {mcpResult && !mcpResult.all_satisfied && ()} )} {step === STEP_COMPLETE && (<>
)}
); } // --------------------------------------------------------------------------- // Export button (inline, for project cards) // --------------------------------------------------------------------------- export function PersonaExportButton({ projectId, backendUrl, apiKey, className, }) { const [exporting, setExporting] = useState(false); const handleExport = async (e) => { e.stopPropagation(); if (exporting) return; setExporting(true); try { await exportPersona({ backendUrl, apiKey, projectId }); } catch (err) { alert(`Export failed: ${err.message}`); } finally { setExporting(false); } }; return (); }