'use client' import { useCallback, useMemo, useState, ChangeEvent } from 'react' import { Handle, Position, NodeProps, useReactFlow } from 'reactflow' import Combobox from '@/components/editor/Combobox' import type { ComboboxItem } from '@/components/editor/Combobox' import { useEditorContext } from '@/components/editor/EditorContext' import NodeLockIndicator from '@/components/editor/NodeLockIndicator' type DialogueNodeData = { speaker?: string characterId?: string text: string } const RANDOM_COLORS = [ '#EF4444', '#F97316', '#F59E0B', '#10B981', '#3B82F6', '#8B5CF6', '#EC4899', '#14B8A6', '#6366F1', '#F43F5E', '#84CC16', '#06B6D4', ] function randomColor(): string { return RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)] } export default function DialogueNode({ id, data }: NodeProps) { const { setNodes } = useReactFlow() const { characters, onAddCharacter, nodeLocks, onNodeFocus, onNodeBlur } = useEditorContext() const lockInfo = nodeLocks.get(id) const isLockedByOther = !!lockInfo const [showAddForm, setShowAddForm] = useState(false) const [newName, setNewName] = useState('') const [newColor, setNewColor] = useState(randomColor) const characterItems: ComboboxItem[] = useMemo( () => characters.map((c) => ({ id: c.id, label: c.name, color: c.color, })), [characters] ) const hasInvalidReference = useMemo(() => { if (!data.characterId) return false return !characters.some((c) => c.id === data.characterId) }, [data.characterId, characters]) const updateNodeData = useCallback( (updates: Partial) => { setNodes((nodes) => nodes.map((node) => node.id === id ? { ...node, data: { ...node.data, ...updates } } : node ) ) }, [id, setNodes] ) const handleCharacterSelect = useCallback( (characterId: string) => { const character = characters.find((c) => c.id === characterId) updateNodeData({ characterId, speaker: character?.name || '', }) }, [characters, updateNodeData] ) const handleAddNew = useCallback(() => { setShowAddForm(true) setNewName('') setNewColor(randomColor()) }, []) const handleSubmitNew = useCallback(() => { if (!newName.trim()) return const newId = onAddCharacter(newName.trim(), newColor) updateNodeData({ characterId: newId, speaker: newName.trim(), }) setShowAddForm(false) }, [newName, newColor, onAddCharacter, updateNodeData]) const handleCancelNew = useCallback(() => { setShowAddForm(false) }, []) const handleTextChange = useCallback( (e: ChangeEvent) => { updateNodeData({ text: e.target.value }) }, [updateNodeData] ) const handleFocus = useCallback(() => { onNodeFocus(id) }, [id, onNodeFocus]) return (
{isLockedByOther && ( )} {isLockedByOther && (
Being edited by {lockInfo.displayName}
)}
Dialogue
{hasInvalidReference && (
Character not found
)}
{showAddForm && (
New character
setNewColor(e.target.value)} className="h-6 w-6 shrink-0 cursor-pointer rounded border border-zinc-300 dark:border-zinc-600" /> setNewName(e.target.value)} placeholder="Name" className="w-full rounded border border-zinc-300 bg-white px-2 py-0.5 text-sm focus:border-blue-500 focus:outline-none dark:border-zinc-600 dark:bg-zinc-700 dark:text-white dark:placeholder-zinc-400" onKeyDown={(e) => { if (e.key === 'Enter') handleSubmitNew() if (e.key === 'Escape') handleCancelNew() }} autoFocus />
)}