@magicborn/dialogue-forge 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demo/app/page.tsx +26 -12
- package/demo/public/logo.svg +14 -1
- package/dist/components/CharacterSelector.d.ts +15 -0
- package/dist/components/CharacterSelector.js +125 -0
- package/dist/components/ConditionalNodeV2.d.ts +2 -0
- package/dist/components/ConditionalNodeV2.js +50 -29
- package/dist/components/DialogueEditorV2.d.ts +2 -0
- package/dist/components/DialogueEditorV2.js +43 -6
- package/dist/components/GuidePanel.js +99 -16
- package/dist/components/NPCNodeV2.d.ts +2 -0
- package/dist/components/NPCNodeV2.js +31 -20
- package/dist/components/NodeEditor.d.ts +3 -1
- package/dist/components/NodeEditor.js +42 -13
- package/dist/components/PlayerNodeV2.d.ts +2 -0
- package/dist/components/PlayerNodeV2.js +58 -39
- package/dist/components/ReactFlowPOC.js +3 -3
- package/dist/esm/components/CharacterSelector.d.ts +15 -0
- package/dist/esm/components/CharacterSelector.js +89 -0
- package/dist/esm/components/ConditionalNodeV2.d.ts +2 -0
- package/dist/esm/components/ConditionalNodeV2.js +51 -30
- package/dist/esm/components/DialogueEditorV2.d.ts +2 -0
- package/dist/esm/components/DialogueEditorV2.js +43 -6
- package/dist/esm/components/GuidePanel.js +99 -16
- package/dist/esm/components/NPCNodeV2.d.ts +2 -0
- package/dist/esm/components/NPCNodeV2.js +32 -21
- package/dist/esm/components/NodeEditor.d.ts +3 -1
- package/dist/esm/components/NodeEditor.js +42 -13
- package/dist/esm/components/PlayerNodeV2.d.ts +2 -0
- package/dist/esm/components/PlayerNodeV2.js +59 -40
- package/dist/esm/components/ReactFlowPOC.js +3 -3
- package/dist/esm/examples/example-characters.d.ts +19 -0
- package/dist/esm/examples/example-characters.js +67 -0
- package/dist/esm/examples/index.d.ts +1 -0
- package/dist/esm/examples/index.js +2 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/types/characters.d.ts +15 -0
- package/dist/esm/types/characters.js +6 -0
- package/dist/esm/types/game-state.d.ts +2 -0
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/examples/example-characters.d.ts +19 -0
- package/dist/examples/example-characters.js +73 -0
- package/dist/examples/index.d.ts +1 -0
- package/dist/examples/index.js +7 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -1
- package/dist/types/characters.d.ts +15 -0
- package/dist/types/characters.js +7 -0
- package/dist/types/game-state.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
package/demo/app/page.tsx
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
listExamples,
|
|
12
12
|
getExampleDialogue,
|
|
13
13
|
listDemoFlagSchemas,
|
|
14
|
-
getDemoFlagSchema
|
|
14
|
+
getDemoFlagSchema,
|
|
15
|
+
getExampleCharacters
|
|
15
16
|
} from '@magicborn/dialogue-forge/src/examples';
|
|
16
17
|
import { Play, Layout, FileText } from 'lucide-react';
|
|
17
18
|
import { ThemeSwitcher } from '../components/ThemeSwitcher';
|
|
@@ -31,7 +32,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
31
32
|
'start': {
|
|
32
33
|
id: 'start',
|
|
33
34
|
type: 'npc',
|
|
34
|
-
|
|
35
|
+
characterId: 'stranger',
|
|
36
|
+
speaker: 'Stranger', // Fallback
|
|
35
37
|
x: 300,
|
|
36
38
|
y: 100,
|
|
37
39
|
content: "You find yourself at a crossroads. A cloaked figure emerges from the shadows.",
|
|
@@ -40,7 +42,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
40
42
|
'greeting': {
|
|
41
43
|
id: 'greeting',
|
|
42
44
|
type: 'npc',
|
|
43
|
-
|
|
45
|
+
characterId: 'stranger',
|
|
46
|
+
speaker: 'Stranger', // Fallback
|
|
44
47
|
x: 300,
|
|
45
48
|
y: 200,
|
|
46
49
|
content: "\"Traveler... I've been waiting for you. What brings you to these lands?\"",
|
|
@@ -82,7 +85,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
82
85
|
'treasure_response': {
|
|
83
86
|
id: 'treasure_response',
|
|
84
87
|
type: 'npc',
|
|
85
|
-
|
|
88
|
+
characterId: 'stranger',
|
|
89
|
+
speaker: 'Stranger', // Fallback
|
|
86
90
|
x: 200,
|
|
87
91
|
y: 450,
|
|
88
92
|
content: "\"Many have sought the same. Take this map—it shows the entrance to the catacombs.\"",
|
|
@@ -91,7 +95,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
91
95
|
'knowledge_response': {
|
|
92
96
|
id: 'knowledge_response',
|
|
93
97
|
type: 'npc',
|
|
94
|
-
|
|
98
|
+
characterId: 'stranger',
|
|
99
|
+
speaker: 'Stranger', // Fallback
|
|
95
100
|
x: 400,
|
|
96
101
|
y: 450,
|
|
97
102
|
content: "\"A seeker of truth... Take this tome. It contains the riddles you must solve.\"",
|
|
@@ -100,7 +105,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
100
105
|
'high_rep_response': {
|
|
101
106
|
id: 'high_rep_response',
|
|
102
107
|
type: 'npc',
|
|
103
|
-
|
|
108
|
+
characterId: 'stranger',
|
|
109
|
+
speaker: 'Stranger', // Fallback
|
|
104
110
|
x: 500,
|
|
105
111
|
y: 450,
|
|
106
112
|
content: "\"Ah, a hero! Your reputation precedes you. I have something special for you...\"",
|
|
@@ -126,7 +132,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
126
132
|
'bartender_greet': {
|
|
127
133
|
id: 'bartender_greet',
|
|
128
134
|
type: 'npc',
|
|
129
|
-
|
|
135
|
+
characterId: 'bartender',
|
|
136
|
+
speaker: 'Bartender', // Fallback
|
|
130
137
|
x: 300,
|
|
131
138
|
y: 150,
|
|
132
139
|
content: "\"Welcome, stranger! What can I get ya? We've got ale, mead, or if you're looking for work, I might have something.\"",
|
|
@@ -155,7 +162,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
155
162
|
'drink_ale': {
|
|
156
163
|
id: 'drink_ale',
|
|
157
164
|
type: 'npc',
|
|
158
|
-
|
|
165
|
+
characterId: 'bartender',
|
|
166
|
+
speaker: 'Bartender', // Fallback
|
|
159
167
|
x: 100,
|
|
160
168
|
y: 420,
|
|
161
169
|
content: "\"Coming right up!\" He slides a frothy mug across the bar.",
|
|
@@ -164,7 +172,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
164
172
|
'work_info': {
|
|
165
173
|
id: 'work_info',
|
|
166
174
|
type: 'npc',
|
|
167
|
-
|
|
175
|
+
characterId: 'bartender',
|
|
176
|
+
speaker: 'Bartender', // Fallback
|
|
168
177
|
x: 300,
|
|
169
178
|
y: 420,
|
|
170
179
|
content: "\"Rats in the cellar. Big ones. I'll pay 10 gold if you clear 'em out.\"",
|
|
@@ -184,7 +193,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
184
193
|
'quest_accepted': {
|
|
185
194
|
id: 'quest_accepted',
|
|
186
195
|
type: 'npc',
|
|
187
|
-
|
|
196
|
+
characterId: 'bartender',
|
|
197
|
+
speaker: 'Bartender', // Fallback
|
|
188
198
|
x: 300,
|
|
189
199
|
y: 680,
|
|
190
200
|
content: "\"Great! The cellar door is in the back. Good luck!\"",
|
|
@@ -193,7 +203,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
193
203
|
'observe_tavern': {
|
|
194
204
|
id: 'observe_tavern',
|
|
195
205
|
type: 'npc',
|
|
196
|
-
|
|
206
|
+
characterId: 'narrator',
|
|
207
|
+
speaker: 'Narrator', // Fallback
|
|
197
208
|
x: 500,
|
|
198
209
|
y: 420,
|
|
199
210
|
content: "You notice a hooded figure in the corner, watching you intently...",
|
|
@@ -202,7 +213,8 @@ const demoDialogues: Record<string, DialogueTree> = {
|
|
|
202
213
|
'vip_response': {
|
|
203
214
|
id: 'vip_response',
|
|
204
215
|
type: 'npc',
|
|
205
|
-
|
|
216
|
+
characterId: 'bartender',
|
|
217
|
+
speaker: 'Bartender', // Fallback
|
|
206
218
|
x: 600,
|
|
207
219
|
y: 420,
|
|
208
220
|
content: "\"Of course! Right this way to the VIP lounge. Your reputation grants you access.\"",
|
|
@@ -231,6 +243,7 @@ export default function DialogueForgeDemo() {
|
|
|
231
243
|
const [dialogueTree, setDialogueTree] = useState<DialogueTree>(demoDialogues['mysterious-stranger']);
|
|
232
244
|
const [flagSchema, setFlagSchema] = useState<FlagSchema>(demoFlagSchema);
|
|
233
245
|
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
|
246
|
+
const characters = getExampleCharacters(); // Get example characters
|
|
234
247
|
|
|
235
248
|
// Panel states
|
|
236
249
|
const [showFlagManager, setShowFlagManager] = useState(false);
|
|
@@ -343,6 +356,7 @@ export default function DialogueForgeDemo() {
|
|
|
343
356
|
onChange={setDialogueTree}
|
|
344
357
|
onExportYarn={handleExportYarn}
|
|
345
358
|
flagSchema={flagSchema}
|
|
359
|
+
characters={characters}
|
|
346
360
|
viewMode={viewMode}
|
|
347
361
|
onViewModeChange={setViewMode}
|
|
348
362
|
className="w-full h-full"
|
package/demo/public/logo.svg
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-label="Dialogue Forge icon" preserveAspectRatio="xMidYMid meet">
|
|
2
|
+
<defs>
|
|
3
|
+
<style>
|
|
4
|
+
.ink{ fill:none; stroke:currentColor; stroke-width:6; stroke-linecap:round; stroke-linejoin:round; }
|
|
5
|
+
</style>
|
|
6
|
+
</defs>
|
|
7
|
+
|
|
8
|
+
<!-- Brackets -->
|
|
9
|
+
<path class="ink" d="M22 22v34c0 10 8 18 18 18h12"/>
|
|
10
|
+
<path class="ink" d="M74 22v34c0 10-8 18-18 18H44"/>
|
|
11
|
+
|
|
12
|
+
<!-- Construction bar -->
|
|
13
|
+
<path class="ink" d="M48 18v60"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CharacterSelector - Combobox with search for selecting characters
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { Character } from '../types/characters';
|
|
6
|
+
interface CharacterSelectorProps {
|
|
7
|
+
characters?: Record<string, Character>;
|
|
8
|
+
selectedCharacterId?: string;
|
|
9
|
+
onSelect: (characterId: string | undefined) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function CharacterSelector({ characters, selectedCharacterId, onSelect, placeholder, className, compact, }: CharacterSelectorProps): React.JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CharacterSelector - Combobox with search for selecting characters
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.CharacterSelector = CharacterSelector;
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
41
|
+
const lucide_react_1 = require("lucide-react");
|
|
42
|
+
function CharacterSelector({ characters = {}, selectedCharacterId, onSelect, placeholder = 'Select character...', className = '', compact = false, }) {
|
|
43
|
+
const [isOpen, setIsOpen] = (0, react_1.useState)(false);
|
|
44
|
+
const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
|
|
45
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
46
|
+
const inputRef = (0, react_1.useRef)(null);
|
|
47
|
+
const selectedCharacter = selectedCharacterId ? characters[selectedCharacterId] : undefined;
|
|
48
|
+
// Filter characters based on search query
|
|
49
|
+
const filteredCharacters = Object.entries(characters).filter(([id, character]) => {
|
|
50
|
+
if (!searchQuery.trim())
|
|
51
|
+
return true;
|
|
52
|
+
const query = searchQuery.toLowerCase();
|
|
53
|
+
return (character.name.toLowerCase().includes(query) ||
|
|
54
|
+
character.id.toLowerCase().includes(query) ||
|
|
55
|
+
(character.description && character.description.toLowerCase().includes(query)));
|
|
56
|
+
});
|
|
57
|
+
// Close dropdown when clicking outside
|
|
58
|
+
(0, react_1.useEffect)(() => {
|
|
59
|
+
function handleClickOutside(event) {
|
|
60
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
61
|
+
setIsOpen(false);
|
|
62
|
+
setSearchQuery('');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (isOpen) {
|
|
66
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
67
|
+
// Focus input when dropdown opens
|
|
68
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
72
|
+
};
|
|
73
|
+
}, [isOpen]);
|
|
74
|
+
const handleSelect = (characterId) => {
|
|
75
|
+
onSelect(characterId);
|
|
76
|
+
setIsOpen(false);
|
|
77
|
+
setSearchQuery('');
|
|
78
|
+
};
|
|
79
|
+
const handleClear = (e) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
onSelect(undefined);
|
|
82
|
+
setIsOpen(false);
|
|
83
|
+
setSearchQuery('');
|
|
84
|
+
};
|
|
85
|
+
// Compact mode for conditional blocks
|
|
86
|
+
if (compact) {
|
|
87
|
+
return (react_1.default.createElement("div", { ref: containerRef, className: `relative ${className}` },
|
|
88
|
+
react_1.default.createElement("button", { type: "button", onClick: () => setIsOpen(!isOpen), className: "flex items-center gap-1.5 bg-df-elevated border border-df-control-border rounded px-1.5 py-0.5 text-[10px] text-df-text-primary hover:border-df-control-hover transition-colors" }, selectedCharacter ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
89
|
+
react_1.default.createElement("span", { className: "text-sm flex-shrink-0" }, selectedCharacter.avatar || '👤'),
|
|
90
|
+
react_1.default.createElement("span", { className: "text-[10px] truncate max-w-[80px]" }, selectedCharacter.name),
|
|
91
|
+
react_1.default.createElement("button", { onClick: handleClear, className: "flex-shrink-0 p-0.5 hover:bg-df-control-hover rounded", title: "Clear character" },
|
|
92
|
+
react_1.default.createElement(lucide_react_1.X, { size: 10, className: "text-df-text-secondary" })))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
93
|
+
react_1.default.createElement(lucide_react_1.User, { size: 10, className: "text-df-text-secondary flex-shrink-0" }),
|
|
94
|
+
react_1.default.createElement("span", { className: "text-[10px] text-df-text-secondary" }, placeholder)))),
|
|
95
|
+
isOpen && (react_1.default.createElement("div", { className: "absolute z-50 mt-1 w-48 bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-xl max-h-48 overflow-hidden flex flex-col" },
|
|
96
|
+
react_1.default.createElement("div", { className: "p-1.5 border-b border-df-sidebar-border" },
|
|
97
|
+
react_1.default.createElement("div", { className: "relative" },
|
|
98
|
+
react_1.default.createElement(lucide_react_1.Search, { size: 12, className: "absolute left-1.5 top-1/2 -translate-y-1/2 text-df-text-secondary" }),
|
|
99
|
+
react_1.default.createElement("input", { ref: inputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", className: "w-full bg-df-elevated border border-df-control-border rounded px-1.5 py-1 pl-6 text-[10px] text-df-text-primary placeholder:text-df-text-tertiary focus:border-df-control-hover outline-none" }))),
|
|
100
|
+
react_1.default.createElement("div", { className: "overflow-y-auto" }, filteredCharacters.length === 0 ? (react_1.default.createElement("div", { className: "p-2 text-[10px] text-df-text-secondary text-center" }, searchQuery ? 'No matches' : 'No characters')) : (filteredCharacters.map(([id, character]) => (react_1.default.createElement("button", { key: id, type: "button", onClick: () => handleSelect(id), className: `w-full flex items-center gap-1.5 px-2 py-1 text-left hover:bg-df-elevated transition-colors ${selectedCharacterId === id ? 'bg-df-npc-selected/20' : ''}` },
|
|
101
|
+
react_1.default.createElement("span", { className: "text-sm flex-shrink-0" }, character.avatar || '👤'),
|
|
102
|
+
react_1.default.createElement("span", { className: "text-[10px] text-df-text-primary truncate flex-1" }, character.name),
|
|
103
|
+
selectedCharacterId === id && (react_1.default.createElement("div", { className: "flex-shrink-0 w-1.5 h-1.5 rounded-full bg-df-npc-selected" })))))))))));
|
|
104
|
+
}
|
|
105
|
+
// Full mode (existing)
|
|
106
|
+
return (react_1.default.createElement("div", { ref: containerRef, className: `relative ${className}` },
|
|
107
|
+
react_1.default.createElement("button", { type: "button", onClick: () => setIsOpen(!isOpen), className: "w-full flex items-center gap-2 bg-df-elevated border border-df-control-border rounded px-2 py-1.5 text-sm text-df-text-primary hover:border-df-control-hover transition-colors" }, selectedCharacter ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
108
|
+
react_1.default.createElement("span", { className: "text-lg flex-shrink-0" }, selectedCharacter.avatar || '👤'),
|
|
109
|
+
react_1.default.createElement("span", { className: "flex-1 text-left truncate" }, selectedCharacter.name),
|
|
110
|
+
react_1.default.createElement("button", { onClick: handleClear, className: "flex-shrink-0 p-0.5 hover:bg-df-control-hover rounded", title: "Clear character" },
|
|
111
|
+
react_1.default.createElement(lucide_react_1.X, { size: 12, className: "text-df-text-secondary" })))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
112
|
+
react_1.default.createElement(lucide_react_1.User, { size: 14, className: "text-df-text-secondary flex-shrink-0" }),
|
|
113
|
+
react_1.default.createElement("span", { className: "flex-1 text-left text-df-text-secondary" }, placeholder)))),
|
|
114
|
+
isOpen && (react_1.default.createElement("div", { className: "absolute z-50 mt-1 w-full bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-xl max-h-64 overflow-hidden flex flex-col" },
|
|
115
|
+
react_1.default.createElement("div", { className: "p-2 border-b border-df-sidebar-border" },
|
|
116
|
+
react_1.default.createElement("div", { className: "relative" },
|
|
117
|
+
react_1.default.createElement(lucide_react_1.Search, { size: 14, className: "absolute left-2 top-1/2 -translate-y-1/2 text-df-text-secondary" }),
|
|
118
|
+
react_1.default.createElement("input", { ref: inputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search characters...", className: "w-full bg-df-elevated border border-df-control-border rounded px-2 py-1.5 pl-7 text-sm text-df-text-primary placeholder:text-df-text-tertiary focus:border-df-control-hover outline-none" }))),
|
|
119
|
+
react_1.default.createElement("div", { className: "overflow-y-auto" }, filteredCharacters.length === 0 ? (react_1.default.createElement("div", { className: "p-3 text-sm text-df-text-secondary text-center" }, searchQuery ? 'No characters found' : 'No characters available')) : (filteredCharacters.map(([id, character]) => (react_1.default.createElement("button", { key: id, type: "button", onClick: () => handleSelect(id), className: `w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-df-elevated transition-colors ${selectedCharacterId === id ? 'bg-df-npc-selected/20' : ''}` },
|
|
120
|
+
react_1.default.createElement("span", { className: "text-lg flex-shrink-0" }, character.avatar || '👤'),
|
|
121
|
+
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
122
|
+
react_1.default.createElement("div", { className: "text-sm text-df-text-primary font-medium truncate" }, character.name),
|
|
123
|
+
character.description && (react_1.default.createElement("div", { className: "text-xs text-df-text-secondary truncate" }, character.description))),
|
|
124
|
+
selectedCharacterId === id && (react_1.default.createElement("div", { className: "flex-shrink-0 w-2 h-2 rounded-full bg-df-npc-selected" })))))))))));
|
|
125
|
+
}
|
|
@@ -2,10 +2,12 @@ import React from 'react';
|
|
|
2
2
|
import { NodeProps } from 'reactflow';
|
|
3
3
|
import { DialogueNode } from '../types';
|
|
4
4
|
import { FlagSchema } from '../types/flags';
|
|
5
|
+
import { Character } from '../types/characters';
|
|
5
6
|
import { LayoutDirection } from '../utils/layout';
|
|
6
7
|
interface ConditionalNodeData {
|
|
7
8
|
node: DialogueNode;
|
|
8
9
|
flagSchema?: FlagSchema;
|
|
10
|
+
characters?: Record<string, Character>;
|
|
9
11
|
isDimmed?: boolean;
|
|
10
12
|
isInPath?: boolean;
|
|
11
13
|
layoutDirection?: LayoutDirection;
|
|
@@ -40,7 +40,7 @@ const lucide_react_1 = require("lucide-react");
|
|
|
40
40
|
// Color scheme for conditional block edges
|
|
41
41
|
const CONDITIONAL_COLORS = ['#3b82f6', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
|
|
42
42
|
function ConditionalNodeV2({ data, selected }) {
|
|
43
|
-
const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
43
|
+
const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
|
|
44
44
|
const blocks = node.conditionalBlocks || [];
|
|
45
45
|
const updateNodeInternals = (0, reactflow_1.useUpdateNodeInternals)();
|
|
46
46
|
const headerRef = (0, react_1.useRef)(null);
|
|
@@ -49,6 +49,7 @@ function ConditionalNodeV2({ data, selected }) {
|
|
|
49
49
|
// Handle positions based on layout direction
|
|
50
50
|
const isHorizontal = layoutDirection === 'LR';
|
|
51
51
|
const targetPosition = isHorizontal ? reactflow_1.Position.Left : reactflow_1.Position.Top;
|
|
52
|
+
const sourcePosition = isHorizontal ? reactflow_1.Position.Right : reactflow_1.Position.Bottom;
|
|
52
53
|
// Calculate handle positions based on actual rendered heights
|
|
53
54
|
(0, react_1.useEffect)(() => {
|
|
54
55
|
if (headerRef.current && blocks.length > 0) {
|
|
@@ -85,23 +86,41 @@ function ConditionalNodeV2({ data, selected }) {
|
|
|
85
86
|
: isEndNode
|
|
86
87
|
? 'border-df-end shadow-md'
|
|
87
88
|
: 'border-df-conditional-border';
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// Header background for conditional nodes
|
|
90
|
+
const headerBgClass = isStartNode
|
|
91
|
+
? 'bg-df-start-bg'
|
|
92
|
+
: isEndNode
|
|
93
|
+
? 'bg-df-end-bg'
|
|
94
|
+
: 'bg-df-conditional-header';
|
|
95
|
+
return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-conditional-border/70' : ''} bg-df-conditional-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
|
|
95
96
|
react_1.default.createElement(reactflow_1.Handle, { type: "target", position: targetPosition, className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full" }),
|
|
96
|
-
react_1.default.createElement("div", { ref: headerRef, className:
|
|
97
|
-
react_1.default.createElement(
|
|
98
|
-
|
|
99
|
-
react_1.default.createElement("
|
|
100
|
-
|
|
97
|
+
react_1.default.createElement("div", { ref: headerRef, className: `${headerBgClass} border-b-2 border-df-conditional-border px-3 py-2.5 flex items-center gap-3 relative` },
|
|
98
|
+
react_1.default.createElement("div", { className: "w-14 h-14 rounded-full bg-df-conditional-bg border-[3px] border-df-conditional-border flex items-center justify-center shadow-lg flex-shrink-0" },
|
|
99
|
+
react_1.default.createElement(lucide_react_1.Code, { size: 20, className: "text-df-conditional-selected" })),
|
|
100
|
+
react_1.default.createElement("div", { className: "flex-1 min-w-0" },
|
|
101
|
+
react_1.default.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, "Conditional Logic")),
|
|
102
|
+
react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
|
|
103
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-base/50 border border-df-control-border", title: `Node ID: ${node.id}` },
|
|
104
|
+
react_1.default.createElement(lucide_react_1.Hash, { size: 12, className: "text-df-text-secondary" }),
|
|
105
|
+
react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
|
|
106
|
+
react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-conditional-selected/20 border border-df-conditional-selected/50", title: "Conditional Node" },
|
|
107
|
+
react_1.default.createElement(lucide_react_1.GitBranch, { size: 14, className: "text-df-conditional-selected" }),
|
|
108
|
+
react_1.default.createElement("span", { className: "text-[10px] font-semibold text-df-conditional-selected" }, "IF/ELSE"))),
|
|
109
|
+
isStartNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
110
|
+
react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
|
|
111
|
+
" START")),
|
|
112
|
+
isEndNode && (react_1.default.createElement("div", { className: "absolute top-1 right-1 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded flex items-center gap-0.5 shadow-lg z-20" },
|
|
113
|
+
react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
|
|
114
|
+
" END"))),
|
|
115
|
+
react_1.default.createElement("div", { className: "px-4 py-3" }, blocks.map((block, idx) => {
|
|
101
116
|
const color = CONDITIONAL_COLORS[idx % CONDITIONAL_COLORS.length];
|
|
102
117
|
const blockType = block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE';
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
// Get character if characterId is set
|
|
119
|
+
const character = block.characterId ? characters[block.characterId] : undefined;
|
|
120
|
+
const displayName = character ? character.name : (block.speaker || undefined);
|
|
121
|
+
const avatar = character?.avatar || '👤';
|
|
122
|
+
return (react_1.default.createElement("div", { key: block.id, ref: el => { blockRefs.current[idx] = el; }, className: "px-3 py-2 border-b border-df-control-border last:border-b-0" },
|
|
123
|
+
react_1.default.createElement("div", { className: "flex items-center gap-2 mb-1.5" },
|
|
105
124
|
react_1.default.createElement("span", { className: "text-[9px] px-1.5 py-0.5 rounded bg-df-base text-df-text-primary font-semibold" }, blockType),
|
|
106
125
|
block.condition && block.condition.length > 0 && (react_1.default.createElement("span", { className: "text-[9px] text-df-text-secondary font-mono truncate flex-1" }, block.condition.map((c) => {
|
|
107
126
|
const varName = `$${c.flag}`;
|
|
@@ -121,27 +140,29 @@ function ConditionalNodeV2({ data, selected }) {
|
|
|
121
140
|
}
|
|
122
141
|
return '';
|
|
123
142
|
}).filter(c => c).join(' and ').slice(0, 30)))),
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
displayName && (react_1.default.createElement("div", { className: "flex items-center gap-1.5 mb-1.5" },
|
|
144
|
+
react_1.default.createElement("span", { className: "text-sm flex-shrink-0" }, avatar),
|
|
145
|
+
react_1.default.createElement("span", { className: "text-[10px] text-df-conditional-selected font-medium" }, displayName))),
|
|
146
|
+
react_1.default.createElement("div", { className: "text-xs text-df-text-primary line-clamp-2 bg-df-elevated border border-df-control-border rounded px-3 py-1.5" },
|
|
126
147
|
"\"",
|
|
127
|
-
block.content.slice(0,
|
|
148
|
+
block.content.slice(0, 60) + (block.content.length > 60 ? '...' : ''),
|
|
128
149
|
"\""),
|
|
129
150
|
react_1.default.createElement(reactflow_1.Handle, { type: "source", position: reactflow_1.Position.Right, id: `block-${idx}`, style: {
|
|
130
151
|
top: `${handlePositions[idx] || 0}px`,
|
|
131
152
|
right: -8,
|
|
132
|
-
}, className: "!bg-
|
|
153
|
+
}, className: "!bg-df-control-bg !border-df-control-border !w-3 !h-3 !rounded-full hover:!border-df-conditional-selected hover:!bg-df-conditional-selected/20" })));
|
|
133
154
|
})),
|
|
134
|
-
node.setFlags && node.setFlags.length > 0 && (react_1.default.createElement("div", { className: "px-
|
|
155
|
+
node.setFlags && node.setFlags.length > 0 && (react_1.default.createElement("div", { className: "px-4 py-2 border-t border-df-control-border flex flex-wrap gap-1" }, node.setFlags.map(flagId => {
|
|
135
156
|
const flag = flagSchema?.flags.find(f => f.id === flagId);
|
|
136
157
|
const flagType = flag?.type || 'dialogue';
|
|
137
|
-
const colorClass = flagType === 'dialogue' ? 'bg-
|
|
138
|
-
flagType === 'quest' ? 'bg-
|
|
139
|
-
flagType === 'achievement' ? 'bg-
|
|
140
|
-
flagType === 'item' ? 'bg-
|
|
141
|
-
flagType === 'stat' ? 'bg-
|
|
142
|
-
flagType === 'title' ? 'bg-
|
|
143
|
-
flagType === 'global' ? 'bg-
|
|
144
|
-
'bg-
|
|
145
|
-
return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
|
|
158
|
+
const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
|
|
159
|
+
flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
|
|
160
|
+
flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
|
|
161
|
+
flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
|
|
162
|
+
flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
|
|
163
|
+
flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
|
|
164
|
+
flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
|
|
165
|
+
'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
|
|
166
|
+
return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1.5 py-0.5 rounded-full border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
|
|
146
167
|
})))));
|
|
147
168
|
}
|
|
@@ -8,9 +8,11 @@ import React from 'react';
|
|
|
8
8
|
import 'reactflow/dist/style.css';
|
|
9
9
|
import { DialogueEditorProps } from '../types';
|
|
10
10
|
import { FlagSchema } from '../types/flags';
|
|
11
|
+
import { Character } from '../types/characters';
|
|
11
12
|
type ViewMode = 'graph' | 'yarn' | 'play';
|
|
12
13
|
export declare function DialogueEditorV2(props: DialogueEditorProps & {
|
|
13
14
|
flagSchema?: FlagSchema;
|
|
15
|
+
characters?: Record<string, Character>;
|
|
14
16
|
initialViewMode?: ViewMode;
|
|
15
17
|
viewMode?: ViewMode;
|
|
16
18
|
onViewModeChange?: (mode: ViewMode) => void;
|
|
@@ -68,7 +68,7 @@ const edgeTypes = {
|
|
|
68
68
|
choice: ChoiceEdgeV2_1.ChoiceEdgeV2,
|
|
69
69
|
default: NPCEdgeV2_1.NPCEdgeV2, // Use custom component for NPC edges instead of React Flow default
|
|
70
70
|
};
|
|
71
|
-
function DialogueEditorV2Internal({ dialogue, onChange, onExportYarn, onExportJSON, className = '', showTitleEditor = true, flagSchema, initialViewMode = 'graph', viewMode: controlledViewMode, onViewModeChange, layoutStrategy: propLayoutStrategy = 'dagre', // Accept from parent
|
|
71
|
+
function DialogueEditorV2Internal({ dialogue, onChange, onExportYarn, onExportJSON, className = '', showTitleEditor = true, flagSchema, characters = {}, initialViewMode = 'graph', viewMode: controlledViewMode, onViewModeChange, layoutStrategy: propLayoutStrategy = 'dagre', // Accept from parent
|
|
72
72
|
onLayoutStrategyChange, onOpenFlagManager, onOpenGuide, onLoadExampleDialogue, onLoadExampleFlags,
|
|
73
73
|
// Event hooks
|
|
74
74
|
onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, onNodeSelect, onNodeDoubleClick: onNodeDoubleClickHook, }) {
|
|
@@ -84,6 +84,8 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
84
84
|
const [layoutDirection, setLayoutDirection] = (0, react_1.useState)('TB');
|
|
85
85
|
const layoutStrategy = propLayoutStrategy; // Use prop instead of state
|
|
86
86
|
const [autoOrganize, setAutoOrganize] = (0, react_1.useState)(false); // Auto-layout on changes
|
|
87
|
+
// Track if we've made a direct React Flow update to avoid unnecessary conversions
|
|
88
|
+
const directUpdateRef = (0, react_1.useRef)(null);
|
|
87
89
|
const [showPathHighlight, setShowPathHighlight] = (0, react_1.useState)(true); // Toggle path highlighting
|
|
88
90
|
const [showBackEdges, setShowBackEdges] = (0, react_1.useState)(true); // Toggle back-edge styling
|
|
89
91
|
const [showLayoutMenu, setShowLayoutMenu] = (0, react_1.useState)(false);
|
|
@@ -174,13 +176,20 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
174
176
|
return { edgesToSelectedNode: edgesOnPath, nodeDepths: nodeDepthMap };
|
|
175
177
|
}, [selectedNodeId, dialogue]);
|
|
176
178
|
// Update nodes/edges when dialogue changes externally
|
|
179
|
+
// Skip conversion if we just made a direct React Flow update (for simple text changes)
|
|
177
180
|
react_1.default.useEffect(() => {
|
|
178
181
|
if (dialogue) {
|
|
182
|
+
// If we just updated a node directly in React Flow, skip full conversion
|
|
183
|
+
// The direct update already handled the visual change
|
|
184
|
+
if (directUpdateRef.current) {
|
|
185
|
+
directUpdateRef.current = null; // Clear the flag
|
|
186
|
+
return; // Skip conversion - React Flow is already updated
|
|
187
|
+
}
|
|
179
188
|
const { nodes: newNodes, edges: newEdges } = (0, reactflow_converter_1.convertDialogueTreeToReactFlow)(dialogue, layoutDirection);
|
|
180
189
|
setNodes(newNodes);
|
|
181
190
|
setEdges(newEdges);
|
|
182
191
|
}
|
|
183
|
-
}, [dialogue]);
|
|
192
|
+
}, [dialogue, layoutDirection]);
|
|
184
193
|
// Calculate end nodes (nodes with no outgoing connections)
|
|
185
194
|
const endNodeIds = (0, react_1.useMemo)(() => {
|
|
186
195
|
if (!dialogue)
|
|
@@ -196,7 +205,7 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
196
205
|
});
|
|
197
206
|
return ends;
|
|
198
207
|
}, [dialogue]);
|
|
199
|
-
// Add flagSchema, dim state, and layout direction to node data
|
|
208
|
+
// Add flagSchema, characters, dim state, and layout direction to node data
|
|
200
209
|
const nodesWithFlags = (0, react_1.useMemo)(() => {
|
|
201
210
|
const hasSelection = selectedNodeId !== null && showPathHighlight;
|
|
202
211
|
const startNodeId = dialogue?.startNodeId;
|
|
@@ -212,6 +221,7 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
212
221
|
data: {
|
|
213
222
|
...node.data,
|
|
214
223
|
flagSchema,
|
|
224
|
+
characters, // Pass characters to all nodes including conditional
|
|
215
225
|
isDimmed,
|
|
216
226
|
isInPath,
|
|
217
227
|
layoutDirection,
|
|
@@ -220,7 +230,7 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
220
230
|
},
|
|
221
231
|
};
|
|
222
232
|
});
|
|
223
|
-
}, [nodes, flagSchema, nodeDepths, selectedNodeId, layoutDirection, showPathHighlight, dialogue, endNodeIds]);
|
|
233
|
+
}, [nodes, flagSchema, characters, nodeDepths, selectedNodeId, layoutDirection, showPathHighlight, dialogue, endNodeIds]);
|
|
224
234
|
if (!dialogue) {
|
|
225
235
|
return (react_1.default.createElement("div", { className: `dialogue-editor-v2-empty ${className}` },
|
|
226
236
|
react_1.default.createElement("p", null, "No dialogue loaded. Please provide a dialogue tree.")));
|
|
@@ -782,6 +792,33 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
782
792
|
// Handle node updates
|
|
783
793
|
const handleUpdateNode = (0, react_1.useCallback)((nodeId, updates) => {
|
|
784
794
|
const updatedNode = { ...dialogue.nodes[nodeId], ...updates };
|
|
795
|
+
// Check if this is a "simple" update (just text/content changes, not structural)
|
|
796
|
+
// Simple updates: speaker, content, characterId (non-structural properties)
|
|
797
|
+
// Structural updates: choices, conditionalBlocks, nextNodeId (affect edges/connections)
|
|
798
|
+
const isSimpleUpdate = Object.keys(updates).every(key => ['speaker', 'content', 'characterId', 'setFlags'].includes(key));
|
|
799
|
+
if (isSimpleUpdate && reactFlowInstance) {
|
|
800
|
+
// For simple updates, update React Flow directly without full tree conversion
|
|
801
|
+
// This is much faster and avoids expensive recalculations
|
|
802
|
+
const allNodes = reactFlowInstance.getNodes();
|
|
803
|
+
const nodeToUpdate = allNodes.find(n => n.id === nodeId);
|
|
804
|
+
if (nodeToUpdate) {
|
|
805
|
+
// Mark that we're doing a direct update to skip full conversion
|
|
806
|
+
directUpdateRef.current = nodeId;
|
|
807
|
+
// Update the node data directly in React Flow
|
|
808
|
+
const updatedReactFlowNode = {
|
|
809
|
+
...nodeToUpdate,
|
|
810
|
+
data: {
|
|
811
|
+
...nodeToUpdate.data,
|
|
812
|
+
node: updatedNode, // Update the dialogue node in the data
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
// Update just this node in React Flow
|
|
816
|
+
const updatedNodes = allNodes.map(n => n.id === nodeId ? updatedReactFlowNode : n);
|
|
817
|
+
reactFlowInstance.setNodes(updatedNodes);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Always update the dialogue tree (source of truth) - but this triggers full conversion
|
|
821
|
+
// The useEffect will handle the full conversion, but React Flow is already updated above
|
|
785
822
|
onChange({
|
|
786
823
|
...dialogue,
|
|
787
824
|
nodes: {
|
|
@@ -791,7 +828,7 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
791
828
|
});
|
|
792
829
|
// Call onNodeUpdate hook
|
|
793
830
|
onNodeUpdate?.(nodeId, updates);
|
|
794
|
-
}, [dialogue, onChange, onNodeUpdate]);
|
|
831
|
+
}, [dialogue, onChange, onNodeUpdate, reactFlowInstance]);
|
|
795
832
|
// Handle choice updates
|
|
796
833
|
const handleAddChoice = (0, react_1.useCallback)((nodeId) => {
|
|
797
834
|
const updated = (0, node_helpers_1.addChoiceToNode)(dialogue.nodes[nodeId]);
|
|
@@ -1111,7 +1148,7 @@ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, o
|
|
|
1111
1148
|
" Delete"))));
|
|
1112
1149
|
})(),
|
|
1113
1150
|
react_1.default.createElement("button", { onClick: () => setNodeContextMenu(null), className: "w-full px-4 py-1.5 text-xs text-df-text-secondary hover:text-df-text-primary border-t border-df-control-border mt-1" }, "Cancel")))))),
|
|
1114
|
-
selectedNode && (react_1.default.createElement(NodeEditor_1.NodeEditor, { node: selectedNode, dialogue: dialogue, onUpdate: (updates) => handleUpdateNode(selectedNode.id, updates), onFocusNode: (nodeId) => {
|
|
1151
|
+
selectedNode && (react_1.default.createElement(NodeEditor_1.NodeEditor, { node: selectedNode, dialogue: dialogue, characters: characters, onUpdate: (updates) => handleUpdateNode(selectedNode.id, updates), onFocusNode: (nodeId) => {
|
|
1115
1152
|
const targetNode = nodes.find(n => n.id === nodeId);
|
|
1116
1153
|
if (targetNode && reactFlowInstance) {
|
|
1117
1154
|
// Set selectedNodeId first so NodeEditor updates
|