'use client' import { useState } from 'react' import { nanoid } from 'nanoid' import type { Character, Variable } from '@/types/flowchart' import ImportFromProjectModal from './ImportFromProjectModal' type Tab = 'characters' | 'variables' type ImportModalState = { open: boolean; mode: 'characters' | 'variables' } type ProjectSettingsModalProps = { projectId: string characters: Character[] variables: Variable[] onCharactersChange: (characters: Character[]) => void onVariablesChange: (variables: Variable[]) => void onClose: () => void getCharacterUsageCount: (characterId: string) => number getVariableUsageCount: (variableId: string) => number } function randomHexColor(): string { const colors = [ '#ef4444', '#f97316', '#f59e0b', '#84cc16', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#f43f5e', ] return colors[Math.floor(Math.random() * colors.length)] } export default function ProjectSettingsModal({ projectId, characters, variables, onCharactersChange, onVariablesChange, onClose, getCharacterUsageCount, getVariableUsageCount, }: ProjectSettingsModalProps) { const [activeTab, setActiveTab] = useState('characters') const [importModal, setImportModal] = useState({ open: false, mode: 'characters' }) const handleImportCharacters = (imported: Character[]) => { onCharactersChange([...characters, ...imported]) } const handleImportVariables = (imported: Variable[]) => { onVariablesChange([...variables, ...imported]) } return (
) } // Characters Tab type CharactersTabProps = { characters: Character[] onChange: (characters: Character[]) => void getUsageCount: (characterId: string) => number onImport: () => void } type CharacterFormData = { name: string color: string description: string } function CharactersTab({ characters, onChange, getUsageCount, onImport }: CharactersTabProps) { const [isAdding, setIsAdding] = useState(false) const [editingId, setEditingId] = useState(null) const [formData, setFormData] = useState({ name: '', color: randomHexColor(), description: '' }) const [formError, setFormError] = useState(null) const [deleteConfirm, setDeleteConfirm] = useState(null) const resetForm = () => { setFormData({ name: '', color: randomHexColor(), description: '' }) setFormError(null) } const validateName = (name: string, excludeId?: string): boolean => { if (!name.trim()) { setFormError('Name is required') return false } const duplicate = characters.find( (c) => c.name.toLowerCase() === name.trim().toLowerCase() && c.id !== excludeId ) if (duplicate) { setFormError('A character with this name already exists') return false } setFormError(null) return true } const handleAdd = () => { if (!validateName(formData.name)) return const newCharacter: Character = { id: nanoid(), name: formData.name.trim(), color: formData.color, description: formData.description.trim() || undefined, } onChange([...characters, newCharacter]) setIsAdding(false) resetForm() } const handleEdit = (character: Character) => { setEditingId(character.id) setFormData({ name: character.name, color: character.color, description: character.description || '', }) setFormError(null) setIsAdding(false) } const handleSaveEdit = () => { if (!editingId) return if (!validateName(formData.name, editingId)) return onChange( characters.map((c) => c.id === editingId ? { ...c, name: formData.name.trim(), color: formData.color, description: formData.description.trim() || undefined } : c ) ) setEditingId(null) resetForm() } const handleDelete = (id: string) => { const usageCount = getUsageCount(id) if (usageCount > 0 && deleteConfirm !== id) { setDeleteConfirm(id) return } onChange(characters.filter((c) => c.id !== id)) setDeleteConfirm(null) } const handleCancelForm = () => { setIsAdding(false) setEditingId(null) resetForm() } return (

Define characters that can be referenced in dialogue nodes.

