import VrApp from '../../vrApp'; import React, {useEffect, useState} from "react"; import {Affix, Burger, Group, Menu, Alert, Button, Text} from "@mantine/core"; import VrTemplate from "../vrTemplate"; import {IconStar, IconInfoCircle, IconMessageCircle} from "@tabler/icons-react"; import VrMenuItem from "../components/vrMenuItem"; import CreateDiagramModal from "./createDiagramModal"; import ManageDiagramsModal from "./manageDiagramsModal"; import {useNavigate, useParams} from "react-router-dom"; import {useDisclosure} from "@mantine/hooks"; import ConfigModal from "./configModal"; import log from "loglevel"; import {useFeatureState, useUserTier} from "../hooks/useFeatures"; import {useAuth0} from "@auth0/auth0-react"; import {GUEST_MODE_BANNER} from "../../content/upgradeCopy"; import {exportDiagramAsJSON} from "../../util/functions/exportDiagramAsJSON"; import {isMobileVRDevice} from "../../util/deviceDetection"; import {DefaultScene} from "../../defaultScene"; import VREntryPrompt from "../components/VREntryPrompt"; import ComingSoonBadge from "../components/ComingSoonBadge"; import UpgradeBadge from "../components/UpgradeBadge"; import ChatPanel from "../components/ChatPanel"; import {getDbType} from "../../util/functions/getPath"; import PouchDB from 'pouchdb'; import {v4} from "uuid"; let vrApp: VrApp = null; const defaultCreate = window.localStorage.getItem('createOpened') === 'true'; const defaultConfig = window.localStorage.getItem('configOpened') === 'true'; const defaultManage = window.localStorage.getItem('manageOpened') === 'true'; const defaultMenuOpened = window.localStorage.getItem('menuOpened') !== 'false'; // Default to true (open) export default function VrExperience() { const logger = log.getLogger('vrExperience'); const params = useParams(); const { isAuthenticated, loginWithRedirect } = useAuth0(); const [guestBannerDismissed, setGuestBannerDismissed] = useState(false); const [menuOpened, setMenuOpened] = useState(defaultMenuOpened); // Feature flags - get states instead of just enabled boolean const createDiagramState = useFeatureState('createDiagram'); const createFromTemplateState = useFeatureState('createFromTemplate'); const manageDiagramsState = useFeatureState('manageDiagrams'); const shareCollaborateState = useFeatureState('shareCollaborate'); console.log('[Share] shareCollaborateState:', shareCollaborateState); const editDataState = useFeatureState('editData'); const configState = useFeatureState('config'); const enterImmersiveState = useFeatureState('enterImmersive'); const launchMetaQuestState = useFeatureState('launchMetaQuest'); const userTier = useUserTier(); // Helper to check if feature should be shown (not 'off') const shouldShow = (state) => state !== 'off'; const isEnabled = (state) => state === 'on'; // Get the appropriate click handler based on feature state const getClickHandler = (state, enabledHandler) => { if (state === 'on') { return enabledHandler; // Feature is enabled, use the normal handler } if (state === 'basic') { return handleSignUp; // Feature requires sign up, trigger auth } // For 'coming-soon', 'pro', or other states, no click handler return null; }; const handleSignUp = () => { loginWithRedirect({ appState: { returnTo: window.location.pathname } }); }; const handleExportJSON = async () => { try { await exportDiagramAsJSON(dbName); logger.info('Diagram exported successfully'); } catch (error) { logger.error('Failed to export diagram:', error); } }; const saveState = (key, value) => { logger.debug('saving', key, value) window.localStorage.setItem(key, value ? 'true' : 'false'); } const toggleMenu = () => { const newState = !menuOpened; setMenuOpened(newState); window.localStorage.setItem('menuOpened', newState ? 'true' : 'false'); }; const [createOpened, {open: openCreate, close: closeCreate}] = useDisclosure(defaultCreate, { onOpen: () => { saveState('createOpened', true) }, onClose: () => { saveState('createOpened', false) } }); const [manageOpened, {open: openManage, close: closeManage}] = useDisclosure( defaultManage, { onOpen: () => { saveState('manageOpened', true) }, onClose: () => { saveState('manageOpened', false) } }) const [configOpened, {open: openConfig, close: closeConfig}] = useDisclosure( defaultConfig, { onOpen: () => { saveState('configOpened', true) }, onClose: () => { saveState('configOpened', false) } }) const [rerender, setRerender] = useState(0); const [dbName, setDbName] = useState(params.db); const [showVRPrompt, setShowVRPrompt] = useState(false); const [chatOpen, setChatOpen] = useState(!isMobileVRDevice()); // Show chat by default on desktop // Handle share based on current database type: // - Public: copy current URL to clipboard // - Local: create one-time copy to public, share new URL // - Private: show "not yet supported" message const handleShare = async () => { const dbType = getDbType(); logger.info(`[Share] Sharing diagram - type: ${dbType}, dbName: ${dbName}`); if (dbType === 'private') { alert('Sharing private diagrams is not yet supported.\n\nPrivate diagram sharing with access control is coming soon!'); return; } if (dbType === 'local') { // Create a one-time copy to public logger.info('[Share] Creating public copy of local diagram...'); try { // Generate new ID for the public copy const publicDbName = 'diagram-' + v4(); const localDb = new PouchDB(dbName); const remoteUrl = `${window.location.origin}/pouchdb/public-${publicDbName}`; const remoteDb = new PouchDB(remoteUrl); // Get all docs from local database const allDocs = await localDb.allDocs({ include_docs: true }); logger.debug(`[Share] Found ${allDocs.rows.length} documents to copy`); // Copy each document to the remote database for (const row of allDocs.rows) { if (row.doc) { // Remove PouchDB internal fields for clean insert const { _rev, ...docWithoutRev } = row.doc as any; try { await remoteDb.put(docWithoutRev); } catch (err) { logger.warn(`[Share] Failed to copy doc ${row.id}:`, err); } } } const publicUrl = `${window.location.origin}/db/public/${publicDbName}`; logger.info(`[Share] Public copy created at: ${publicUrl}`); // Copy URL to clipboard let copied = false; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(publicUrl); copied = true; } } catch (clipboardError) { logger.warn('Clipboard API failed:', clipboardError); } if (copied) { alert(`Public copy created!\n\nURL copied to clipboard:\n${publicUrl}\n\nNote: Your local diagram remains unchanged. The public copy will diverge independently.`); } else { prompt('Public copy created! Share URL (copy manually):', publicUrl); } } catch (err) { logger.error('[Share] Failed to create public copy:', err); alert('Failed to create public copy. Please try again.'); } return; } // Public diagram - just copy current URL const shareUrl = window.location.href; logger.info(`[Share] Sharing public URL: ${shareUrl}`); // Try to copy URL to clipboard with fallback let copied = false; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(shareUrl); copied = true; } } catch (clipboardError) { logger.warn('Clipboard API failed:', clipboardError); } if (copied) { alert(`URL copied to clipboard!\n\n${shareUrl}`); } else { // Fallback: show URL in prompt so user can copy manually prompt('Share URL (copy manually):', shareUrl); } }; useEffect(() => { const canvas = document.getElementById('vrCanvas'); if (!canvas) { logger.error('no canvas'); return; } if (vrApp) { logger.debug('destroying vrApp'); vrApp.dispose(); } console.log('[Share] Initializing VrApp with dbName:', dbName); vrApp = new VrApp(canvas as HTMLCanvasElement, dbName); closeManage(); // Show VR entry prompt for all Quest users navigating to any /db/** path const isQuest = isMobileVRDevice(); logger.info(`Device check: isMobileVRDevice=${isQuest}, userAgent=${navigator.userAgent}`); if (isQuest) { logger.info('Quest device detected, will show VR prompt when ready'); // Wait for XR to be ready, then show the prompt let attempts = 0; const maxAttempts = 50; const waitForXRReady = setInterval(() => { attempts++; const scene = DefaultScene.Scene; const groundMesh = scene?.getMeshByName('ground'); logger.debug(`XR readiness check attempt ${attempts}: scene=${!!scene}, groundMesh=${!!groundMesh}`); if (groundMesh || attempts >= maxAttempts) { clearInterval(waitForXRReady); if (groundMesh) { logger.info('XR ready, showing VR entry prompt'); setShowVRPrompt(true); logger.info(`showVRPrompt state set to true`); } else { logger.warn('XR setup timeout, cannot show VR prompt'); } } }, 500); } }, [dbName]); const [immersiveDisabled, setImmersiveDisabled] = useState(true); const navigate = useNavigate(); // Get the appropriate indicator for a feature based on its state const getFeatureIndicator = (featureState) => { // 'off' - don't show at all (handled by shouldShow) // 'on' - no badge needed, feature is fully accessible // 'coming-soon' - show Coming Soon badge (visible to all) // 'basic' - show Sign Up badge (requires basic tier) - clickable to sign up // 'pro' - show Upgrade to Pro badge (requires pro tier) if (featureState === 'coming-soon') { return ; } if (featureState === 'basic') { return ; } if (featureState === 'pro') { return ; } // 'on' state - no indicator needed return null; } const enterImmersive = (e) => { logger.info('entering immersive mode'); e.preventDefault(); const event = new CustomEvent('enterXr', {bubbles: true}); window.dispatchEvent(event); } const createModal = () => { if (createOpened) { return } else { return <> } } const manageModal = () => { if (manageOpened) { return } else { return <> } } return ( {/* Guest Mode Banner - Non-aggressive, dismissible (hidden for demo) */} {!isAuthenticated && !guestBannerDismissed && dbName !== 'demo' && ( } withCloseButton onClose={() => setGuestBannerDismissed(true)} > {GUEST_MODE_BANNER.message} )} {createModal()} {manageModal()} {/* Home is always visible */} { navigate("/") }} label="Home" availableIcon={null}/> {shouldShow(enterImmersiveState) && ( )} {shouldShow(launchMetaQuestState) && ( { window.open('https://www.oculus.com/open_url/?url=' + window.location.href, 'launchQuest', 'popup') })} label="Launch On Meta Quest" availableIcon={getFeatureIndicator(launchMetaQuestState)}/> )} {shouldShow(editDataState) && ( <> )} {(shouldShow(createDiagramState) || shouldShow(createFromTemplateState) || shouldShow(manageDiagramsState)) && } {shouldShow(createDiagramState) && ( )} {shouldShow(createFromTemplateState) && ( )} {shouldShow(manageDiagramsState) && ( )} {/* Export JSON - Always available for creating templates */} {(shouldShow(shareCollaborateState) || shouldShow(configState)) && } {shouldShow(shareCollaborateState) && ( )} {shouldShow(configState) && ( )} setChatOpen(!chatOpen)} availableIcon={}/>
{chatOpen && setChatOpen(false)}/>}
{/* VR Entry Prompt - Rendered AFTER canvas to ensure it's on top in DOM order */} { setShowVRPrompt(false); const event = new CustomEvent('enterXr', {bubbles: true}); window.dispatchEvent(event); }} onSkip={() => setShowVRPrompt(false)} />
) }