Working on dialog editor, on dialogs, on phone
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user