diff --git a/src/components/onboarding/FTUXModelDefaultsStep.tsx b/src/components/onboarding/FTUXModelDefaultsStep.tsx index b5cb9fe3..d5573b6a 100644 --- a/src/components/onboarding/FTUXModelDefaultsStep.tsx +++ b/src/components/onboarding/FTUXModelDefaultsStep.tsx @@ -106,7 +106,7 @@ export function FTUXModelDefaultsStep({}: FTUXStepProps) { > ) : ( <> - Gemini nano-banana-pro + None set setShowImageDialog(true)} diff --git a/src/components/onboarding/FTUXReadyStep.tsx b/src/components/onboarding/FTUXReadyStep.tsx index 9526d7fb..953a7dce 100644 --- a/src/components/onboarding/FTUXReadyStep.tsx +++ b/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 ( diff --git a/src/components/onboarding/TutorialOverlay.tsx b/src/components/onboarding/TutorialOverlay.tsx index 2dd464da..472bbe69 100644 --- a/src/components/onboarding/TutorialOverlay.tsx +++ b/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[]>([]); 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