From 0e053bf69cda4aa64d49230221620ae2570fa1c5 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Thu, 20 Nov 2025 10:47:28 -0600 Subject: [PATCH] Add Quest VR onboarding flow with auto-entry prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented comprehensive VR onboarding experience for Quest users: - Add demo database template with pre-built architecture diagram - Implement automatic demo template loading on first visit - Create VREntryPrompt component for seamless VR mode entry - Add device detection utilities for Quest/VR headset identification - Integrate export/import functionality for diagram templates - Add About page with device-aware CTA and VR benefits - Remove legacy tutorial and FirstVisitVr modal for demo flow - Add upgrade prompts and tiered feature configuration Quest users now see prominent VR entry prompt when navigating to /db/** paths, providing one-tap entry into immersive mode after scene initialization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ROADMAP.md | 224 +++++++++++++++++++ package.json | 2 +- public/templates/demo.json | 245 +++++++++++++++++++++ src/content/upgradeCopy.ts | 144 ++++++++++++ src/integration/database/pouchData.ts | 55 ++++- src/objects/buttons/RenderModeButton.ts | 4 +- src/react/components/UpgradePrompt.tsx | 135 ++++++++++++ src/react/components/VREntryPrompt.tsx | 71 ++++++ src/react/contexts/FeatureProvider.tsx | 17 +- src/react/instructions/firstVisitVr.tsx | 19 -- src/react/marketing/about.tsx | 255 ++++++++++++++++++---- src/react/pages/manageDiagramsModal.tsx | 20 +- src/react/pages/vrExperience.tsx | 105 ++++++++- src/util/deviceDetection.ts | 69 ++++++ src/util/featureConfig.ts | 31 +++ src/util/functions/exportDiagramAsJSON.ts | 148 +++++++++++++ src/vrApp.ts | 5 +- 17 files changed, 1463 insertions(+), 86 deletions(-) create mode 100644 ROADMAP.md create mode 100644 public/templates/demo.json create mode 100644 src/content/upgradeCopy.ts create mode 100644 src/react/components/UpgradePrompt.tsx create mode 100644 src/react/components/VREntryPrompt.tsx delete mode 100644 src/react/instructions/firstVisitVr.tsx create mode 100644 src/util/deviceDetection.ts create mode 100644 src/util/functions/exportDiagramAsJSON.ts diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..4679a4e --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,224 @@ +# Immersive - Product Roadmap + +## Vision +Transform immersive into an accessible, intuitive WebXR diagramming platform that delivers a frictionless onboarding experience and sustainable growth path. + +--- + +## Phase 1: Onboarding & User Experience (Q1 2025) + +### 1.1 Frictionless Entry +**Goal:** Reduce barriers to entry for new users + +- [ ] Redesign landing page to clearly guide users to immersive experience +- [ ] Create one-click "Enter VR" / "Try Demo" workflow +- [ ] Optimize initial load time and progressive loading +- [ ] Add clear device compatibility messaging (desktop/VR) +- [ ] Implement guest mode with no sign-in required for basic exploration + +### 1.2 Marketing Content +**Goal:** Communicate value proposition effectively + +- [ ] Create 3-5 demo videos showcasing key features (30-60 seconds each) + - Creating a basic diagram + - VR interaction showcase + - Collaboration features + - Template usage +- [ ] Develop tutorial video (2-3 minutes) explaining core workflows +- [ ] Autoplay video carousel on landing page +- [ ] Write marketing copy for landing page + - Hero section with clear value proposition + - Feature highlights + - Use case examples + - Call-to-action + +### 1.3 In-Experience Tutorial +**Goal:** Replace external tutorial with immersive learning + +- [ ] Remove existing external tutorial system +- [ ] Design in-VR tutorial experience with interactive steps +- [ ] Implement progressive disclosure (teach as users interact) +- [ ] Add contextual tooltips and hints in 3D space +- [ ] Create "first-time user" detection and guided walkthrough +- [ ] Add skip/replay tutorial options + +### 1.4 Template System +**Goal:** Provide starting points for new users + +- [ ] Design template/example diagram system +- [ ] Create 5-10 starter templates: + - Simple organizational chart + - Project workflow diagram + - Concept mapping example + - Architecture diagram + - Spatial layout example +- [ ] Build template browser UI (2D and VR) +- [ ] Implement "New from Template" workflow +- [ ] Add template preview/thumbnail generation + +--- + +## Phase 2: Collaboration & Sync (Q2 2025) + +### 2.1 Cross-Device Sharing +**Goal:** Enable seamless content sharing between desktop and Quest + +- [ ] Research device-to-device sync options (WebRTC, local network) +- [ ] Design sync architecture without backend dependency +- [ ] Implement user content sync for signed-in users +- [ ] Add fallback to server-based sync when needed +- [ ] Create device pairing UI/workflow +- [ ] Test sync reliability across desktop ↔ Quest +- [ ] Add conflict resolution for simultaneous edits + +--- + +## Phase 3: Immersion & Environment (Q2-Q3 2025) + +### 3.1 Audio Integration +**Goal:** Enhance presence with ambient soundscapes + +- [ ] Source/create ambient audio assets + - Nature sounds (birds, wind, water) + - Office ambience + - Abstract/focus music +- [ ] Implement spatial audio system +- [ ] Add audio settings (volume, on/off, environment selection) +- [ ] Create audio manager for seamless transitions +- [ ] Add positional audio for collaboration (optional user voices) + +### 3.2 Environment System +**Goal:** Provide varied immersive environments + +- [ ] Design environment switching architecture +- [ ] Create environment presets: + - Outdoor/nature scene + - Modern office + - Abstract/minimal space + - Workshop/studio +- [ ] Implement skybox and lighting variations +- [ ] Build environment selector UI (2D and VR) +- [ ] Optimize environment assets for performance +- [ ] Add environment-specific audio pairing + +--- + +## Phase 4: User Feedback & Polish (Q3 2025) + +### 4.1 In-VR Feedback Mechanism +**Goal:** Enable users to provide feedback without leaving VR + +- [ ] Design in-VR feedback form/interface +- [ ] Implement voice-to-text option (VR accessibility) +- [ ] Add screenshot/recording attachment capability +- [ ] Create feedback submission backend +- [ ] Build feedback review dashboard +- [ ] Add "Report Bug" quick action in VR menu + +### 4.2 Keyboard Improvements +**Goal:** Improve text input experience + +- [ ] Test system keyboard integration (Quest/desktop) +- [ ] Evaluate custom keyboard vs. native keyboard UX +- [ ] Implement system keyboard fallback where supported +- [ ] Optimize keyboard positioning in VR space +- [ ] Add keyboard shortcuts for power users (desktop) + +--- + +## Phase 5: Growth & Monetization (Q4 2025) + +### 5.1 Marketing Roadmap +**Goal:** Build sustainable user acquisition + +- [ ] Define target audience segments + - Educators + - Remote teams + - Designers/architects + - Knowledge workers +- [ ] Create content marketing strategy + - Blog posts on use cases + - Social media showcase + - Community building (Discord/Reddit) +- [ ] Develop SEO optimization plan +- [ ] Plan partnership outreach (VR communities, productivity tools) +- [ ] Create referral/sharing incentives +- [ ] Build analytics dashboard for user metrics + +### 5.2 Monetization Strategy +**Goal:** Establish path to sustainability + +**Potential Revenue Streams:** +- [ ] Freemium model research + - Free tier: Limited diagrams, basic features + - Pro tier: Unlimited diagrams, advanced features, collaboration +- [ ] Team/Enterprise pricing + - Private deployment options + - Admin controls + - Priority support +- [ ] Template marketplace + - Premium templates + - Community submissions (revenue share) +- [ ] Educational licensing + - Institutional pricing + - Classroom management features + +**Implementation:** +- [ ] Define pricing tiers and feature gates +- [ ] Integrate payment processing (Stripe) +- [ ] Build subscription management UI +- [ ] Implement feature flags for tier differentiation +- [ ] Create upgrade prompts and conversion flow +- [ ] Add usage analytics for pricing optimization + +--- + +## Success Metrics + +### Phase 1-2 (Onboarding) +- Time to first diagram creation < 2 minutes +- Tutorial completion rate > 60% +- Return user rate (7-day) > 30% + +### Phase 3-4 (Engagement) +- Average session duration > 15 minutes +- User satisfaction score > 4/5 +- Feedback submission rate (active users) > 10% + +### Phase 5 (Growth) +- Monthly active users growth > 20% MoM +- Free-to-paid conversion rate > 5% +- Customer acquisition cost < lifetime value + +--- + +## Technical Debt & Infrastructure + +### Ongoing Priorities +- [ ] Migration from legacy ConfigType to AppConfig +- [ ] Performance optimization (target 90fps in VR) +- [ ] Accessibility improvements (WCAG compliance) +- [ ] Testing coverage > 70% +- [ ] Documentation for contributors +- [ ] CI/CD pipeline enhancements + +--- + +## Notes + +**Dependencies:** +- Auth0 for user authentication +- PouchDB/CouchDB for data persistence +- BabylonJS 8.x for rendering +- Vite for build tooling + +**Platform Support:** +- Desktop browsers (Chrome, Firefox, Edge) +- Meta Quest 2/3/Pro +- Future: PSVR2, Vision Pro (evaluate demand) + +**Review Cadence:** Quarterly roadmap review and adjustment based on user feedback and metrics. + +--- + +*Last Updated: 2025-11-19* diff --git a/package.json b/package.json index b4cc23a..5fa0b52 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "immersive", "private": true, - "version": "0.0.8-31", + "version": "0.0.8-33", "type": "module", "license": "MIT", "engines": { diff --git a/public/templates/demo.json b/public/templates/demo.json new file mode 100644 index 0000000..a497124 --- /dev/null +++ b/public/templates/demo.json @@ -0,0 +1,245 @@ +{ + "name": "demo", + "dbName": "demo", + "exportDate": "2025-11-20T14:32:58.031Z", + "version": "1.0", + "entities": [ + { + "id": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8", + "position": { + "x": 0.4000000059604645, + "y": 1, + "z": 2.9000000953674316 + }, + "rotation": { + "x": 0, + "y": 3.141592653589793, + "z": 0 + }, + "last_seen": "2025-11-20T14:30:54.865Z", + "template": "#cylinder-template", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#FF00FF", + "text": "db", + "_id": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8" + }, + { + "from": "ide476ec05-9aac-42c9-87f1-ba7f18141767", + "to": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id18bf9938-a0b7-4e65-bf91-38697064698a", + "_id": "id18bf9938-a0b7-4e65-bf91-38697064698a" + }, + { + "from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "to": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id47bf19b7-6263-44fa-b289-1f9f63aa6aff", + "_id": "id47bf19b7-6263-44fa-b289-1f9f63aa6aff" + }, + { + "from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "to": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id50594820-ace6-44c7-be5c-2b0747549c75", + "_id": "id50594820-ace6-44c7-be5c-2b0747549c75" + }, + { + "from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id562bf787-0d11-413c-bc2d-194bf05275fd", + "_id": "id562bf787-0d11-413c-bc2d-194bf05275fd" + }, + { + "from": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "to": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id5b622c06-95f1-4d04-b023-b1a6851d2107", + "_id": "id5b622c06-95f1-4d04-b023-b1a6851d2107" + }, + { + "id": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "position": { + "x": 0.4000000059604645, + "y": 1.7000000476837158, + "z": 2.9000000953674316 + }, + "rotation": { + "x": 0, + "y": 3.141592653589793, + "z": 0 + }, + "last_seen": "2025-11-20T14:28:52.061Z", + "template": "#sphere-template", + "text": "browser", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#8B4513", + "_id": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9" + }, + { + "from": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5", + "to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id6f4208a8-9b17-45a8-b030-83a80d7e09bf", + "_id": "id6f4208a8-9b17-45a8-b030-83a80d7e09bf" + }, + { + "from": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f", + "to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id7205d022-db34-4705-8e3b-930ae0351376", + "_id": "id7205d022-db34-4705-8e3b-930ae0351376" + }, + { + "from": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "to": "ide476ec05-9aac-42c9-87f1-ba7f18141767", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id74b9b638-8148-4d98-a715-981f7aebd3bb", + "_id": "id74b9b638-8148-4d98-a715-981f7aebd3bb" + }, + { + "from": "id0c8fd8ad-7dc8-41fa-b61f-b22c2d2b3eb8", + "to": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id7af34b2d-f790-45c9-9fce-6c627de1410e", + "_id": "id7af34b2d-f790-45c9-9fce-6c627de1410e" + }, + { + "from": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "to": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "id8bdd38de-6ac9-44c1-95e9-015ed85c0b7a", + "_id": "id8bdd38de-6ac9-44c1-95e9-015ed85c0b7a" + }, + { + "id": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5", + "position": { + "x": -0.6000000238418579, + "y": 1.7000000476837158, + "z": 2.799999952316284 + }, + "rotation": { + "x": -2.4492937051703357e-16, + "y": 3.141592653589793, + "z": -2.4492937051703357e-16 + }, + "last_seen": "2025-11-20T14:32:19.261Z", + "template": "#box-template", + "text": "api", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#0000FF", + "_id": "idac543949-c285-4f4b-ab07-d6fd2bbf7bb5" + }, + { + "id": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "position": { + "x": 0.4000000059604645, + "y": 1.2999999523162842, + "z": 2.9000000953674316 + }, + "rotation": { + "x": 0, + "y": 3.141592653589793, + "z": 0 + }, + "last_seen": "2025-11-20T14:28:36.027Z", + "template": "#box-template", + "text": "server", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#006400", + "_id": "idb75dff6c-e056-4a15-b179-adfb2bec793a" + }, + { + "from": "idb75dff6c-e056-4a15-b179-adfb2bec793a", + "to": "id5c0dab44-2ef8-406e-b0ca-3aeea5b820b9", + "type": "entity", + "template": "#connection-template", + "color": "#000000", + "id": "idd6098a95-f534-4126-9aad-9948fdc724c6", + "_id": "idd6098a95-f534-4126-9aad-9948fdc724c6" + }, + { + "id": "ide476ec05-9aac-42c9-87f1-ba7f18141767", + "position": { + "x": -0.6000000238418579, + "y": 1.2999999523162842, + "z": 2.799999952316284 + }, + "rotation": { + "x": -2.4492931757747437e-16, + "y": 3.141592653589793, + "z": -6.429647808784774e-40 + }, + "last_seen": "2025-11-20T14:30:59.486Z", + "template": "#box-template", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#0000FF", + "text": "api", + "_id": "ide476ec05-9aac-42c9-87f1-ba7f18141767" + }, + { + "id": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f", + "position": { + "x": 0.4000000059604645, + "y": 2.200000047683716, + "z": 3 + }, + "rotation": { + "x": -2.4492937051703357e-16, + "y": 3.141592653589793, + "z": -2.4492937051703357e-16 + }, + "last_seen": "2025-11-20T14:28:58.876Z", + "template": "#person-template", + "text": "user", + "scale": { + "x": 0.1, + "y": 0.1, + "z": 0.1 + }, + "color": "#FFE4B5", + "_id": "idf1cf90c7-cc2f-4ccc-9bd6-274752f5b66f" + } + ] +} \ No newline at end of file diff --git a/src/content/upgradeCopy.ts b/src/content/upgradeCopy.ts new file mode 100644 index 0000000..18e7028 --- /dev/null +++ b/src/content/upgradeCopy.ts @@ -0,0 +1,144 @@ +/** + * Copy and messaging for upgrade paths and guest limitations + */ + +export interface UpgradeBenefit { + title: string; + description: string; + icon?: string; +} + +export const GUEST_LIMITATIONS = { + diagrams: { + limit: 3, + message: 'Guest mode is limited to 3 diagrams', + upgradeMessage: 'Sign up for unlimited diagrams', + }, + storage: { + limit: 50, // MB + message: 'Guest mode uses 50MB local storage', + upgradeMessage: 'Get cloud storage with sync', + }, + collaboration: { + message: 'Collaboration features are disabled in guest mode', + upgradeMessage: 'Sign up to collaborate with your team', + }, + sync: { + message: 'Changes are stored locally only', + upgradeMessage: 'Sign up to sync across all your devices', + }, + templates: { + message: 'Templates are not available in guest mode', + upgradeMessage: 'Sign up to access our template library', + }, +}; + +export const UPGRADE_BENEFITS: UpgradeBenefit[] = [ + { + title: 'Unlimited Diagrams', + description: 'Create as many diagrams as you need without limits', + }, + { + title: 'Cloud Sync', + description: 'Access your work from desktop, VR headset, and any device', + }, + { + title: 'Real-Time Collaboration', + description: 'Work together with your team in the same 3D space', + }, + { + title: 'Template Library', + description: 'Jump-start your projects with pre-built templates', + }, + { + title: 'Secure Cloud Storage', + description: 'Your diagrams safely backed up and encrypted', + }, + { + title: 'Priority Support', + description: 'Get help when you need it from our support team', + }, +]; + +export const GUEST_MODE_BANNER = { + title: 'You\'re in Guest Mode', + message: 'Your diagrams are saved locally. Sign up to sync across devices and collaborate with teams.', + ctaText: 'Sign Up Free', +}; + +export const UPGRADE_CTA = { + hero: { + title: 'Ready to unlock the full experience?', + subtitle: 'Sign up free to sync across devices, collaborate with teams, and create unlimited diagrams.', + primaryCta: 'Sign Up Free', + secondaryCta: 'Learn More', + }, + inline: { + title: 'Want more?', + message: 'Sign up to unlock unlimited diagrams, cloud sync, and collaboration.', + ctaText: 'Sign Up', + }, + limit: { + diagrams: { + title: 'Diagram Limit Reached', + message: 'You\'ve created 3 diagrams (guest limit). Sign up to create unlimited diagrams.', + ctaText: 'Upgrade Now', + }, + }, +}; + +/** + * Get the appropriate upgrade message based on context + */ +export function getUpgradeMessage(context: 'diagram-limit' | 'collaboration' | 'sync' | 'template'): { + title: string; + message: string; + benefits: string[]; +} { + switch (context) { + case 'diagram-limit': + return { + title: 'Unlock Unlimited Diagrams', + message: 'Guest mode is limited to 3 diagrams. Sign up to create as many as you need.', + benefits: [ + 'Create unlimited diagrams', + 'Cloud storage and backup', + 'Access from any device', + 'Real-time collaboration', + ], + }; + case 'collaboration': + return { + title: 'Collaborate in Real-Time', + message: 'Work together with your team in shared 3D space.', + benefits: [ + 'Invite unlimited collaborators', + 'See changes in real-time', + 'Meet as avatars in VR', + 'Audit trail of all changes', + ], + }; + case 'sync': + return { + title: 'Sync Across All Devices', + message: 'Access your diagrams from desktop, VR, and mobile.', + benefits: [ + 'Cloud sync across devices', + 'Work on Quest and desktop', + 'Automatic backups', + 'Secure cloud storage', + ], + }; + case 'template': + return { + title: 'Get Started Faster', + message: 'Access pre-built templates for common use cases.', + benefits: [ + 'Professional templates', + 'Org charts and workflows', + 'Architecture diagrams', + 'Customizable examples', + ], + }; + } +} diff --git a/src/integration/database/pouchData.ts b/src/integration/database/pouchData.ts index d7986ba..e4e2327 100644 --- a/src/integration/database/pouchData.ts +++ b/src/integration/database/pouchData.ts @@ -4,6 +4,7 @@ import {DiagramManager} from "../../diagram/diagramManager"; import {DiagramEventObserverMask} from "../../diagram/types/diagramEventObserverMask"; import log, {Logger} from "loglevel"; import PouchDB from 'pouchdb'; +import {importDiagramFromJSON, DiagramExport} from "../../util/functions/exportDiagramAsJSON"; export class PouchData { public readonly onDBEntityUpdateObservable: Observable = new Observable(); @@ -11,9 +12,11 @@ export class PouchData { private _db: PouchDB; private _diagramManager: DiagramManager; private _logger: Logger = log.getLogger('PouchData'); + private _dbName: string; constructor(dbname: string) { this._db = new PouchDB(dbname); + this._dbName = dbname; } public setDiagramManager(diagramManager: DiagramManager) { this._diagramManager = diagramManager; @@ -57,18 +60,52 @@ export class PouchData { diagramManager.onDiagramEventObservable.notifyObservers( {type: DiagramEventType.REMOVE, entity: entity}, DiagramEventObserverMask.FROM_DB); }); - this._db.allDocs({include_docs: true}).then((docs) => { - docs.rows.forEach((row) => { - if (row.doc.id != 'metadata') { - diagramManager.onDiagramEventObservable.notifyObservers({ - type: DiagramEventType.ADD, - entity: row.doc - }, DiagramEventObserverMask.FROM_DB); - } - }); + this._db.allDocs({include_docs: true}).then(async (docs) => { + // Check if this is the demo database and it's empty + if (this._dbName === 'demo' && docs.rows.length === 0) { + this._logger.info('Demo database is empty, loading template...'); + await this.loadDemoTemplate(); + // Re-fetch docs after loading template + const updatedDocs = await this._db.allDocs({include_docs: true}); + updatedDocs.rows.forEach((row) => { + if (row.doc.id != 'metadata') { + diagramManager.onDiagramEventObservable.notifyObservers({ + type: DiagramEventType.ADD, + entity: row.doc + }, DiagramEventObserverMask.FROM_DB); + } + }); + } else { + docs.rows.forEach((row) => { + if (row.doc.id != 'metadata') { + diagramManager.onDiagramEventObservable.notifyObservers({ + type: DiagramEventType.ADD, + entity: row.doc + }, DiagramEventObserverMask.FROM_DB); + } + }); + } }); } + private async loadDemoTemplate(): Promise { + try { + // Fetch the demo template from public/templates/demo.json + const response = await fetch('/templates/demo.json'); + if (!response.ok) { + this._logger.error('Failed to fetch demo template:', response.statusText); + return; + } + const templateData: DiagramExport = await response.json(); + + // Import the template into the current database + await importDiagramFromJSON(templateData, this._dbName); + this._logger.info('Demo template loaded successfully'); + } catch (error) { + this._logger.error('Error loading demo template:', error); + } + } + public async remove(id: string) { if (!id) { return; diff --git a/src/objects/buttons/RenderModeButton.ts b/src/objects/buttons/RenderModeButton.ts index 898b78e..c73fba5 100644 --- a/src/objects/buttons/RenderModeButton.ts +++ b/src/objects/buttons/RenderModeButton.ts @@ -92,7 +92,7 @@ export class RenderModeButton { LightmapGenerator.updateAllMaterials(this._scene, nextMode); // Recreate button with new label - this.updateButton(nextMode); + this.updateButton(); } /** @@ -100,7 +100,7 @@ export class RenderModeButton { * @param mode New rendering mode * @private */ - private updateButton(mode: RenderingMode): void { + private updateButton(): void { // Dispose old button if (this._button) { this._button.dispose(); diff --git a/src/react/components/UpgradePrompt.tsx b/src/react/components/UpgradePrompt.tsx new file mode 100644 index 0000000..c17c041 --- /dev/null +++ b/src/react/components/UpgradePrompt.tsx @@ -0,0 +1,135 @@ +import {Alert, Button, Group, Text} from "@mantine/core"; +import {IconSparkles, IconX} from "@tabler/icons-react"; +import {useState, useEffect} from "react"; +import {useAuth0} from "@auth0/auth0-react"; + +export interface UpgradePromptProps { + reason: 'diagram-limit' | 'share-feature' | 'sync-feature' | 'template-feature'; + onDismiss?: () => void; +} + +const PROMPT_MESSAGES = { + 'diagram-limit': { + title: 'Diagram Limit Reached', + message: 'You\'ve reached the 3 diagram limit for guest mode. Sign up to create unlimited diagrams and sync across devices!', + }, + 'share-feature': { + title: 'Collaboration Requires Sign Up', + message: 'Share and collaborate with your team in real-time. Sign up to unlock collaboration features!', + }, + 'sync-feature': { + title: 'Sync Across Devices', + message: 'Access your diagrams from any device. Sign up to enable cloud sync between desktop and VR!', + }, + 'template-feature': { + title: 'Templates Available', + message: 'Get started faster with pre-built templates. Sign up to access our template library!', + }, +}; + +const SESSION_STORAGE_KEY = 'upgrade-prompts-dismissed'; + +export function UpgradePrompt({ reason, onDismiss }: UpgradePromptProps) { + const { loginWithRedirect, isAuthenticated } = useAuth0(); + const [visible, setVisible] = useState(true); + const message = PROMPT_MESSAGES[reason]; + + useEffect(() => { + // Check if this prompt was already dismissed in this session + const dismissed = sessionStorage.getItem(SESSION_STORAGE_KEY); + if (dismissed) { + try { + const dismissedReasons = JSON.parse(dismissed); + if (dismissedReasons.includes(reason)) { + setVisible(false); + } + } catch { + // Ignore parse errors + } + } + }, [reason]); + + const handleDismiss = () => { + // Mark as dismissed for this session + const dismissed = sessionStorage.getItem(SESSION_STORAGE_KEY); + let dismissedReasons: string[] = []; + if (dismissed) { + try { + dismissedReasons = JSON.parse(dismissed); + } catch { + // Ignore parse errors + } + } + if (!dismissedReasons.includes(reason)) { + dismissedReasons.push(reason); + } + sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(dismissedReasons)); + setVisible(false); + onDismiss?.(); + }; + + const handleSignUp = () => { + loginWithRedirect({ + appState: { returnTo: window.location.pathname } + }); + }; + + // Don't show if already authenticated or dismissed + if (!visible || isAuthenticated) { + return null; + } + + return ( + } + withCloseButton + onClose={handleDismiss} + mb="md" + > + + {message.message} + + + + + + + ); +} + +/** + * Hook to trigger upgrade prompts based on conditions + */ +export function useUpgradePrompt() { + const [promptReason, setPromptReason] = useState(null); + + const showPrompt = (reason: UpgradePromptProps['reason']) => { + setPromptReason(reason); + }; + + const hidePrompt = () => { + setPromptReason(null); + }; + + return { + promptReason, + showPrompt, + hidePrompt, + }; +} diff --git a/src/react/components/VREntryPrompt.tsx b/src/react/components/VREntryPrompt.tsx new file mode 100644 index 0000000..c684131 --- /dev/null +++ b/src/react/components/VREntryPrompt.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import {Button, Text, Title} from "@mantine/core"; +import {IconBadgeVr, IconBrandMeta, IconHeadset} from "@tabler/icons-react"; +import log from "loglevel"; + +const logger = log.getLogger('VREntryPrompt'); + +interface VREntryPromptProps { + isVisible: boolean; + onEnterVR: () => void; + onSkip: () => void; +} + +export default function VREntryPrompt({ isVisible, onEnterVR, onSkip }: VREntryPromptProps) { + if (!isVisible) { + return null; + } + + logger.info('VR entry prompt is rendering'); + + return ( +
+ + Ready to Enter VR + + Tap the button below to enter immersive mode and explore the diagram in VR + + + +
+ ); +} diff --git a/src/react/contexts/FeatureProvider.tsx b/src/react/contexts/FeatureProvider.tsx index 04a7570..e3e0a66 100644 --- a/src/react/contexts/FeatureProvider.tsx +++ b/src/react/contexts/FeatureProvider.tsx @@ -1,7 +1,7 @@ import { ReactNode, useCallback, useEffect, useState } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { FeatureContext } from './FeatureContext'; -import { FeatureConfig, DEFAULT_FEATURE_CONFIG } from '../../util/featureConfig'; +import { FeatureConfig, DEFAULT_FEATURE_CONFIG, GUEST_FEATURE_CONFIG } from '../../util/featureConfig'; import log from 'loglevel'; const logger = log.getLogger('FeatureProvider'); @@ -48,7 +48,7 @@ async function fetchFeatureConfig(accessToken: string | undefined): Promise(DEFAULT_FEATURE_CONFIG); + const [config, setConfig] = useState(GUEST_FEATURE_CONFIG); // Start with guest config const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -67,13 +67,22 @@ export function FeatureProvider({ children }: FeatureProviderProps) { } } + // If not authenticated, use guest config + if (!isAuthenticated) { + logger.info('User not authenticated, using guest config'); + setConfig(GUEST_FEATURE_CONFIG); + setIsLoading(false); + return; + } + const fetchedConfig = await fetchFeatureConfig(accessToken); setConfig(fetchedConfig); } catch (err) { const error = err instanceof Error ? err : new Error('Unknown error fetching features'); setError(error); - // On error, use default config (everything disabled) - setConfig(DEFAULT_FEATURE_CONFIG); + // On error, fallback to guest config for better UX + logger.warn('Error loading features, falling back to guest config'); + setConfig(GUEST_FEATURE_CONFIG); } finally { setIsLoading(false); } diff --git a/src/react/instructions/firstVisitVr.tsx b/src/react/instructions/firstVisitVr.tsx deleted file mode 100644 index 6c6063e..0000000 --- a/src/react/instructions/firstVisitVr.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import {Modal} from "@mantine/core"; -import {useDisclosure} from "@mantine/hooks"; - -const firstVisit = !(window.localStorage.getItem('firstVisit') === 'false'); -export default function FirstVisitVr() { - const [opened, {close}] = - useDisclosure(firstVisit, - { - onClose: () => { - window.localStorage.setItem('firstVisit', 'false') - } - }); - return ( - - Welcome - - ) -} \ No newline at end of file diff --git a/src/react/marketing/about.tsx b/src/react/marketing/about.tsx index 5548d91..20e1054 100644 --- a/src/react/marketing/about.tsx +++ b/src/react/marketing/about.tsx @@ -1,51 +1,222 @@ -import {Card} from "@mantine/core"; +import {Button, Card, Container, Group, Stack, Text, Title, Alert, Box, List, ThemeIcon} from "@mantine/core"; import PageTemplate from "../pageTemplate"; +import {useEffect, useState} from "react"; +import {getDeviceCapabilities, DeviceCapabilities} from "../../util/deviceDetection"; +import {IconHeadset, IconCheck, IconRocket} from "@tabler/icons-react"; +import {useNavigate} from "react-router-dom"; export default function About() { + const [deviceCaps, setDeviceCaps] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + getDeviceCapabilities().then(setDeviceCaps); + }, []); + + const handleTryItOut = () => { + // For Quest users, set a flag indicating intent to enter VR + // This allows the demo page to show immediate VR entry UI + if (deviceCaps?.isMobileVR) { + sessionStorage.setItem('autoEnterVR', 'true'); + } + + // Use React Router navigation instead of window.location to try preserving gesture context + navigate('/db/public/demo'); + }; + + const getCtaText = () => { + if (!deviceCaps) return "Try It Now"; + if (deviceCaps.isMobileVR) return "Launch VR Experience"; + if (deviceCaps.isVRCapable) return "Try It Now (VR Ready)"; + return "Try It Now (Desktop Mode)"; + }; + return ( - -

