@magicborn/dialogue-forge 0.1.0 → 0.1.1

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
@@ -399,9 +399,9 @@ const [gameState, setGameState] = useState<GameState>({
399
399
  }}
400
400
  flagSchema={flagSchema}
401
401
  // Event hooks
402
- onNodeAdd={(node) => console.log('Node added:', node.id)}
403
- onNodeDelete={(nodeId) => console.log('Node deleted:', nodeId)}
404
- onConnect={(source, target) => console.log('Connected:', source, '->', target)}
402
+ onNodeAdd={(node) => {/* Example: handle node add */}}
403
+ onNodeDelete={(nodeId) => {/* Example: handle node delete */}}
404
+ onConnect={(source, target) => {/* Example: handle connect */}}
405
405
  />`, language: "typescript" }),
406
406
  react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "6. Define Game State"),
407
407
  react_1.default.createElement("p", { className: "text-gray-400 text-sm mb-2" }, "Game state can be any JSON object. Flags should represent the dialogue-relevant portion of your game state."),
@@ -465,21 +465,21 @@ const gameState: GameState = {
465
465
  }}
466
466
  // Event hooks
467
467
  onNodeEnter={(nodeId, node) => {
468
- console.log('Entered node:', nodeId, node);
468
+ // Example: handle node enter
469
469
  // Trigger animations, sound effects, etc.
470
470
  }}
471
471
  onNodeExit={(nodeId, node) => {
472
- console.log('Exited node:', nodeId, node);
472
+ // Example: handle node exit
473
473
  }}
474
474
  onChoiceSelect={(nodeId, choice) => {
475
- console.log('Selected choice:', choice.text);
475
+ // Example: handle choice select
476
476
  // Track player decisions
477
477
  }}
478
478
  onDialogueStart={() => {
479
- console.log('Dialogue started');
479
+ // Example: handle dialogue start
480
480
  }}
481
481
  onDialogueEnd={() => {
482
- console.log('Dialogue ended');
482
+ // Example: handle dialogue end
483
483
  }}
484
484
  />`, language: "typescript" }),
485
485
  react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Complete Example"),
@@ -538,23 +538,23 @@ const dialogue = importFromYarn(yarnFile, 'Merchant');
538
538
  flagSchema={flagSchema}
539
539
  // Event hooks
540
540
  onNodeAdd={(node) => {
541
- console.log('Node added:', node.id);
541
+ // Example: handle node add
542
542
  // Track node creation
543
543
  }}
544
544
  onNodeDelete={(nodeId) => {
545
- console.log('Node deleted:', nodeId);
545
+ // Example: handle node delete
546
546
  }}
547
547
  onNodeUpdate={(nodeId, updates) => {
548
- console.log('Node updated:', nodeId, updates);
548
+ // Example: handle node update
549
549
  }}
550
550
  onConnect={(sourceId, targetId, sourceHandle) => {
551
- console.log('Connected:', sourceId, '->', targetId);
551
+ // Example: handle connect
552
552
  }}
553
553
  onDisconnect={(edgeId, sourceId, targetId) => {
554
- console.log('Disconnected:', sourceId, '->', targetId);
554
+ // Example: handle disconnect
555
555
  }}
556
556
  onNodeSelect={(nodeId) => {
557
- console.log('Node selected:', nodeId);
557
+ // Example: handle node select
558
558
  }}
559
559
  />
560
560
 
@@ -570,11 +570,11 @@ const dialogue = importFromYarn(yarnFile, 'Merchant');
570
570
  }));
571
571
  }}
572
572
  onNodeEnter={(nodeId, node) => {
573
- console.log('Entered node:', nodeId);
573
+ // Example: handle node enter
574
574
  // Play animations, sound effects
575
575
  }}
576
576
  onChoiceSelect={(nodeId, choice) => {
577
- console.log('Selected:', choice.text);
577
+ // Example: handle choice select
578
578
  // Track player decisions
579
579
  }}
580
580
  />`, language: "typescript" }),
@@ -1044,6 +1044,89 @@ await saveFile('dialogue.yarn', newYarn);`))))
1044
1044
  react_1.default.createElement("strong", { className: "text-white text-xs" }, "Square Selection"),
