@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.
Files changed (51) hide show
  1. package/demo/app/page.tsx +26 -12
  2. package/demo/public/logo.svg +14 -1
  3. package/dist/components/CharacterSelector.d.ts +15 -0
  4. package/dist/components/CharacterSelector.js +125 -0
  5. package/dist/components/ConditionalNodeV2.d.ts +2 -0
  6. package/dist/components/ConditionalNodeV2.js +50 -29
  7. package/dist/components/DialogueEditorV2.d.ts +2 -0
  8. package/dist/components/DialogueEditorV2.js +43 -6
  9. package/dist/components/GuidePanel.js +99 -16
  10. package/dist/components/NPCNodeV2.d.ts +2 -0
  11. package/dist/components/NPCNodeV2.js +31 -20
  12. package/dist/components/NodeEditor.d.ts +3 -1
  13. package/dist/components/NodeEditor.js +42 -13
  14. package/dist/components/PlayerNodeV2.d.ts +2 -0
  15. package/dist/components/PlayerNodeV2.js +58 -39
  16. package/dist/components/ReactFlowPOC.js +3 -3
  17. package/dist/esm/components/CharacterSelector.d.ts +15 -0
  18. package/dist/esm/components/CharacterSelector.js +89 -0
  19. package/dist/esm/components/ConditionalNodeV2.d.ts +2 -0
  20. package/dist/esm/components/ConditionalNodeV2.js +51 -30
  21. package/dist/esm/components/DialogueEditorV2.d.ts +2 -0
  22. package/dist/esm/components/DialogueEditorV2.js +43 -6
  23. package/dist/esm/components/GuidePanel.js +99 -16
  24. package/dist/esm/components/NPCNodeV2.d.ts +2 -0
  25. package/dist/esm/components/NPCNodeV2.js +32 -21
  26. package/dist/esm/components/NodeEditor.d.ts +3 -1
  27. package/dist/esm/components/NodeEditor.js +42 -13
  28. package/dist/esm/components/PlayerNodeV2.d.ts +2 -0
  29. package/dist/esm/components/PlayerNodeV2.js +59 -40
  30. package/dist/esm/components/ReactFlowPOC.js +3 -3
  31. package/dist/esm/examples/example-characters.d.ts +19 -0
  32. package/dist/esm/examples/example-characters.js +67 -0
  33. package/dist/esm/examples/index.d.ts +1 -0
  34. package/dist/esm/examples/index.js +2 -0
  35. package/dist/esm/index.d.ts +3 -0
  36. package/dist/esm/index.js +3 -0
  37. package/dist/esm/types/characters.d.ts +15 -0
  38. package/dist/esm/types/characters.js +6 -0
  39. package/dist/esm/types/game-state.d.ts +2 -0
  40. package/dist/esm/types/index.d.ts +2 -0
  41. package/dist/examples/example-characters.d.ts +19 -0
  42. package/dist/examples/example-characters.js +73 -0
  43. package/dist/examples/index.d.ts +1 -0
  44. package/dist/examples/index.js +7 -1
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +9 -1
  47. package/dist/types/characters.d.ts +15 -0
  48. package/dist/types/characters.js +7 -0
  49. package/dist/types/game-state.d.ts +2 -0
  50. package/dist/types/index.d.ts +2 -0
  51. 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
- speaker: 'Stranger',
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
- speaker: 'Stranger',
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
- speaker: 'Stranger',
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
- speaker: 'Stranger',
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
- speaker: 'Stranger',
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
- speaker: 'Bartender',
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
- speaker: 'Bartender',
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
- speaker: 'Bartender',
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
- speaker: 'Bartender',
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
- speaker: 'Narrator',
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
- speaker: 'Bartender',
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"
@@ -1 +1,14 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z"/></svg>
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
- 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-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
89
- isStartNode && (react_1.default.createElement("div", { className: "absolute -top-2 -left-2 bg-df-start text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
90
- react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
91
- " START")),
92
- isEndNode && (react_1.default.createElement("div", { className: "absolute -top-2 -right-2 bg-df-end text-df-text-primary text-[8px] font-bold px-1.5 py-0.5 rounded-full flex items-center gap-0.5 shadow-lg z-10" },
93
- react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
94
- " END")),
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: "px-3 py-1.5 border-b border-df-control-border bg-df-conditional-header flex items-center gap-2 rounded-t-lg" },
97
- react_1.default.createElement(lucide_react_1.GitBranch, { size: 12, className: "text-df-conditional-border" }),
98
- react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
99
- react_1.default.createElement("span", { className: "text-[10px] text-df-conditional-border" }, "IF/ELSE")),
100
- react_1.default.createElement("div", { className: "py-1" }, blocks.map((block, idx) => {
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
- return (react_1.default.createElement("div", { key: block.id, ref: el => { blockRefs.current[idx] = el; }, className: "px-3 py-1.5 border-b border-df-control-border last:border-b-0" },
104
- react_1.default.createElement("div", { className: "flex items-center gap-2 mb-1" },
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
- block.speaker && (react_1.default.createElement("div", { className: "text-[9px] text-df-conditional-border font-medium" }, block.speaker)),
125
- react_1.default.createElement("div", { className: "text-[10px] text-df-text-primary line-clamp-1 bg-df-base border border-df-control-border rounded px-2 py-1 mt-1" },
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, 40) + (block.content.length > 40 ? '...' : ''),
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-[#2a2a3e] !border-[#4a4a6a] !w-3 !h-3 !rounded-full hover:!border-blue-400 hover:!bg-blue-400/20" })));
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-3 py-1 border-t border-[#2a2a3e] flex flex-wrap gap-1" }, node.setFlags.map(flagId => {
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-gray-500/20 text-gray-400 border-gray-500/30' :
138
- flagType === 'quest' ? 'bg-blue-500/20 text-blue-400 border-blue-500/30' :
139
- flagType === 'achievement' ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' :
140
- flagType === 'item' ? 'bg-green-500/20 text-green-400 border-green-500/30' :
141
- flagType === 'stat' ? 'bg-purple-500/20 text-purple-400 border-purple-500/30' :
142
- flagType === 'title' ? 'bg-pink-500/20 text-pink-400 border-pink-500/30' :
143
- flagType === 'global' ? 'bg-orange-500/20 text-orange-400 border-orange-500/30' :
144
- 'bg-gray-500/20 text-gray-400 border-gray-500/30';
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