Files
plume/components/WorldBuilder.tsx

722 lines
30 KiB
TypeScript

import React, { useState, useMemo, useEffect } from 'react';
import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '../types';
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react';
import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '../constants';
interface WorldBuilderProps {
entities: Entity[];
onCreate: (entity: Omit<Entity, 'id'>) => Promise<string | null>;
onUpdate: (id: string, updates: Partial<Entity>) => void;
onDelete: (id: string) => void;
templates: EntityTemplate[];
onUpdateTemplates: (templates: EntityTemplate[]) => void;
initialSelectedId?: string | null;
}
const DEFAULT_CHAR_ATTRIBUTES: CharacterAttributes = {
age: 30,
height: 175,
hair: 'Brun',
eyes: 'Marron',
archetype: 'Le Héros',
role: 'support',
personality: {
spectrumIntrovertExtravert: 50,
spectrumEmotionalRational: 50,
spectrumChaoticLawful: 50,
},
physicalQuirk: '',
behavioralQuirk: ''
};
const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }) => {
const [editingId, setEditingId] = useState<string | null>(null);
const [tempEntity, setTempEntity] = useState<Entity | null>(null);
const [mode, setMode] = useState<'entities' | 'templates'>('entities');
// Template Editor State
const [activeTemplateType, setActiveTemplateType] = useState<EntityType>(EntityType.CHARACTER);
// Handle external navigation request (deep link)
useEffect(() => {
if (initialSelectedId) {
const entity = entities.find(e => e.id === initialSelectedId);
if (entity) {
handleEdit(entity);
setMode('entities');
}
}
}, [initialSelectedId, entities]);
// Dynamic Archetypes List
const allArchetypes = useMemo(() => {
const existing = entities
.filter(e => e.type === EntityType.CHARACTER && e.attributes?.archetype)
.map(e => e.attributes!.archetype);
return Array.from(new Set([...ARCHETYPES, ...existing])).sort();
}, [entities]);
// --- ENTITY ACTIONS ---
const handleAdd = (type: EntityType) => {
const newEntity: Entity = {
id: Date.now().toString(), // Helper ID for UI
type,
name: '',
description: '',
details: '',
storyContext: '',
attributes: type === EntityType.CHARACTER ? { ...DEFAULT_CHAR_ATTRIBUTES } : undefined,
customValues: {}
};
setTempEntity(newEntity);
setEditingId('NEW');
};
const handleEdit = (entity: Entity) => {
// Ensure attributes exist if it's a character (backward compatibility)
const entityToEdit = { ...entity };
if (entity.type === EntityType.CHARACTER && !entity.attributes) {
entityToEdit.attributes = { ...DEFAULT_CHAR_ATTRIBUTES };
}
if (!entity.customValues) {
entityToEdit.customValues = {};
}
setTempEntity(entityToEdit);
setEditingId(entity.id);
};
const handleSave = async () => {
if (!tempEntity || !tempEntity.name) return;
if (editingId === 'NEW') {
const { id, ...entityData } = tempEntity;
await onCreate(entityData);
} else {
onUpdate(tempEntity.id, tempEntity);
}
setEditingId(null);
setTempEntity(null);
};
const handleDelete = (id: string) => {
if (confirm('Supprimer cet élément ?')) {
onDelete(id);
if (editingId === id) {
setEditingId(null);
setTempEntity(null);
}
}
};
const updateAttribute = (key: keyof CharacterAttributes, value: any) => {
if (tempEntity && tempEntity.attributes) {
setTempEntity({
...tempEntity,
attributes: { ...tempEntity.attributes, [key]: value }
});
}
};
const updatePersonality = (key: keyof CharacterAttributes['personality'], value: number) => {
if (tempEntity && tempEntity.attributes) {
setTempEntity({
...tempEntity,
attributes: {
...tempEntity.attributes,
personality: { ...tempEntity.attributes.personality, [key]: value }
}
});
}
};
const updateCustomValue = (fieldId: string, value: any) => {
if (tempEntity) {
setTempEntity({
...tempEntity,
customValues: {
...tempEntity.customValues,
[fieldId]: value
}
});
}
};
// --- TEMPLATE ACTIONS ---
const addCustomField = (type: EntityType) => {
const newField: CustomFieldDefinition = {
id: `field-${Date.now()}`,
label: 'Nouveau Champ',
type: 'text',
placeholder: ''
};
// Correct immutable update
const updatedTemplates = templates.map(t => {
if (t.entityType === type) {
return {
...t,
fields: [...t.fields, newField]
};
}
return t;
});
// If template didn't exist (unlikely given App.tsx init, but safe)
if (!updatedTemplates.some(t => t.entityType === type)) {
updatedTemplates.push({ entityType: type, fields: [newField] });
}
onUpdateTemplates(updatedTemplates);
};
const updateCustomField = (type: EntityType, fieldId: string, updates: Partial<CustomFieldDefinition>) => {
const updatedTemplates = templates.map(t => {
if (t.entityType !== type) return t;
return {
...t,
fields: t.fields.map(f => f.id === fieldId ? { ...f, ...updates } : f)
};
});
onUpdateTemplates(updatedTemplates);
};
const deleteCustomField = (type: EntityType, fieldId: string) => {
const updatedTemplates = templates.map(t => {
if (t.entityType !== type) return t;
return {
...t,
fields: t.fields.filter(f => f.id !== fieldId)
};
});
onUpdateTemplates(updatedTemplates);
};
const filterByType = (type: EntityType) => entities.filter(e => e.type === type);
// --- RENDER HELPERS ---
const renderCharacterEditor = () => {
if (!tempEntity?.attributes) return null;
const attrs = tempEntity.attributes;
return (
<div className="space-y-8 border-t border-slate-100 pt-6 mt-4">
{/* SECTION 1: ROLE & ARCHETYPE */}
<div className="bg-[#eef2ff] p-4 rounded-lg border border-indigo-100">
<h3 className="text-sm font-bold text-slate-700 uppercase mb-4 flex items-center gap-2">
<User size={16} /> Identité Narrative
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-semibold text-slate-500 mb-2">Archétype</label>
<input
type="text"
list="archetype-suggestions"
value={attrs.archetype}
onChange={(e) => updateAttribute('archetype', e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-blue-500"
placeholder="Ex: Le Héros, Le Sage..."
/>
<datalist id="archetype-suggestions">
{allArchetypes.map(a => <option key={a} value={a} />)}
</datalist>
</div>
<div>
<label className="block text-xs font-semibold text-slate-500 mb-2">Rôle dans l'histoire</label>
<div className="flex gap-2 flex-wrap">
{[
{ val: 'protagonist', label: 'Protagoniste' },
{ val: 'antagonist', label: 'Antagoniste' },
{ val: 'support', label: 'Secondaire' },
{ val: 'extra', label: 'Figurant' }
].map(opt => (
<label key={opt.val} className={`cursor-pointer px-3 py-1.5 rounded text-xs border transition-colors ${attrs.role === opt.val ? 'bg-indigo-100 border-indigo-300 text-indigo-700 font-bold' : 'bg-[#eef2ff] border-slate-200 text-slate-600 hover:bg-slate-100'}`}>
<input
type="radio"
name="role"
value={opt.val}
checked={attrs.role === opt.val}
onChange={() => updateAttribute('role', opt.val)}
className="hidden"
/>
{opt.label}
</label>
))}
</div>
</div>
</div>
</div>
{/* SECTION 2: PHYSIQUE */}
<div className="bg-[#eef2ff] p-4 rounded-lg border border-indigo-100">
<h3 className="text-sm font-bold text-slate-700 uppercase mb-4 flex items-center gap-2">
<Ruler size={16} /> Apparence Physique
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-6">
<div>
<div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-slate-600">Âge (ans)</label>
</div>
<div className="flex items-center gap-3">
<input
type="range" min="1" max="100"
value={Math.min(attrs.age, 100)}
onChange={(e) => updateAttribute('age', parseInt(e.target.value))}
className="flex-1 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
<input
type="number"
value={attrs.age}
onChange={(e) => updateAttribute('age', parseInt(e.target.value))}
className="w-20 p-1 text-right text-sm border border-slate-300 rounded font-mono text-indigo-700 bg-[#eef2ff] focus:border-indigo-500 outline-none"
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-slate-600">Taille (cm)</label>
</div>
<div className="flex items-center gap-3">
<input
type="range" min="50" max="250"
value={Math.min(attrs.height, 250)}
onChange={(e) => updateAttribute('height', parseInt(e.target.value))}
className="flex-1 h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
<input
type="number"
value={attrs.height}
onChange={(e) => updateAttribute('height', parseInt(e.target.value))}
className="w-20 p-1 text-right text-sm border border-slate-300 rounded font-mono text-indigo-700 bg-[#eef2ff] focus:border-indigo-500 outline-none"
/>
</div>
</div>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-500 mb-1">Cheveux</label>
<select
value={attrs.hair}
onChange={(e) => updateAttribute('hair', e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm"
>
{HAIR_COLORS.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-500 mb-1">Yeux</label>
<select
value={attrs.eyes}
onChange={(e) => updateAttribute('eyes', e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm"
>
{EYE_COLORS.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-500 mb-1">Signe distinctif</label>
<input
type="text"
value={attrs.physicalQuirk}
onChange={(e) => updateAttribute('physicalQuirk', e.target.value)}
placeholder="Cicatrice, tatouage..."
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-indigo-400"
/>
</div>
</div>
</div>
</div>
{/* SECTION 3: PSYCHOLOGIE */}
<div className="bg-[#eef2ff] p-4 rounded-lg border border-indigo-100">
<h3 className="text-sm font-bold text-slate-700 uppercase mb-4 flex items-center gap-2">
<Brain size={16} /> Psychologie & Comportement
</h3>
<div className="space-y-6">
<div className="space-y-4 px-2">
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-slate-500 mb-1">
<span>Introverti</span>
<span>Extraverti</span>
</div>
<input
type="range" min="0" max="100"
value={attrs.personality.spectrumIntrovertExtravert}
onChange={(e) => updatePersonality('spectrumIntrovertExtravert', parseInt(e.target.value))}
className="w-full h-2 bg-gradient-to-r from-slate-300 via-indigo-200 to-slate-300 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-slate-500 mb-1">
<span>Émotionnel</span>
<span>Rationnel</span>
</div>
<input
type="range" min="0" max="100"
value={attrs.personality.spectrumEmotionalRational}
onChange={(e) => updatePersonality('spectrumEmotionalRational', parseInt(e.target.value))}
className="w-full h-2 bg-gradient-to-r from-red-200 via-purple-200 to-blue-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-slate-500 mb-1">
<span>Chaotique</span>
<span>Loyal</span>
</div>
<input
type="range" min="0" max="100"
value={attrs.personality.spectrumChaoticLawful}
onChange={(e) => updatePersonality('spectrumChaoticLawful', parseInt(e.target.value))}
className="w-full h-2 bg-gradient-to-r from-orange-200 via-yellow-100 to-green-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
/>
</div>
</div>
<div className="border-t border-slate-200 pt-4">
<label className="block text-xs font-semibold text-slate-500 mb-1">Toc ou habitude comportementale</label>
<input
type="text"
value={attrs.behavioralQuirk}
onChange={(e) => updateAttribute('behavioralQuirk', e.target.value)}
placeholder="Joue avec sa bague, bégaie quand il ment..."
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-indigo-400"
/>
</div>
</div>
</div>
</div>
);
};
const renderCustomFieldsEditor = () => {
const currentTemplate = templates.find(t => t.entityType === tempEntity?.type);
if (!currentTemplate || currentTemplate.fields.length === 0) return null;
return (
<div className="bg-[#eef2ff] p-4 rounded-lg border border-indigo-100 mt-6">
<h3 className="text-sm font-bold text-slate-700 uppercase mb-4 flex items-center gap-2">
<List size={16} /> Champs Personnalisés
</h3>
<div className="grid grid-cols-1 gap-4">
{currentTemplate.fields.map(field => {
const value = tempEntity?.customValues?.[field.id] ?? '';
return (
<div key={field.id}>
<label className="block text-xs font-semibold text-slate-500 mb-1">{field.label}</label>
{field.type === 'textarea' ? (
<textarea
value={value}
onChange={(e) => updateCustomValue(field.id, e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-indigo-400"
placeholder={field.placeholder}
/>
) : field.type === 'select' ? (
<select
value={value}
onChange={(e) => updateCustomValue(field.id, e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-indigo-400"
>
<option value="">Sélectionner...</option>
{field.options?.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : field.type === 'boolean' ? (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={!!value}
onChange={(e) => updateCustomValue(field.id, e.target.checked)}
className="w-4 h-4 text-indigo-600 rounded border-slate-300 focus:ring-indigo-500"
/>
<span className="text-sm text-slate-700">Activé / Oui</span>
</label>
) : (
<input
type={field.type === 'number' ? 'number' : 'text'}
value={value}
onChange={(e) => updateCustomValue(field.id, e.target.value)}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm outline-none focus:border-indigo-400"
placeholder={field.placeholder}
/>
)}
</div>
);
})}
</div>
</div>
);
};
const renderTemplateManager = () => {
const template = templates.find(t => t.entityType === activeTemplateType) || { entityType: activeTemplateType, fields: [] };
return (
<div className="flex-1 bg-white rounded-xl shadow-lg border border-slate-200 p-8 overflow-y-auto">
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
<Layout size={24} className="text-indigo-600" /> Éditeur de Modèles
</h2>
<p className="text-slate-500 text-sm mt-1">
Configurez les champs personnalisés pour chaque type de fiche.
</p>
</div>
<button onClick={() => setMode('entities')} className="p-2 text-slate-500 hover:bg-slate-100 rounded-full">
<X size={20} />
</button>
</div>
<div className="flex gap-2 mb-8 border-b border-slate-200 pb-1">
{Object.values(EntityType).map(type => (
<button
key={type}
onClick={() => setActiveTemplateType(type)}
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${activeTemplateType === type
? 'bg-indigo-50 text-indigo-700 border-b-2 border-indigo-600'
: 'text-slate-500 hover:text-slate-800 hover:bg-slate-50'
}`}
>
{type}
</button>
))}
</div>
<div className="space-y-4">
{template.fields.map((field, idx) => (
<div key={field.id} className="bg-[#eef2ff] border border-indigo-100 rounded-lg p-4 flex gap-4 items-start group">
<div className="flex-1 grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-500 mb-1">Nom du champ</label>
<input
type="text"
value={field.label}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { label: e.target.value })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-500 mb-1">Type</label>
<select
value={field.type}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { type: e.target.value as CustomFieldType })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm"
>
<option value="text">Texte court</option>
<option value="textarea">Texte long</option>
<option value="number">Nombre</option>
<option value="boolean">Case à cocher</option>
<option value="select">Liste déroulante</option>
</select>
</div>
{field.type === 'select' && (
<div className="col-span-2">
<label className="block text-xs font-semibold text-slate-500 mb-1">Options (séparées par des virgules)</label>
<input
type="text"
value={field.options?.join(',') || ''}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { options: e.target.value.split(',').map(s => s.trim()) })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded text-sm"
placeholder="Option A, Option B, Option C"
/>
</div>
)}
</div>
<button
onClick={() => deleteCustomField(activeTemplateType, field.id)}
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded mt-5"
>
<Trash2 size={16} />
</button>
</div>
))}
<button
onClick={() => addCustomField(activeTemplateType)}
className="w-full py-3 border-2 border-dashed border-slate-300 rounded-lg text-slate-500 hover:border-indigo-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all flex items-center justify-center gap-2"
>
<Plus size={20} /> Ajouter un champ
</button>
</div>
</div>
);
};
if (mode === 'templates') {
return (
<div className="flex h-full gap-6 p-6 bg-[#eef2ff]">
<div className="w-1/3 opacity-50 pointer-events-none filter blur-[1px]">
<div className="bg-white rounded-lg p-6 shadow-sm border border-slate-200">
<h3 className="font-bold text-slate-700 mb-4">Aperçu Fiches</h3>
<div className="space-y-2">
<div className="h-10 bg-indigo-50 rounded"></div>
<div className="h-10 bg-indigo-50 rounded"></div>
<div className="h-10 bg-indigo-50 rounded"></div>
</div>
</div>
</div>
{renderTemplateManager()}
</div>
);
}
return (
<div className="flex h-full gap-6 p-6 bg-[#eef2ff]">
<div className="w-1/3 flex flex-col gap-4">
<div className="flex justify-between items-center px-1">
<h2 className="text-lg font-bold text-slate-700">Explorateur</h2>
<button
onClick={() => setMode('templates')}
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded text-xs font-medium transition-colors"
title="Gérer les modèles de fiches"
>
<Settings size={14} /> Modèles
</button>
</div>
<div className="space-y-6 overflow-y-auto pr-2 pb-4 flex-1">
{Object.values(EntityType).map(type => (
<div key={type} className="bg-white rounded-lg shadow-sm border border-slate-200 overflow-hidden">
<div className="bg-indigo-50 p-3 border-b border-indigo-100 flex justify-between items-center">
<h3 className="font-semibold text-slate-700 flex items-center gap-2">
<span>{ENTITY_ICONS[type]}</span> {type}s
</h3>
<button
onClick={() => handleAdd(type)}
className="p-1 hover:bg-indigo-100 rounded text-indigo-600 transition-colors"
>
<Plus size={16} />
</button>
</div>
<div className="divide-y divide-slate-100">
{filterByType(type).length === 0 && (
<p className="p-4 text-sm text-slate-400 italic text-center">Aucun élément</p>
)}
{filterByType(type).map(entity => (
<div
key={entity.id}
onClick={() => handleEdit(entity)}
className={`p-3 cursor-pointer hover:bg-blue-50 transition-colors flex justify-between group ${editingId === entity.id ? 'bg-blue-50 border-l-4 border-blue-500' : ''}`}
>
<div>
<div className="font-medium text-slate-800">{entity.name}</div>
<div className="text-xs text-slate-500 truncate">{entity.description}</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); handleDelete(entity.id); }}
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 transition-opacity"
>
<Trash2 size={14} />
</button>
</div>
))}
</div>
</div>
))}
</div>
</div>
<div className="flex-1 bg-white rounded-xl shadow-lg border border-slate-200 p-8 overflow-y-auto">
{editingId && tempEntity ? (
<div className="space-y-6 animate-in fade-in duration-200">
<div className="flex justify-between items-start">
<div className="space-y-1">
<span className={`inline-block px-2 py-1 rounded text-xs font-bold uppercase tracking-wider ${ENTITY_COLORS[tempEntity.type]}`}>
{tempEntity.type}
</span>
<h2 className="text-2xl font-bold text-slate-800">
{tempEntity.type === EntityType.CHARACTER ? 'Fiche Personnage' : 'Édition de la fiche'}
</h2>
</div>
<div className="flex gap-2">
<button onClick={() => setEditingId(null)} className="p-2 text-slate-500 hover:bg-slate-100 rounded-full">
<X size={20} />
</button>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nom</label>
<input
type="text"
value={tempEntity.name}
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg"
placeholder="Ex: Gandalf le Gris"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Description Courte (pour l'IA)</label>
<textarea
value={tempEntity.description}
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20"
placeholder="Un magicien puissant qui guide la communauté..."
/>
</div>
{tempEntity.type === EntityType.CHARACTER && renderCharacterEditor()}
{renderCustomFieldsEditor()}
<div className="mt-6 border-t border-slate-100 pt-6">
<div>
<label className="block text-sm font-medium text-indigo-700 mb-1 flex items-center gap-2">
<Sparkles size={14} /> Contexte Narratif (Auto-généré)
</label>
<textarea
value={tempEntity.storyContext || ''}
onChange={e => setTempEntity({ ...tempEntity, storyContext: e.target.value })}
className="w-full p-2 border border-indigo-200 bg-indigo-50 rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-24 italic text-slate-600"
placeholder="Les événements vécus par ce personnage apparaîtront ici..."
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-slate-700 mb-1">Notes & Biographie Complète</label>
<textarea
value={tempEntity.details}
onChange={e => setTempEntity({ ...tempEntity, details: e.target.value })}
className="w-full p-2 bg-[#eef2ff] border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none h-48 font-serif"
placeholder="Histoire détaillée, secrets, origines..."
/>
</div>
</div>
<div className="pt-4 flex justify-end">
<button
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-md"
>
<Save size={18} />
Enregistrer la fiche
</button>
</div>
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-slate-400">
<div className="text-6xl mb-4 opacity-20">🌍</div>
<p className="text-lg">Sélectionnez ou créez une fiche pour commencer.</p>
<p className="text-sm">Ces informations aideront l'IA à rester cohérente.</p>
</div>
)}
</div>
</div>
);
};
export default WorldBuilder;