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()}
{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)}
/>
)
}