'use client'; /** * AppStateMachine Provider * * Wraps the application with the app-level state machine. * Provides state and send function to all child components via context. * Also handles responsive mode detection and route synchronization. */ import { useEffect, useRef } from 'react'; import { useSelector } from '@xstate/react'; import { createActor } from 'xstate'; import { usePathname, useRouter } from 'next/navigation'; import { useMediaQuery } from '@mantine/hooks'; import { appMachine } from '@/lib/app-machine'; import { AppMachineContext } from '@/hooks/useAppMachine'; // Create the actor singleton outside the component to persist state const appActor = createActor(appMachine); appActor.start(); export function AppStateMachineProvider({ children }: { children: React.ReactNode }) { const state = useSelector(appActor, (state) => state); const send = appActor.send; const pathname = usePathname(); const router = useRouter(); // Track if this is the initial mount const isInitializedRef = useRef(false); // Track the last path we navigated to, to prevent loops const lastNavigatedPathRef = useRef(null); // Detect mobile vs desktop const isMobile = useMediaQuery('(max-width: 768px)'); // Update mode in state machine useEffect(() => { send({ type: 'SET_MODE', mode: isMobile ? 'mobile' : 'desktop' }); }, [isMobile, send]); // Initialize state machine from URL on first mount ONLY useEffect(() => { if (isInitializedRef.current) return; console.log('[App Provider] Initializing state from URL:', pathname); // Determine which state the current path corresponds to let initialEvent: string | null = null; if (pathname === '/chat') { initialEvent = 'NAVIGATE_TO_CONVO'; } else if (pathname === '/edit') { initialEvent = 'NAVIGATE_TO_EDIT'; } else if (pathname === '/galaxy' || pathname.startsWith('/galaxy/')) { initialEvent = 'NAVIGATE_TO_GALAXY'; } // Send the event to initialize state from URL if (initialEvent) { console.log('[App Provider] Setting initial state:', initialEvent); send({ type: initialEvent as any }); } else { // No specific route matched, we're probably on homepage or other route // Mark as initialized immediately so we don't interfere isInitializedRef.current = true; } // DON'T mark as initialized yet if we sent an event // Wait for the state change to complete }, [pathname, send]); // Remove 'state' from dependencies! // Mark as initialized once state matches the target from URL useEffect(() => { if (isInitializedRef.current) return; const targetStateForPath = pathname === '/chat' ? 'convo' : pathname === '/edit' ? 'edit' : (pathname === '/galaxy' || pathname.startsWith('/galaxy/')) ? 'galaxy' : null; if (targetStateForPath && state.matches(targetStateForPath)) { console.log('[App Provider] State initialized from URL, marking as ready'); isInitializedRef.current = true; } }, [pathname, state]); // State machine is source of truth: sync state → URL only // This effect ONLY runs when state changes, not when pathname changes useEffect(() => { // Don't navigate until initialized if (!isInitializedRef.current) { return; } let targetPath: string | null = null; if (state.matches('convo')) { targetPath = '/chat'; } else if (state.matches('edit')) { targetPath = '/edit'; } else if (state.matches('galaxy')) { targetPath = '/galaxy'; } // ONLY navigate if we need to change the pathname (not the whole URL) // This preserves query params and prevents unnecessary navigation if (targetPath && pathname !== targetPath) { console.log('[App Provider] State machine navigating to:', targetPath); lastNavigatedPathRef.current = targetPath; router.push(targetPath); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.value]); // ONLY depend on state.value, NOT pathname or router! // Log state changes useEffect(() => { console.log('[App Provider] State:', state.value); console.log('[App Provider] Tags:', Array.from(state.tags)); console.log('[App Provider] Context:', state.context); }, [state]); return ( {children} ); }