@magicborn/dialogue-forge 0.1.0

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 (241) hide show
  1. package/README.md +233 -0
  2. package/bin/dialogue-forge.js +78 -0
  3. package/demo/app/layout.tsx +36 -0
  4. package/demo/app/page.tsx +440 -0
  5. package/demo/components/ThemeSwitcher.tsx +611 -0
  6. package/demo/next.config.mjs +7 -0
  7. package/demo/package.json +29 -0
  8. package/demo/postcss.config.mjs +7 -0
  9. package/demo/public/logo.svg +1 -0
  10. package/demo/styles/globals.css +19 -0
  11. package/demo/tailwind.config.ts +90 -0
  12. package/demo/tsconfig.json +42 -0
  13. package/dist/components/ChoiceEdgeV2.d.ts +3 -0
  14. package/dist/components/ChoiceEdgeV2.js +103 -0
  15. package/dist/components/CodeBlock.d.ts +8 -0
  16. package/dist/components/CodeBlock.js +24 -0
  17. package/dist/components/ConditionAutocomplete.d.ts +14 -0
  18. package/dist/components/ConditionAutocomplete.js +284 -0
  19. package/dist/components/ConditionalNodeV2.d.ts +16 -0
  20. package/dist/components/ConditionalNodeV2.js +147 -0
  21. package/dist/components/DialogueEditorV2.d.ts +22 -0
  22. package/dist/components/DialogueEditorV2.js +1170 -0
  23. package/dist/components/EdgeIcon.d.ts +8 -0
  24. package/dist/components/EdgeIcon.js +13 -0
  25. package/dist/components/ExampleLoader.d.ts +11 -0
  26. package/dist/components/ExampleLoader.js +52 -0
  27. package/dist/components/ExampleLoaderButton.d.ts +15 -0
  28. package/dist/components/ExampleLoaderButton.js +102 -0
  29. package/dist/components/FlagManager.d.ts +11 -0
  30. package/dist/components/FlagManager.js +282 -0
  31. package/dist/components/FlagSelector.d.ts +11 -0
  32. package/dist/components/FlagSelector.js +235 -0
  33. package/dist/components/GuidePanel.d.ts +7 -0
  34. package/dist/components/GuidePanel.js +1176 -0
  35. package/dist/components/Minimap.d.ts +16 -0
  36. package/dist/components/Minimap.js +93 -0
  37. package/dist/components/NPCEdgeV2.d.ts +3 -0
  38. package/dist/components/NPCEdgeV2.js +104 -0
  39. package/dist/components/NPCNodeV2.d.ts +26 -0
  40. package/dist/components/NPCNodeV2.js +86 -0
  41. package/dist/components/NodeEditor.d.ts +18 -0
  42. package/dist/components/NodeEditor.js +1025 -0
  43. package/dist/components/PlayView.d.ts +12 -0
  44. package/dist/components/PlayView.js +307 -0
  45. package/dist/components/PlayerNodeV2.d.ts +16 -0
  46. package/dist/components/PlayerNodeV2.js +139 -0
  47. package/dist/components/ReactFlowPOC.d.ts +61 -0
  48. package/dist/components/ReactFlowPOC.js +312 -0
  49. package/dist/components/ScenePlayer.d.ts +18 -0
  50. package/dist/components/ScenePlayer.js +196 -0
  51. package/dist/components/YarnView.d.ts +9 -0
  52. package/dist/components/YarnView.js +45 -0
  53. package/dist/components/ZoomControls.d.ts +11 -0
  54. package/dist/components/ZoomControls.js +34 -0
  55. package/dist/esm/components/ChoiceEdgeV2.d.ts +3 -0
  56. package/dist/esm/components/ChoiceEdgeV2.js +67 -0
  57. package/dist/esm/components/CodeBlock.d.ts +8 -0
  58. package/dist/esm/components/CodeBlock.js +18 -0
  59. package/dist/esm/components/ConditionAutocomplete.d.ts +14 -0
  60. package/dist/esm/components/ConditionAutocomplete.js +248 -0
  61. package/dist/esm/components/ConditionalNodeV2.d.ts +16 -0
  62. package/dist/esm/components/ConditionalNodeV2.js +111 -0
  63. package/dist/esm/components/DialogueEditorV2.d.ts +22 -0
  64. package/dist/esm/components/DialogueEditorV2.js +1134 -0
  65. package/dist/esm/components/EdgeIcon.d.ts +8 -0
  66. package/dist/esm/components/EdgeIcon.js +7 -0
  67. package/dist/esm/components/ExampleLoader.d.ts +11 -0
  68. package/dist/esm/components/ExampleLoader.js +46 -0
  69. package/dist/esm/components/ExampleLoaderButton.d.ts +15 -0
  70. package/dist/esm/components/ExampleLoaderButton.js +66 -0
  71. package/dist/esm/components/FlagManager.d.ts +11 -0
  72. package/dist/esm/components/FlagManager.js +246 -0
  73. package/dist/esm/components/FlagSelector.d.ts +11 -0
  74. package/dist/esm/components/FlagSelector.js +199 -0
  75. package/dist/esm/components/GuidePanel.d.ts +7 -0
  76. package/dist/esm/components/GuidePanel.js +1140 -0
  77. package/dist/esm/components/Minimap.d.ts +16 -0
  78. package/dist/esm/components/Minimap.js +57 -0
  79. package/dist/esm/components/NPCEdgeV2.d.ts +3 -0
  80. package/dist/esm/components/NPCEdgeV2.js +68 -0
  81. package/dist/esm/components/NPCNodeV2.d.ts +26 -0
  82. package/dist/esm/components/NPCNodeV2.js +80 -0
  83. package/dist/esm/components/NodeEditor.d.ts +18 -0
  84. package/dist/esm/components/NodeEditor.js +989 -0
  85. package/dist/esm/components/PlayView.d.ts +12 -0
  86. package/dist/esm/components/PlayView.js +271 -0
  87. package/dist/esm/components/PlayerNodeV2.d.ts +16 -0
  88. package/dist/esm/components/PlayerNodeV2.js +103 -0
  89. package/dist/esm/components/ReactFlowPOC.d.ts +61 -0
  90. package/dist/esm/components/ReactFlowPOC.js +275 -0
  91. package/dist/esm/components/ScenePlayer.d.ts +18 -0
  92. package/dist/esm/components/ScenePlayer.js +160 -0
  93. package/dist/esm/components/YarnView.d.ts +9 -0
  94. package/dist/esm/components/YarnView.js +39 -0
  95. package/dist/esm/components/ZoomControls.d.ts +11 -0
  96. package/dist/esm/components/ZoomControls.js +28 -0
  97. package/dist/esm/examples/example-loader.d.ts +29 -0
  98. package/dist/esm/examples/example-loader.js +103 -0
  99. package/dist/esm/examples/examples-registry.d.ts +38 -0
  100. package/dist/esm/examples/examples-registry.js +153 -0
  101. package/dist/esm/examples/index.d.ts +26 -0
  102. package/dist/esm/examples/index.js +50 -0
  103. package/dist/esm/examples/legacy-examples.d.ts +9 -0
  104. package/dist/esm/examples/legacy-examples.js +814 -0
  105. package/dist/esm/examples/yarn-examples.d.ts +35 -0
  106. package/dist/esm/examples/yarn-examples.js +181 -0
  107. package/dist/esm/index.d.ts +21 -0
  108. package/dist/esm/index.js +26 -0
  109. package/dist/esm/lib/flag-manager.d.ts +21 -0
  110. package/dist/esm/lib/flag-manager.js +93 -0
  111. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  112. package/dist/esm/lib/yarn-converter/__tests__/round-trip.test.js +169 -0
  113. package/dist/esm/lib/yarn-converter.d.ts +17 -0
  114. package/dist/esm/lib/yarn-converter.js +521 -0
  115. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  116. package/dist/esm/lib/yarn-runner/__tests__/condition-evaluator.test.js +171 -0
  117. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  118. package/dist/esm/lib/yarn-runner/__tests__/node-processor.test.js +237 -0
  119. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  120. package/dist/esm/lib/yarn-runner/__tests__/variable-manager.test.js +106 -0
  121. package/dist/esm/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  122. package/dist/esm/lib/yarn-runner/condition-evaluator.js +56 -0
  123. package/dist/esm/lib/yarn-runner/index.d.ts +12 -0
  124. package/dist/esm/lib/yarn-runner/index.js +11 -0
  125. package/dist/esm/lib/yarn-runner/node-processor.d.ts +18 -0
  126. package/dist/esm/lib/yarn-runner/node-processor.js +129 -0
  127. package/dist/esm/lib/yarn-runner/variable-manager.d.ts +51 -0
  128. package/dist/esm/lib/yarn-runner/variable-manager.js +120 -0
  129. package/dist/esm/lib/yarn-runner/variable-operations.d.ts +16 -0
  130. package/dist/esm/lib/yarn-runner/variable-operations.js +88 -0
  131. package/dist/esm/types/conditionals.d.ts +29 -0
  132. package/dist/esm/types/conditionals.js +1 -0
  133. package/dist/esm/types/constants.d.ts +59 -0
  134. package/dist/esm/types/constants.js +55 -0
  135. package/dist/esm/types/flags.d.ts +49 -0
  136. package/dist/esm/types/flags.js +49 -0
  137. package/dist/esm/types/game-state.d.ts +62 -0
  138. package/dist/esm/types/game-state.js +6 -0
  139. package/dist/esm/types/index.d.ts +77 -0
  140. package/dist/esm/types/index.js +1 -0
  141. package/dist/esm/utils/constants.d.ts +5 -0
  142. package/dist/esm/utils/constants.js +5 -0
  143. package/dist/esm/utils/feature-flags.d.ts +11 -0
  144. package/dist/esm/utils/feature-flags.js +11 -0
  145. package/dist/esm/utils/game-state-flattener.d.ts +41 -0
  146. package/dist/esm/utils/game-state-flattener.js +135 -0
  147. package/dist/esm/utils/layout/collision.d.ts +27 -0
  148. package/dist/esm/utils/layout/collision.js +74 -0
  149. package/dist/esm/utils/layout/index.d.ts +82 -0
  150. package/dist/esm/utils/layout/index.js +98 -0
  151. package/dist/esm/utils/layout/registry.d.ts +91 -0
  152. package/dist/esm/utils/layout/registry.js +148 -0
  153. package/dist/esm/utils/layout/strategies/dagre.d.ts +19 -0
  154. package/dist/esm/utils/layout/strategies/dagre.js +182 -0
  155. package/dist/esm/utils/layout/strategies/force.d.ts +21 -0
  156. package/dist/esm/utils/layout/strategies/force.js +178 -0
  157. package/dist/esm/utils/layout/strategies/grid.d.ts +17 -0
  158. package/dist/esm/utils/layout/strategies/grid.js +91 -0
  159. package/dist/esm/utils/layout/strategies/index.d.ts +8 -0
  160. package/dist/esm/utils/layout/strategies/index.js +8 -0
  161. package/dist/esm/utils/layout/types.d.ts +100 -0
  162. package/dist/esm/utils/layout/types.js +7 -0
  163. package/dist/esm/utils/layout.d.ts +9 -0
  164. package/dist/esm/utils/layout.js +17 -0
  165. package/dist/esm/utils/node-helpers.d.ts +7 -0
  166. package/dist/esm/utils/node-helpers.js +94 -0
  167. package/dist/esm/utils/reactflow-converter.d.ts +42 -0
  168. package/dist/esm/utils/reactflow-converter.js +217 -0
  169. package/dist/examples/example-loader.d.ts +29 -0
  170. package/dist/examples/example-loader.js +109 -0
  171. package/dist/examples/examples-registry.d.ts +38 -0
  172. package/dist/examples/examples-registry.js +160 -0
  173. package/dist/examples/index.d.ts +26 -0
  174. package/dist/examples/index.js +63 -0
  175. package/dist/examples/legacy-examples.d.ts +9 -0
  176. package/dist/examples/legacy-examples.js +817 -0
  177. package/dist/examples/yarn-examples.d.ts +35 -0
  178. package/dist/examples/yarn-examples.js +189 -0
  179. package/dist/index.d.ts +21 -0
  180. package/dist/index.js +66 -0
  181. package/dist/lib/flag-manager.d.ts +21 -0
  182. package/dist/lib/flag-manager.js +99 -0
  183. package/dist/lib/yarn-converter/__tests__/round-trip.test.d.ts +1 -0
  184. package/dist/lib/yarn-converter/__tests__/round-trip.test.js +171 -0
  185. package/dist/lib/yarn-converter.d.ts +17 -0
  186. package/dist/lib/yarn-converter.js +525 -0
  187. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.d.ts +1 -0
  188. package/dist/lib/yarn-runner/__tests__/condition-evaluator.test.js +173 -0
  189. package/dist/lib/yarn-runner/__tests__/node-processor.test.d.ts +1 -0
  190. package/dist/lib/yarn-runner/__tests__/node-processor.test.js +239 -0
  191. package/dist/lib/yarn-runner/__tests__/variable-manager.test.d.ts +1 -0
  192. package/dist/lib/yarn-runner/__tests__/variable-manager.test.js +108 -0
  193. package/dist/lib/yarn-runner/condition-evaluator.d.ts +12 -0
  194. package/dist/lib/yarn-runner/condition-evaluator.js +60 -0
  195. package/dist/lib/yarn-runner/index.d.ts +12 -0
  196. package/dist/lib/yarn-runner/index.js +21 -0
  197. package/dist/lib/yarn-runner/node-processor.d.ts +18 -0
  198. package/dist/lib/yarn-runner/node-processor.js +133 -0
  199. package/dist/lib/yarn-runner/variable-manager.d.ts +51 -0
  200. package/dist/lib/yarn-runner/variable-manager.js +124 -0
  201. package/dist/lib/yarn-runner/variable-operations.d.ts +16 -0
  202. package/dist/lib/yarn-runner/variable-operations.js +92 -0
  203. package/dist/types/conditionals.d.ts +29 -0
  204. package/dist/types/conditionals.js +2 -0
  205. package/dist/types/constants.d.ts +59 -0
  206. package/dist/types/constants.js +58 -0
  207. package/dist/types/flags.d.ts +49 -0
  208. package/dist/types/flags.js +52 -0
  209. package/dist/types/game-state.d.ts +62 -0
  210. package/dist/types/game-state.js +7 -0
  211. package/dist/types/index.d.ts +77 -0
  212. package/dist/types/index.js +2 -0
  213. package/dist/utils/constants.d.ts +5 -0
  214. package/dist/utils/constants.js +8 -0
  215. package/dist/utils/feature-flags.d.ts +11 -0
  216. package/dist/utils/feature-flags.js +14 -0
  217. package/dist/utils/game-state-flattener.d.ts +41 -0
  218. package/dist/utils/game-state-flattener.js +140 -0
  219. package/dist/utils/layout/collision.d.ts +27 -0
  220. package/dist/utils/layout/collision.js +77 -0
  221. package/dist/utils/layout/index.d.ts +82 -0
  222. package/dist/utils/layout/index.js +109 -0
  223. package/dist/utils/layout/registry.d.ts +91 -0
  224. package/dist/utils/layout/registry.js +151 -0
  225. package/dist/utils/layout/strategies/dagre.d.ts +19 -0
  226. package/dist/utils/layout/strategies/dagre.js +189 -0
  227. package/dist/utils/layout/strategies/force.d.ts +21 -0
  228. package/dist/utils/layout/strategies/force.js +182 -0
  229. package/dist/utils/layout/strategies/grid.d.ts +17 -0
  230. package/dist/utils/layout/strategies/grid.js +95 -0
  231. package/dist/utils/layout/strategies/index.d.ts +8 -0
  232. package/dist/utils/layout/strategies/index.js +14 -0
  233. package/dist/utils/layout/types.d.ts +100 -0
  234. package/dist/utils/layout/types.js +8 -0
  235. package/dist/utils/layout.d.ts +9 -0
  236. package/dist/utils/layout.js +25 -0
  237. package/dist/utils/node-helpers.d.ts +7 -0
  238. package/dist/utils/node-helpers.js +101 -0
  239. package/dist/utils/reactflow-converter.d.ts +42 -0
  240. package/dist/utils/reactflow-converter.js +223 -0
  241. package/package.json +70 -0