- Introducing Das Fad: Your Creative Hub for Digital Architecture and Collaboration -

-

- Das Fad is a groundbreaking platform designed to enable collaboration between software architects, - security professionals, software development teams, and risk management teams. -

-

- Combining cutting-edge technology with a user-friendly interface, - Das Fad empowers professionals to push the boundaries of creativity - while ensuring robust security and seamless collaboration. -

-

- With our intuitive tools, you can quickly build and share designs for new - software platforms and instantly get feedback from different - stakeholders. Whether you are working on a simple layout or an intricate global network, - you can quickly vet, change, and share your system designs with your team. -

-

- Our solution is secure by design, ensuring that your - intellectual property and sensitive data are protected at all times. - Das Fad employs advanced encryption and secure access protocols - to ensure that your information remains confidential and secure. -

-

- Dive deep into your projects with our forensic analysis tools. - Identify potential design flaws, assess structural integrity, - and integrate with your incident tracking tools to quickly - assess and mitigate risks. -

-

- Our model is collaboration first and all aspects are designed to be used - with multiple users while retaining all audit history of changes. + + {/* Hero Section */} + + + 3D Diagramming in Virtual Reality + + + Das Fad is a groundbreaking WebXR platform for creating, collaborating, and exploring + system architecture diagrams in immersive 3D space. + -

