Skip to content

Commit 4ddb648

Browse files
committed
fix(tour): auto-dismiss when user manually navigates away mid-tour
1 parent ed2a59c commit 4ddb648

2 files changed

Lines changed: 59 additions & 6 deletions

File tree

src/dashboard/src/App.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { DEMO_EVENTS } from './components/tour/demoData';
4747
import {
4848
createDashboardTabHref,
4949
getDashboardTabAndSubFromHref,
50+
getDashboardTabFromHref,
5051
type DashboardTab,
5152
} from './tab-routing';
5253
import styles from './App.module.scss';
@@ -425,12 +426,45 @@ function AppContent() {
425426
// expensive when real run data is loaded).
426427
}, [activeTab]);
427428

428-
// The GuidedTour is only ever started by an explicit HOW IT WORKS
429-
// click. The prior auto-start fired on first visit, set tourActive
430-
// true, painted a 55%-black SVG scrim over the whole viewport, and
431-
// then survived any tab navigation the user made before reaching the
432-
// first dismissal control — so a user who clicked VIZ before reading
433-
// the tour card saw a permanently dimmed page with no obvious cause.
429+
// Auto-start the GuidedTour on the user's FIRST visit to the sim
430+
// page so new viewers get oriented without having to find the
431+
// HOW IT WORKS button. Gated on a localStorage flag
432+
// (`paracosm:tourSeen`) so returning users don't get the tour
433+
// replayed every time they open the app.
434+
//
435+
// We set the flag IMMEDIATELY on auto-start fire (not just when
436+
// the tour ends). Reason: React 19's StrictMode double-runs
437+
// effects in dev, SPA navigations / query-param changes can
438+
// remount AppContent, and various dismissal paths (click-away,
439+
// Escape, browser back) don't all reliably call handleTourEnd
440+
// before the component unmounts. Pinning the flag at fire-time
441+
// guarantees once-ever auto-start behavior regardless of how
442+
// the user exits the tour. Manual re-play via HOW IT WORKS still
443+
// works since that path bypasses this effect.
444+
//
445+
// Mobile gate: skip auto-start below 640px.
446+
useEffect(() => {
447+
try {
448+
if (localStorage.getItem('paracosm:tourSeen') === '1') return;
449+
localStorage.setItem('paracosm:tourSeen', '1');
450+
} catch {
451+
return;
452+
}
453+
if (typeof window !== 'undefined' && window.innerWidth < 640) {
454+
return;
455+
}
456+
const timer = setTimeout(() => {
457+
const currentTab = getDashboardTabFromHref(window.location.href);
458+
if (currentTab !== 'quickstart') {
459+
return;
460+
}
461+
preTourTabRef.current = currentTab;
462+
setTourActive(true);
463+
}, 600);
464+
return () => clearTimeout(timer);
465+
// eslint-disable-next-line react-hooks/exhaustive-deps
466+
}, []);
467+
434468

435469
// Chat handoff from the VIZ drilldown. Sets the URL hash so
436470
// ChatPanel can read it on mount or on hashchange, then switches

src/dashboard/src/components/tour/GuidedTour.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,25 @@ export function GuidedTour({ activeTab, chatEnabled = true, onTabChange, onClose
203203
if (s) onTabChange(s.tab);
204204
}, [step, onTabChange, steps]);
205205

206+
// Auto-dismiss when the user manually navigates to a tab the current
207+
// step doesn't target. Without this, clicking VIZ while parked on
208+
// (e.g.) the quickstart step leaves the viewport-wide SVG scrim
209+
// painted on a tab where the highlight target doesn't exist — the
210+
// user sees a permanent dim wash with no card and no obvious
211+
// dismissal affordance. Short delay lets tour-driven onTabChange
212+
// settle before we judge "mismatched" (without it the step-change
213+
// effect above briefly desyncs activeTab and aborts the tour
214+
// mid-progression).
215+
useEffect(() => {
216+
const s = steps[step];
217+
if (!s || s.tab === activeTab) return;
218+
const t = setTimeout(() => {
219+
const stillMismatched = steps[step]?.tab !== activeTab;
220+
if (stillMismatched) onClose();
221+
}, 200);
222+
return () => clearTimeout(t);
223+
}, [activeTab, step, steps, onClose]);
224+
206225
// Highlight target element and measure its rect.
207226
const attemptCancelRef = useRef<(() => void) | null>(null);
208227
const measure = useCallback(() => {

0 commit comments

Comments
 (0)