Browse Source

fix: onboarding timeout cleanup, prop safety, and placeholder text

- TutorialOverlay: track all nested setTimeout IDs and clear on cleanup
  to prevent ghost animations when tutorial is skipped mid-demonstration
- FTUXReadyStep: use strict local interface requiring both callbacks
  instead of optional FTUXStepProps
- FTUXModelDefaultsStep: replace hardcoded "Gemini nano-banana-pro" with
  "None set" for the no-model-selected state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
de1d07afb1
  1. 2
      src/components/onboarding/FTUXModelDefaultsStep.tsx
  2. 7
      src/components/onboarding/FTUXReadyStep.tsx
  3. 49
      src/components/onboarding/TutorialOverlay.tsx

2
src/components/onboarding/FTUXModelDefaultsStep.tsx

@ -106,7 +106,7 @@ export function FTUXModelDefaultsStep({}: FTUXStepProps) {
</>
) : (
<>
<span className="text-xs text-neutral-400">Gemini nano-banana-pro</span>
<span className="text-xs text-neutral-400">None set</span>
<button
type="button"
onClick={() => setShowImageDialog(true)}

7
src/components/onboarding/FTUXReadyStep.tsx

@ -1,8 +1,11 @@
"use client";
import { FTUXStepProps } from "@/types/ftux";
interface FTUXReadyStepProps {
onStartTutorial: () => void;
onComplete: () => void;
}
export function FTUXReadyStep({ onStartTutorial, onComplete }: FTUXStepProps) {
export function FTUXReadyStep({ onStartTutorial, onComplete }: FTUXReadyStepProps) {
return (
<div className="flex flex-col items-center justify-center py-12 px-8">
<h2 className="text-2xl font-semibold text-neutral-100 mb-4">

49
src/components/onboarding/TutorialOverlay.tsx

@ -17,6 +17,7 @@ export function TutorialOverlay() {
const [showHighlight, setShowHighlight] = useState(false);
const nodesPopulated = useRef(false);
const demonstrateNodesAdded = useRef(false);
const demonstrateTimeoutIds = useRef<ReturnType<typeof setTimeout>[]>([]);
const tutorialActive = useFTUXStore((state) => state.tutorialActive);
const currentTutorialStep = useFTUXStore((state) => state.currentTutorialStep);
@ -211,8 +212,15 @@ export function TutorialOverlay() {
const onConnect = useWorkflowStore.getState().onConnect;
const updateNodeData = useWorkflowStore.getState().updateNodeData;
// Track all timeouts so cleanup can clear them if tutorial is skipped
const timeoutIds = demonstrateTimeoutIds.current;
timeoutIds.length = 0;
const schedule = (fn: () => void, ms: number) => {
timeoutIds.push(setTimeout(fn, ms));
};
// Initial delay to show message
setTimeout(() => {
schedule(() => {
// Find the Generate Image node
const generateNode = nodes.find((n) => n.type === "nanoBanana");
if (!generateNode) return;
@ -227,14 +235,14 @@ export function TutorialOverlay() {
y: baseY - 350,
});
setTimeout(() => {
schedule(() => {
// Add Generate Video node
const videoNodeId = addNode("generateVideo", {
x: baseX + 750,
y: baseY - 350,
});
setTimeout(() => {
schedule(() => {
// Connect Prompt → Video (text)
onConnect({
source: videoPromptId,
@ -243,7 +251,7 @@ export function TutorialOverlay() {
targetHandle: "text",
});
setTimeout(() => {
schedule(() => {
// Connect Image → Video (image)
onConnect({
source: generateNode.id,
@ -252,20 +260,20 @@ export function TutorialOverlay() {
targetHandle: "image",
});
setTimeout(() => {
schedule(() => {
// Populate video prompt
updateNodeData(videoPromptId, {
prompt: "A bird soaring through clouds at sunset",
});
setTimeout(() => {
schedule(() => {
// Add Output for video
const videoOutputId = addNode("output", {
x: baseX + 1100,
y: baseY - 350,
});
setTimeout(() => {
schedule(() => {
// Connect Video → Output
onConnect({
source: videoNodeId,
@ -275,21 +283,21 @@ export function TutorialOverlay() {
});
// LLM ANALYSIS BRANCH (bottom) - Clean horizontal layout with generous spacing
setTimeout(() => {
schedule(() => {
// Add Prompt node for LLM
const llmPromptId = addNode("prompt", {
x: baseX + 400,
y: baseY + 350,
});
setTimeout(() => {
schedule(() => {
// Add LLM Generate node
const llmNodeId = addNode("llmGenerate", {
x: baseX + 750,
y: baseY + 350,
});
setTimeout(() => {
schedule(() => {
// Connect Prompt → LLM (text)
onConnect({
source: llmPromptId,
@ -298,7 +306,7 @@ export function TutorialOverlay() {
targetHandle: "text",
});
setTimeout(() => {
schedule(() => {
// Connect Image → LLM (image for analysis)
onConnect({
source: generateNode.id,
@ -307,21 +315,21 @@ export function TutorialOverlay() {
targetHandle: "image",
});
setTimeout(() => {
schedule(() => {
// Populate LLM prompt
updateNodeData(llmPromptId, {
prompt:
"Give me an image generation prompt that shows this bird in a nightclub filled with other birds, also in costume. Only output the prompt and nothing else.",
});
setTimeout(() => {
schedule(() => {
// Add second Generate Image node
const generateNode2Id = addNode("nanoBanana", {
x: baseX + 1100,
y: baseY + 350,
});
setTimeout(() => {
schedule(() => {
// Connect LLM → Generate Image #2 (text prompt)
onConnect({
source: llmNodeId,
@ -330,7 +338,7 @@ export function TutorialOverlay() {
targetHandle: "text",
});
setTimeout(() => {
schedule(() => {
// Also connect original bird image as reference
onConnect({
source: generateNode.id,
@ -339,14 +347,14 @@ export function TutorialOverlay() {
targetHandle: "image",
});
setTimeout(() => {
schedule(() => {
// Add final Output
const finalOutputId = addNode("output", {
x: baseX + 1450,
y: baseY + 350,
});
setTimeout(() => {
schedule(() => {
// Connect Generate Image #2 → Output
onConnect({
source: generateNode2Id,
@ -356,7 +364,7 @@ export function TutorialOverlay() {
});
// Final delay before advancing
setTimeout(() => {
schedule(() => {
completeCurrentStep();
nextTutorialStep();
}, 1000);
@ -377,6 +385,11 @@ export function TutorialOverlay() {
}, 400);
}, 400);
}, 1000);
return () => {
timeoutIds.forEach(clearTimeout);
timeoutIds.length = 0;
};
}
// Reset ref when tutorial ends

Loading…
Cancel
Save