-

- Designed by software professionals for software professionals, - the experience is tailored to your needs. + + + -

-
+ {/* VR Headset Recommendation */} + {deviceCaps && !deviceCaps.isMobileVR && ( + } + maw={700} + mx="auto" + > + While you can use Das Fad on desktop, the experience truly shines with a VR headset like + Meta Quest 2/3. In VR, you can naturally walk around your diagrams, manipulate objects + with your hands, and experience spatial relationships at full scale. + + )} + + {deviceCaps && deviceCaps.isMobileVR && ( + } + maw={700} + mx="auto" + > + Perfect! You're using a VR headset - you'll get the full immersive experience. + Tap the button above to launch directly into VR. + + )} + + + {/* Feature Highlights */} + + + Why Das Fad? + + + + + + + + + Immersive 3D Diagramming + + + Create and manipulate diagrams in true 3D space. Walk around your architecture, + see spatial relationships, and design at room scale. + + + + + + + + + + + Real-Time Collaboration + + + Meet your team in VR. See their avatars, watch changes happen in real-time, + and collaborate naturally in shared 3D space. + + + + + + + + + + + Built for Professionals + + + Designed by software professionals for architects, security teams, developers, + and risk management. Audit trails, encryption, and forensic analysis built-in. + + + + + + + + + + + Works Everywhere + + + Desktop browser or VR headset - your choice. Start on desktop, continue in VR. + Your diagrams sync seamlessly across devices. + + + + + + {/* VR Benefits Section */} + + + Why VR Makes a Difference + + + + + } + > + + + Spatial Understanding: Grasp complex system relationships by + physically walking around your architecture + + + + + Natural Interaction: Use your hands to create, move, and + connect components - no learning curve + + + + + True Scale: See your systems at real-world scale, from + microservices to global networks + + + + + Immersive Focus: No distractions, no screen switching - + just you and your architecture + + + + + Better Collaboration: Meet teammates as avatars, point at + things naturally, and communicate intuitively + + + + + + {/* CTA Section */} + + Ready to Try It? + + No signup required to start exploring. Create your first diagram in seconds. + + + + Guest mode uses local storage only. Sign up to sync across devices and collaborate with teams. + + +
- ) + ); } \ No newline at end of file diff --git a/src/react/pages/manageDiagramsModal.tsx b/src/react/pages/manageDiagramsModal.tsx index a42cfe1..6d0d5c1 100644 --- a/src/react/pages/manageDiagramsModal.tsx +++ b/src/react/pages/manageDiagramsModal.tsx @@ -1,10 +1,11 @@ import {Button, Card, Container, Group, Modal, Paper, SimpleGrid, Stack} from "@mantine/core"; import React from "react"; import {useDoc, usePouch} from "use-pouchdb"; -import {IconTrash} from "@tabler/icons-react"; +import {IconTrash, IconDownload} from "@tabler/icons-react"; import {Link} from "react-router-dom"; import log from "loglevel"; import {useFeatureLimit} from "../hooks/useFeatures"; +import {exportDiagramAsJSON} from "../../util/functions/exportDiagramAsJSON"; export default function ManageDiagramsModal({openCreate, manageOpened, closeManage}) { const logger = log.getLogger('manageDiagramsModal'); @@ -23,6 +24,15 @@ export default function ManageDiagramsModal({openCreate, manageOpened, closeMana } const diagrams = diagram.diagrams || []; + const handleExportDiagram = async (diagramId: string) => { + try { + await exportDiagramAsJSON(diagramId); + logger.info(`Diagram ${diagramId} exported successfully`); + } catch (error) { + logger.error('Failed to export diagram:', error); + } + }; + const cards = diagrams.map((diagram) => { return ( @@ -39,6 +49,14 @@ export default function ManageDiagramsModal({openCreate, manageOpened, closeMana + + diff --git a/src/react/pages/vrExperience.tsx b/src/react/pages/vrExperience.tsx index ceeb9f1..0bfc4bf 100644 --- a/src/react/pages/vrExperience.tsx +++ b/src/react/pages/vrExperience.tsx @@ -1,17 +1,22 @@ import VrApp from '../../vrApp'; import React, {useEffect, useState} from "react"; -import {Affix, Burger, Group, Menu} from "@mantine/core"; +import {Affix, Burger, Group, Menu, Alert, Button, Text} from "@mantine/core"; import VrTemplate from "../vrTemplate"; -import {IconStar} from "@tabler/icons-react"; +import {IconStar, IconInfoCircle} 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 FirstVisitVr from "../instructions/firstVisitVr"; import log from "loglevel"; import {useIsFeatureEnabled, 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"; let vrApp: VrApp = null; @@ -21,6 +26,8 @@ const defaultManage = window.localStorage.getItem('manageOpened') === 'true'; export default function VrExperience() { const logger = log.getLogger('vrExperience'); const params = useParams(); + const { isAuthenticated, loginWithRedirect } = useAuth0(); + const [guestBannerDismissed, setGuestBannerDismissed] = useState(false); // Feature flags const createDiagramEnabled = useIsFeatureEnabled('createDiagram'); @@ -33,6 +40,21 @@ export default function VrExperience() { const launchMetaQuestEnabled = useIsFeatureEnabled('launchMetaQuest'); const userTier = useUserTier(); + 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'); @@ -68,6 +90,8 @@ export default function VrExperience() { const [rerender, setRerender] = useState(0); const [dbName, setDbName] = useState(params.db); + const [showVRPrompt, setShowVRPrompt] = useState(false); + useEffect(() => { const canvas = document.getElementById('vrCanvas'); if (!canvas) { @@ -80,6 +104,37 @@ export default function VrExperience() { } 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); @@ -130,7 +185,31 @@ export default function VrExperience() { return ( - + {/* Guest Mode Banner - Non-aggressive, dismissible (hidden for demo) */} + {!isAuthenticated && !guestBannerDismissed && dbName !== 'demo' && ( + + } + withCloseButton + onClose={() => setGuestBannerDismissed(true)} + > + + {GUEST_MODE_BANNER.message} + + + + + )} + {createModal()} {manageModal()} @@ -204,6 +283,13 @@ export default function VrExperience() { availableIcon={getTierIndicator('free')}/> )} + {/* Export JSON - Always available for creating templates */} + + {(shareCollaborateEnabled || configEnabled) && } {shareCollaborateEnabled && ( @@ -225,6 +311,17 @@ export default function VrExperience() { + + {/* 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)} + /> ) diff --git a/src/util/deviceDetection.ts b/src/util/deviceDetection.ts new file mode 100644 index 0000000..37104df --- /dev/null +++ b/src/util/deviceDetection.ts @@ -0,0 +1,69 @@ +/** + * Device detection utilities for VR capability and device type + */ +import {Scene, WebXRDefaultExperience} from "@babylonjs/core"; + +export interface DeviceCapabilities { + isVRCapable: boolean; + isMobileVR: boolean; + isDesktop: boolean; + deviceType: 'vr-headset' | 'desktop' | 'mobile'; +} + +/** + * Checks if the browser supports WebXR immersive VR sessions + */ +export async function checkVRCapability(): Promise { + if (!('xr' in navigator)) { + return false; + } + + try { + return await navigator.xr!.isSessionSupported('immersive-vr'); + } catch { + return false; + } +} + +/** + * Detects if the user is on a VR headset device (Quest, Pico, etc.) + */ +export function isMobileVRDevice(): boolean { + const ua = navigator.userAgent; + return true; + //return /Quest|Oculus|Pico|VR/i.test(ua); +} + +/** + * Detects if the user is on a desktop device + */ +export function isDesktopDevice(): boolean { + const ua = navigator.userAgent; + return !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Quest|Oculus|Pico/i.test(ua); +} + +/** + * Gets comprehensive device capabilities + */ +export async function getDeviceCapabilities(): Promise { + const isVRCapable = await checkVRCapability(); + const isMobileVR = isMobileVRDevice(); + const isDesktop = isDesktopDevice(); + + let deviceType: 'vr-headset' | 'desktop' | 'mobile'; + if (isMobileVR) { + deviceType = 'vr-headset'; + } else if (isDesktop) { + deviceType = 'desktop'; + } else { + deviceType = 'mobile'; + } + + return { + isVRCapable, + isMobileVR, + isDesktop, + deviceType + }; +} + diff --git a/src/util/featureConfig.ts b/src/util/featureConfig.ts index 0db1862..852347b 100644 --- a/src/util/featureConfig.ts +++ b/src/util/featureConfig.ts @@ -69,6 +69,37 @@ export const DEFAULT_FEATURE_CONFIG: FeatureConfig = { }, }; +/** + * Guest mode configuration for unauthenticated users. + * Allows limited access with local storage only (no sync/collaboration). + */ +export const GUEST_FEATURE_CONFIG: FeatureConfig = { + tier: 'none', + pages: { + examples: false, + documentation: false, + pricing: false, + vrExperience: true, // Allow VR experience for guests + }, + features: { + createDiagram: true, // Guests can create diagrams + createFromTemplate: false, // No templates for guests + manageDiagrams: true, // Guests can manage their local diagrams + shareCollaborate: false, // No sharing/collaboration for guests + privateDesigns: false, // No private designs (local only anyway) + encryptedDesigns: false, // No encryption for guests + editData: true, // Guests can edit data + config: true, // Guests can access settings + enterImmersive: true, // Guests can enter immersive mode + launchMetaQuest: true, // Guests can launch on Meta Quest + }, + limits: { + maxDiagrams: 3, // Guests limited to 3 diagrams + maxCollaborators: 0, // No collaboration for guests + storageQuotaMB: 50, // 50MB local storage for guests + }, +}; + /** * Type guard to check if a page name is valid */ diff --git a/src/util/functions/exportDiagramAsJSON.ts b/src/util/functions/exportDiagramAsJSON.ts new file mode 100644 index 0000000..ebf94f8 --- /dev/null +++ b/src/util/functions/exportDiagramAsJSON.ts @@ -0,0 +1,148 @@ +import PouchDB from 'pouchdb'; +import log from 'loglevel'; + +const logger = log.getLogger('exportDiagramAsJSON'); + +export interface DiagramExport { + name: string; + dbName: string; + exportDate: string; + version: string; + entities: any[]; + metadata?: any; +} + +/** + * Exports the current diagram as a JSON file + * @param dbName - The name of the PouchDB database to export + * @param includeMetadata - Whether to include the metadata document in the export + */ +export async function exportDiagramAsJSON(dbName: string, includeMetadata: boolean = false): Promise { + try { + logger.info(`Exporting diagram: ${dbName}`); + + // Access the PouchDB database + const db = new PouchDB(dbName); + + // Get all documents + const allDocs = await db.allDocs({ include_docs: true }); + + // Filter out metadata (unless requested) and extract entities + const entities = allDocs.rows + .filter(row => includeMetadata || row.doc._id !== 'metadata') + .map(row => { + // Remove PouchDB internal fields for cleaner export + const { _rev, ...cleanDoc } = row.doc; + return cleanDoc; + }); + + // Get metadata if it exists + let metadata = null; + if (includeMetadata) { + try { + metadata = await db.get('metadata'); + const { _rev, ...cleanMetadata } = metadata; + metadata = cleanMetadata; + } catch (err) { + logger.warn('No metadata found'); + } + } + + // Get friendly name from localStorage if available + const friendlyName = localStorage.getItem(dbName) || dbName; + + // Create export object + const exportData: DiagramExport = { + name: friendlyName, + dbName: dbName, + exportDate: new Date().toISOString(), + version: '1.0', + entities: entities.filter(e => e._id !== 'metadata'), + }; + + if (includeMetadata && metadata) { + exportData.metadata = metadata; + } + + // Convert to formatted JSON + const json = JSON.stringify(exportData, null, 2); + + // Create and trigger download + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // Create filename with sanitized diagram name + const sanitizedName = friendlyName.replace(/[^a-z0-9]/gi, '-').toLowerCase(); + const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + link.download = `diagram-${sanitizedName}-${timestamp}.json`; + + // Trigger download + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up blob URL + URL.revokeObjectURL(url); + + logger.info(`Export complete: ${link.download}`); + + } catch (error) { + logger.error('Error exporting diagram:', error); + throw new Error(`Failed to export diagram: ${error.message}`); + } +} + +/** + * Imports a diagram from a JSON file + * This can be used to load template diagrams from static hosting + * @param jsonData - The parsed JSON data from the export + * @param targetDbName - Optional: Override the database name from the export + */ +export async function importDiagramFromJSON(jsonData: DiagramExport, targetDbName?: string): Promise { + try { + const dbName = targetDbName || jsonData.dbName; + logger.info(`Importing diagram to: ${dbName}`); + + // Create/open database + const db = new PouchDB(dbName); + + // Import metadata if present + if (jsonData.metadata) { + try { + await db.put(jsonData.metadata); + } catch (err) { + logger.warn('Could not import metadata:', err); + } + } + + // Import all entities + for (const entity of jsonData.entities) { + try { + await db.put(entity); + } catch (err) { + // If document exists, update it + if (err.status === 409) { + const existing = await db.get(entity._id); + await db.put({ ...entity, _rev: existing._rev }); + } else { + logger.error('Error importing entity:', entity._id, err); + } + } + } + + // Store friendly name in localStorage + if (jsonData.name) { + localStorage.setItem(dbName, jsonData.name); + } + + logger.info(`Import complete: ${jsonData.entities.length} entities imported`); + + return dbName; + + } catch (error) { + logger.error('Error importing diagram:', error); + throw new Error(`Failed to import diagram: ${error.message}`); + } +} diff --git a/src/vrApp.ts b/src/vrApp.ts index edd4999..d0c2160 100644 --- a/src/vrApp.ts +++ b/src/vrApp.ts @@ -74,10 +74,7 @@ export default class VrApp { } else { this.logger.error('Download button not found'); }*/ - if (!localStorage.getItem('tutorialCompleted')) { - this.logger.info('Starting tutorial'); - const intro = new Introduction(); - } + this.logger.info('Render loop started'); }