"use client"; import { Button } from "@repo/ui/components/button"; import { GlassMenuEffect } from "@repo/ui/other/glass-effect"; import { AnimatePresence, motion } from "motion/react"; import * as React from "react"; import { analytics } from "@/lib/analytics"; // Types export interface TourStep { content: React.ReactNode; selectorId: string; position?: "top" | "bottom" | "left" | "right" | "center"; onClickWithinArea?: () => void; } interface TourContextType { currentStep: number; totalSteps: number; nextStep: () => void; previousStep: () => void; endTour: () => void; isActive: boolean; isPaused: boolean; startTour: () => void; setSteps: (steps: TourStep[]) => void; steps: TourStep[]; isTourCompleted: boolean; setIsTourCompleted: (completed: boolean) => void; // Expansion state tracking setMenuExpanded: (expanded: boolean) => void; setChatExpanded: (expanded: boolean) => void; } // Context const TourContext = React.createContext(undefined); export function useTour() { const context = React.useContext(TourContext); if (!context) { throw new Error("useTour must be used within a TourProvider"); } return context; } // Provider interface TourProviderProps { children: React.ReactNode; onComplete?: () => void; className?: string; isTourCompleted?: boolean; } export function TourProvider({ children, onComplete, className, isTourCompleted: initialCompleted = false, }: TourProviderProps) { const [currentStep, setCurrentStep] = React.useState(-1); const [steps, setSteps] = React.useState([]); const [isActive, setIsActive] = React.useState(false); const [isTourCompleted, setIsTourCompleted] = React.useState(initialCompleted); // Track expansion states const [isMenuExpanded, setIsMenuExpanded] = React.useState(false); const [isChatExpanded, setIsChatExpanded] = React.useState(false); // Calculate if tour should be paused const isPaused = React.useMemo(() => { return isActive && (isMenuExpanded || isChatExpanded); }, [isActive, isMenuExpanded, isChatExpanded]); const startTour = React.useCallback(() => { console.debug("Starting tour with", steps.length, "steps"); analytics.tourStarted(); setCurrentStep(0); setIsActive(true); }, [steps]); const endTour = React.useCallback(() => { setCurrentStep(-1); setIsActive(false); setIsTourCompleted(true); // Mark tour as completed when ended/skipped analytics.tourSkipped(); if (onComplete) { onComplete(); } }, [onComplete]); const nextStep = React.useCallback(() => { if (currentStep < steps.length - 1) { setCurrentStep(currentStep + 1); } else { analytics.tourCompleted(); endTour(); setIsTourCompleted(true); } }, [currentStep, steps.length, endTour]); const previousStep = React.useCallback(() => { if (currentStep > 0) { setCurrentStep(currentStep - 1); } }, [currentStep]); const setMenuExpanded = React.useCallback((expanded: boolean) => { setIsMenuExpanded(expanded); }, []); const setChatExpanded = React.useCallback((expanded: boolean) => { setIsChatExpanded(expanded); }, []); const value = React.useMemo( () => ({ currentStep, totalSteps: steps.length, nextStep, previousStep, endTour, isActive, isPaused, startTour, setSteps, steps, isTourCompleted, setIsTourCompleted, setMenuExpanded, setChatExpanded, }), [ currentStep, steps, nextStep, previousStep, endTour, isActive, isPaused, startTour, isTourCompleted, setMenuExpanded, setChatExpanded, ], ); return ( {children} {isActive && !isPaused && ( <> {console.log( "Rendering TourHighlight for step:", currentStep, currentStep >= 0 && currentStep < steps.length ? steps[currentStep] : "No step", )} )} ); } // Tour Highlight Component function TourHighlight({ currentStepIndex, steps, className, }: { currentStepIndex: number; steps: TourStep[]; className?: string; }) { const { nextStep, previousStep, endTour } = useTour(); const [elementRect, setElementRect] = React.useState(null); // Get current step safely const step = currentStepIndex >= 0 && currentStepIndex < steps.length ? steps[currentStepIndex] : null; React.useEffect(() => { if (!step) return; // Use requestAnimationFrame to ensure DOM is ready const rafId = requestAnimationFrame(() => { const element = document.getElementById(step.selectorId); console.debug( "Looking for element with ID:", step.selectorId, "Found:", !!element, ); if (element) { const rect = element.getBoundingClientRect(); console.debug("Element rect:", { id: step.selectorId, top: rect.top, left: rect.left, width: rect.width, height: rect.height, bottom: rect.bottom, right: rect.right, }); setElementRect(rect); } }); // Add click listener for onClickWithinArea let clickHandler: ((e: MouseEvent) => void) | null = null; if (step.onClickWithinArea) { const element = document.getElementById(step.selectorId); if (element) { clickHandler = (e: MouseEvent) => { if (element.contains(e.target as Node)) { step.onClickWithinArea?.(); } }; document.addEventListener("click", clickHandler); } } return () => { cancelAnimationFrame(rafId); if (clickHandler) { document.removeEventListener("click", clickHandler); } }; }, [step]); if (!step) return null; // Keep the wrapper mounted but animate the content return ( {elementRect && ( {/* Highlight Border */}
{/* Tooltip with Glass Effect */} { const baseTop = step.position === "bottom" ? elementRect.bottom + 8 : step.position === "top" ? elementRect.top - 200 : elementRect.top + elementRect.height / 2 - 100; // Ensure tooltip stays within viewport const maxTop = window.innerHeight - 250; // Leave space for tooltip height const minTop = 10; return Math.max(minTop, Math.min(baseTop, maxTop)); })(), left: (() => { const baseLeft = step.position === "right" ? elementRect.right + 8 : step.position === "left" ? elementRect.left - 300 : elementRect.left + elementRect.width / 2 - 150; // Ensure tooltip stays within viewport const maxLeft = window.innerWidth - 300; // Tooltip width const minLeft = 10; return Math.max(minLeft, Math.min(baseLeft, maxLeft)); })(), }} > {/* Glass effect background */} {/* Content */}
{step.content}
{currentStepIndex + 1} / {steps.length}
{currentStepIndex > 0 && ( )}
)} ); } // Tour Alert Dialog interface TourAlertDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) { const { startTour, setIsTourCompleted } = useTour(); const handleStart = () => { console.debug("TourAlertDialog: Starting tour"); onOpenChange(false); startTour(); }; const handleSkip = () => { analytics.tourSkipped(); setIsTourCompleted(true); // Mark tour as completed when skipped onOpenChange(false); }; if (!open) return null; return ( {/* Glass effect background */} {/* Content */}

Welcome to supermemory™

This is your personal knowledge graph where all your memories are stored and connected. Let's take a quick tour to help you get familiar with the interface.

); }