import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { validateCharacter } from '../lib/SchemaValidator'; const STORAGE_KEY = 'homeai_characters'; const ACTIVE_KEY = 'homeai_active_character'; function loadProfiles() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } } function saveProfiles(profiles) { localStorage.setItem(STORAGE_KEY, JSON.stringify(profiles)); } function getActiveId() { return localStorage.getItem(ACTIVE_KEY) || null; } function setActiveId(id) { localStorage.setItem(ACTIVE_KEY, id); } export default function Characters() { const [profiles, setProfiles] = useState(loadProfiles); const [activeId, setActive] = useState(getActiveId); const [error, setError] = useState(null); const [dragOver, setDragOver] = useState(false); const navigate = useNavigate(); useEffect(() => { saveProfiles(profiles); }, [profiles]); const handleImport = (e) => { const files = Array.from(e.target?.files || []); importFiles(files); if (e.target) e.target.value = ''; }; const importFiles = (files) => { files.forEach(file => { if (!file.name.endsWith('.json')) return; const reader = new FileReader(); reader.onload = (ev) => { try { const data = JSON.parse(ev.target.result); validateCharacter(data); const id = data.name + '_' + Date.now(); setProfiles(prev => [...prev, { id, data, image: null, addedAt: new Date().toISOString() }]); setError(null); } catch (err) { setError(`Import failed for ${file.name}: ${err.message}`); } }; reader.readAsText(file); }); }; const handleDrop = (e) => { e.preventDefault(); setDragOver(false); const files = Array.from(e.dataTransfer.files); importFiles(files); }; const handleImageUpload = (profileId, e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { setProfiles(prev => prev.map(p => p.id === profileId ? { ...p, image: ev.target.result } : p) ); }; reader.readAsDataURL(file); }; const removeProfile = (id) => { setProfiles(prev => prev.filter(p => p.id !== id)); if (activeId === id) { setActive(null); localStorage.removeItem(ACTIVE_KEY); } }; const activateProfile = (id) => { setActive(id); setActiveId(id); }; const exportProfile = (profile) => { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(profile.data, null, 2)); const a = document.createElement('a'); a.href = dataStr; a.download = `${profile.data.name || 'character'}.json`; a.click(); }; const editProfile = (profile) => { sessionStorage.setItem('edit_character', JSON.stringify(profile.data)); sessionStorage.setItem('edit_character_profile_id', profile.id); navigate('/editor'); }; const activeProfile = profiles.find(p => p.id === activeId); return (
{/* Header */}

Characters

{profiles.length} profile{profiles.length !== 1 ? 's' : ''} stored {activeProfile && ( Active: {activeProfile.data.display_name || activeProfile.data.name} )}

{error && (
{error}
)} {/* Drop zone */}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${ dragOver ? 'border-indigo-500 bg-indigo-500/10' : 'border-gray-700 hover:border-gray-600' }`} >

Drop character JSON files here to import

{/* Profile grid */} {profiles.length === 0 ? (

No character profiles yet. Import a JSON file to get started.

) : (
{profiles.map(profile => { const isActive = profile.id === activeId; const char = profile.data; return (
{/* Image area */}
{profile.image ? ( {char.display_name ) : (
{(char.display_name || char.name || '?')[0].toUpperCase()}
)} {isActive && ( Active )}
{/* Info */}

{char.display_name || char.name}

{char.description}

{char.tts?.engine || 'kokoro'} {char.model_overrides?.primary || 'default'} {char.tts?.kokoro_voice && ( {char.tts.kokoro_voice} )}
{!isActive ? ( ) : ( )}
); })}
)}
); }