import React, { useEffect, useMemo } from 'react'; import { TourProvider, useTour } from '@reactour/tour'; import { Hand, GraduationCap, Plus, MessageCircle, Paperclip, BarChart3 } from 'lucide-react'; import { useTheme } from '../contexts/ThemeContext'; import { useAppConfig } from '../contexts/AppConfigContext'; import { TESTING_ONBOARDING } from '../App'; import '../styles/OnboardingTour.css'; const STORAGE_KEY = 'hasSeenOnboardingTour'; // Fallbacks used when config.onboarding.* fields aren't provided by the backend. const DEFAULT_FEATURES = [ { Icon: GraduationCap, label: 'Get advice from specialized AI advisors' }, { Icon: MessageCircle, label: 'Save and revisit every conversation' }, { Icon: Paperclip, label: 'Upload PDFs and documents for context-aware answers' }, { Icon: BarChart3, label: 'Track your progress on a structured canvas' }, ]; const DEFAULT_CANVAS_STEP = { title: 'Progress Canvas', body: 'A dashboard view of your training journey — program progress, nutrition, next steps, all in one place.', }; const buildAdvisorBody = (advisors) => { const names = Object.values(advisors || {}).map((a) => a.name).filter(Boolean); if (names.length === 0) { return "AI personas are ready to help. Click here anytime to see who's available."; } const firstThree = names.slice(0, 3).join(', '); return `${names.length} AI personas are ready to help — ${firstThree}, and more. Click here anytime to see who's available.`; }; const buildFeatures = (config, resolveIcon) => { const fromConfig = config?.onboarding?.features; if (!Array.isArray(fromConfig) || fromConfig.length === 0) return DEFAULT_FEATURES; return fromConfig.map((f) => ({ Icon: f.icon ? resolveIcon(f.icon) : GraduationCap, label: f.label || f.description || f.title || '', })); }; // Theme-resolved colors that match the rest of the app exactly const palette = (isDark) => ({ bg: isDark ? '#1F2937' : '#FFFFFF', bgSubtle: isDark ? '#111827' : '#F9FAFB', text: isDark ? '#F9FAFB' : '#111827', textMuted: isDark ? '#D1D5DB' : '#6B7280', textDim: isDark ? '#9CA3AF' : '#9CA3AF', border: isDark ? '#374151' : '#E5E7EB', accent: '#2663EB', accentGrad: '#2663EB', accentShadow: 'rgba(38, 99, 235, 0.45)', }); // --- Step content ------------------------------------------------------- const titleStyle = (c) => ({ margin: 0, fontSize: 22, fontWeight: 700, lineHeight: 1.2, color: c.text, WebkitTextFillColor: c.text, letterSpacing: '-0.02em', }); const bodyStyle = (c) => ({ margin: 0, fontSize: 15.5, lineHeight: 1.55, color: c.textMuted, WebkitTextFillColor: c.textMuted, }); const StepBody = ({ title, body, Icon, c }) => (
{title}
{body}
); const WelcomeBody = ({ c, title, subtitle, features }) => { return (
Welcome to your
{title}
{subtitle}
{features.map(({ Icon, label }) => (
{label}
))}
); }; const buildSteps = (c, data) => [ { selector: 'body', position: 'center', content: , styles: { maskArea: (base) => ({ ...base, x: -10000, y: -10000, width: 0, height: 0 }), popover: (base) => ({ ...base, maxWidth: 540, minWidth: 460, padding: '32px 30px 24px', borderRadius: 22, background: c.bg, backgroundColor: c.bg, color: c.text, border: `1px solid ${c.border}`, boxShadow: '0 30px 80px -10px rgba(0,0,0,0.5)', }), }, }, { selector: '.advisor-status-button', content: , }, { selector: '.new-chat-button', content: , }, { selector: '.sessions-list', content: , }, { selector: '.enhanced-chat-input-container', content: , }, { selector: '.sidebar-canvas-btn', content: , }, ]; // --- Custom Buttons ----------------------------------------------------- const makeButtons = (c) => { const ghostBtn = { background: 'transparent', color: c.textMuted, border: `1px solid ${c.border}`, padding: '10px 18px', borderRadius: 10, fontSize: 14, fontWeight: 600, cursor: 'pointer', }; const primaryBtn = { background: c.accent, color: '#fff', border: 'none', padding: '10px 22px', borderRadius: 10, fontSize: 14, fontWeight: 600, cursor: 'pointer', boxShadow: `0 4px 14px -4px ${c.accentShadow}`, }; const PrevButton = ({ currentStep, setCurrentStep }) => { if (currentStep === 0) return ; return ( ); }; const NextButton = ({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => { const isFirst = currentStep === 0; const isLast = currentStep === stepsLength - 1; const label = isFirst ? 'Begin tour' : isLast ? 'Get started' : 'Next'; const padded = isFirst ? { ...primaryBtn, padding: '12px 28px', fontSize: 15 } : primaryBtn; return ( ); }; const SkipButton = ({ onClick }) => ( ); return { PrevButton, NextButton, SkipButton }; }; // Auto-opens the tour, writes flag on close. const TourLauncher = () => { const { setIsOpen, isOpen } = useTour(); useEffect(() => { const seen = localStorage.getItem(STORAGE_KEY) === 'true'; if (TESTING_ONBOARDING || !seen) { const t = setTimeout(() => setIsOpen(true), 400); return () => clearTimeout(t); } }, [setIsOpen]); useEffect(() => { if (!isOpen) { if (sessionStorage.getItem('__tourStarted__')) { localStorage.setItem(STORAGE_KEY, 'true'); } } else { sessionStorage.setItem('__tourStarted__', '1'); } }, [isOpen]); return null; }; const OnboardingTour = ({ children }) => { const { isDark } = useTheme(); const { config, advisors, resolveIcon } = useAppConfig(); const c = useMemo(() => palette(isDark), [isDark]); const stepData = useMemo(() => ({ title: config?.app?.title || 'Advisor Panel', subtitle: config?.app?.subtitle || 'AI-Powered Guidance', features: buildFeatures(config, resolveIcon), advisorBody: buildAdvisorBody(advisors), canvas: { title: config?.onboarding?.tour_title || DEFAULT_CANVAS_STEP.title, body: config?.onboarding?.tour_body || DEFAULT_CANVAS_STEP.body, }, }), [config, advisors, resolveIcon]); const steps = useMemo(() => buildSteps(c, stepData), [c, stepData]); const { PrevButton, NextButton, SkipButton } = useMemo(() => makeButtons(c), [c]); return ( ({ ...base, borderRadius: 18, padding: '28px 26px 22px', maxWidth: 440, minWidth: 360, background: c.bg, backgroundColor: c.bg, color: c.text, boxShadow: '0 30px 80px -10px rgba(0,0,0,0.5)', border: `1px solid ${c.border}`, }), maskWrapper: (base) => ({ ...base, color: 'rgba(0, 0, 0, 0.82)' }), maskArea: (base) => ({ ...base, rx: 14 }), controls: (base) => ({ ...base, marginTop: 22, paddingTop: 18, borderTop: `1px solid ${c.border}`, alignItems: 'center', justifyContent: 'space-between', }), dot: (base, { current }) => ({ ...base, width: current ? 24 : 8, height: 8, borderRadius: 999, background: current ? c.accent : c.border, transition: 'all 0.25s ease', }), navigation: (base) => ({ ...base, gap: 7 }), }} > {children} ); }; export default OnboardingTour;