@@ -0,0 +1,1170 @@
1
+ "use strict";
2
+ /**
3
+ * Dialogue Editor V2 - React Flow Implementation
4
+ *
5
+ * This is the new version using React Flow for graph rendering.
6
+ * See V2_MIGRATION_PLAN.md for implementation details.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.DialogueEditorV2 = DialogueEditorV2;
43
+ const react_1 = __importStar(require("react"));
44
+ const reactflow_1 = __importStar(require("reactflow"));
45
+ const lucide_react_1 = require("lucide-react");
46
+ const ExampleLoaderButton_1 = require("./ExampleLoaderButton");
47
+ const feature_flags_1 = require("../utils/feature-flags");
48
+ require("reactflow/dist/style.css");
49
+ const yarn_converter_1 = require("../lib/yarn-converter");
50
+ const reactflow_converter_1 = require("../utils/reactflow-converter");
51
+ const node_helpers_1 = require("../utils/node-helpers");
52
+ const layout_1 = require("../utils/layout");
53
+ const NodeEditor_1 = require("./NodeEditor");
54
+ const YarnView_1 = require("./YarnView");
55
+ const PlayView_1 = require("./PlayView");
56
+ const NPCNodeV2_1 = require("./NPCNodeV2");
57
+ const PlayerNodeV2_1 = require("./PlayerNodeV2");
58
+ const ConditionalNodeV2_1 = require("./ConditionalNodeV2");
59
+ const ChoiceEdgeV2_1 = require("./ChoiceEdgeV2");
60
+ const NPCEdgeV2_1 = require("./NPCEdgeV2");
61
+ // Define node and edge types outside component for stability
62
+ const nodeTypes = {
63
+ npc: NPCNodeV2_1.NPCNodeV2,
64
+ player: PlayerNodeV2_1.PlayerNodeV2,
65
+ conditional: ConditionalNodeV2_1.ConditionalNodeV2,
66
+ };
67
+ const edgeTypes = {
68
+ choice: ChoiceEdgeV2_1.ChoiceEdgeV2,
69
+ default: NPCEdgeV2_1.NPCEdgeV2, // Use custom component for NPC edges instead of React Flow default
70
+ };
71
+ function DialogueEditorV2Internal({ dialogue, onChange, onExportYarn, onExportJSON, className = '', showTitleEditor = true, flagSchema, initialViewMode = 'graph', viewMode: controlledViewMode, onViewModeChange, layoutStrategy: propLayoutStrategy = 'dagre', // Accept from parent
72
+ onLayoutStrategyChange, onOpenFlagManager, onOpenGuide, onLoadExampleDialogue, onLoadExampleFlags,
73
+ // Event hooks
74
+ onNodeAdd, onNodeDelete, onNodeUpdate, onConnect: onConnectHook, onDisconnect, onNodeSelect, onNodeDoubleClick: onNodeDoubleClickHook, }) {
75
+ // Use controlled viewMode if provided, otherwise use internal state
76
+ const [internalViewMode, setInternalViewMode] = (0, react_1.useState)(initialViewMode);
77
+ const viewMode = controlledViewMode ?? internalViewMode;
78
+ const setViewMode = (mode) => {
79
+ if (controlledViewMode === undefined) {
80
+ setInternalViewMode(mode);
81
+ }
82
+ onViewModeChange?.(mode);
83
+ };
84
+ const [layoutDirection, setLayoutDirection] = (0, react_1.useState)('TB');
85
+ const layoutStrategy = propLayoutStrategy; // Use prop instead of state
86
+ const [autoOrganize, setAutoOrganize] = (0, react_1.useState)(false); // Auto-layout on changes
87
+ const [showPathHighlight, setShowPathHighlight] = (0, react_1.useState)(true); // Toggle path highlighting
88
+ const [showBackEdges, setShowBackEdges] = (0, react_1.useState)(true); // Toggle back-edge styling
89
+ const [showLayoutMenu, setShowLayoutMenu] = (0, react_1.useState)(false);
90
+ const lastWheelClickRef = (0, react_1.useRef)(0);
91
+ // Memoize nodeTypes and edgeTypes to prevent React Flow warnings
92
+ const memoizedNodeTypes = (0, react_1.useMemo)(() => nodeTypes, []);
93
+ const memoizedEdgeTypes = (0, react_1.useMemo)(() => edgeTypes, []);
94
+ const [selectedNodeId, setSelectedNodeId] = (0, react_1.useState)(null);
95
+ const [contextMenu, setContextMenu] = (0, react_1.useState)(null);
96
+ const [nodeContextMenu, setNodeContextMenu] = (0, react_1.useState)(null);
97
+ const [edgeContextMenu, setEdgeContextMenu] = (0, react_1.useState)(null);
98
+ const [edgeDropMenu, setEdgeDropMenu] = (0, react_1.useState)(null);
99
+ const reactFlowInstance = (0, reactflow_1.useReactFlow)();
100
+ const connectingRef = (0, react_1.useRef)(null);
101
+ // Convert DialogueTree to React Flow format
102
+ const { nodes: initialNodes, edges: initialEdges } = (0, react_1.useMemo)(() => dialogue ? (0, reactflow_converter_1.convertDialogueTreeToReactFlow)(dialogue, layoutDirection) : { nodes: [], edges: [] }, [dialogue, layoutDirection]);
103
+ const [nodes, setNodes] = (0, react_1.useState)(initialNodes);
104
+ const [edges, setEdges] = (0, react_1.useState)(initialEdges);
105
+ // Find all edges that lead to the selected node by tracing FORWARD from start
106
+ // This avoids including back-edges and only shows the actual forward path
107
+ const { edgesToSelectedNode, nodeDepths } = (0, react_1.useMemo)(() => {
108
+ if (!selectedNodeId || !dialogue || !dialogue.startNodeId) {
109
+ return { edgesToSelectedNode: new Set(), nodeDepths: new Map() };
110
+ }
111
+ // Step 1: Find all forward paths from start that reach the selected node
112
+ // Use DFS to trace forward, tracking the path
113
+ const nodesOnPath = new Set();
114
+ const edgesOnPath = new Set();
115
+ const nodeDepthMap = new Map();
116
+ // DFS that returns true if this path leads to the selected node
117
+ const findPathToTarget = (currentNodeId, visitedInPath, depth) => {
118
+ // Found the target!
119
+ if (currentNodeId === selectedNodeId) {
120
+ nodesOnPath.add(currentNodeId);
121
+ nodeDepthMap.set(currentNodeId, depth);
122
+ return true;
123
+ }
124
+ // Avoid cycles in THIS path (back-edges)
125
+ if (visitedInPath.has(currentNodeId)) {
126
+ return false;
127
+ }
128
+ const node = dialogue.nodes[currentNodeId];
129
+ if (!node)
130
+ return false;
131
+ visitedInPath.add(currentNodeId);
132
+ let foundPath = false;
133
+ // Check NPC nextNodeId
134
+ if (node.nextNodeId && dialogue.nodes[node.nextNodeId]) {
135
+ if (findPathToTarget(node.nextNodeId, new Set(visitedInPath), depth + 1)) {
136
+ foundPath = true;
137
+ edgesOnPath.add(`${currentNodeId}-next`);
138
+ }
139
+ }
140
+ // Check player choices
141
+ if (node.choices) {
142
+ node.choices.forEach((choice, idx) => {
143
+ if (choice.nextNodeId && dialogue.nodes[choice.nextNodeId]) {
144
+ if (findPathToTarget(choice.nextNodeId, new Set(visitedInPath), depth + 1)) {
145
+ foundPath = true;
146
+ edgesOnPath.add(`${currentNodeId}-choice-${idx}`);
147
+ }
148
+ }
149
+ });
150
+ }
151
+ // Check conditional blocks
152
+ if (node.conditionalBlocks) {
153
+ node.conditionalBlocks.forEach((block, idx) => {
154
+ if (block.nextNodeId && dialogue.nodes[block.nextNodeId]) {
155
+ if (findPathToTarget(block.nextNodeId, new Set(visitedInPath), depth + 1)) {
156
+ foundPath = true;
157
+ edgesOnPath.add(`${currentNodeId}-block-${idx}`);
158
+ }
159
+ }
160
+ });
161
+ }
162
+ // If any path from this node leads to target, include this node
163
+ if (foundPath) {
164
+ nodesOnPath.add(currentNodeId);
165
+ // Keep the minimum depth (closest to start)
166
+ if (!nodeDepthMap.has(currentNodeId) || nodeDepthMap.get(currentNodeId) > depth) {
167
+ nodeDepthMap.set(currentNodeId, depth);
168
+ }
169
+ }
170
+ return foundPath;
171
+ };
172
+ // Start the search from the dialogue's start node
173
+ findPathToTarget(dialogue.startNodeId, new Set(), 0);
174
+ return { edgesToSelectedNode: edgesOnPath, nodeDepths: nodeDepthMap };
175
+ }, [selectedNodeId, dialogue]);
176
+ // Update nodes/edges when dialogue changes externally
177
+ react_1.default.useEffect(() => {
178
+ if (dialogue) {
179
+ const { nodes: newNodes, edges: newEdges } = (0, reactflow_converter_1.convertDialogueTreeToReactFlow)(dialogue, layoutDirection);
180
+ setNodes(newNodes);
181
+ setEdges(newEdges);
182
+ }
183
+ }, [dialogue]);
184
+ // Calculate end nodes (nodes with no outgoing connections)
185
+ const endNodeIds = (0, react_1.useMemo)(() => {
186
+ if (!dialogue)
187
+ return new Set();
188
+ const ends = new Set();
189
+ Object.values(dialogue.nodes).forEach(node => {
190
+ const hasNextNode = !!node.nextNodeId;
191
+ const hasChoiceConnections = node.choices?.some(c => c.nextNodeId) || false;
192
+ const hasBlockConnections = node.conditionalBlocks?.some(b => b.nextNodeId) || false;
193
+ if (!hasNextNode && !hasChoiceConnections && !hasBlockConnections) {
194
+ ends.add(node.id);
195
+ }
196
+ });
197
+ return ends;
198
+ }, [dialogue]);
199
+ // Add flagSchema, dim state, and layout direction to node data
200
+ const nodesWithFlags = (0, react_1.useMemo)(() => {
201
+ const hasSelection = selectedNodeId !== null && showPathHighlight;
202
+ const startNodeId = dialogue?.startNodeId;
203
+ return nodes.map(node => {
204
+ const isInPath = showPathHighlight && nodeDepths.has(node.id);
205
+ const isSelected = node.id === selectedNodeId;
206
+ // Dim nodes that aren't in the path when something is selected (only if path highlight is on)
207
+ const isDimmed = hasSelection && !isInPath && !isSelected;
208
+ const isStartNode = node.id === startNodeId;
209
+ const isEndNode = endNodeIds.has(node.id);
210
+ return {
211
+ ...node,
212
+ data: {
213
+ ...node.data,
214
+ flagSchema,
215
+ isDimmed,
216
+ isInPath,
217
+ layoutDirection,
218
+ isStartNode,
219
+ isEndNode,
220
+ },
221
+ };
222
+ });
223
+ }, [nodes, flagSchema, nodeDepths, selectedNodeId, layoutDirection, showPathHighlight, dialogue, endNodeIds]);
224
+ if (!dialogue) {
225
+ return (react_1.default.createElement("div", { className: `dialogue-editor-v2-empty ${className}` },
226
+ react_1.default.createElement("p", null, "No dialogue loaded. Please provide a dialogue tree.")));
227
+ }
228
+ // Get selected node - use useMemo to ensure it updates when dialogue changes
229
+ const selectedNode = (0, react_1.useMemo)(() => {
230
+ if (!selectedNodeId || !dialogue)
231
+ return null;
232
+ const node = dialogue.nodes[selectedNodeId];
233
+ if (!node)
234
+ return null;
235
+ // Return a fresh copy to ensure React detects changes
236
+ return {
237
+ ...node,
238
+ choices: node.choices ? node.choices.map(c => ({ ...c })) : undefined,
239
+ setFlags: node.setFlags ? [...node.setFlags] : undefined,
240
+ conditionalBlocks: node.conditionalBlocks ? node.conditionalBlocks.map(b => ({
241
+ ...b,
242
+ condition: b.condition ? [...b.condition] : undefined,
243
+ })) : undefined,
244
+ };
245
+ }, [selectedNodeId, dialogue]);
246
+ // Handle node deletion (multi-delete support)
247
+ const onNodesDelete = (0, react_1.useCallback)((deleted) => {
248
+ let updatedNodes = { ...dialogue.nodes };
249
+ let shouldClearSelection = false;
250
+ deleted.forEach(node => {
251
+ const dialogueNode = dialogue.nodes[node.id];
252
+ delete updatedNodes[node.id];
253
+ if (selectedNodeId === node.id) {
254
+ shouldClearSelection = true;
255
+ }
256
+ // Call onNodeDelete hook
257
+ onNodeDelete?.(node.id);
258
+ });
259
+ let newDialogue = { ...dialogue, nodes: updatedNodes };
260
+ // Auto-organize if enabled
261
+ if (autoOrganize) {
262
+ const result = (0, layout_1.applyLayout)(newDialogue, layoutStrategy, { direction: layoutDirection });
263
+ newDialogue = result.dialogue;
264
+ setTimeout(() => {
265
+ if (reactFlowInstance) {
266
+ reactFlowInstance.fitView({ padding: 0.2, duration: 300 });
267
+ }
268
+ }, 50);
269
+ }
270
+ onChange(newDialogue);
271
+ if (shouldClearSelection) {
272
+ setSelectedNodeId(null);
273
+ }
274
+ }, [dialogue, onChange, selectedNodeId, autoOrganize, layoutDirection, reactFlowInstance]);
275
+ // Handle node changes (drag, delete, etc.)
276
+ const onNodesChange = (0, react_1.useCallback)((changes) => {
277
+ setNodes((nds) => (0, reactflow_1.applyNodeChanges)(changes, nds));
278
+ // Handle deletions (backup in case onNodesDelete doesn't fire)
279
+ const deletions = changes.filter(c => c.type === 'remove');
280
+ if (deletions.length > 0) {
281
+ let updatedNodes = { ...dialogue.nodes };
282
+ let shouldClearSelection = false;
283
+ deletions.forEach(change => {
284
+ if (change.type === 'remove') {
285
+ delete updatedNodes[change.id];
286
+ if (selectedNodeId === change.id) {
287
+ shouldClearSelection = true;
288
+ }
289
+ }
290
+ });
291
+ onChange({ ...dialogue, nodes: updatedNodes });
292
+ if (shouldClearSelection) {
293
+ setSelectedNodeId(null);
294
+ }
295
+ }
296
+ // Sync position changes back to DialogueTree
297
+ changes.forEach(change => {
298
+ if (change.type === 'position' && change.position) {
299
+ const node = dialogue.nodes[change.id];
300
+ if (node && (node.x !== change.position.x || node.y !== change.position.y)) {
301
+ // Create a new node object to avoid mutating the original
302
+ const updatedNode = {
303
+ ...dialogue.nodes[change.id],
304
+ x: change.position.x,
305
+ y: change.position.y,
306
+ };
307
+ onChange({
308
+ ...dialogue,
309
+ nodes: {
310
+ ...dialogue.nodes,
311
+ [change.id]: updatedNode,
312
+ },
313
+ });
314
+ }
315
+ }
316
+ });
317
+ }, [dialogue, onChange, selectedNodeId]);
318
+ // Handle edge changes (delete, etc.)
319
+ const onEdgesChange = (0, react_1.useCallback)((changes) => {
320
+ setEdges((eds) => (0, reactflow_1.applyEdgeChanges)(changes, eds));
321
+ // Sync edge deletions back to DialogueTree
322
+ changes.forEach(change => {
323
+ if (change.type === 'remove') {
324
+ // Find the edge before it's removed
325
+ const currentEdges = edges;
326
+ const edge = currentEdges.find(e => e.id === change.id);
327
+ if (edge) {
328
+ const sourceNode = dialogue.nodes[edge.source];
329
+ if (sourceNode) {
330
+ if (edge.sourceHandle === 'next' && sourceNode.type === 'npc') {
331
+ // Remove NPC next connection
332
+ onChange({
333
+ ...dialogue,
334
+ nodes: {
335
+ ...dialogue.nodes,
336
+ [edge.source]: {
337
+ ...sourceNode,
338
+ nextNodeId: undefined,
339
+ },
340
+ },
341
+ });
342
+ }
343
+ else if (edge.sourceHandle?.startsWith('choice-')) {
344
+ // Remove Player choice connection
345
+ const choiceIdx = parseInt(edge.sourceHandle.replace('choice-', ''));
346
+ if (sourceNode.choices && sourceNode.choices[choiceIdx]) {
347
+ const updated = (0, node_helpers_1.updateChoiceInNode)(sourceNode, choiceIdx, { nextNodeId: '' });
348
+ onChange({
349
+ ...dialogue,
350
+ nodes: {
351
+ ...dialogue.nodes,
352
+ [edge.source]: updated,
353
+ },
354
+ });
355
+ }
356
+ }
357
+ else if (edge.sourceHandle?.startsWith('block-') && sourceNode.type === 'conditional') {
358
+ // Remove Conditional block connection
359
+ const blockIdx = parseInt(edge.sourceHandle.replace('block-', ''));
360
+ if (sourceNode.conditionalBlocks && sourceNode.conditionalBlocks[blockIdx]) {
361
+ const updatedBlocks = [...sourceNode.conditionalBlocks];
362
+ updatedBlocks[blockIdx] = {
363
+ ...updatedBlocks[blockIdx],
364
+ nextNodeId: undefined,
365
+ };
366
+ onChange({
367
+ ...dialogue,
368
+ nodes: {
369
+ ...dialogue.nodes,
370
+ [edge.source]: {
371
+ ...sourceNode,
372
+ conditionalBlocks: updatedBlocks,
373
+ },
374
+ },
375
+ });
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ });
382
+ }, [dialogue, onChange, edges]);
383
+ // Handle edge deletion (when Delete key is pressed on selected edges)
384
+ const onEdgesDelete = (0, react_1.useCallback)((deletedEdges) => {
385
+ deletedEdges.forEach(edge => {
386
+ // Call onDisconnect hook
387
+ onDisconnect?.(edge.id, edge.source, edge.target);
388
+ const sourceNode = dialogue.nodes[edge.source];
389
+ if (sourceNode) {
390
+ if (edge.sourceHandle === 'next' && sourceNode.type === 'npc') {
391
+ // Remove NPC next connection
392
+ onChange({
393
+ ...dialogue,
394
+ nodes: {
395
+ ...dialogue.nodes,
396
+ [edge.source]: {
397
+ ...sourceNode,
398
+ nextNodeId: undefined,
399
+ },
400
+ },
401
+ });
402
+ }
403
+ else if (edge.sourceHandle?.startsWith('choice-')) {
404
+ // Remove Player choice connection
405
+ const choiceIdx = parseInt(edge.sourceHandle.replace('choice-', ''));
406
+ if (sourceNode.choices && sourceNode.choices[choiceIdx]) {
407
+ const updated = (0, node_helpers_1.updateChoiceInNode)(sourceNode, choiceIdx, { nextNodeId: '' });
408
+ onChange({
409
+ ...dialogue,
410
+ nodes: {
411
+ ...dialogue.nodes,
412
+ [edge.source]: updated,
413
+ },
414
+ });
415
+ }
416
+ }
417
+ else if (edge.sourceHandle?.startsWith('block-') && sourceNode.type === 'conditional') {
418
+ // Remove Conditional block connection
419
+ const blockIdx = parseInt(edge.sourceHandle.replace('block-', ''));
420
+ if (sourceNode.conditionalBlocks && sourceNode.conditionalBlocks[blockIdx]) {
421
+ const updatedBlocks = [...sourceNode.conditionalBlocks];
422
+ updatedBlocks[blockIdx] = {
423
+ ...updatedBlocks[blockIdx],
424
+ nextNodeId: undefined,
425
+ };
426
+ onChange({
427
+ ...dialogue,
428
+ nodes: {
429
+ ...dialogue.nodes,
430
+ [edge.source]: {
431
+ ...sourceNode,
432
+ conditionalBlocks: updatedBlocks,
433
+ },
434
+ },
435
+ });
436
+ }
437
+ }
438
+ }
439
+ });
440
+ }, [dialogue, onChange]);
441
+ // Handle connection start (track what we're connecting from)
442
+ const onConnectStart = (0, react_1.useCallback)((_event, { nodeId, handleId }) => {
443
+ if (!nodeId)
444
+ return;
445
+ const sourceNode = dialogue.nodes[nodeId];
446
+ if (!sourceNode)
447
+ return;
448
+ if (handleId === 'next' && sourceNode.type === 'npc') {
449
+ connectingRef.current = { fromNodeId: nodeId, sourceHandle: 'next' };
450
+ }
451
+ else if (handleId?.startsWith('choice-')) {
452
+ const choiceIdx = parseInt(handleId.replace('choice-', ''));
453
+ connectingRef.current = { fromNodeId: nodeId, fromChoiceIdx: choiceIdx, sourceHandle: handleId };
454
+ }
455
+ else if (handleId?.startsWith('block-')) {
456
+ const blockIdx = parseInt(handleId.replace('block-', ''));
457
+ connectingRef.current = { fromNodeId: nodeId, fromBlockIdx: blockIdx, sourceHandle: handleId };
458
+ }
459
+ }, [dialogue]);
460
+ // Handle connection end (check if dropped on empty space)
461
+ const onConnectEnd = (0, react_1.useCallback)((event) => {
462
+ if (!connectingRef.current)
463
+ return;
464
+ const targetIsNode = event.target.closest('.react-flow__node');
465
+ if (!targetIsNode) {
466
+ // Dropped on empty space - show edge drop menu
467
+ const clientX = 'clientX' in event ? event.clientX : (event.touches?.[0]?.clientX || 0);
468
+ const clientY = 'clientY' in event ? event.clientY : (event.touches?.[0]?.clientY || 0);
469
+ const point = reactFlowInstance.screenToFlowPosition({
470
+ x: clientX,
471
+ y: clientY,
472
+ });
473
+ setEdgeDropMenu({
474
+ x: clientX,
475
+ y: clientY,
476
+ graphX: point.x,
477
+ graphY: point.y,
478
+ fromNodeId: connectingRef.current.fromNodeId,
479
+ fromChoiceIdx: connectingRef.current.fromChoiceIdx,
480
+ fromBlockIdx: connectingRef.current.fromBlockIdx,
481
+ sourceHandle: connectingRef.current.sourceHandle,
482
+ });
483
+ }
484
+ connectingRef.current = null;
485
+ }, [reactFlowInstance]);
486
+ // Handle new connections
487
+ const onConnect = (0, react_1.useCallback)((connection) => {
488
+ if (!connection.source || !connection.target)
489
+ return;
490
+ const newEdge = (0, reactflow_1.addEdge)(connection, edges);
491
+ setEdges(newEdge);
492
+ setEdgeDropMenu(null); // Close edge drop menu if open
493
+ // Call onConnect hook
494
+ onConnectHook?.(connection.source, connection.target, connection.sourceHandle || undefined);
495
+ // Update DialogueTree
496
+ const sourceNode = dialogue.nodes[connection.source];
497
+ if (!sourceNode)
498
+ return;
499
+ if (connection.sourceHandle === 'next' && sourceNode.type === 'npc') {
500
+ // NPC next connection
501
+ onChange({
502
+ ...dialogue,
503
+ nodes: {
504
+ ...dialogue.nodes,
505
+ [connection.source]: {
506
+ ...sourceNode,
507
+ nextNodeId: connection.target,
508
+ },
509
+ },
510
+ });
511
+ }
512
+ else if (connection.sourceHandle?.startsWith('choice-')) {
513
+ // Player choice connection
514
+ const choiceIdx = parseInt(connection.sourceHandle.replace('choice-', ''));
515
+ if (sourceNode.choices && sourceNode.choices[choiceIdx]) {
516
+ const updated = (0, node_helpers_1.updateChoiceInNode)(sourceNode, choiceIdx, { nextNodeId: connection.target });
517
+ onChange({
518
+ ...dialogue,
519
+ nodes: {
520
+ ...dialogue.nodes,
521
+ [connection.source]: updated,
522
+ },
523
+ });
524
+ }
525
+ }
526
+ else if (connection.sourceHandle?.startsWith('block-') && sourceNode.type === 'conditional') {
527
+ // Conditional block connection
528
+ const blockIdx = parseInt(connection.sourceHandle.replace('block-', ''));
529
+ if (sourceNode.conditionalBlocks && sourceNode.conditionalBlocks[blockIdx]) {
530
+ const updatedBlocks = [...sourceNode.conditionalBlocks];
531
+ updatedBlocks[blockIdx] = {
532
+ ...updatedBlocks[blockIdx],
533
+ nextNodeId: connection.target,
534
+ };
535
+ onChange({
536
+ ...dialogue,
537
+ nodes: {
538
+ ...dialogue.nodes,
539
+ [connection.source]: {
540
+ ...sourceNode,
541
+ conditionalBlocks: updatedBlocks,
542
+ },
543
+ },
544
+ });
545
+ }
546
+ }
547
+ connectingRef.current = null;
548
+ }, [dialogue, onChange, edges]);
549
+ // Handle node selection
550
+ const onNodeClick = (0, react_1.useCallback)((_event, node) => {
551
+ setSelectedNodeId(node.id);
552
+ setNodeContextMenu(null);
553
+ onNodeSelect?.(node.id);
554
+ }, [onNodeSelect]);
555
+ // Handle node double-click - zoom to node
556
+ const onNodeDoubleClick = (0, react_1.useCallback)((_event, node) => {
557
+ if (reactFlowInstance) {
558
+ reactFlowInstance.setCenter(node.position.x + 110, // Half of NODE_WIDTH
559
+ node.position.y + 60, // Half of NODE_HEIGHT
560
+ { zoom: 1.5, duration: 500 });
561
+ }
562
+ onNodeDoubleClickHook?.(node.id);
563
+ }, [reactFlowInstance, onNodeDoubleClickHook]);
564
+ // Handle pane double-click - fit view to all nodes (like default zoom)
565
+ // We'll handle this via useEffect since React Flow doesn't have onPaneDoubleClick
566
+ const reactFlowWrapperRef = (0, react_1.useRef)(null);
567
+ (0, react_1.useEffect)(() => {
568
+ const handleDoubleClick = (event) => {
569
+ // Check if clicking on the pane (not on a node or edge)
570
+ const target = event.target;
571
+ if (target.closest('.react-flow__node') || target.closest('.react-flow__edge')) {
572
+ return; // Don't handle if clicking on node/edge
573
+ }
574
+ if (reactFlowInstance) {
575
+ reactFlowInstance.fitView({ padding: 0.2, duration: 500 });
576
+ }
577
+ };
578
+ const container = reactFlowWrapperRef.current;
579
+ if (container) {
580
+ container.addEventListener('dblclick', handleDoubleClick);
581
+ return () => {
582
+ container.removeEventListener('dblclick', handleDoubleClick);
583
+ };
584
+ }
585
+ }, [reactFlowInstance]);
586
+ // Track mouse wheel clicks for double-click detection
587
+ (0, react_1.useEffect)(() => {
588
+ const handleMouseDown = (event) => {
589
+ const mouseEvent = event;
590
+ if (mouseEvent.button === 1) { // Middle mouse button (wheel)
591
+ const now = Date.now();
592
+ if (now - lastWheelClickRef.current < 300) {
593
+ // Double-click detected - fit view
594
+ mouseEvent.preventDefault();
595
+ if (reactFlowInstance) {
596
+ reactFlowInstance.fitView({ padding: 0.2, duration: 500 });
597
+ }
598
+ lastWheelClickRef.current = 0;
599
+ }
600
+ else {
601
+ lastWheelClickRef.current = now;
602
+ }
603
+ }
604
+ };
605
+ const container = document.querySelector('.react-flow');
606
+ if (container) {
607
+ container.addEventListener('mousedown', handleMouseDown);
608
+ return () => {
609
+ container.removeEventListener('mousedown', handleMouseDown);
610
+ };
611
+ }
612
+ }, [reactFlowInstance]);
613
+ // Handle pane context menu (right-click on empty space)
614
+ const onPaneContextMenu = (0, react_1.useCallback)((event) => {
615
+ event.preventDefault();
616
+ const point = reactFlowInstance.screenToFlowPosition({
617
+ x: event.clientX,
618
+ y: event.clientY,
619
+ });
620
+ setContextMenu({
621
+ x: event.clientX,
622
+ y: event.clientY,
623
+ graphX: point.x,
624
+ graphY: point.y,
625
+ });
626
+ }, [reactFlowInstance]);
627
+ // Handle node context menu
628
+ const onNodeContextMenu = (0, react_1.useCallback)((event, node) => {
629
+ event.preventDefault();
630
+ setNodeContextMenu({
631
+ x: event.clientX,
632
+ y: event.clientY,
633
+ nodeId: node.id,
634
+ });
635
+ setContextMenu(null);
636
+ }, []);
637
+ // Handle edge context menu (right-click on edge to insert node)
638
+ const onEdgeContextMenu = (0, react_1.useCallback)((event, edge) => {
639
+ event.preventDefault();
640
+ // Calculate midpoint position on the edge
641
+ const sourceNodePosition = nodes.find(n => n.id === edge.source)?.position;
642
+ const targetNodePosition = nodes.find(n => n.id === edge.target)?.position;
643
+ if (!sourceNodePosition || !targetNodePosition)
644
+ return;
645
+ // Calculate midpoint in flow coordinates
646
+ const midX = (sourceNodePosition.x + targetNodePosition.x) / 2;
647
+ const midY = (sourceNodePosition.y + targetNodePosition.y) / 2;
648
+ // Convert to screen coordinates for menu positioning
649
+ const point = reactFlowInstance.flowToScreenPosition({ x: midX, y: midY });
650
+ setEdgeContextMenu({
651
+ x: point.x,
652
+ y: point.y,
653
+ edgeId: edge.id,
654
+ graphX: midX,
655
+ graphY: midY,
656
+ });
657
+ setContextMenu(null);
658
+ setNodeContextMenu(null);
659
+ }, [nodes, reactFlowInstance]);
660
+ // Insert node between two connected nodes
661
+ const handleInsertNode = (0, react_1.useCallback)((type, edgeId, x, y) => {
662
+ // Find the edge
663
+ const edge = edges.find(e => e.id === edgeId);
664
+ if (!edge)
665
+ return;
666
+ // Get the source and target nodes
667
+ const sourceNode = dialogue.nodes[edge.source];
668
+ const targetNode = dialogue.nodes[edge.target];
669
+ if (!sourceNode || !targetNode)
670
+ return;
671
+ // Create new node
672
+ const newId = `${type}_${Date.now()}`;
673
+ const newNode = (0, node_helpers_1.createNode)(type, newId, x, y);
674
+ // Update dialogue tree: break old connection, add new node, connect source->new->target
675
+ const updatedNodes = { ...dialogue.nodes, [newId]: newNode };
676
+ // Break the old connection and reconnect through new node
677
+ if (edge.sourceHandle === 'next' && sourceNode.type === 'npc') {
678
+ // NPC connection
679
+ updatedNodes[edge.source] = {
680
+ ...sourceNode,
681
+ nextNodeId: newId, // Connect source to new node
682
+ };
683
+ updatedNodes[newId] = {
684
+ ...newNode,
685
+ nextNodeId: edge.target, // Connect new node to target
686
+ };
687
+ }
688
+ else if (edge.sourceHandle?.startsWith('choice-')) {
689
+ // Player choice connection
690
+ const choiceIdx = parseInt(edge.sourceHandle.replace('choice-', ''));
691
+ if (sourceNode.choices && sourceNode.choices[choiceIdx]) {
692
+ const updatedChoices = [...sourceNode.choices];
693
+ updatedChoices[choiceIdx] = {
694
+ ...updatedChoices[choiceIdx],
695
+ nextNodeId: newId, // Connect choice to new node
696
+ };
697
+ updatedNodes[edge.source] = {
698
+ ...sourceNode,
699
+ choices: updatedChoices,
700
+ };
701
+ updatedNodes[newId] = {
702
+ ...newNode,
703
+ nextNodeId: edge.target, // Connect new node to target
704
+ };
705
+ }
706
+ }
707
+ else if (edge.sourceHandle?.startsWith('block-')) {
708
+ // Conditional block connection
709
+ const blockIdx = parseInt(edge.sourceHandle.replace('block-', ''));
710
+ if (sourceNode.conditionalBlocks && sourceNode.conditionalBlocks[blockIdx]) {
711
+ const updatedBlocks = [...sourceNode.conditionalBlocks];
712
+ updatedBlocks[blockIdx] = {
713
+ ...updatedBlocks[blockIdx],
714
+ nextNodeId: newId, // Connect block to new node
715
+ };
716
+ updatedNodes[edge.source] = {
717
+ ...sourceNode,
718
+ conditionalBlocks: updatedBlocks,
719
+ };
720
+ updatedNodes[newId] = {
721
+ ...newNode,
722
+ nextNodeId: edge.target, // Connect new node to target
723
+ };
724
+ }
725
+ }
726
+ onChange({
727
+ ...dialogue,
728
+ nodes: updatedNodes,
729
+ });
730
+ setEdgeContextMenu(null);
731
+ }, [dialogue, onChange, edges]);
732
+ // Add node from context menu or edge drop
733
+ const handleAddNode = (0, react_1.useCallback)((type, x, y, autoConnect) => {
734
+ const newId = `${type}_${Date.now()}`;
735
+ const newNode = (0, node_helpers_1.createNode)(type, newId, x, y);
736
+ // Call onNodeAdd hook
737
+ onNodeAdd?.(newNode);
738
+ // Build the complete new dialogue state in one go
739
+ let newDialogue = {
740
+ ...dialogue,
741
+ nodes: { ...dialogue.nodes, [newId]: newNode }
742
+ };
743
+ // If auto-connecting, include that connection
744
+ if (autoConnect) {
745
+ const sourceNode = dialogue.nodes[autoConnect.fromNodeId];
746
+ if (sourceNode) {
747
+ if (autoConnect.sourceHandle === 'next' && sourceNode.type === 'npc') {
748
+ newDialogue.nodes[autoConnect.fromNodeId] = { ...sourceNode, nextNodeId: newId };
749
+ }
750
+ else if (autoConnect.fromChoiceIdx !== undefined && sourceNode.choices) {
751
+ const newChoices = [...sourceNode.choices];
752
+ newChoices[autoConnect.fromChoiceIdx] = { ...newChoices[autoConnect.fromChoiceIdx], nextNodeId: newId };
753
+ newDialogue.nodes[autoConnect.fromNodeId] = { ...sourceNode, choices: newChoices };
754
+ }
755
+ else if (autoConnect.fromBlockIdx !== undefined && sourceNode.type === 'conditional' && sourceNode.conditionalBlocks) {
756
+ const newBlocks = [...sourceNode.conditionalBlocks];
757
+ newBlocks[autoConnect.fromBlockIdx] = { ...newBlocks[autoConnect.fromBlockIdx], nextNodeId: newId };
758
+ newDialogue.nodes[autoConnect.fromNodeId] = { ...sourceNode, conditionalBlocks: newBlocks };
759
+ }
760
+ }
761
+ }
762
+ // Apply layout if auto-organize is enabled
763
+ if (autoOrganize) {
764
+ const result = (0, layout_1.applyLayout)(newDialogue, layoutStrategy, { direction: layoutDirection });
765
+ newDialogue = result.dialogue;
766
+ }
767
+ // Single onChange call with all updates
768
+ onChange(newDialogue);
769
+ setSelectedNodeId(newId);
770
+ setContextMenu(null);
771
+ setEdgeDropMenu(null);
772
+ connectingRef.current = null;
773
+ // Fit view after layout (only if auto-organize is on)
774
+ if (autoOrganize) {
775
+ setTimeout(() => {
776
+ if (reactFlowInstance) {
777
+ reactFlowInstance.fitView({ padding: 0.2, duration: 300 });
778
+ }
779
+ }, 50);
780
+ }
781
+ }, [dialogue, onChange, autoOrganize, layoutDirection, reactFlowInstance]);
782
+ // Handle node updates
783
+ const handleUpdateNode = (0, react_1.useCallback)((nodeId, updates) => {
784
+ const updatedNode = { ...dialogue.nodes[nodeId], ...updates };
785
+ onChange({
786
+ ...dialogue,
787
+ nodes: {
788
+ ...dialogue.nodes,
789
+ [nodeId]: updatedNode
790
+ }
791
+ });
792
+ // Call onNodeUpdate hook
793
+ onNodeUpdate?.(nodeId, updates);
794
+ }, [dialogue, onChange, onNodeUpdate]);
795
+ // Handle choice updates
796
+ const handleAddChoice = (0, react_1.useCallback)((nodeId) => {
797
+ const updated = (0, node_helpers_1.addChoiceToNode)(dialogue.nodes[nodeId]);
798
+ handleUpdateNode(nodeId, updated);
799
+ }, [dialogue, handleUpdateNode]);
800
+ const handleUpdateChoice = (0, react_1.useCallback)((nodeId, choiceIdx, updates) => {
801
+ const updated = (0, node_helpers_1.updateChoiceInNode)(dialogue.nodes[nodeId], choiceIdx, updates);
802
+ handleUpdateNode(nodeId, updated);
803
+ }, [dialogue, handleUpdateNode]);
804
+ const handleRemoveChoice = (0, react_1.useCallback)((nodeId, choiceIdx) => {
805
+ const updated = (0, node_helpers_1.removeChoiceFromNode)(dialogue.nodes[nodeId], choiceIdx);
806
+ handleUpdateNode(nodeId, updated);
807
+ }, [dialogue, handleUpdateNode]);
808
+ const handleDeleteNode = (0, react_1.useCallback)((nodeId) => {
809
+ try {
810
+ let newDialogue = (0, node_helpers_1.deleteNodeFromTree)(dialogue, nodeId);
811
+ // Auto-organize if enabled
812
+ if (autoOrganize) {
813
+ const result = (0, layout_1.applyLayout)(newDialogue, layoutStrategy, { direction: layoutDirection });
814
+ newDialogue = result.dialogue;
815
+ setTimeout(() => {
816
+ if (reactFlowInstance) {
817
+ reactFlowInstance.fitView({ padding: 0.2, duration: 300 });
818
+ }
819
+ }, 50);
820
+ }
821
+ onChange(newDialogue);
822
+ setSelectedNodeId(null);
823
+ }
824
+ catch (e) {
825
+ alert(e.message);
826
+ }
827
+ }, [dialogue, onChange, autoOrganize, layoutDirection, reactFlowInstance]);
828
+ // Handle node drag stop - resolve collisions in freeform mode
829
+ const onNodeDragStop = (0, react_1.useCallback)((event, node) => {
830
+ // In freeform mode, resolve collisions after drag
831
+ if (!autoOrganize) {
832
+ const collisionResolved = (0, layout_1.resolveNodeCollisions)(dialogue, {
833
+ maxIterations: 50,
834
+ overlapThreshold: 0.3,
835
+ margin: 20,
836
+ });
837
+ // Only update if positions actually changed
838
+ const hasChanges = Object.keys(collisionResolved.nodes).some(id => {
839
+ const orig = dialogue.nodes[id];
840
+ const resolved = collisionResolved.nodes[id];
841
+ return orig && resolved && (orig.x !== resolved.x || orig.y !== resolved.y);
842
+ });
843
+ if (hasChanges) {
844
+ onChange(collisionResolved);
845
+ }
846
+ }
847
+ }, [dialogue, onChange, autoOrganize]);
848
+ // Handle auto-layout with direction (strategy comes from prop)
849
+ const handleAutoLayout = (0, react_1.useCallback)((direction) => {
850
+ const dir = direction || layoutDirection;
851
+ if (direction) {
852
+ setLayoutDirection(direction);
853
+ }
854
+ const result = (0, layout_1.applyLayout)(dialogue, layoutStrategy, { direction: dir });
855
+ onChange(result.dialogue);
856
+ // Fit view after a short delay to allow React Flow to update
857
+ setTimeout(() => {
858
+ if (reactFlowInstance) {
859
+ reactFlowInstance.fitView({ padding: 0.2, duration: 500 });
860
+ }
861
+ }, 100);
862
+ }, [dialogue, onChange, reactFlowInstance, layoutDirection, layoutStrategy]);
863
+ return (react_1.default.createElement("div", { className: `dialogue-editor-v2 ${className} w-full h-full flex flex-col` },
864
+ viewMode === 'graph' && (react_1.default.createElement("div", { className: "flex-1 flex overflow-hidden" },
865
+ react_1.default.createElement("div", { className: "flex-1 relative w-full h-full", ref: reactFlowWrapperRef, style: { minHeight: 0 } },
866
+ react_1.default.createElement(reactflow_1.default, { nodes: nodesWithFlags, edges: edges.map(edge => {
867
+ // Detect back-edges (loops) based on layout direction
868
+ const sourceNode = nodes.find(n => n.id === edge.source);
869
+ const targetNode = nodes.find(n => n.id === edge.target);
870
+ // For TB layout: back-edge if target Y < source Y (going up)
871
+ // For LR layout: back-edge if target X < source X (going left)
872
+ const isBackEdge = showBackEdges && sourceNode && targetNode && (layoutDirection === 'TB'
873
+ ? targetNode.position.y < sourceNode.position.y
874
+ : targetNode.position.x < sourceNode.position.x);
875
+ const isInPath = edgesToSelectedNode.has(edge.id);
876
+ // Dim edges not in the path when path highlighting is on and something is selected
877
+ const isDimmed = showPathHighlight && selectedNodeId !== null && !isInPath;
878
+ return {
879
+ ...edge,
880
+ data: {
881
+ ...edge.data,
882
+ isInPathToSelected: showPathHighlight && isInPath,
883
+ isBackEdge,
884
+ isDimmed,
885
+ },
886
+ };
887
+ }), nodeTypes: memoizedNodeTypes, edgeTypes: memoizedEdgeTypes, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onNodesDelete: onNodesDelete, onEdgesDelete: onEdgesDelete, onNodeDragStop: onNodeDragStop, nodesDraggable: !autoOrganize, onConnect: onConnect, onConnectStart: onConnectStart, onConnectEnd: onConnectEnd, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onPaneContextMenu: onPaneContextMenu, onNodeContextMenu: onNodeContextMenu, onEdgeContextMenu: onEdgeContextMenu, onPaneClick: () => {
888
+ // Close context menus and deselect node when clicking on pane (not nodes)
889
+ setContextMenu(null);
890
+ setNodeContextMenu(null);
891
+ setSelectedNodeId(null);
892
+ setShowLayoutMenu(false);
893
+ }, fitView: true, className: "bg-df-canvas-bg", style: { background: 'radial-gradient(circle, var(--color-df-canvas-grid) 1px, var(--color-df-canvas-bg) 1px)', backgroundSize: '20px 20px' }, defaultEdgeOptions: { type: 'default' }, connectionLineStyle: { stroke: '#e94560', strokeWidth: 2 }, connectionLineType: reactflow_1.ConnectionLineType.SmoothStep, snapToGrid: false, nodesConnectable: true, elementsSelectable: true, selectionOnDrag: true, panOnDrag: true, panOnScroll: true, zoomOnScroll: true, zoomOnPinch: true, preventScrolling: false,
894
+ // Behavior:
895
+ // - Click and drag a node = moves the node (React Flow handles this automatically)
896
+ // - Click and drag empty space = pans canvas
897
+ // - Trackpad two-finger swipe = pans canvas (works with panOnDrag)
898
+ // - Scroll wheel/trackpad scroll = zooms
899
+ // - Shift+Scroll = pans
900
+ // Note: React Flow automatically detects if you're dragging a node vs empty space
901
+ zoomOnDoubleClick: false, minZoom: 0.1, maxZoom: 3, deleteKeyCode: ['Delete', 'Backspace'], tabIndex: 0 },
902
+ react_1.default.createElement(reactflow_1.Background, { variant: reactflow_1.BackgroundVariant.Dots, gap: 20, size: 1, color: "#1a1a2e" }),
903
+ react_1.default.createElement(reactflow_1.Panel, { position: "bottom-right", className: "!p-0 !m-2" },
904
+ react_1.default.createElement("div", { className: "bg-df-sidebar-bg border border-df-sidebar-border rounded-lg overflow-hidden shadow-xl" },
905
+ react_1.default.createElement("div", { className: "px-3 py-1.5 border-b border-df-sidebar-border flex items-center justify-between bg-df-elevated" },
906
+ react_1.default.createElement("span", { className: "text-[10px] font-medium text-df-text-secondary uppercase tracking-wider" }, "Overview"),
907
+ react_1.default.createElement("div", { className: "flex items-center gap-1" },
908
+ react_1.default.createElement("span", { className: "w-2 h-2 rounded-full bg-df-npc-selected", title: "NPC Node" }),
909
+ react_1.default.createElement("span", { className: "w-2 h-2 rounded-full bg-df-player-selected", title: "Player Node" }),
910
+ react_1.default.createElement("span", { className: "w-2 h-2 rounded-full bg-df-conditional-border", title: "Conditional" }))),
911
+ react_1.default.createElement(reactflow_1.MiniMap, { style: {
912
+ width: 180,
913
+ height: 120,
914
+ backgroundColor: '#08080c',
915
+ }, maskColor: "rgba(0, 0, 0, 0.7)", nodeColor: (node) => {
916
+ if (node.type === 'npc')
917
+ return '#e94560';
918
+ if (node.type === 'player')
919
+ return '#8b5cf6';
920
+ if (node.type === 'conditional')
921
+ return '#3b82f6';
922
+ return '#4a4a6a';
923
+ }, nodeStrokeWidth: 2, pannable: true, zoomable: true }))),
924
+ react_1.default.createElement(reactflow_1.Panel, { position: "top-left", className: "!bg-transparent !border-0 !p-0 !m-2" },
925
+ react_1.default.createElement("div", { className: "flex flex-col gap-1.5 bg-df-sidebar-bg border border-df-sidebar-border rounded-lg p-1.5 shadow-lg" },
926
+ react_1.default.createElement("div", { className: "relative" },
927
+ react_1.default.createElement("button", { onClick: () => setShowLayoutMenu(!showLayoutMenu), className: `p-1.5 rounded transition-colors ${showLayoutMenu
928
+ ? 'bg-df-npc-selected/20 text-df-npc-selected border border-df-npc-selected'
929
+ : 'bg-df-elevated border border-df-control-border text-df-text-secondary hover:text-df-text-primary hover:border-df-control-hover'}`, title: `Layout: ${(0, layout_1.listLayouts)().find(l => l.id === layoutStrategy)?.name || layoutStrategy}` },
930
+ react_1.default.createElement(lucide_react_1.Grid3x3, { size: 14 })),
931
+ showLayoutMenu && (react_1.default.createElement("div", { className: "absolute left-full ml-2 top-0 z-50 bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-xl p-1 min-w-[200px]" },
932
+ react_1.default.createElement("div", { className: "text-[10px] text-df-text-secondary uppercase tracking-wider px-2 py-1 border-b border-df-sidebar-border" }, "Layout Algorithm"),
933
+ (0, layout_1.listLayouts)().map(layout => (react_1.default.createElement("button", { key: layout.id, onClick: () => {
934
+ if (onLayoutStrategyChange) {
935
+ onLayoutStrategyChange(layout.id);
936
+ setShowLayoutMenu(false);
937
+ // Trigger layout update with new strategy
938
+ setTimeout(() => handleAutoLayout(), 0);
939
+ }
940
+ }, className: `w-full text-left px-3 py-2 text-sm rounded transition-colors ${layoutStrategy === layout.id
941
+ ? 'bg-df-npc-selected/20 text-df-npc-selected'
942
+ : 'text-df-text-primary hover:bg-df-elevated'}` },
943
+ react_1.default.createElement("div", { className: "font-medium" },
944
+ layout.name,
945
+ " ",
946
+ layout.isDefault && '(default)'),
947
+ react_1.default.createElement("div", { className: "text-[10px] text-df-text-secondary mt-0.5" }, layout.description))))))),
948
+ onOpenFlagManager && (react_1.default.createElement("button", { onClick: onOpenFlagManager, className: "p-1.5 bg-df-elevated border border-df-control-border rounded text-df-text-secondary hover:text-df-text-primary hover:border-df-control-hover transition-colors", title: "Manage Flags" },
949
+ react_1.default.createElement(lucide_react_1.Settings, { size: 14 }))),
950
+ onOpenGuide && (react_1.default.createElement("button", { onClick: onOpenGuide, className: "p-1.5 bg-df-elevated border border-df-control-border rounded text-df-text-secondary hover:text-df-text-primary hover:border-df-control-hover transition-colors", title: "Guide & Documentation" },
951
+ react_1.default.createElement(lucide_react_1.BookOpen, { size: 14 }))),
952
+ feature_flags_1.ENABLE_DEBUG_TOOLS && onLoadExampleDialogue && onLoadExampleFlags && (react_1.default.createElement(ExampleLoaderButton_1.ExampleLoaderButton, { onLoadDialogue: onLoadExampleDialogue, onLoadFlags: onLoadExampleFlags })))),
953
+ react_1.default.createElement(reactflow_1.Panel, { position: "top-right", className: "!bg-transparent !border-0 !p-0 !m-2" },
954
+ react_1.default.createElement("div", { className: "flex items-center gap-1.5 bg-df-sidebar-bg border border-df-sidebar-border rounded-lg p-1.5 shadow-lg" },
955
+ react_1.default.createElement("button", { onClick: () => {
956
+ const newAutoOrganize = !autoOrganize;
957
+ setAutoOrganize(newAutoOrganize);
958
+ // If turning on, immediately apply layout
959
+ if (newAutoOrganize) {
960
+ handleAutoLayout();
961
+ }
962
+ }, className: `p-1.5 rounded transition-colors ${autoOrganize
963
+ ? 'bg-df-success/20 text-df-success border border-df-success'
964
+ : 'bg-df-elevated text-df-text-secondary hover:text-df-text-primary border border-df-control-border'}`, title: autoOrganize ? `Auto Layout ON - Nodes auto-arrange` : "Auto Layout OFF - Free placement" },
965
+ react_1.default.createElement(lucide_react_1.Magnet, { size: 14 })),
966
+ react_1.default.createElement("div", { className: "w-px h-5 bg-df-control-border" }),
967
+ react_1.default.createElement("div", { className: "flex border border-df-control-border rounded overflow-hidden" },
968
+ react_1.default.createElement("button", { onClick: () => handleAutoLayout('TB'), className: `p-1.5 transition-colors ${layoutDirection === 'TB'
969
+ ? 'bg-df-npc-selected/20 text-df-npc-selected'
970
+ : 'bg-df-elevated text-df-text-secondary hover:text-df-text-primary'} border-r border-df-control-border`, title: "Vertical Layout (Top to Bottom)" },
971
+ react_1.default.createElement(lucide_react_1.ArrowDown, { size: 14 })),
972
+ react_1.default.createElement("button", { onClick: () => handleAutoLayout('LR'), className: `p-1.5 transition-colors ${layoutDirection === 'LR'
973
+ ? 'bg-df-player-selected/20 text-df-player-selected'
974
+ : 'bg-df-elevated text-df-text-secondary hover:text-df-text-primary'}`, title: "Horizontal Layout (Left to Right)" },
975
+ react_1.default.createElement(lucide_react_1.ArrowRight, { size: 14 }))),
976
+ react_1.default.createElement("button", { onClick: () => handleAutoLayout(), className: "p-1.5 bg-df-elevated border border-df-control-border rounded text-df-text-secondary hover:text-df-text-primary hover:border-df-control-hover transition-colors", title: "Re-apply Layout" },
977
+ react_1.default.createElement(lucide_react_1.Layout, { size: 14 })),
978
+ react_1.default.createElement("div", { className: "w-px h-5 bg-df-control-border" }),
979
+ react_1.default.createElement("button", { onClick: () => setShowPathHighlight(!showPathHighlight), className: `p-1.5 rounded transition-colors ${showPathHighlight
980
+ ? 'bg-df-info/20 text-df-info border border-df-info'
981
+ : 'bg-df-elevated text-df-text-secondary hover:text-df-text-primary border border-df-control-border'}`, title: showPathHighlight ? "Path Highlight ON" : "Path Highlight OFF" },
982
+ react_1.default.createElement(lucide_react_1.Sparkles, { size: 14 })),
983
+ react_1.default.createElement("button", { onClick: () => setShowBackEdges(!showBackEdges), className: `p-1.5 rounded transition-colors ${showBackEdges
984
+ ? 'bg-df-warning/20 text-df-warning border border-df-warning'
985
+ : 'bg-df-elevated text-df-text-secondary hover:text-df-text-primary border border-df-control-border'}`, title: showBackEdges ? "Loop Edges Styled" : "Loop Edges Normal" },
986
+ react_1.default.createElement(lucide_react_1.Undo2, { size: 14 })),
987
+ react_1.default.createElement("div", { className: "w-px h-5 bg-df-control-border" }),
988
+ react_1.default.createElement("button", { onClick: () => {
989
+ if (dialogue?.startNodeId) {
990
+ setSelectedNodeId(dialogue.startNodeId);
991
+ // Center on start node
992
+ const startNode = nodes.find(n => n.id === dialogue.startNodeId);
993
+ if (startNode && reactFlowInstance) {
994
+ reactFlowInstance.setCenter(startNode.position.x + 110, startNode.position.y + 60, { zoom: 1, duration: 500 });
995
+ }
996
+ }
997
+ }, className: "p-1.5 bg-df-start/20 text-df-start border border-df-start rounded transition-colors hover:bg-df-start/30", title: "Go to Start Node" },
998
+ react_1.default.createElement(lucide_react_1.Home, { size: 14 })),
999
+ react_1.default.createElement("button", { onClick: () => {
1000
+ const endNodes = Array.from(endNodeIds);
1001
+ if (endNodes.length > 0) {
1002
+ // Cycle through end nodes or select first one
1003
+ const currentIdx = selectedNodeId ? endNodes.indexOf(selectedNodeId) : -1;
1004
+ const nextIdx = (currentIdx + 1) % endNodes.length;
1005
+ const nextEndNodeId = endNodes[nextIdx];
1006
+ setSelectedNodeId(nextEndNodeId);
1007
+ // Center on end node
1008
+ const endNode = nodes.find(n => n.id === nextEndNodeId);
1009
+ if (endNode && reactFlowInstance) {
1010
+ reactFlowInstance.setCenter(endNode.position.x + 110, endNode.position.y + 60, { zoom: 1, duration: 500 });
1011
+ }
1012
+ }
1013
+ }, className: "p-1.5 bg-df-end/20 text-df-end border border-df-end rounded transition-colors hover:bg-df-end/30", title: `Go to End Node (${endNodeIds.size} total)` },
1014
+ react_1.default.createElement(lucide_react_1.Flag, { size: 14 })))),
1015
+ contextMenu && (react_1.default.createElement("div", { className: "fixed z-50", style: { left: contextMenu.x, top: contextMenu.y } },
1016
+ react_1.default.createElement("div", { className: "bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-lg p-1 min-w-[150px]" },
1017
+ react_1.default.createElement("button", { onClick: () => {
1018
+ handleAddNode('npc', contextMenu.graphX, contextMenu.graphY);
1019
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add NPC Node"),
1020
+ react_1.default.createElement("button", { onClick: () => {
1021
+ handleAddNode('player', contextMenu.graphX, contextMenu.graphY);
1022
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add Player Node"),
1023
+ react_1.default.createElement("button", { onClick: () => {
1024
+ handleAddNode('conditional', contextMenu.graphX, contextMenu.graphY);
1025
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add Conditional Node"),
1026
+ react_1.default.createElement("button", { onClick: () => setContextMenu(null), className: "w-full text-left px-3 py-2 text-sm text-df-text-secondary hover:bg-df-elevated rounded" }, "Cancel")))),
1027
+ edgeDropMenu && (react_1.default.createElement("div", { className: "fixed z-50", style: { left: edgeDropMenu.x, top: edgeDropMenu.y } },
1028
+ react_1.default.createElement("div", { className: "bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-lg p-1 min-w-[150px]" },
1029
+ react_1.default.createElement("div", { className: "px-3 py-1 text-[10px] text-df-text-secondary uppercase border-b border-df-sidebar-border" }, "Create Node"),
1030
+ react_1.default.createElement("button", { onClick: () => {
1031
+ handleAddNode('npc', edgeDropMenu.graphX, edgeDropMenu.graphY, {
1032
+ fromNodeId: edgeDropMenu.fromNodeId,
1033
+ fromChoiceIdx: edgeDropMenu.fromChoiceIdx,
1034
+ fromBlockIdx: edgeDropMenu.fromBlockIdx,
1035
+ sourceHandle: edgeDropMenu.sourceHandle,
1036
+ });
1037
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add NPC Node"),
1038
+ react_1.default.createElement("button", { onClick: () => {
1039
+ handleAddNode('player', edgeDropMenu.graphX, edgeDropMenu.graphY, {
1040
+ fromNodeId: edgeDropMenu.fromNodeId,
1041
+ fromChoiceIdx: edgeDropMenu.fromChoiceIdx,
1042
+ fromBlockIdx: edgeDropMenu.fromBlockIdx,
1043
+ sourceHandle: edgeDropMenu.sourceHandle,
1044
+ });
1045
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add Player Node"),
1046
+ react_1.default.createElement("button", { onClick: () => {
1047
+ handleAddNode('conditional', edgeDropMenu.graphX, edgeDropMenu.graphY, {
1048
+ fromNodeId: edgeDropMenu.fromNodeId,
1049
+ fromChoiceIdx: edgeDropMenu.fromChoiceIdx,
1050
+ fromBlockIdx: edgeDropMenu.fromBlockIdx,
1051
+ sourceHandle: edgeDropMenu.sourceHandle,
1052
+ });
1053
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Add Conditional Node"),
1054
+ react_1.default.createElement("button", { onClick: () => {
1055
+ setEdgeDropMenu(null);
1056
+ connectingRef.current = null;
1057
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-secondary hover:bg-df-elevated rounded" }, "Cancel")))),
1058
+ edgeContextMenu && (react_1.default.createElement("div", { className: "fixed z-50", style: { left: edgeContextMenu.x, top: edgeContextMenu.y } },
1059
+ react_1.default.createElement("div", { className: "bg-df-sidebar-bg border border-df-sidebar-border rounded-lg shadow-lg p-1 min-w-[180px]" },
1060
+ react_1.default.createElement("div", { className: "px-3 py-1 text-[10px] text-df-text-secondary uppercase border-b border-df-sidebar-border" }, "Insert Node"),
1061
+ react_1.default.createElement("button", { onClick: () => {
1062
+ handleInsertNode('npc', edgeContextMenu.edgeId, edgeContextMenu.graphX, edgeContextMenu.graphY);
1063
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Insert NPC Node"),
1064
+ react_1.default.createElement("button", { onClick: () => {
1065
+ handleInsertNode('player', edgeContextMenu.edgeId, edgeContextMenu.graphX, edgeContextMenu.graphY);
1066
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Insert Player Node"),
1067
+ react_1.default.createElement("button", { onClick: () => {
1068
+ handleInsertNode('conditional', edgeContextMenu.edgeId, edgeContextMenu.graphX, edgeContextMenu.graphY);
1069
+ }, className: "w-full text-left px-3 py-2 text-sm text-df-text-primary hover:bg-df-elevated rounded" }, "Insert Conditional Node"),
1070
+ react_1.default.createElement("button", { onClick: () => setEdgeContextMenu(null), className: "w-full text-left px-3 py-2 text-sm text-df-text-secondary hover:bg-df-elevated rounded" }, "Cancel")))),
1071
+ nodeContextMenu && (react_1.default.createElement("div", { className: "fixed z-50", style: { left: nodeContextMenu.x, top: nodeContextMenu.y } },
1072
+ react_1.default.createElement("div", { className: "bg-df-elevated border border-df-player-border rounded-lg shadow-xl py-1 min-w-[180px]" },
1073
+ (() => {
1074
+ const node = dialogue.nodes[nodeContextMenu.nodeId];
1075
+ if (!node)
1076
+ return null;
1077
+ return (react_1.default.createElement(react_1.default.Fragment, null,
1078
+ react_1.default.createElement("div", { className: "px-3 py-1 text-[10px] text-df-text-secondary uppercase border-b border-df-control-border" }, node.id),
1079
+ react_1.default.createElement("button", { onClick: () => {
1080
+ setSelectedNodeId(nodeContextMenu.nodeId);
1081
+ setNodeContextMenu(null);
1082
+ }, className: "w-full px-4 py-2 text-sm text-left text-df-text-primary hover:bg-df-control-hover flex items-center gap-2" },
1083
+ react_1.default.createElement(lucide_react_1.Edit3, { size: 14, className: "text-df-npc-selected" }),
1084
+ " Edit Node"),
1085
+ node.type === 'player' && (react_1.default.createElement("button", { onClick: () => {
1086
+ handleAddChoice(nodeContextMenu.nodeId);
1087
+ setNodeContextMenu(null);
1088
+ }, className: "w-full px-4 py-2 text-sm text-left text-df-text-primary hover:bg-df-control-hover flex items-center gap-2" },
1089
+ react_1.default.createElement(lucide_react_1.Plus, { size: 14, className: "text-df-player-selected" }),
1090
+ " Add Choice")),
1091
+ node.type === 'npc' && !node.conditionalBlocks && (react_1.default.createElement("button", { onClick: () => {
1092
+ handleUpdateNode(nodeContextMenu.nodeId, {
1093
+ conditionalBlocks: [{
1094
+ id: `block_${Date.now()}`,
1095
+ type: 'if',
1096
+ condition: [],
1097
+ content: node.content,
1098
+ speaker: node.speaker
1099
+ }]
1100
+ });
1101
+ setSelectedNodeId(nodeContextMenu.nodeId);
1102
+ setNodeContextMenu(null);
1103
+ }, className: "w-full px-4 py-2 text-sm text-left text-df-text-primary hover:bg-df-control-hover flex items-center gap-2" },
1104
+ react_1.default.createElement(lucide_react_1.Plus, { size: 14, className: "text-df-conditional-border" }),
1105
+ " Add Conditionals")),
1106
+ node.id !== dialogue.startNodeId && (react_1.default.createElement("button", { onClick: () => {
1107
+ handleDeleteNode(nodeContextMenu.nodeId);
1108
+ setNodeContextMenu(null);
1109
+ }, className: "w-full px-4 py-2 text-sm text-left text-df-error hover:bg-df-control-hover flex items-center gap-2" },
1110
+ react_1.default.createElement(lucide_react_1.Trash2, { size: 14 }),
1111
+ " Delete"))));
1112
+ })(),
1113
+ 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) => {
1115
+ const targetNode = nodes.find(n => n.id === nodeId);
1116
+ if (targetNode && reactFlowInstance) {
1117
+ // Set selectedNodeId first so NodeEditor updates
1118
+ setSelectedNodeId(nodeId);
1119
+ // Update nodes using React Flow instance to ensure proper selection
1120
+ const allNodes = reactFlowInstance.getNodes();
1121
+ const updatedNodes = allNodes.map((n) => ({
1122
+ ...n,
1123
+ selected: n.id === nodeId
1124
+ }));
1125
+ reactFlowInstance.setNodes(updatedNodes);
1126
+ // Also update local state to keep in sync
1127
+ setNodes(updatedNodes);
1128
+ // Focus on the target node with animation
1129
+ setTimeout(() => {
1130
+ reactFlowInstance.fitView({
1131
+ nodes: [{ id: nodeId }],
1132
+ padding: 0.2,
1133
+ duration: 500,
1134
+ minZoom: 0.5,
1135
+ maxZoom: 2
1136
+ });
1137
+ }, 0);
1138
+ }
1139
+ }, onDelete: () => handleDeleteNode(selectedNode.id), onAddChoice: () => handleAddChoice(selectedNode.id), onUpdateChoice: (idx, updates) => handleUpdateChoice(selectedNode.id, idx, updates), onRemoveChoice: (idx) => handleRemoveChoice(selectedNode.id, idx), onClose: () => setSelectedNodeId(null), flagSchema: flagSchema })))),
1140
+ viewMode === 'yarn' && (react_1.default.createElement(YarnView_1.YarnView, { dialogue: dialogue, onExport: () => {
1141
+ const yarn = (0, yarn_converter_1.exportToYarn)(dialogue);
1142
+ if (onExportYarn) {
1143
+ onExportYarn(yarn);
1144
+ }
1145
+ else {
1146
+ // Default: download file
1147
+ const blob = new Blob([yarn], { type: 'text/plain' });
1148
+ const url = URL.createObjectURL(blob);
1149
+ const a = document.createElement('a');
1150
+ a.href = url;
1151
+ a.download = `${dialogue.title.replace(/\s+/g, '_')}.yarn`;
1152
+ a.click();
1153
+ URL.revokeObjectURL(url);
1154
+ }
1155
+ }, onImport: (yarn) => {
1156
+ try {
1157
+ const importedDialogue = (0, yarn_converter_1.importFromYarn)(yarn, dialogue.title);
1158
+ onChange(importedDialogue);
1159
+ }
1160
+ catch (err) {
1161
+ console.error('Failed to import Yarn:', err);
1162
+ alert('Failed to import Yarn file. Please check the format.');
1163
+ }
1164
+ } })),
1165
+ viewMode === 'play' && (react_1.default.createElement(PlayView_1.PlayView, { dialogue: dialogue, flagSchema: flagSchema }))));
1166
+ }
1167
+ function DialogueEditorV2(props) {
1168
+ return (react_1.default.createElement(reactflow_1.ReactFlowProvider, null,
1169
+ react_1.default.createElement(DialogueEditorV2Internal, { ...props })));
1170
+ }