Skip to content

Commit d6d8380

Browse files
committed
Improve onboarding scaffold & demo choices
Refactor onboarding scaffolding and add a read-only onboarding demo for choices. Key changes: - Moved onboarding scaffold startup out of App.jsx and into SidebarEditor/useStoryState. - useStoryState: refactored ensureOnboardingScaffold to use a selectedNodeId ref, more robust target selection, seed demo choices, and set selection after nodes update. - SidebarEditor: triggers scaffold creation for onboarding steps and shows a read-only demo node when no node is selected during the choices tour. - ChoicesEditor & ChoiceRow: support a readOnly demo preview (hides add/remove/edit controls and shows explanatory hint). - onboardingDemo.js: added ONBOARDING_DEMO_NODE and helper functions. Files changed: src/App.jsx, src/hooks/useStoryState.js, src/components/editor/SidebarEditor.jsx, src/components/editor/ChoicesEditor.jsx, src/components/editor/ChoiceRow.jsx, src/data/onboardingDemo.js This makes the onboarding tour reliably display example choices without modifying real story data.
1 parent 0c51491 commit d6d8380

6 files changed

Lines changed: 161 additions & 70 deletions

File tree

src/App.jsx

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1+
import { useEffect, useMemo, useRef, useState } from "react";
22
import StoryCanvas from "./components/canvas/StoryCanvas";
33
import SidebarEditor from "./components/editor/SidebarEditor";
44
import VariablesScreen from "./components/editor/VariablesScreen";
@@ -108,23 +108,6 @@ function EditorApp() {
108108
return () => window.clearTimeout(timerId);
109109
}, [activeScreen, onboarding.shouldAutoStart, onboarding.start]);
110110