1045
1045
  react_1.default.createElement("p", { className: "text-gray-400 text-xs mt-1" }, "Selection box doesn't always capture all nodes within the selection area (deprioritized)")))))))
1046
1046
  },
1047
+ characters: {
1048
+ title: 'Characters',
1049
+ content: (react_1.default.createElement("div", { className: "space-y-4 text-sm" },
1050
+ react_1.default.createElement("p", { className: "text-gray-300" }, "Dialogue Forge supports character assignment for NPC and Player nodes. Characters are defined in your game state and can be selected from a searchable dropdown."),
1051
+ react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Character System"),
1052
+ react_1.default.createElement("p", { className: "text-gray-300 mb-3" }, "Each node (NPC or Player) can be assigned a character from your game state. When a character is assigned:"),
1053
+ react_1.default.createElement("ul", { className: "list-disc list-inside space-y-1 text-sm ml-2 text-gray-300" },
1054
+ react_1.default.createElement("li", null, "The character's avatar and name are displayed on the node in the graph"),
1055
+ react_1.default.createElement("li", null, "The character name is used as the speaker name"),
1056
+ react_1.default.createElement("li", null, "You can still override with a custom speaker name if needed")),
1057
+ react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Game State Structure"),
1058
+ react_1.default.createElement("p", { className: "text-gray-300 mb-3" },
1059
+ "Characters should be defined in your game state under the ",
1060
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "characters"),
1061
+ " property:"),
1062
+ react_1.default.createElement(CodeBlock_1.CodeBlock, { code: `interface GameState {
1063
+ flags?: FlagState;
1064
+ characters?: {
1065
+ [characterId: string]: Character;
1066
+ };
1067
+ }
1068
+
1069
+ interface Character {
1070
+ id: string;
1071
+ name: string;
1072
+ avatar?: string; // URL or emoji (e.g., "👤", "🧙", "/avatars/wizard.png")
1073
+ description?: string;
1074
+ }`, language: "typescript" }),
1075
+ react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Using Characters"),
1076
+ react_1.default.createElement("ol", { className: "list-decimal list-inside space-y-2 text-sm ml-2 text-gray-300" },
1077
+ react_1.default.createElement("li", null,
1078
+ react_1.default.createElement("strong", null, "Define characters"),
1079
+ " in your game state with ",
1080
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "id"),
1081
+ ", ",
1082
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "name"),
1083
+ ", and optionally ",
1084
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "avatar")),
1085
+ react_1.default.createElement("li", null,
1086
+ react_1.default.createElement("strong", null, "Pass characters"),
1087
+ " to ",
1088
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "DialogueEditorV2"),
1089
+ " via the ",
1090
+ react_1.default.createElement("code", { className: "bg-[#0d0d14] px-1 rounded" }, "characters"),
1091
+ " prop"),
1092
+ react_1.default.createElement("li", null,
1093
+ react_1.default.createElement("strong", null, "Select a character"),
1094
+ " in the Node Editor using the character dropdown (searchable combobox)"),
1095
+ react_1.default.createElement("li", null,
1096
+ react_1.default.createElement("strong", null, "View on graph"),
1097
+ " - The character's avatar and name appear on the node")),
1098
+ react_1.default.createElement("h3", { className: "text-lg font-semibold mt-6 mb-2 text-white" }, "Example"),
1099
+ react_1.default.createElement(CodeBlock_1.CodeBlock, { code: `// Game state with characters
1100
+ const gameState = {
1101
+ flags: { reputation: 50 },
1102
+ characters: {
1103
+ stranger: {
1104
+ id: 'stranger',
1105
+ name: 'Mysterious Stranger',
1106
+ avatar: '👤',
1107
+ description: 'A cloaked figure'
1108
+ },
1109
+ player: {
1110
+ id: 'player',
1111
+ name: 'Player',
1112
+ avatar: '🎮',
1113
+ description: 'The player character'
1114
+ }
1115
+ }
1116
+ };
1117
+
1118
+ // Pass to DialogueEditorV2
1119
+ <DialogueEditorV2
1120
+ dialogue={dialogueTree}
1121
+ characters={gameState.characters}
1122
+ flagSchema={flagSchema}
1123
+ onChange={setDialogueTree}
1124
+ />`, language: "typescript" }),
1125
+ react_1.default.createElement("div", { className: "bg-[#1a2a3e] border-l-4 border-blue-500 p-4 rounded mt-4" },
1126
+ react_1.default.createElement("p", { className: "text-gray-300 text-xs" },
1127
+ react_1.default.createElement("strong", null, "Note:"),
1128
+ " If a character is not assigned, you can still use a custom speaker name. The character system is optional but recommended for consistency across your dialogue system."))))
1129
+ },
1047
1130
  theming: {
1048
1131
  title: 'Theming',
1049
1132
  content: (react_1.default.createElement("div", { className: "space-y-4 text-sm" },
@@ -11,11 +11,13 @@
11
11
  import React from 'react';
12
12
  import { NodeProps } from 'reactflow';
13
13
  import { DialogueNode } from '../types';
14
+ import { Character } from '../types/characters';
14
15
  import { FlagSchema } from '../types/flags';
15
16
  import { LayoutDirection } from '../utils/layout';
16
17
  interface NPCNodeData {
17
18
  node: DialogueNode;
18
19
  flagSchema?: FlagSchema;
20
+ characters?: Record<string, Character>;
19
21
  isDimmed?: boolean;
20
22
  isInPath?: boolean;
21
23
  layoutDirection?: LayoutDirection;
@@ -36,7 +36,11 @@ function getFlagColorClass(type) {
36
36
  // Component
37
37
  // ============================================================================
38
38
  function NPCNodeV2({ data, selected }) {
39
- const { node, flagSchema, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
39
+ const { node, flagSchema, characters = {}, isDimmed, isInPath, layoutDirection = 'TB', isStartNode, isEndNode } = data;
40
+ // Get character if characterId is set
41
+ const character = node.characterId ? characters[node.characterId] : undefined;
42
+ const displayName = character ? character.name : (node.speaker || 'NPC');
43
+ const avatar = character?.avatar || '👤';
40
44
  // Handle positions based on layout direction
41
45
  const isHorizontal = layoutDirection === 'LR';
42
46
  const targetPosition = isHorizontal ? reactflow_1.Position.Left : reactflow_1.Position.Top;
@@ -59,28 +63,35 @@ function NPCNodeV2({ data, selected }) {
59
63
  const contentPreview = node.content.length > 60
60
64
  ? node.content.slice(0, 60) + '...'
61
65
  : node.content;
62
- return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
63
- 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" },
64
- react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
65
- " START")),
66
- 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" },
67
- react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
68
- " END")),
66
+ return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-node-selected/70' : ''} bg-df-npc-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
69
67
  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" }),
70
- react_1.default.createElement("div", { className: `px-3 py-1.5 border-b border-df-control-border flex items-center gap-2 rounded-t-lg ${headerBgClass}` },
71
- react_1.default.createElement(lucide_react_1.MessageSquare, { size: 12, className: "text-df-npc-selected" }),
72
- react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
73
- react_1.default.createElement("span", { className: "text-[10px] text-df-text-tertiary" }, "NPC")),
74
- react_1.default.createElement("div", { className: "px-3 py-2 min-h-[50px]" },
75
- node.speaker && (react_1.default.createElement("div", { className: "text-[10px] text-df-npc-selected font-medium mb-1" }, node.speaker)),
76
- react_1.default.createElement("div", { className: "text-xs text-df-text-primary line-clamp-2 bg-df-base border border-df-control-border rounded px-2 py-1" },
77
- "\"",
78
- contentPreview,
79
- "\""),
80
- node.setFlags && node.setFlags.length > 0 && (react_1.default.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
68
+ react_1.default.createElement("div", { className: `${headerBgClass} border-b-2 border-df-npc-border px-3 py-2.5 flex items-center gap-3 relative` },
69
+ react_1.default.createElement("div", { className: "w-14 h-14 rounded-full bg-df-npc-bg border-[3px] border-df-npc-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
70
+ react_1.default.createElement("div", { className: "flex-1 min-w-0" },
71
+ react_1.default.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
72
+ react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
73
+ 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}` },
74
+ react_1.default.createElement(lucide_react_1.Hash, { size: 12, className: "text-df-text-secondary" }),
75
+ react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
76
+ react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-npc-selected/20 border border-df-npc-selected/50", title: "NPC Node" },
77
+ react_1.default.createElement(lucide_react_1.MessageSquare, { size: 14, className: "text-df-npc-selected" }),
78
+ react_1.default.createElement("span", { className: "text-[10px] font-semibold text-df-npc-selected" }, "NPC"))),
79
+ 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" },
80
+ react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
81
+ " START")),
82
+ 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" },
83
+ react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
84
+ " END"))),
85
+ react_1.default.createElement("div", { className: "px-4 py-3" },
86
+ react_1.default.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-4 py-3 mb-2" },
87
+ react_1.default.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
88
+ "\"",
89
+ contentPreview,
90
+ "\"")),
91
+ node.setFlags && node.setFlags.length > 0 && (react_1.default.createElement("div", { className: "flex flex-wrap gap-1" }, node.setFlags.map((flagId) => {
81
92
  const flag = flagSchema?.flags.find((f) => f.id === flagId);
82
93
  const flagType = flag?.type || 'dialogue';
83
- return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
94
+ return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1.5 py-0.5 rounded-full border ${getFlagColorClass(flagType)}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
84
95
  })))),
85
96
  react_1.default.createElement(reactflow_1.Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-npc-selected hover:!bg-df-npc-selected/20" })));
86
97
  }
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { DialogueNode, DialogueTree, Choice } from '../types';
3
3
  import { FlagSchema } from '../types/flags';
4
+ import { Character } from '../types/characters';
4
5
  interface NodeEditorProps {
5
6
  node: DialogueNode;
6
7
  dialogue: DialogueTree;
@@ -13,6 +14,7 @@ interface NodeEditorProps {
13
14
  onPlayFromHere?: (nodeId: string) => void;
14
15
  onFocusNode?: (nodeId: string) => void;
15
16
  flagSchema?: FlagSchema;
17
+ characters?: Record<string, Character>;
16
18
  }
17
- export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }: NodeEditorProps): React.JSX.Element;
19
+ export declare function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters, }: NodeEditorProps): React.JSX.Element;
18
20
  export {};
@@ -36,12 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.NodeEditor = NodeEditor;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const FlagSelector_1 = require("./FlagSelector");
39
+ const CharacterSelector_1 = require("./CharacterSelector");
39
40
  const constants_1 = require("../types/constants");
40
41
  const lucide_react_1 = require("lucide-react");
41
42
  const reactflow_converter_1 = require("../utils/reactflow-converter");
42
43
  const EdgeIcon_1 = require("./EdgeIcon");
43
44
  const ConditionAutocomplete_1 = require("./ConditionAutocomplete");
44
- function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema }) {
45
+ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateChoice, onRemoveChoice, onClose, onPlayFromHere, onFocusNode, flagSchema, characters = {}, }) {
45
46
  // Local state for condition input values (keyed by block id for conditional blocks, choice id for choices)
46
47
  const [conditionInputs, setConditionInputs] = (0, react_1.useState)({});
47
48
  const [debouncedConditionInputs, setDebouncedConditionInputs] = (0, react_1.useState)({});
@@ -347,11 +348,21 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
347
348
  react_1.default.createElement("input", { value: node.id, disabled: true, className: "w-full bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-xs text-gray-500 font-mono" })),
348
349
  node.type === 'npc' && (react_1.default.createElement(react_1.default.Fragment, null,
349
350
  react_1.default.createElement("div", null,
350
- react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
351
+ react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
352
+ react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
353
+ const character = characterId ? characters[characterId] : undefined;
354
+ onUpdate({
355
+ characterId,
356
+ speaker: character ? character.name : node.speaker, // Keep speaker as fallback
357
+ });
358
+ }, placeholder: "Select character...", className: "mb-2" }),
359
+ react_1.default.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
360
+ react_1.default.createElement("div", null,
361
+ react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
351
362
  react_1.default.createElement("div", { className: "flex items-center gap-2" },
352
- react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
353
- react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-gray-500" })),
354
- react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Character name" }))),
363
+ react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
364
+ react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-df-text-secondary" })),
365
+ react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
355
366
  react_1.default.createElement("div", null,
356
367
  react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Content"),
357
368
  react_1.default.createElement("textarea", { value: node.content, onChange: (e) => onUpdate({ content: e.target.value }), className: "w-full bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-npc-selected outline-none min-h-[100px] resize-y", placeholder: "What the character says..." })),
@@ -411,14 +422,22 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
411
422
  return (react_1.default.createElement("div", { key: block.id, className: `rounded p-2 space-y-2 ${styles.bg} ${styles.border} border-2` },
412
423
  react_1.default.createElement("div", { className: "flex items-center gap-2" },
413
424
  react_1.default.createElement("span", { className: `text-[9px] px-1.5 py-0.5 rounded ${styles.tagBg} ${styles.tagText} font-semibold` }, block.type === 'if' ? 'IF' : block.type === 'elseif' ? 'ELSE IF' : 'ELSE'),
414
- react_1.default.createElement("div", { className: "flex items-center gap-2 flex-1" },
415
- react_1.default.createElement("div", { className: "w-6 h-6 rounded-full bg-[#2a2a2a] flex items-center justify-center flex-shrink-0" },
416
- react_1.default.createElement(lucide_react_1.User, { size: 12, className: "text-gray-400" })),
425
+ react_1.default.createElement("div", { className: "flex items-center gap-1.5 flex-1" },
426
+ react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: block.characterId, onSelect: (characterId) => {
427
+ const newBlocks = [...node.conditionalBlocks];
428
+ const character = characterId ? characters[characterId] : undefined;
429
+ newBlocks[idx] = {
430
+ ...block,
431
+ characterId,
432
+ speaker: character ? character.name : block.speaker, // Keep speaker as fallback
433
+ };
434
+ onUpdate({ conditionalBlocks: newBlocks });
435
+ }, placeholder: "Speaker...", compact: true, className: "flex-1" }),
417
436
  react_1.default.createElement("input", { type: "text", value: block.speaker || '', onChange: (e) => {
418
437
  const newBlocks = [...node.conditionalBlocks];
419
438
  newBlocks[idx] = { ...block, speaker: e.target.value || undefined };
420
439
  onUpdate({ conditionalBlocks: newBlocks });
421
- }, className: `flex-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-xs ${styles.text} focus:border-blue-500 outline-none`, placeholder: "Speaker (optional)" }))),
440
+ }, className: `flex-1 bg-df-elevated border border-df-control-border rounded px-1.5 py-0.5 text-[10px] text-df-text-primary focus:border-df-conditional-selected outline-none`, placeholder: "Custom name" }))),
422
441
  block.type !== 'else' && (() => {
423
442
  const parseCondition = (conditionStr) => {
424
443
  const conditions = [];
@@ -726,11 +745,21 @@ function NodeEditor({ node, dialogue, onUpdate, onDelete, onAddChoice, onUpdateC
726
745
  }, className: "text-xs px-2 py-1 bg-[#12121a] border border-[#2a2a3e] rounded text-gray-400 hover:text-gray-200" }, "+ Add Else"))))) : (react_1.default.createElement("div", { className: "text-xs text-gray-500 p-4 text-center border border-[#2a2a3e] rounded" }, "No conditional blocks. Add an \"If\" block to start."))))),
727
746
  node.type === 'player' && (react_1.default.createElement("div", null,
728
747
  react_1.default.createElement("div", null,
729
- react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Speaker"),
748
+ react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Character"),
749
+ react_1.default.createElement(CharacterSelector_1.CharacterSelector, { characters: characters, selectedCharacterId: node.characterId, onSelect: (characterId) => {
750
+ const character = characterId ? characters[characterId] : undefined;
751
+ onUpdate({
752
+ characterId,
753
+ speaker: character ? character.name : node.speaker, // Keep speaker as fallback
754
+ });
755
+ }, placeholder: "Select character...", className: "mb-2" }),
756
+ react_1.default.createElement("div", { className: "text-[9px] text-df-text-tertiary mt-1" }, "Or enter custom speaker name below")),
757
+ react_1.default.createElement("div", null,
758
+ react_1.default.createElement("label", { className: "text-[10px] text-df-text-secondary uppercase" }, "Speaker (Custom)"),
730
759
  react_1.default.createElement("div", { className: "flex items-center gap-2" },
731
- react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-[#2a2a3e] border border-[#2a2a3e] flex items-center justify-center flex-shrink-0" },
732
- react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-gray-500" })),
733
- react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-[#12121a] border border-[#2a2a3e] rounded px-2 py-1 text-sm text-gray-200 focus:border-[#8b5cf6] outline-none", placeholder: "Character name (optional)" }))),
760
+ react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-df-control-bg border border-df-control-border flex items-center justify-center flex-shrink-0" },
761
+ react_1.default.createElement(lucide_react_1.User, { size: 16, className: "text-df-text-secondary" })),
762
+ react_1.default.createElement("input", { type: "text", value: node.speaker || '', onChange: (e) => onUpdate({ speaker: e.target.value }), className: "flex-1 bg-df-elevated border border-df-control-border rounded px-2 py-1 text-sm text-df-text-primary focus:border-df-player-selected outline-none", placeholder: "Custom speaker name (optional)" }))),
734
763
  react_1.default.createElement("div", { className: "flex items-center justify-between mb-2 mt-4" },
735
764
  react_1.default.createElement("label", { className: "text-[10px] text-gray-500 uppercase" }, "Choices"),
736
765
  react_1.default.createElement("button", { onClick: onAddChoice, className: "text-[10px] text-[#e94560] hover:text-[#ff6b6b]" }, "+ Add")),
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
2
  import { NodeProps } from 'reactflow';
3
3
  import { DialogueNode } from '../types';
4
+ import { Character } from '../types/characters';
4
5
  import { FlagSchema } from '../types/flags';
5
6
  import { LayoutDirection } from '../utils/layout';
6
7
  interface PlayerNodeData {
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,8 +40,12 @@ const lucide_react_1 = require("lucide-react");
40
40
  // Color scheme for choice edges (same as current implementation)
41
41
  const CHOICE_COLORS = ['#e94560', '#8b5cf6', '#06b6d4', '#22c55e', '#f59e0b'];
42
42
  function PlayerNodeV2({ 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 choices = node.choices || [];
45
+ // Get character if characterId is set
46
+ const character = node.characterId ? characters[node.characterId] : undefined;
47
+ const displayName = character ? character.name : (node.speaker || 'Player');
48
+ const avatar = character?.avatar || '🎮';
45
49
  const updateNodeInternals = (0, reactflow_1.useUpdateNodeInternals)();
46
50
  const headerRef = (0, react_1.useRef)(null);
47
51
  const choiceRefs = (0, react_1.useRef)([]);
@@ -93,47 +97,62 @@ function PlayerNodeV2({ data, selected }) {
93
97
  : isEndNode
94
98
  ? 'border-df-end shadow-md'
95
99
  : 'border-df-player-border';
96
- return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[200px] relative`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
97
- 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" },
98
- react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
99
- " START")),
100
- 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" },
101
- react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
102
- " END")),
100
+ // Header background for player nodes
101
+ const headerBgClass = isStartNode
102
+ ? 'bg-df-start-bg'
103
+ : isEndNode
104
+ ? 'bg-df-end-bg'
105
+ : 'bg-df-player-header';
106
+ return (react_1.default.createElement("div", { className: `rounded-lg border-2 transition-all duration-300 ${borderClass} ${isInPath ? 'border-df-player-selected/70' : ''} bg-df-player-bg min-w-[320px] max-w-[450px] relative overflow-hidden`, style: isDimmed ? { opacity: 0.35, filter: 'saturate(0.3)' } : undefined },
103
107
  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" }),
104
- react_1.default.createElement("div", { ref: headerRef, className: "px-3 py-1.5 border-b border-df-control-border bg-df-player-header flex items-center gap-2 rounded-t-lg" },
105
- react_1.default.createElement(lucide_react_1.GitBranch, { size: 12, className: "text-df-player-selected" }),
106
- react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary truncate flex-1" }, node.id),
107
- react_1.default.createElement("span", { className: "text-[10px] text-df-text-tertiary" }, "PLAYER")),
108
- react_1.default.createElement("div", { className: "border-t border-df-control-border" }, choices.map((choice, idx) => {
108
+ react_1.default.createElement("div", { ref: headerRef, className: `${headerBgClass} border-b-2 border-df-player-border px-3 py-2.5 flex items-center gap-3 relative` },
109
+ react_1.default.createElement("div", { className: "w-14 h-14 rounded-full bg-df-player-bg border-[3px] border-df-player-border flex items-center justify-center text-3xl shadow-lg flex-shrink-0" }, avatar),
110
+ react_1.default.createElement("div", { className: "flex-1 min-w-0" },
111
+ react_1.default.createElement("h3", { className: "text-base font-bold text-df-text-primary truncate leading-tight" }, displayName)),
112
+ react_1.default.createElement("div", { className: "flex items-center gap-2 flex-shrink-0" },
113
+ 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}` },
114
+ react_1.default.createElement(lucide_react_1.Hash, { size: 12, className: "text-df-text-secondary" }),
115
+ react_1.default.createElement("span", { className: "text-[10px] font-mono text-df-text-secondary" }, node.id)),
116
+ react_1.default.createElement("div", { className: "flex items-center gap-1 px-2 py-1 rounded bg-df-player-selected/20 border border-df-player-selected/50", title: "Player Node" },
117
+ react_1.default.createElement(lucide_react_1.GitBranch, { size: 14, className: "text-df-player-selected" }),
118
+ react_1.default.createElement("span", { className: "text-[10px] font-semibold text-df-player-selected" }, "PLAYER"))),
119
+ 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" },
120
+ react_1.default.createElement(lucide_react_1.Play, { size: 8, fill: "currentColor" }),
121
+ " START")),
122
+ 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" },
123
+ react_1.default.createElement(lucide_react_1.Flag, { size: 8 }),
124
+ " END"))),
125
+ react_1.default.createElement("div", { className: "px-4 py-3 space-y-2" }, choices.map((choice, idx) => {
109
126
  // Use calculated position or fallback
110
127
  const choiceColor = CHOICE_COLORS[idx % CHOICE_COLORS.length];
111
128
  return (react_1.default.createElement("div", { key: choice.id, ref: el => {
112
129
  choiceRefs.current[idx] = el;
113
- }, className: "px-3 py-1.5 text-[10px] text-df-text-secondary flex items-center gap-2 border-b border-df-control-border last:border-0 relative" },
114
- react_1.default.createElement("div", { className: "flex-1 min-w-0" },
115
- react_1.default.createElement("span", { className: "truncate block bg-df-base border border-df-control-border rounded px-2 py-1 text-df-text-primary" },
116
- "\"",
117
- choice.text || 'Empty choice',
118
- "\""),
119
- choice.setFlags && choice.setFlags.length > 0 && (react_1.default.createElement("div", { className: "mt-0.5 flex flex-wrap gap-0.5" }, choice.setFlags.map(flagId => {
120
- const flag = flagSchema?.flags.find(f => f.id === flagId);
121
- const flagType = flag?.type || 'dialogue';
122
- const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
123
- flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
124
- flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
125
- flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
126
- flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
127
- flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
128
- flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
129
- 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
130
- return (react_1.default.createElement("span", { key: flagId, className: `text-[7px] px-0.5 py-0 rounded border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
131
- })))),
132
- react_1.default.createElement(reactflow_1.Handle, { type: "source", position: reactflow_1.Position.Right, id: `choice-${idx}`, style: {
133
- top: `50%`,
134
- transform: `translateY(-50%)`,
135
- right: '-6px',
136
- borderColor: choiceColor,
137
- }, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" })));
138
- }))));
130
+ }, className: "relative group" },
131
+ react_1.default.createElement("div", { className: "bg-df-elevated border border-df-control-border rounded-lg px-3 py-2 flex items-start gap-2 hover:border-df-player-selected/50 transition-colors" },
132
+ react_1.default.createElement("div", { className: "flex-1 min-w-0" },
133
+ react_1.default.createElement("p", { className: "text-sm text-df-text-primary leading-relaxed" },
134
+ "\"",
135
+ choice.text || 'Empty choice',
136
+ "\""),
137
+ choice.setFlags && choice.setFlags.length > 0 && (react_1.default.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, choice.setFlags.map(flagId => {
138
+ const flag = flagSchema?.flags.find(f => f.id === flagId);
139
+ const flagType = flag?.type || 'dialogue';
140
+ const colorClass = flagType === 'dialogue' ? 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue' :
141
+ flagType === 'quest' ? 'bg-df-flag-quest-bg text-df-flag-quest border-df-flag-quest' :
142
+ flagType === 'achievement' ? 'bg-df-flag-achievement-bg text-df-flag-achievement border-df-flag-achievement' :
143
+ flagType === 'item' ? 'bg-df-flag-item-bg text-df-flag-item border-df-flag-item' :
144
+ flagType === 'stat' ? 'bg-df-flag-stat-bg text-df-flag-stat border-df-flag-stat' :
145
+ flagType === 'title' ? 'bg-df-flag-title-bg text-df-flag-title border-df-flag-title' :
146
+ flagType === 'global' ? 'bg-df-flag-global-bg text-df-flag-global border-df-flag-global' :
147
+ 'bg-df-flag-dialogue-bg text-df-flag-dialogue border-df-flag-dialogue';
148
+ return (react_1.default.createElement("span", { key: flagId, className: `text-[8px] px-1 py-0.5 rounded-full border ${colorClass}`, title: flag?.name || flagId }, flagType === 'dialogue' ? 't' : flagType[0]));
149
+ })))),
150
+ react_1.default.createElement(reactflow_1.Handle, { type: "source", position: reactflow_1.Position.Right, id: `choice-${idx}`, style: {
151
+ top: `50%`,
152
+ transform: `translateY(-50%)`,
153
+ right: '-6px',
154
+ borderColor: choiceColor,
155
+ }, className: "!bg-df-control-bg !border-2 hover:!border-df-player-selected hover:!bg-df-player-selected/20 !w-3 !h-3 !rounded-full" }))));
156
+ })),
157
+ react_1.default.createElement(reactflow_1.Handle, { type: "source", position: sourcePosition, id: "next", className: "!bg-df-control-bg !border-df-control-border !w-4 !h-4 !rounded-full hover:!border-df-player-selected hover:!bg-df-player-selected/20" })));
139
158
  }
@@ -271,17 +271,17 @@ function ReactFlowPOC({ dialogue }) {
271
271
  const onNodesChange = (0, react_1.useCallback)((changes) => {
272
272
  // Update dialogue tree positions
273
273
  // This would sync back to our DialogueTree structure
274
- console.log('Nodes changed:', changes);
274
+ // Handle node changes
275
275
  }, []);
276
276
  // Handle edge connections
277
277
  const onConnect = (0, react_1.useCallback)((connection) => {
278
278
  // Handle new edge connections
279
279
  // This would update our DialogueTree structure
280
- console.log('Connected:', connection);
280
+ // Handle connection
281
281
  }, []);
282
282
  // Handle edge changes (delete, etc.)
283
283
  const onEdgesChange = (0, react_1.useCallback)((changes) => {
284
- console.log('Edges changed:', changes);
284
+ // Handle edge changes
285
285
  }, []);
286
286
  return (react_1.default.createElement("div", { className: "w-full h-full" },
287
287
  react_1.default.createElement("div", { className: "p-8 text-gray-400" },
@@ -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 {};