import { ContentEditable } from "@common/components/ContentEditable"; import { useState } from "preact/hooks"; import styles from '../../assets/character-editor.module.css'; import { CharacterRole, useAppState, type Character } from "../../contexts/state"; import LLM from "../../utils/llm"; // TODO fix delete button size export const CharacterEditor = ({ visible }: { visible: boolean }) => { const { currentWorld, currentStory, mergedCharacters, dispatch, connection, model } = useAppState(); const [newNickname, setNewNickname] = useState>({}); const [newRelation, setNewRelation] = useState>({}); const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); const [generatingShortDesc, setGeneratingShortDesc] = useState(null); if (!currentWorld || !visible) { return null; } // When a story is selected, edit story-level characters; otherwise world-level const storyId = currentStory?.id ?? null; const worldId = currentWorld.id; const characters = currentStory ? currentStory.characters : currentWorld.characters; const handleAddCharacter = () => { dispatch({ type: 'ADD_CHARACTER', worldId, storyId, character: { id: crypto.randomUUID(), name: 'New Character', role: CharacterRole.Main, nicknames: [], shortDescription: '', description: '', relations: [], }, }); }; const handleEditCharacter = (characterId: string, field: keyof Character, value: any) => { dispatch({ type: 'EDIT_CHARACTER', worldId, storyId, characterId, updates: { [field]: value }, }); }; const handleDeleteCharacter = (characterId: string) => { dispatch({ type: 'DELETE_CHARACTER', worldId, storyId, characterId, }); }; const handleAddRelation = (characterId: string) => { const rel = newRelation[characterId] || { name: '', relation: '' }; if (!rel.name.trim() || !rel.relation.trim()) return; dispatch({ type: 'ADD_CHARACTER_RELATION', worldId, storyId, characterId, relation: { name: rel.name.trim(), relation: rel.relation.trim() }, }); setNewRelation({ ...newRelation, [characterId]: { name: '', relation: '' } }); }; const handleEditRelation = (characterId: string, targetName: string, field: 'name' | 'relation', value: string) => { dispatch({ type: 'EDIT_CHARACTER_RELATION', worldId, storyId, characterId, targetName, updates: { [field]: value }, }); }; const handleDeleteRelation = (characterId: string, targetName: string) => { dispatch({ type: 'DELETE_CHARACTER_RELATION', worldId, storyId, characterId, targetName, }); }; const handleNicknameAdd = (characterId: string) => { const nickname = (newNickname[characterId] || '').trim(); if (!nickname) return; const character = characters.find(c => c.id === characterId); if (character) { handleEditCharacter(characterId, 'nicknames', [...character.nicknames, nickname]); setNewNickname({ ...newNickname, [characterId]: '' }); } }; const handleNicknameDelete = (characterId: string, nickname: string) => { const character = characters.find(c => c.id === characterId); if (character) { handleEditCharacter(characterId, 'nicknames', character.nicknames.filter(n => n !== nickname)); } }; const handleNewRelationChange = (characterId: string, field: 'name' | 'relation', value: string) => { const current = newRelation[characterId] || { name: '', relation: '' }; setNewRelation({ ...newRelation, [characterId]: { ...current, [field]: value } }); }; const handleGenerateShortDescription = async (characterId: string) => { if (!connection || !model) return; const character = characters.find(c => c.id === characterId); if (!character || !character.description.trim()) return; setGeneratingShortDesc(characterId); try { const shortDesc = await LLM.summarize(connection, model.id, character.description, 'sentence'); handleEditCharacter(characterId, 'shortDescription', shortDesc.trim()); } catch (error) { console.error('Failed to generate short description:', error); } finally { setGeneratingShortDesc(null); } }; return (

{currentStory ? 'Story Characters' : 'World Characters'}

{characters.length === 0 && (

No characters yet. Add your first character!

)} {characters.map((character) => (
handleEditCharacter(character.id, 'name', e.currentTarget.value)} onFocus={(e) => e.currentTarget.select()} placeholder="Character name" /> {showDeleteConfirm === character.id ? (
Delete?
) : ( )}
Role
Description
handleEditCharacter(character.id, 'description', e.currentTarget.textContent)} placeholder="Full character description..." />
Short Description