111-
useLayoutEffect(() => {
112-
if (!onboarding.isActive || activeScreen !== "editor") return;
113-
114-
const stepId = onboarding.step?.id;
115-
if (stepId === "sidebar") {
116-
story.ensureOnboardingScaffold({ seedChoices: false });
117-
}
118-
if (stepId === "choices" || stepId === "choice-expand") {
119-
story.ensureOnboardingScaffold({ seedChoices: true });
120-
}
121-
}, [
122-
onboarding.isActive,
123-
onboarding.step?.id,
124-
activeScreen,
125-
story.ensureOnboardingScaffold,
126-
]);
127-
128111
const selectedMiniGame = useMemo(() => {
129112
if (!story.selectedNode || !isSupportedMiniGameBlock(story.selectedNode)) {
130113
return null;

src/components/editor/ChoiceRow.jsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default function ChoiceRow({
2020
highlightChevron = false,
2121
revealOnboarding = false,
2222
revealDelayMs = 0,
23+
readOnly = false,
2324
onExpand,
2425
onUpdate,
2526
onRemove,
@@ -100,6 +101,13 @@ export default function ChoiceRow({
100101

101102
{isExpanded && (
102103
<>
104+
{readOnly ? (
105+
<p className="sidebar-hint" style={{ marginTop: 8 }}>
106+
This is where you edit the choice label, target block, conditions, and
107+
effects.
108+
</p>
109+
) : (
110+
<>
103111
{isChatBlock && (
104112
<div className="form-group">
105113
<label className="form-label">Choice Type</label>
@@ -216,13 +224,17 @@ export default function ChoiceRow({
216224
onUpdate={(field, value) => onUpdate(choiceIndex, field, value)}
217225
/>
218226

219-
<button
220-
type="button"
221-
className="danger-button"
222-
onClick={() => onRemove(choiceIndex)}
223-
>
224-
Remove Choice
225-
</button>
227+
{!readOnly && (
228+
<button
229+
type="button"
230+
className="danger-button"
231+
onClick={() => onRemove(choiceIndex)}
232+
>
233+
Remove Choice
234+
</button>
235+
)}
236+
</>
237+
)}
226238
</>
227239
)}
228240
</div>
Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import ChoiceRow from "./ChoiceRow";
3+
import {
4+
ONBOARDING_DEMO_CHOICES,
5+
isOnboardingChoiceStep,
6+
} from "../../data/onboardingDemo";
37

48
export default function ChoicesEditor({
59
selectedNode,
610
nodes,
711
variables,
812
characters = [],
913
onboardingStepId = null,
14+
isOnboardingDemoPreview = false,
1015
addChoiceToSelectedNode,
1116
updateChoiceOnSelectedNode,
1217
removeChoiceFromSelectedNode,
1318
}) {
14-
const choices = selectedNode?.data?.choices || [];
1519
const blockType = selectedNode?.data?.blockType || "narrative";
1620
const selectedNodeId = selectedNode?.id;
21+
const storyChoices = selectedNode?.data?.choices || [];
22+
const showOnboardingChoiceDemo = isOnboardingChoiceStep(onboardingStepId);
23+
const visibleChoices = useMemo(() => {
24+
if (showOnboardingChoiceDemo && storyChoices.length === 0) {
25+
return ONBOARDING_DEMO_CHOICES;
26+
}
27+
return storyChoices;
28+
}, [showOnboardingChoiceDemo, storyChoices]);
29+
const readOnly = isOnboardingDemoPreview;
30+
1731
const [expandedChoiceIndex, setExpandedChoiceIndex] = useState(null);
1832
const showChoiceReveal = onboardingStepId === "choices";
1933
const highlightChoiceChevron = onboardingStepId === "choice-expand";
2034

2135
useEffect(() => {
2236
setExpandedChoiceIndex(null);
23-
}, [selectedNodeId, blockType]);
37+
}, [selectedNodeId, blockType, onboardingStepId]);
2438

2539
useEffect(() => {
2640
if (!highlightChoiceChevron) return;
@@ -33,13 +47,13 @@ export default function ChoicesEditor({
3347
});
3448

3549
return () => window.cancelAnimationFrame(frameId);
36-
}, [highlightChoiceChevron, choices.length]);
50+
}, [highlightChoiceChevron, visibleChoices.length]);
3751

3852
useEffect(() => {
39-
if (expandedChoiceIndex !== null && expandedChoiceIndex >= choices.length) {
53+
if (expandedChoiceIndex !== null && expandedChoiceIndex >= visibleChoices.length) {
4054
setExpandedChoiceIndex(null);
4155
}
42-
}, [choices.length, expandedChoiceIndex]);
56+
}, [visibleChoices.length, expandedChoiceIndex]);
4357

4458
function handleChoiceExpand(index) {
4559
setExpandedChoiceIndex((current) => (current === index ? null : index));
@@ -49,16 +63,18 @@ export default function ChoicesEditor({
4963
<div className="editor-section">
5064
<div className="editor-section-header">
5165
<h3 className="section-title">Choices</h3>
52-
<button className="toolbar-button" onClick={addChoiceToSelectedNode}>
53-
+ Add Choice
54-
</button>
66+
{!readOnly && (
67+
<button className="toolbar-button" onClick={addChoiceToSelectedNode}>
68+
+ Add Choice
69+
</button>
70+
)}
5571
</div>
5672

57-
{choices.length === 0 ? (
73+
{visibleChoices.length === 0 ? (
5874
<p className="sidebar-hint">No choices yet.</p>
5975
) : (
6076
<div className="choice-list">
61-
{choices.map((choice, index) => (
77+
{visibleChoices.map((choice, index) => (
6278
<ChoiceRow
6379
key={`${selectedNode.id}-choice-${index}`}
6480
choiceIndex={index}
@@ -72,13 +88,14 @@ export default function ChoicesEditor({
7288
highlightChevron={highlightChoiceChevron && index === 0}
7389
revealOnboarding={showChoiceReveal}
7490
revealDelayMs={index * 180}
91+
readOnly={readOnly}
7592
onExpand={() => handleChoiceExpand(index)}
76-
onUpdate={updateChoiceOnSelectedNode}
77-
onRemove={removeChoiceFromSelectedNode}
93+
onUpdate={readOnly ? undefined : updateChoiceOnSelectedNode}
94+
onRemove={readOnly ? undefined : removeChoiceFromSelectedNode}
7895
/>
7996
))}
8097
</div>
8198
)}
8299
</div>
83100
);
84-
}
101+
}

src/components/editor/SidebarEditor.jsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useLayoutEffect, useState } from "react";
22
import ChoicesEditor from "./ChoicesEditor";
33
import StoryDiagnostics from "./StoryDiagnostics";
44
import ReferenceTextarea from "./ReferenceTextarea";
5+
import {
6+
ONBOARDING_DEMO_NODE,
7+
isOnboardingChoiceStep,
8+
} from "../../data/onboardingDemo";
59

610
export default function SidebarEditor({
711
nodes,
812
variables,
913
characters = [],
1014
selectedNode,
1115
onboardingStepId = null,
16+
ensureOnboardingScaffold,
1217
updateSelectedNodeField,
1318
deleteSelectedNode,
1419
addChoiceToSelectedNode,
@@ -18,12 +23,54 @@ export default function SidebarEditor({
1823
onOpenVariables,
1924
}) {
2025
const [isNarrativeContentOpen, setIsNarrativeContentOpen] = useState(false);
26+
const showOnboardingChoiceDemo = isOnboardingChoiceStep(onboardingStepId);
27+
28+
useLayoutEffect(() => {
29+
if (!ensureOnboardingScaffold || !onboardingStepId) return;
30+
31+
if (onboardingStepId === "sidebar") {
32+
ensureOnboardingScaffold({ seedChoices: false });
33+
return;
34+
}
35+
36+
if (isOnboardingChoiceStep(onboardingStepId)) {
37+
ensureOnboardingScaffold({ seedChoices: true });
38+
}
39+
}, [ensureOnboardingScaffold, onboardingStepId]);
40+
41+
useEffect(() => {
42+
if (!ensureOnboardingScaffold || !isOnboardingChoiceStep(onboardingStepId)) return;
43+
ensureOnboardingScaffold({ seedChoices: true });
44+
}, [ensureOnboardingScaffold, onboardingStepId, selectedNode?.id]);
2145

2246
useEffect(() => {
2347
setIsNarrativeContentOpen(false);
2448
}, [selectedNode?.id]);
2549

2650
if (!selectedNode) {
51+
if (showOnboardingChoiceDemo) {
52+
return (
53+
<div>
54+
<h2 className="section-title">Block Editor</h2>
55+
<p className="sidebar-hint">Tutorial Scene — example choices below.</p>
56+
57+
<div data-onboarding="choices">
58+
<ChoicesEditor
59+
selectedNode={ONBOARDING_DEMO_NODE}
60+
nodes={nodes}
61+
variables={variables}
62+
characters={characters}
63+
onboardingStepId={onboardingStepId}
64+
isOnboardingDemoPreview
65+
addChoiceToSelectedNode={addChoiceToSelectedNode}
66+
updateChoiceOnSelectedNode={updateChoiceOnSelectedNode}
67+
removeChoiceFromSelectedNode={removeChoiceFromSelectedNode}
68+
/>
69+
</div>
70+
</div>
71+
);
72+
}
73+
2774
return (
2875
<div>
2976
<h2 className="section-title">Block Editor</h2>

src/data/onboardingDemo.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ export const ONBOARDING_DEMO_CHOICES = [
1515
effects: [],
1616
},
1717
];
18+
19+
export const ONBOARDING_DEMO_NODE = {
20+
id: ONBOARDING_SCAFFOLD_NODE_ID,
21+
data: {
22+
title: "Tutorial Scene",
23+
blockType: "narrative",
24+
choices: ONBOARDING_DEMO_CHOICES,
25+
},
26+
};
27+
28+
/** Tour steps that need example choices visible in the sidebar. */
29+
export function isOnboardingChoiceStep(stepId) {
30+
return stepId === "choices" || stepId === "choice-expand";
31+
}
32+
33+
export function isOnboardingSidebarStep(stepId) {
34+
return stepId === "sidebar" || isOnboardingChoiceStep(stepId);
35+
}

src/hooks/useStoryState.js

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useMemo, useState } from "react";
1+
import { useCallback, useMemo, useRef, useState } from "react";
22
import { cloneDemoStoryById, DEMO_STORIES } from "../data/demoStoriesCatalog";
33
import {
44
ONBOARDING_DEMO_CHOICES,
@@ -70,6 +70,8 @@ export default function useStoryState() {
7070
const [variables, setVariables] = useState(initial.variables);
7171
const [characters, setCharacters] = useState(initial.characters);
7272
const [selectedNodeId, setSelectedNodeId] = useState(null);
73+
const selectedNodeIdRef = useRef(selectedNodeId);
74+
selectedNodeIdRef.current = selectedNodeId;
7375

7476
const [storyBaselineSignature, setStoryBaselineSignature] = useState(() =>
7577
stableDemoSignature(initial.nodes, initial.variables, initial.characters)
@@ -383,57 +385,69 @@ export default function useStoryState() {
383385
}
384386

385387
const ensureOnboardingScaffold = useCallback(({ seedChoices = false } = {}) => {
386-
let targetId = null;
388+
let targetIdToSelect = null;
387389

388390
setNodes((prev) => {
389391
let next = [...prev];
390392

391-
let targetNode = next.find(
392-
(node) =>
393-
node.id === ONBOARDING_SCAFFOLD_NODE_ID || node.data?.isOnboardingScaffold
394-
);
393+
let targetId =
394+
selectedNodeIdRef.current &&
395+
next.some((node) => node.id === selectedNodeIdRef.current)
396+
? selectedNodeIdRef.current
397+
: null;
395398

396-
if (!targetNode && next.length === 0) {
397-
targetNode = {
398-
id: ONBOARDING_SCAFFOLD_NODE_ID,
399-
type: "storyNode",
400-
position: { x: 260, y: 120 },
401-
data: {
402-
title: "Tutorial Scene",
403-
content: "Write what the player reads when they reach this scene.",
404-
blockType: "narrative",
405-
choices: [],
406-
isOnboardingScaffold: true,
407-
enterEffects: [],
408-
graphIssues: [],
409-
},
410-
};
411-
next.push(targetNode);
412-
} else if (!targetNode) {
413-
targetNode = next[0];
414-
}
399+
if (!targetId) {
400+
const existingScaffold = next.find(
401+
(node) =>
402+
node.id === ONBOARDING_SCAFFOLD_NODE_ID || node.data?.isOnboardingScaffold
403+
);
415404

416-
targetId = targetNode.id;
405+
if (existingScaffold) {
406+
targetId = existingScaffold.id;
407+
} else if (next.length === 0) {
408+
const scaffold = {
409+
id: ONBOARDING_SCAFFOLD_NODE_ID,
410+
type: "storyNode",
411+
position: { x: 260, y: 120 },
412+
data: {
413+
title: "Tutorial Scene",
414+
content: "Write what the player reads when they reach this scene.",
415+
blockType: "narrative",
416+
choices: [],
417+
isOnboardingScaffold: true,
418+
enterEffects: [],
419+
graphIssues: [],
420+
},
421+
};
422+
next = [...next, scaffold];
423+
targetId = scaffold.id;
424+
} else {
425+
targetId = next[0]?.id || null;
426+
}
427+
}
417428

418-
if (seedChoices && (targetNode.data?.choices || []).length === 0) {
429+
if (seedChoices && targetId) {
430+
const demoChoices = ONBOARDING_DEMO_CHOICES.map((choice) => ({ ...choice }));
419431
next = next.map((node) =>
420432
node.id === targetId
421433
? {
422434
...node,
423435
data: {
424436
...node.data,
425-
choices: ONBOARDING_DEMO_CHOICES.map((choice) => ({ ...choice })),
437+
blockType: node.data?.blockType || "narrative",
438+
choices: demoChoices,
426439
},
427440
}
428441
: node
429442
);
430443
}
431444

445+
targetIdToSelect = targetId;
432446
return next;
433447
});
434448

435-
if (targetId) {
436-
setSelectedNodeId(targetId);
449+
if (targetIdToSelect) {
450+
setSelectedNodeId(targetIdToSelect);
437451
}
438452
}, []);
439453

0 commit comments

Comments
 (0)