diff --git a/.claude/memory.md b/.claude/memory.md index 78e063821..7bd4064ae 100644 --- a/.claude/memory.md +++ b/.claude/memory.md @@ -46,6 +46,7 @@ Quick reference for anyone starting with Claude on this project. Updated by the - **Notification z-index stacking** — ErrorReportNotification: z-[10000] bottom-right. OnboardingOverlay: z-[9999]. LocalAIDownloadSnackbar: z-[9998] bottom-left. - **React Compiler lint** — `useCallback` deps must match the full inferred closure. Using `user?._id` as dep when the closure captures `user` triggers `preserve-manual-memoization`. Use `user` as the dep instead. - **`setState` in effects** — ESLint `react-hooks/set-state-in-effect` catches synchronous setState in useEffect bodies. Use lazy initializers, compute at render, or event handlers instead. +- **Walkthrough is multi-page (9 steps)** — Uses react-joyride v3 `Step.before` async hooks to navigate between pages (`/home → /chat → /skills → /intelligence → /settings → /home`). Steps factory: `createWalkthroughSteps(navigate)` in `walkthroughSteps.ts`. `waitForTarget(selector, timeout)` polls via rAF until DOM target appears. Re-trigger from Settings via `resetWalkthrough()` + `walkthrough:restart` CustomEvent. `AppWalkthrough` is mounted inside Router context (can use `useNavigate` directly). BottomTabBar attr is `tab-notifications` (not `tab-automation`). - **`OnboardingNextButton` is the shared primary CTA** — All onboarding steps use `app/src/pages/onboarding/components/OnboardingNextButton.tsx`. New steps must use this component for the primary navigation button. - **Onboarding is 3 steps: Welcome(0) → Skills(1) → ContextGathering(2)** — Referral step was removed (issue #752). `ReferralApplyStep.tsx` is preserved but unused. `referralApi` is still used on the Rewards page. `WelcomeStep` no longer has `nextDisabled`/`nextLoading`/`nextLoadingLabel` props (those gated on referral stats prefetch). - **Recovery Phrase moved to Settings** — MnemonicStep was removed from onboarding (was step 5). The same BIP39 generate/import functionality now lives in `app/src/components/settings/panels/RecoveryPhrasePanel.tsx`, accessible via Settings > Recovery Phrase. Onboarding completion logic moved into `handleSkillsNext` in `Onboarding.tsx`. diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx index 16aee9bb4..1e565644d 100644 --- a/app/src/components/BottomTabBar.tsx +++ b/app/src/components/BottomTabBar.tsx @@ -218,11 +218,11 @@ const BottomTabBar = () => { const active = isActive(tab.path); const showBadge = tab.id === 'notifications' && unreadCount > 0; // data-walkthrough attributes for the Joyride walkthrough steps. - // Maps tab ids to their walkthrough target names (steps 3–6). + // Maps tab ids to their walkthrough target names. const walkthroughAttr: Record = { chat: 'tab-chat', skills: 'tab-skills', - notifications: 'tab-automation', + notifications: 'tab-notifications', settings: 'tab-settings', }; return ( diff --git a/app/src/components/settings/SettingsHome.tsx b/app/src/components/settings/SettingsHome.tsx index ed48431cb..364fe686f 100644 --- a/app/src/components/settings/SettingsHome.tsx +++ b/app/src/components/settings/SettingsHome.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useCoreState } from '../../providers/CoreStateProvider'; import { persistor } from '../../store'; @@ -9,11 +10,13 @@ import { restartApp, scheduleCefProfilePurge, } from '../../utils/tauriCommands'; +import { resetWalkthrough } from '../walkthrough/AppWalkthrough'; import SettingsHeader from './components/SettingsHeader'; import SettingsMenuItem from './components/SettingsMenuItem'; import { useSettingsNavigation } from './hooks/useSettingsNavigation'; const SettingsHome = () => { + const navigate = useNavigate(); const { navigateToSettings } = useSettingsNavigation(); const { clearSession, snapshot } = useCoreState(); const [showLogoutAndClearModal, setShowLogoutAndClearModal] = useState(false); @@ -200,6 +203,26 @@ const SettingsHome = () => { onClick: () => navigateToSettings('notification-routing'), dangerous: false, }, + { + id: 'restart-tour', + title: 'Restart Tour', + description: 'Replay the product walkthrough from the beginning', + icon: ( + + + + ), + onClick: () => { + resetWalkthrough(); + navigate('/home'); + }, + dangerous: false, + }, { id: 'about', title: 'About', @@ -276,7 +299,9 @@ const SettingsHome = () => { return (
- +
+ +
{/* Grouped Settings */} diff --git a/app/src/components/walkthrough/AppWalkthrough.tsx b/app/src/components/walkthrough/AppWalkthrough.tsx index 6124dc399..db533d1cc 100644 --- a/app/src/components/walkthrough/AppWalkthrough.tsx +++ b/app/src/components/walkthrough/AppWalkthrough.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { type EventData, EVENTS, Joyride, STATUS } from 'react-joyride'; +import { useNavigate } from 'react-router-dom'; -import { WALKTHROUGH_STEPS } from './walkthroughSteps'; +import { createWalkthroughSteps } from './walkthroughSteps'; import WalkthroughTooltip from './WalkthroughTooltip'; // ── localStorage keys ────────────────────────────────────────────────────── @@ -63,23 +64,61 @@ export function markWalkthroughComplete(): void { } } +/** + * Resets the walkthrough so it will play again on next visit to /home. + * + * - Removes the completed flag from localStorage. + * - Sets the pending flag so `isWalkthroughPending()` returns true. + * - Dispatches a `CustomEvent('walkthrough:restart')` on `window` so any + * mounted `AppWalkthrough` instance can react and restart immediately. + */ +export function resetWalkthrough(): void { + try { + localStorage.removeItem(WALKTHROUGH_KEY); + localStorage.setItem(WALKTHROUGH_PENDING_KEY, 'true'); + console.debug('[walkthrough] reset — pending flag set, completed flag removed'); + } catch (e) { + console.warn('[walkthrough] could not reset walkthrough in localStorage', e); + } + window.dispatchEvent(new CustomEvent('walkthrough:restart')); +} + // ── Component ────────────────────────────────────────────────────────────── /** * Renders the post-onboarding Joyride walkthrough overlay (react-joyride v3). * - * Only mounts the Joyride instance when `isWalkthroughPending()` is true. - * On finish or skip (EVENTS.TOUR_END), calls `markWalkthroughComplete()` so - * it never shows again. + * Mounts the Joyride instance when `isWalkthroughPending()` is true or when a + * `walkthrough:restart` event is received. On finish or skip (EVENTS.TOUR_END), + * calls `markWalkthroughComplete()` so the tour never shows again until reset. * - * Mount this inside the Home page so it runs after the tab bar and home card - * are in the DOM (all `data-walkthrough="*"` targets must exist). + * Mount this inside the Router context so `useNavigate` is available. The + * steps include `before` hooks that navigate to other pages before focusing + * the target element. */ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => { + const navigate = useNavigate(); + // Only start running if the walkthrough is pending on first render. // Using a lazy initializer keeps this stable across re-renders. const [run, setRun] = useState(() => isWalkthroughPending(onboarded)); + // Memoize steps so they are only recreated when `navigate` identity changes. + const steps = useMemo(() => createWalkthroughSteps(navigate), [navigate]); + + // Listen for the `walkthrough:restart` custom event (dispatched by + // `resetWalkthrough()`) and restart the tour immediately. + useEffect(() => { + const handleRestart = () => { + console.debug('[walkthrough] restart event received — restarting tour'); + setRun(true); + }; + window.addEventListener('walkthrough:restart', handleRestart); + return () => { + window.removeEventListener('walkthrough:restart', handleRestart); + }; + }, []); + const handleEvent = (data: EventData) => { const { type, status } = data; console.debug('[walkthrough] event', { type, status, index: data.index }); @@ -98,7 +137,7 @@ const AppWalkthrough = ({ onboarded = false }: { onboarded?: boolean }) => { return ( { }); }); +// ── resetWalkthrough tests ──────────────────────────────────────────────── + +describe('resetWalkthrough', () => { + it('removes completed flag and sets pending flag in localStorage', () => { + localStorage.setItem(WALKTHROUGH_KEY, 'true'); + localStorage.setItem(WALKTHROUGH_PENDING_KEY, 'false'); + + resetWalkthrough(); + + expect(localStorage.getItem(WALKTHROUGH_KEY)).toBeNull(); + expect(localStorage.getItem(WALKTHROUGH_PENDING_KEY)).toBe('true'); + }); + + it('dispatches walkthrough:restart CustomEvent on window', () => { + const handler = vi.fn(); + window.addEventListener('walkthrough:restart', handler); + + try { + resetWalkthrough(); + expect(handler).toHaveBeenCalledTimes(1); + } finally { + window.removeEventListener('walkthrough:restart', handler); + } + }); + + it('swallows localStorage errors but still dispatches the event', () => { + const realStorage = globalThis.localStorage; + const handler = vi.fn(); + window.addEventListener('walkthrough:restart', handler); + + Object.defineProperty(globalThis, 'localStorage', { + value: { + ...realStorage, + removeItem() { + throw new DOMException('QuotaExceededError', 'QuotaExceededError'); + }, + setItem() { + throw new DOMException('QuotaExceededError', 'QuotaExceededError'); + }, + }, + configurable: true, + writable: true, + }); + + try { + expect(() => resetWalkthrough()).not.toThrow(); + // Even if localStorage fails, the event must still be dispatched. + expect(handler).toHaveBeenCalledTimes(1); + } finally { + window.removeEventListener('walkthrough:restart', handler); + Object.defineProperty(globalThis, 'localStorage', { + value: realStorage, + configurable: true, + writable: true, + }); + } + }); +}); + // ── AppWalkthrough component tests ──────────────────────────────────────── describe('AppWalkthrough component', () => { @@ -188,7 +253,11 @@ describe('AppWalkthrough component', () => { setWalkthroughPending(); const { default: AppWalkthrough } = await import('../AppWalkthrough'); - render(); + render( + + + + ); expect(screen.getByTestId('joyride-mock')).toBeInTheDocument(); expect(screen.getByTestId('joyride-mock').getAttribute('data-run')).toBe('true'); @@ -198,7 +267,11 @@ describe('AppWalkthrough component', () => { // No pending flag set const { default: AppWalkthrough } = await import('../AppWalkthrough'); - const { container } = render(); + const { container } = render( + + + + ); expect(container.firstChild).toBeNull(); }); @@ -209,7 +282,11 @@ describe('AppWalkthrough component', () => { localStorage.setItem(WALKTHROUGH_KEY, 'true'); const { default: AppWalkthrough } = await import('../AppWalkthrough'); - const { container } = render(); + const { container } = render( + + + + ); expect(container.firstChild).toBeNull(); }); @@ -218,14 +295,18 @@ describe('AppWalkthrough component', () => { setWalkthroughPending(); const { default: AppWalkthrough } = await import('../AppWalkthrough'); - render(); + render( + + + + ); // Joyride should be running initially expect(screen.getByTestId('joyride-mock').getAttribute('data-run')).toBe('true'); // Simulate TOUR_END with FINISHED status await act(async () => { - capturedOnEvent?.({ type: 'tour:end', status: 'finished', index: 5 }); + capturedOnEvent?.({ type: 'tour:end', status: 'finished', index: 8 }); }); // Walkthrough should be marked complete in localStorage @@ -237,7 +318,11 @@ describe('AppWalkthrough component', () => { setWalkthroughPending(); const { default: AppWalkthrough } = await import('../AppWalkthrough'); - render(); + render( + + + + ); expect(screen.getByTestId('joyride-mock').getAttribute('data-run')).toBe('true'); @@ -254,7 +339,11 @@ describe('AppWalkthrough component', () => { setWalkthroughPending(); const { default: AppWalkthrough } = await import('../AppWalkthrough'); - render(); + render( + + + + ); // Simulate a step:after event (not tour:end) await act(async () => { @@ -266,6 +355,31 @@ describe('AppWalkthrough component', () => { // Still running expect(screen.getByTestId('joyride-mock')).toBeInTheDocument(); }); + + it('restarts the tour when walkthrough:restart event is dispatched', async () => { + // Start with walkthrough completed — component renders nothing initially. + localStorage.setItem(WALKTHROUGH_KEY, 'true'); + + const { default: AppWalkthrough } = await import('../AppWalkthrough'); + const { container } = render( + + + + ); + + // Should not be rendering joyride since completed. + expect(container.firstChild).toBeNull(); + + // Simulate resetWalkthrough() — clears completed, sets pending, fires event. + await act(async () => { + localStorage.removeItem(WALKTHROUGH_KEY); + localStorage.setItem(WALKTHROUGH_PENDING_KEY, 'true'); + window.dispatchEvent(new CustomEvent('walkthrough:restart')); + }); + + // Component should now render the Joyride instance. + expect(screen.getByTestId('joyride-mock')).toBeInTheDocument(); + }); }); /** Build the minimal props required by WalkthroughTooltip without fighting the full TooltipRenderProps type. */ @@ -336,9 +450,9 @@ describe('WalkthroughTooltip', () => { }); it('renders step counter showing current step of total', () => { - render(); + render(); - expect(screen.getByText('2 of 6')).toBeInTheDocument(); + expect(screen.getByText('2 of 9')).toBeInTheDocument(); }); it('shows Skip button when not on last step', () => { @@ -379,38 +493,137 @@ describe('WalkthroughTooltip', () => { it('renders progress bar', () => { const { container } = render( - + ); - // Gradient progress bar fills based on step progress + // Gradient progress bar fills based on step progress (3/9 ≈ 33.33%) const bar = container.querySelector('div.bg-gradient-to-r'); expect(bar).not.toBeNull(); - expect(bar?.getAttribute('style')).toContain('width: 50%'); + // width rounds to ~33.33% for step 3 of 9 + expect(bar?.getAttribute('style')).toMatch(/width:\s*33\.3/); }); }); -// ── walkthroughSteps tests ──────────────────────────────────────────────── +// ── createWalkthroughSteps tests ────────────────────────────────────────── -describe('WALKTHROUGH_STEPS', () => { - it('has 6 steps', () => { - expect(WALKTHROUGH_STEPS).toHaveLength(6); +describe('createWalkthroughSteps', () => { + it('returns 10 steps', () => { + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + expect(steps).toHaveLength(10); }); - it('first step targets home-card and disables beacon', () => { - const first = WALKTHROUGH_STEPS[0]; - expect(first.target).toBe('[data-walkthrough="home-card"]'); - expect(first.skipBeacon).toBe(true); + it('first step targets home-card', () => { + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + expect(steps[0].target).toBe('[data-walkthrough="home-card"]'); }); it('last step targets tab-settings', () => { - const last = WALKTHROUGH_STEPS[WALKTHROUGH_STEPS.length - 1]; + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + const last = steps[steps.length - 1]; expect(last.target).toBe('[data-walkthrough="tab-settings"]'); }); it('all steps have a title and content', () => { - for (const step of WALKTHROUGH_STEPS) { + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + for (const step of steps) { expect(step.title).toBeTruthy(); expect(step.content).toBeTruthy(); } }); + + it('cross-page steps have before functions', () => { + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + + // Steps: 2=chat, 3=integrations, 4=channels, 5=intelligence, 6=settings, 7=home-return + const crossPageIndices = [2, 3, 4, 5, 6, 7]; + for (const idx of crossPageIndices) { + expect(typeof steps[idx].before, `step[${idx}] should have a before fn`).toBe('function'); + } + }); + + it('home-only steps do not have before functions', () => { + const navigate = vi.fn(); + const steps = createWalkthroughSteps(navigate); + + // Steps: 0=home-card, 1=home-cta, 8=tab-notifications, 9=tab-settings + const homeOnlyIndices = [0, 1, 8, 9]; + for (const idx of homeOnlyIndices) { + expect(steps[idx].before, `step[${idx}] should not have a before fn`).toBeUndefined(); + } + }); + + it.each([ + { idx: 2, route: '/chat', target: 'chat-agent-panel' }, + { idx: 3, route: '/skills', target: 'skills-grid' }, + { idx: 4, route: null, target: 'skills-channels' }, + { idx: 5, route: '/intelligence', target: 'intelligence-header' }, + { idx: 6, route: '/settings', target: 'settings-menu' }, + { idx: 7, route: '/home', target: 'tab-chat' }, + ])('before hook for step $idx calls navigate("$route")', async ({ idx, route, target }) => { + const navigate = vi.fn(); + + const el = document.createElement('div'); + el.setAttribute('data-walkthrough', target); + document.body.appendChild(el); + + try { + const steps = createWalkthroughSteps(navigate); + await (steps[idx].before as unknown as (() => Promise) | undefined)?.(); + if (route) { + expect(navigate).toHaveBeenCalledWith(route); + } + } finally { + document.body.removeChild(el); + } + }); +}); + +// ── waitForTarget tests ─────────────────────────────────────────────────── + +describe('waitForTarget', () => { + it('resolves immediately when element already exists in the DOM', async () => { + const el = document.createElement('div'); + el.setAttribute('data-walkthrough', 'test-target'); + document.body.appendChild(el); + + try { + await expect(waitForTarget('test-target')).resolves.toBeUndefined(); + } finally { + document.body.removeChild(el); + } + }); + + it('resolves when element is added to DOM after a delay', async () => { + vi.useFakeTimers(); + const el = document.createElement('div'); + el.setAttribute('data-walkthrough', 'async-target'); + + const promise = waitForTarget('async-target', 500); + + // Add element after 100ms (two poll intervals). + setTimeout(() => document.body.appendChild(el), 100); + await vi.advanceTimersByTimeAsync(150); + + try { + await expect(promise).resolves.toBeUndefined(); + } finally { + vi.useRealTimers(); + if (el.parentNode) document.body.removeChild(el); + } + }); + + it('rejects when element is not found before timeout', async () => { + vi.useFakeTimers(); + const promise = waitForTarget('nonexistent-target', 100).catch((e: Error) => e); + await vi.advanceTimersByTimeAsync(150); + const result = await promise; + expect(result).toBeInstanceOf(Error); + expect((result as Error).message).toContain('[walkthrough] waitForTarget timed out'); + vi.useRealTimers(); + }); }); diff --git a/app/src/components/walkthrough/walkthroughSteps.ts b/app/src/components/walkthrough/walkthroughSteps.ts index 6032e3d0f..da8cd0137 100644 --- a/app/src/components/walkthrough/walkthroughSteps.ts +++ b/app/src/components/walkthrough/walkthroughSteps.ts @@ -1,54 +1,171 @@ import type { Step } from 'react-joyride'; +import type { NavigateFunction } from 'react-router-dom'; /** - * Step definitions for the post-onboarding product walkthrough. - * Targets must match `data-walkthrough="..."` attributes in the DOM. + * Polls via setTimeout until `[data-walkthrough=""]` appears in the + * DOM, then resolves. Rejects after `timeout` ms (default 3000). * - * Copy is conversational and warm — matching OpenHuman's "calm sophistication" - * design language. Each step has an emoji accent for visual interest. + * Uses setTimeout (not rAF) so tests can advance time with fake timers. */ -export const WALKTHROUGH_STEPS: Step[] = [ - { - target: '[data-walkthrough="home-card"]', - title: 'Your command center', - content: - "Everything starts here — your connections, your conversations, your AI. Think of this as mission control. Let's take a quick look around.", - placement: 'bottom', - skipBeacon: true, - }, - { - target: '[data-walkthrough="home-cta"]', - title: 'Say hello', - content: - "This is the fastest way to talk to your AI assistant. Try asking it to summarize your emails, draft a message, or just say hi — it's surprisingly good at small talk.", - placement: 'bottom', - }, - { - target: '[data-walkthrough="tab-chat"]', - title: 'Conversations that remember', - content: - 'Every chat is saved and searchable. Your assistant remembers context across conversations, so you can pick up right where you left off.', - placement: 'top', - }, - { - target: '[data-walkthrough="tab-skills"]', - title: 'Supercharge your assistant', - content: - 'Connect Gmail, Slack, WhatsApp, and more. The more you connect, the more your assistant can actually do — not just talk about doing.', - placement: 'top', - }, - { - target: '[data-walkthrough="tab-automation"]', - title: 'Set it and forget it', - content: - 'Morning briefings, scheduled check-ins, proactive alerts. Your assistant can work for you even when you are not looking.', - placement: 'top', - }, - { - target: '[data-walkthrough="tab-settings"]', - title: "You're in control", - content: - "That's the quick tour! You can always find settings, billing, and preferences here. Now go explore — your assistant is ready when you are.", - placement: 'top', - }, -]; +export function waitForTarget(selector: string, timeout = 3000): Promise { + const POLL_INTERVAL = 50; + + return new Promise((resolve, reject) => { + let elapsed = 0; + + function check() { + if (document.querySelector(`[data-walkthrough="${selector}"]`)) { + resolve(); + return; + } + elapsed += POLL_INTERVAL; + if (elapsed >= timeout) { + reject( + new Error(`[walkthrough] waitForTarget timed out: [data-walkthrough="${selector}"]`) + ); + return; + } + setTimeout(check, POLL_INTERVAL); + } + + // Initial check — element may already be present. + if (document.querySelector(`[data-walkthrough="${selector}"]`)) { + resolve(); + return; + } + setTimeout(check, POLL_INTERVAL); + }); +} + +/** + * Factory that produces the 9-step walkthrough sequence. + * + * Steps that navigate to a different page receive a `before` async hook that + * calls `navigate(path)` and then waits for the target element to appear in + * the DOM via `waitForTarget`. + * + * All targets follow the `[data-walkthrough=""]` convention — add the + * attribute to the corresponding DOM element in the page/component. + */ +export function createWalkthroughSteps(navigate: NavigateFunction): Step[] { + return [ + // ── Step 1 — /home ──────────────────────────────────────────────────── + { + target: '[data-walkthrough="home-card"]', + title: 'Your command center', + content: + "This is your home base — a quick snapshot of what's happening and what needs your attention.", + placement: 'bottom', + skipBeacon: true, + }, + + // ── Step 2 — /home ──────────────────────────────────────────────────── + { + target: '[data-walkthrough="home-cta"]', + title: 'Say hello', + content: 'Tap here to start a conversation with your AI assistant anytime.', + placement: 'bottom', + skipBeacon: true, + }, + + // ── Step 3 — /chat ──────────────────────────────────────────────────── + { + target: '[data-walkthrough="chat-agent-panel"]', + title: 'Meet your AI', + content: + 'This is where conversations happen. Ask questions, get summaries, or brainstorm. Everything stays searchable.', + placement: 'bottom', + skipBeacon: true, + before: async () => { + navigate('/chat'); + await waitForTarget('chat-agent-panel'); + }, + }, + + // ── Step 4 — /skills ────────────────────────────────────────────────── + { + target: '[data-walkthrough="skills-grid"]', + title: 'Connect your world', + content: + 'Gmail, Slack, WhatsApp, and more — each connection gives your assistant superpowers.', + placement: 'top', + skipBeacon: true, + before: async () => { + navigate('/skills'); + await waitForTarget('skills-grid'); + }, + }, + + // ── Step 5 — /skills (channels) ───────────────────────────────────── + { + target: '[data-walkthrough="skills-channels"]', + title: 'Chat where you already are', + content: + 'WhatsApp, Telegram, Slack, Discord — connect your messaging apps so your assistant can reach you anywhere.', + placement: 'bottom', + skipBeacon: true, + before: async () => { + await waitForTarget('skills-channels'); + }, + }, + + // ── Step 6 — /intelligence ──────────────────────────────────────────── + { + target: '[data-walkthrough="intelligence-header"]', + title: "Your assistant's brain", + content: + 'This is where your assistant learns and remembers. It gets smarter the more you use it.', + placement: 'bottom', + skipBeacon: true, + before: async () => { + navigate('/intelligence'); + await waitForTarget('intelligence-header'); + }, + }, + + // ── Step 6 — /settings ──────────────────────────────────────────────── + { + target: '[data-walkthrough="settings-menu"]', + title: 'Make it yours', + content: + 'Preferences, privacy, notifications — everything is here. You can restart this tour anytime from this page.', + placement: 'top', + skipBeacon: true, + before: async () => { + navigate('/settings'); + await waitForTarget('settings-menu'); + }, + }, + + // ── Step 7 — /home ──────────────────────────────────────────────────── + { + target: '[data-walkthrough="tab-chat"]', + title: 'Quick access', + content: 'These tabs are your shortcuts — always one tap away.', + placement: 'top', + skipBeacon: true, + before: async () => { + navigate('/home'); + await waitForTarget('tab-chat'); + }, + }, + + // ── Step 8 — /home (already there) ─────────────────────────────────── + { + target: '[data-walkthrough="tab-notifications"]', + title: 'Stay in the loop', + content: 'Alerts and automations live here — briefings, notifications, background activity.', + placement: 'top', + skipBeacon: true, + }, + + // ── Step 9 — /home (already there) ─────────────────────────────────── + { + target: '[data-walkthrough="tab-settings"]', + title: "That's the tour!", + content: "You're all set! Go explore. Restart this tour anytime from Settings.", + placement: 'top', + skipBeacon: true, + }, + ]; +} diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index ddcfd36ba..7fca492f9 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -1061,7 +1061,9 @@ const Conversations = ({ variant = 'page' }: ConversationsProps = {}) => { lockdown (#883) so the onboarding chat is just the conversation with no chrome around it. */} {!isSidebar && ( -
+