import { useAppState, type Character } from "../contexts/state"; import { useState } from "preact/hooks"; import styles from '../assets/character-editor.module.css'; import LLM from "../utils/llm"; import { ContentEditable } from "@common/components/ContentEditable"; export const CharacterEditor = () => { const { currentStory, dispatch, connection, model } = useAppState(); const [newNickname, setNewNickname] = useState>({}); const [newRelation, setNewRelation] = useState>({}); const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); const [generatingShortDesc, setGeneratingShortDesc] = useState(null); if (!currentStory) { return null; } const handleAddCharacter = () => { dispatch({ type: 'ADD_CHARACTER', storyId: currentStory.id, character: { id: crypto.randomUUID(), name: 'New Character', nicknames: [], shortDescription: '', description: '', relations: [], }, }); }; const handleEditCharacter = (characterId: string, field: keyof Character, value: any) => { dispatch({ type: 'EDIT_CHARACTER', storyId: currentStory.id, characterId, updates: { [field]: value }, }); }; const handleDeleteCharacter = (characterId: string) => { dispatch({ type: 'DELETE_CHARACTER', storyId: currentStory.id, characterId, }); }; const handleAddRelation = (characterId: string) => { const rel = newRelation[characterId] || { name: '', relation: '' }; if (!rel.name.trim() || !rel.relation.trim()) return; dispatch({ type: 'ADD_CHARACTER_RELATION', storyId: currentStory.id, 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', storyId: currentStory.id, characterId, targetName, updates: { [field]: value }, }); }; const handleDeleteRelation = (characterId: string, targetName: string) => { dispatch({ type: 'DELETE_CHARACTER_RELATION', storyId: currentStory.id, characterId, targetName, }); }; const handleNicknameAdd = (characterId: string) => { const nickname = (newNickname[characterId] || '').trim(); if (!nickname) return; const character = currentStory.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 = currentStory.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 = currentStory.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 (

Characters

{currentStory.characters.length === 0 && (

No characters yet. Add your first character!

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