Working on dialog editor, on dialogs, on phone

This commit is contained in:
Vladislav Khorev committed 2026-06-05 21:17:51 +03:00
1 parent 0e27521bea
commit 8f7797789d
63 files changed
+9502 -1459

No files matched your search

@@ -0,0 +1,156 @@
import { useRef, useState } from 'react';
import { useDialogueStore } from '../../store/dialogueStore';
import { parseDialogueFile } from '../../utils/fileIO';
import { useValidation } from '../../hooks/useValidation';
import styles from './LeftPanel.module.css';
export function LeftPanel() {
const { file, fileName, selectedDialogueId, loadFile, selectDialogue, createDialogue, deleteDialogue, duplicateDialogue, exportFile } = useDialogueStore();
const { issuesByNodeId } = useValidation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [newId, setNewId] = useState('');
const [creating, setCreating] = useState(false);
const [error, setError] = useState('');
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!f) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const parsed = parseDialogueFile(ev.target?.result as string);
loadFile(parsed, f.name);
setError('');
} catch (err) {
setError(String(err));
}
};
reader.readAsText(f);
e.target.value = '';
}
function handleCreate() {
if (!newId.trim()) return;
if (file?.dialogues.some(d => d.id === newId.trim())) {
setError(`ID "${newId.trim()}" already exists`);
return;
}
createDialogue(newId.trim());
setNewId('');
setCreating(false);
setError('');
}
function dialogueHasIssues(dialogueId: string) {
// Simple check: are there any issues for nodes in this dialogue?
// We only have current dialogue issues so we check if this is selected
return selectedDialogueId === dialogueId && Object.keys(issuesByNodeId).length > 0;
}
function dialogueHasErrors(dialogueId: string) {
return selectedDialogueId === dialogueId &&
Object.values(issuesByNodeId).some(list => list.some(i => i.severity === 'error'));
}
return (
<div className={styles.panel}>
<div className={styles.header}>
<span className={styles.title}>Dialogues</span>
{file && <span className={styles.fileName}>{fileName}</span>}
</div>
<div className={styles.fileButtons}>
<button className={styles.btn} onClick={() => fileInputRef.current?.click()}>
📂 Load
</button>
<input
ref={fileInputRef}
type="file"
accept=".json"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
{file && (
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={exportFile}>
💾 Save
</button>
)}
</div>
{error && <div className={styles.errorMsg}>{error}</div>}
<div className={styles.list}>
{!file && (
<div className={styles.emptyMsg}>Load a JSON file to start</div>
)}
{file?.dialogues.map(d => {
const hasErr = dialogueHasErrors(d.id);
const hasWarn = !hasErr && dialogueHasIssues(d.id);
return (
<div
key={d.id}
className={[
styles.dialogueItem,
d.id === selectedDialogueId ? styles.active : '',
].join(' ')}
onClick={() => selectDialogue(d.id)}
>
<span className={styles.dialogueId}>
{hasErr && <span className={styles.errDot} title="Has errors">●</span>}
{hasWarn && <span className={styles.warnDot} title="Has warnings">●</span>}
{d.mobileMode && <span className={styles.mobileDot} title="Mobile mode">📱</span>}
{d.id}
</span>
<button
className={styles.copyBtn}
title="Duplicate dialogue"
onClick={(e) => {
e.stopPropagation();
duplicateDialogue(d.id);
}}
>
⧉
</button>
<button
className={styles.deleteBtn}
title="Delete dialogue"
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete "${d.id}"?`)) deleteDialogue(d.id);
}}
>
×
</button>
</div>
);
})}
</div>
<div className={styles.footer}>
{creating ? (
<div className={styles.createForm}>
<input
className={styles.createInput}
value={newId}
onChange={e => setNewId(e.target.value)}
placeholder="dialogue_id"
onKeyDown={e => {
if (e.key === 'Enter') handleCreate();
if (e.key === 'Escape') { setCreating(false); setNewId(''); }
}}
autoFocus
/>
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={handleCreate}>✓</button>
<button className={styles.btn} onClick={() => { setCreating(false); setNewId(''); }}>✗</button>
</div>
) : (
file && (
<button className={[styles.btn, styles.btnNew].join(' ')} onClick={() => setCreating(true)}>
+ New Dialogue
</button>
)
)}
</div>
</div>
);
}