{!isAdding && !editingId && (
)}
{/* Character List */}
{characters.map((character) => (
{editingId === character.id ? ( ) : (
{character.name} {character.description && (

{character.description}

)}
{deleteConfirm === character.id && ( Used in {getUsageCount(character.id)} node(s). Delete anyway? )}
)}
))} {characters.length === 0 && !isAdding && (

No characters defined yet. Click "Add Character" to create one.

)}
{/* Add Form */} {isAdding && (
)}
) } // Character Form type CharacterFormProps = { formData: CharacterFormData formError: string | null onChange: (data: CharacterFormData) => void onSave: () => void onCancel: () => void saveLabel: string } function CharacterForm({ formData, formError, onChange, onSave, onCancel, saveLabel }: CharacterFormProps) { return (
onChange({ ...formData, color: e.target.value })} className="h-8 w-8 cursor-pointer rounded border border-zinc-300 dark:border-zinc-600" />
onChange({ ...formData, name: e.target.value })} placeholder="Character name" autoFocus className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" />
onChange({ ...formData, description: e.target.value })} placeholder="Optional description" className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" />
{formError && (

{formError}

)}
) } // Variables Tab type VariablesTabProps = { variables: Variable[] onChange: (variables: Variable[]) => void getUsageCount: (variableId: string) => number onImport: () => void } type VariableType = 'numeric' | 'string' | 'boolean' type VariableFormData = { name: string type: VariableType initialValue: string description: string } const defaultInitialValues: Record = { numeric: '0', string: '', boolean: 'false', } function parseInitialValue(type: VariableType, raw: string): number | string | boolean { switch (type) { case 'numeric': return Number(raw) || 0 case 'boolean': return raw === 'true' case 'string': return raw } } function VariablesTab({ variables, onChange, getUsageCount, onImport }: VariablesTabProps) { const [isAdding, setIsAdding] = useState(false) const [editingId, setEditingId] = useState(null) const [formData, setFormData] = useState({ name: '', type: 'numeric', initialValue: '0', description: '' }) const [formError, setFormError] = useState(null) const [deleteConfirm, setDeleteConfirm] = useState(null) const resetForm = () => { setFormData({ name: '', type: 'numeric', initialValue: '0', description: '' }) setFormError(null) } const validateName = (name: string, excludeId?: string): boolean => { if (!name.trim()) { setFormError('Name is required') return false } const duplicate = variables.find( (v) => v.name.toLowerCase() === name.trim().toLowerCase() && v.id !== excludeId ) if (duplicate) { setFormError('A variable with this name already exists') return false } setFormError(null) return true } const handleAdd = () => { if (!validateName(formData.name)) return const newVariable: Variable = { id: nanoid(), name: formData.name.trim(), type: formData.type, initialValue: parseInitialValue(formData.type, formData.initialValue), description: formData.description.trim() || undefined, } onChange([...variables, newVariable]) setIsAdding(false) resetForm() } const handleEdit = (variable: Variable) => { setEditingId(variable.id) setFormData({ name: variable.name, type: variable.type, initialValue: String(variable.initialValue), description: variable.description || '', }) setFormError(null) setIsAdding(false) } const handleSaveEdit = () => { if (!editingId) return if (!validateName(formData.name, editingId)) return onChange( variables.map((v) => v.id === editingId ? { ...v, name: formData.name.trim(), type: formData.type, initialValue: parseInitialValue(formData.type, formData.initialValue), description: formData.description.trim() || undefined, } : v ) ) setEditingId(null) resetForm() } const handleDelete = (id: string) => { const usageCount = getUsageCount(id) if (usageCount > 0 && deleteConfirm !== id) { setDeleteConfirm(id) return } onChange(variables.filter((v) => v.id !== id)) setDeleteConfirm(null) } const handleCancelForm = () => { setIsAdding(false) setEditingId(null) resetForm() } return (

Define variables that can be referenced in variable nodes and edge conditions.

{!isAdding && !editingId && (
)}
{/* Variable List */}
{variables.map((variable) => (
{editingId === variable.id ? ( ) : (
{variable.type}
{variable.name} {variable.description && (

{variable.description}

)}
Initial: {String(variable.initialValue)}
{deleteConfirm === variable.id && ( Used in {getUsageCount(variable.id)} node(s). Delete anyway? )}
)}
))} {variables.length === 0 && !isAdding && (

No variables defined yet. Click "Add Variable" to create one.

)}
{/* Add Form */} {isAdding && (
)}
) } // Variable Form type VariableFormProps = { formData: VariableFormData formError: string | null onChange: (data: VariableFormData) => void onSave: () => void onCancel: () => void saveLabel: string } function VariableForm({ formData, formError, onChange, onSave, onCancel, saveLabel }: VariableFormProps) { const handleTypeChange = (newType: VariableType) => { onChange({ ...formData, type: newType, initialValue: defaultInitialValues[newType] }) } return (
onChange({ ...formData, name: e.target.value })} placeholder="Variable name" autoFocus className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" />
{formData.type === 'boolean' ? ( ) : formData.type === 'numeric' ? ( onChange({ ...formData, initialValue: e.target.value })} className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" /> ) : ( onChange({ ...formData, initialValue: e.target.value })} placeholder="Initial value" className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" /> )}
onChange({ ...formData, description: e.target.value })} placeholder="Optional description" className="block w-full rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" />
{formError && (

{formError}

)}
) }