From 1fb267f286ecd5d73fa39d71e05a1c888f065bae Mon Sep 17 00:00:00 2001 From: Alex Meci Date: Wed, 22 Oct 2025 23:49:42 -0400 Subject: [PATCH 01/34] Optimizations --- .force-cache-refresh-v2.txt | 13 - .force-rebuild-2025-08-22T16-00-13Z.txt | 7 - .render-deploy-trigger | Bin 100 -> 0 bytes .vscode/settings.json | 3 +- _headers | 2 +- add-person.html | 45 +- css/content.css | 349 - css/dashboard-base.css | 1733 +++ css/dashboard-secure.css | 426 - css/emma-chat-clean.css | 416 +- css/emma-chat.css | 1265 -- css/main.css | 167 +- css/theme-tokens.css | 93 + css/voice-capture-premium.css | 924 -- css/voice-capture.css | 411 - dashboard.html | 8005 +----------- docs/EMMA-WEB-VAULT-SECURITY-ANALYSIS.md | 45 + docs/README.md | 9 +- docs/RESIDUAL-SECURITY-RISKS.md | 32 + docs/SECURITY-AND-BUG-FIXES.md | 42 + docs/dashboard-variants.md | 18 + .../extension}/FINAL-STATUS.md | 0 .../extension}/INSTALL.md | 0 docs/extension/README.md | 118 + .../extension}/TESTING.md | 0 .../reports/ARCHITECTURAL-SECURITY-REVIEW.md | 0 .../reports/CTO-FINAL-AUDIT-REPORT.md | 0 .../CTO-SHERLOCK-AUDIT-REPORT-FINAL.md | 0 .../reports/DEMO-READINESS-CHECKLIST.md | 0 .../reports/DEMO-SECURITY-VALIDATION.md | 0 .../reports/EMERGENCY-HARDENING-PLAN.md | 0 .../reports/PERFECTION-ACHIEVED.md | 0 .../reports/PRODUCTION-READINESS-REPORT.md | 0 .../reports/SHERLOCK-FINAL-AUDIT.md | 0 .../reports/VAULT-CLEANUP-COMPLETE.md | 0 .../reports/VAULT-CONSOLIDATION-PLAN.md | 0 docs/themes.md | 96 + emma-agent.js | 96 +- emma-cloud.html | 7 +- emma-vault-extension-fixed/README.md | 119 +- emma-vault-extension-fixed/content-script.js | 4 +- .../generate-icons.html | 2 +- emma-vault-extension-fixed/popup.html | 8 +- index.html | 23 + js/assistant-experience-popup.js | 8 +- js/clean-password-modal.js | 10 +- .../dashboard-init.js | 10686 ++++++++-------- js/dashboard-secure.js | 492 - js/emma-browser-client.js | 233 +- js/emma-chat-experience.js | 211 +- js/emma-input-modal.js | 12 +- js/emma-logger.js | 74 +- js/emma-performance-optimizer.js | 102 +- js/emma-web-vault.js | 349 +- js/memories.js | 13 +- js/memory-detail-modal-clean.js | 4 +- js/options.js | 28 +- js/people-page.js | 2225 ---- js/popup.js | 4 +- js/sanitize.js | 16 + js/secure-password-modal.js | 10 +- js/theme-manager.js | 617 + js/theme-settings-ui.js | 386 + js/voice-capture-experience.js | 29 +- js/webapp-extension-bridge.js | 4 +- js/welcome.js | 6 +- legacy/README.md | 3 + {js => legacy/unused/js}/aurora.js | 0 .../unused/js}/chat/emma-chat-core.js | 0 .../js}/chat/emma-dementia-companion.js | 0 .../unused/js}/chat/emma-intent-classifier.js | 0 .../unused/js}/chat/emma-memory-operations.js | 0 .../unused/js}/chat/emma-person-handler.js | 0 .../unused/js}/chat/emma-photo-manager.js | 0 {js => legacy/unused/js}/chat/index.js | 0 .../unused/js}/dementia-companion-init.js | 0 {js => legacy/unused/js}/memories-gallery.js | 0 .../unused/js}/memory-gallery-clean.js | 0 .../unused/js}/screenshot-photo-capture.js | 0 .../unused/js}/universal-orb-injection.js | 0 {js => legacy/unused/js}/web-memories.js | 0 pages/dashboard-new.html | 5294 -------- pages/dashboard-secure.html | 128 - pages/dashboard.html | 6662 ---------- pages/emma-settings-redesigned.html | 208 +- pages/gallery.html | 28 +- pages/memories.html | 36 +- pages/memory-gallery-new.html | 40 +- pages/options.html | 289 +- pages/people-emma.html | 459 +- pages/people.html | 719 +- pages/persona.html | 20 +- pages/popup.html | 26 +- pages/privacy.html | 5 +- pages/relationships.html | 7 +- pages/reset-vault.html | 2 +- pages/vault-migration.html | 6 +- pages/welcome.html | 20 +- server.js | 163 +- temp_patch.diff | 50 + themes/theme-catalog.js | 223 + "ting headers\357\200\242" | 83 - tmp_segment.txt | 87 + voice-test-backend.html | 136 - voice-test.html | 329 - 105 files changed, 11299 insertions(+), 33721 deletions(-) delete mode 100644 .force-cache-refresh-v2.txt delete mode 100644 .force-rebuild-2025-08-22T16-00-13Z.txt delete mode 100644 .render-deploy-trigger delete mode 100644 css/content.css create mode 100644 css/dashboard-base.css delete mode 100644 css/dashboard-secure.css delete mode 100644 css/emma-chat.css create mode 100644 css/theme-tokens.css delete mode 100644 css/voice-capture-premium.css delete mode 100644 css/voice-capture.css create mode 100644 docs/EMMA-WEB-VAULT-SECURITY-ANALYSIS.md create mode 100644 docs/RESIDUAL-SECURITY-RISKS.md create mode 100644 docs/SECURITY-AND-BUG-FIXES.md create mode 100644 docs/dashboard-variants.md rename {emma-vault-extension-fixed => docs/extension}/FINAL-STATUS.md (100%) rename {emma-vault-extension-fixed => docs/extension}/INSTALL.md (100%) create mode 100644 docs/extension/README.md rename {emma-vault-extension-fixed => docs/extension}/TESTING.md (100%) rename ARCHITECTURAL-SECURITY-REVIEW.md => docs/reports/ARCHITECTURAL-SECURITY-REVIEW.md (100%) rename CTO-FINAL-AUDIT-REPORT.md => docs/reports/CTO-FINAL-AUDIT-REPORT.md (100%) rename CTO-SHERLOCK-AUDIT-REPORT-FINAL.md => docs/reports/CTO-SHERLOCK-AUDIT-REPORT-FINAL.md (100%) rename DEMO-READINESS-CHECKLIST.md => docs/reports/DEMO-READINESS-CHECKLIST.md (100%) rename DEMO-SECURITY-VALIDATION.md => docs/reports/DEMO-SECURITY-VALIDATION.md (100%) rename EMERGENCY-HARDENING-PLAN.md => docs/reports/EMERGENCY-HARDENING-PLAN.md (100%) rename PERFECTION-ACHIEVED.md => docs/reports/PERFECTION-ACHIEVED.md (100%) rename PRODUCTION-READINESS-REPORT.md => docs/reports/PRODUCTION-READINESS-REPORT.md (100%) rename SHERLOCK-FINAL-AUDIT.md => docs/reports/SHERLOCK-FINAL-AUDIT.md (100%) rename VAULT-CLEANUP-COMPLETE.md => docs/reports/VAULT-CLEANUP-COMPLETE.md (100%) rename VAULT-CONSOLIDATION-PLAN.md => docs/reports/VAULT-CONSOLIDATION-PLAN.md (100%) create mode 100644 docs/themes.md rename working-desktop-dashboard.html => js/dashboard-init.js (54%) delete mode 100644 js/dashboard-secure.js delete mode 100644 js/people-page.js create mode 100644 js/sanitize.js create mode 100644 js/theme-manager.js create mode 100644 js/theme-settings-ui.js create mode 100644 legacy/README.md rename {js => legacy/unused/js}/aurora.js (100%) rename {js => legacy/unused/js}/chat/emma-chat-core.js (100%) rename {js => legacy/unused/js}/chat/emma-dementia-companion.js (100%) rename {js => legacy/unused/js}/chat/emma-intent-classifier.js (100%) rename {js => legacy/unused/js}/chat/emma-memory-operations.js (100%) rename {js => legacy/unused/js}/chat/emma-person-handler.js (100%) rename {js => legacy/unused/js}/chat/emma-photo-manager.js (100%) rename {js => legacy/unused/js}/chat/index.js (100%) rename {js => legacy/unused/js}/dementia-companion-init.js (100%) rename {js => legacy/unused/js}/memories-gallery.js (100%) rename {js => legacy/unused/js}/memory-gallery-clean.js (100%) rename {js => legacy/unused/js}/screenshot-photo-capture.js (100%) rename {js => legacy/unused/js}/universal-orb-injection.js (100%) rename {js => legacy/unused/js}/web-memories.js (100%) delete mode 100644 pages/dashboard-new.html delete mode 100644 pages/dashboard-secure.html delete mode 100644 pages/dashboard.html create mode 100644 temp_patch.diff create mode 100644 themes/theme-catalog.js delete mode 100644 "ting headers\357\200\242" create mode 100644 tmp_segment.txt delete mode 100644 voice-test-backend.html delete mode 100644 voice-test.html diff --git a/.force-cache-refresh-v2.txt b/.force-cache-refresh-v2.txt deleted file mode 100644 index e44772da..00000000 --- a/.force-cache-refresh-v2.txt +++ /dev/null @@ -1,13 +0,0 @@ -EXTREME CACHE BUST v2.1 -Timestamp: 2025-08-22T22:58:00Z - -ISSUE: Render.com still serving cached gallery.js WITHOUT escapeHtml function -ERROR: ReferenceError: escapeHtml is not defined at gallery.js:346 - -VERIFIED LOCALLY: -✅ escapeHtml function exists at line 1167 -✅ renderEmptyState function exists at line 1176 -✅ createNewMemory function exists at line 1202 -✅ File passes syntax check - -RENDER.COM NEEDS TO SERVE THE ACTUAL FILE! diff --git a/.force-rebuild-2025-08-22T16-00-13Z.txt b/.force-rebuild-2025-08-22T16-00-13Z.txt deleted file mode 100644 index 72ab788e..00000000 --- a/.force-rebuild-2025-08-22T16-00-13Z.txt +++ /dev/null @@ -1,7 +0,0 @@ -FORCE REBUILD CACHE BUST -Timestamp: 2025-08-22T16:00:13Z - -The memory-gallery-new.js file is CLEAN locally (1090 lines) -But Render.com is serving stale cached version (1278+ lines with broken code) - -This file exists solely to force a fresh deployment. diff --git a/.render-deploy-trigger b/.render-deploy-trigger deleted file mode 100644 index 465bbe099c30f5bb6f2f26ef55c8b6c69ea4b0a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100 zcmezWPnki1!HprGp@<=wAr;6D0+M+QDL}Fa$OF*@3^_pgN`_ppObJj_7l;iQEEx0| ejDQeC8UV2=P}GpYgux0dY6WDQGVn5RF#rI)bq`Ab diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b664107..9e831e75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "git.ignoreLimitWarning": true + "git.ignoreLimitWarning": true, + "chatgpt.openOnStartup": true } \ No newline at end of file diff --git a/_headers b/_headers index d1d1baa9..ced56644 100644 --- a/_headers +++ b/_headers @@ -41,4 +41,4 @@ Referrer-Policy: strict-origin-when-cross-origin X-Frame-Options: DENY Permissions-Policy: microphone=(self), geolocation=(), camera=() - Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://*.onrender.com; connect-src 'self' https://*.onrender.com \ No newline at end of file + Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://*.onrender.com; connect-src 'self' https://*.onrender.com diff --git a/add-person.html b/add-person.html index d7c728a7..47fadd0e 100644 --- a/add-person.html +++ b/add-person.html @@ -4,7 +4,11 @@ Add Person - Emma + + + + - - - -
-
-
- - - - - -
-

emma

-

Your Intelligent Memory Companion

-
- - -
- -
- - -
-
- -
-
- -
-
- - -
-
- - -
-
-
- - - - -
-
Memories
-
-
-
- - - - -
-
People
-
-
-
- - - -
-
Chat
-
-
- - - - -
- -
- - - - -
-
- - -
-
-
0
-
Memories
-
-
-
0
-
People
-
- - -
-
🌟
-
Upgrade to .emma
-
NEW
-
-
-
0
-
Today
-
-
- - - - - - -
-
-
📅
-
Daily Brief
-
-
-

Good morning! You have 3 memories from this day last year.

-

- • Meeting with Sarah about the project
- • Family dinner at Marina Bay
- • Completed the meditation course -

-
-
- -
-
-
💡
-
AI Insights
-
-
-

I noticed you've been capturing more creative moments lately. Your photography memories have increased by 40% this month!

-
-
- -
-
-
-
Quick Actions
-
-
-

Press Space to activate voice capture
- Press Cmd+K to search memories
- Say "Hey Emma" to start

-
-
- - -
-
-
-
-
-
-
-
- Listening... -
- - -
-
-
-

Loading Emma...

-
-
- - - - - - - -
-
-
📅
-
Daily Brief
-
-
-

Good morning! You have 3 memories from this day last year.

-

- • Meeting with Sarah about the project
- • Family dinner at Marina Bay
- • Completed the meditation course -

-
-
- -
-
-
💡
-
AI Insights
-
-
-

I noticed you've been capturing more creative moments lately. Your photography memories have increased by 40% this month!

-
-
- -
-
-
-
Quick Actions
-
-
-

Press Space to activate voice capture
- Press Cmd+K to search memories
- Say "Hey Emma" to start

-
-
- - -
-
-
-
-
-
-
-
- Listening... -
- - -
-
-
-

Loading Emma...

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/js/dashboard-secure.js b/js/dashboard-secure.js deleted file mode 100644 index 1a3b4ad6..00000000 --- a/js/dashboard-secure.js +++ /dev/null @@ -1,492 +0,0 @@ -// app/js/dashboard-secure.js - Extracted dashboard functionality for CSP compliance -// SECURITY: External script file to eliminate inline JavaScript - -// Dashboard Controller -class EmmaDashboard { - constructor() { - this.orb = document.getElementById('emma-orb'); - this.radialMenu = document.getElementById('radial-menu'); - this.voiceIndicator = document.getElementById('voice-indicator'); - this.loading = document.getElementById('loading'); - - console.log('🔧 Constructor - radialMenu:', this.radialMenu); - console.log('🔧 Constructor - radial items:', this.radialMenu ? this.radialMenu.querySelectorAll('.radial-item').length : 'NULL'); - this.panels = { - dailyBrief: document.getElementById('daily-brief'), - aiInsights: document.getElementById('ai-insights'), - quickActions: document.getElementById('quick-actions') - }; - - this.isMenuOpen = false; - this.stats = { memories: 0, people: 0, today: 0 }; - this.particles = []; - this.nodes = []; - this.neuralCanvas = document.getElementById('neural-canvas'); - this.neuralCtx = this.neuralCanvas.getContext('2d'); - - // Neural memory network properties - this.isConstellationMode = false; - this.constellationMemories = []; - this.centralNeuron = null; - - // Initialize WebGL Emma Orb - this.initEmmaOrb(); - - this.init(); - } - - async init() { - // Show loading - this.loading.classList.add('active'); - - // Initialize constellation background - this.initConstellation(); - - // Setup event listeners - this.setupEventListeners(); - - // Load initial data - await this.loadDashboardData(); - - // Animate panels based on time of day - this.showContextualPanels(); - - // Hide loading - setTimeout(() => { - this.loading.classList.remove('active'); - }, 1500); - - // Start background animations - this.startAnimations(); - } - - initConstellation() { - const canvas = document.getElementById('constellation'); - const ctx = canvas.getContext('2d'); - - // Set canvas size - const resize = () => { - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - }; - resize(); - window.addEventListener('resize', resize); - - // Create memory particles - const particleCount = 50; - for (let i = 0; i < particleCount; i++) { - this.particles.push({ - x: Math.random() * canvas.width, - y: Math.random() * canvas.height, - vx: (Math.random() - 0.5) * 0.5, - vy: (Math.random() - 0.5) * 0.5, - radius: Math.random() * 2 + 1, - opacity: Math.random() * 0.5 + 0.2 - }); - } - - // Animation loop - const animate = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Update and draw particles - this.particles.forEach((p, i) => { - // Update position - p.x += p.vx; - p.y += p.vy; - - // Wrap around edges - if (p.x < 0) p.x = canvas.width; - if (p.x > canvas.width) p.x = 0; - if (p.y < 0) p.y = canvas.height; - if (p.y > canvas.height) p.y = 0; - - // Draw particle - ctx.beginPath(); - ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); - ctx.fillStyle = `rgba(134, 88, 255, ${p.opacity})`; - ctx.fill(); - - // Draw connections - this.particles.forEach((p2, j) => { - if (i !== j) { - const dx = p.x - p2.x; - const dy = p.y - p2.y; - const dist = Math.sqrt(dx * dx + dy * dy); - - if (dist < 100) { - ctx.beginPath(); - ctx.moveTo(p.x, p.y); - ctx.lineTo(p2.x, p2.y); - ctx.strokeStyle = `rgba(134, 88, 255, ${(1 - dist / 100) * 0.2})`; - ctx.stroke(); - } - } - }); - }); - - requestAnimationFrame(animate); - }; - animate(); - } - - setupEventListeners() { - // Orb click - exit constellation or toggle menu - this.orb.addEventListener('click', (e) => { - e.stopPropagation(); - if (this.isConstellationMode) { - // Simple: Exit constellation mode back to main dashboard - this.exitMemoryConstellation(); - } else { - this.toggleRadialMenu(); - } - }); - - // Radial menu items - this.radialMenu.querySelectorAll('.radial-item').forEach(item => { - item.addEventListener('click', (e) => { - e.stopPropagation(); - const action = item.dataset.action; - this.handleRadialAction(action); - }); - }); - - // Track mouse position for neural effects - this.mouseX = 0; - this.mouseY = 0; - document.addEventListener('mousemove', (e) => { - this.mouseX = e.clientX; - this.mouseY = e.clientY; - - // Add proximity effects to nodes (both menu and constellation modes) - if ((this.isMenuOpen || this.isConstellationMode) && this.nodes.length > 0) { - this.nodes.forEach(node => { - const dist = Math.sqrt( - Math.pow(this.mouseX - node.x, 2) + - Math.pow(this.mouseY - node.y, 2) - ); - - if (dist < 150) { - // Gentle repel nodes from mouse (petri dish organisms) - const force = (150 - dist) / 150; - const angle = Math.atan2(node.y - this.mouseY, node.x - this.mouseX); - node.vx += Math.cos(angle) * force * 0.5; // Much gentler repulsion - node.vy += Math.sin(angle) * force * 0.5; - - // Add glow effect (stronger for memory nodes) - const glowStrength = this.isConstellationMode ? 60 : 40; - const baseOpacity = this.isConstellationMode ? 0.8 : 0.6; - node.element.style.boxShadow = `0 0 ${glowStrength + force * 30}px rgba(134, 88, 255, ${baseOpacity + force * 0.4})`; - } else { - const defaultGlow = this.isConstellationMode ? '0 0 40px rgba(134, 88, 255, 0.4)' : '0 0 30px rgba(134, 88, 255, 0.2)'; - node.element.style.boxShadow = defaultGlow; - } - }); - } - }); - - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.code === 'Space' && !e.target.matches('input, textarea')) { - e.preventDefault(); - this.startVoiceCapture(); - } else if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - this.handleRadialAction('search'); - } else if (e.key === 'Escape' && this.isMenuOpen) { - this.toggleRadialMenu(); - } - }); - - // Click outside to close menu - document.addEventListener('click', (e) => { - if (this.isMenuOpen && !this.orb.contains(e.target) && !this.radialMenu.contains(e.target)) { - console.log('🚨 Document click closing menu - target:', e.target); - this.toggleRadialMenu(); - } - }); - - // Hover effects for orb - this.orb.addEventListener('mouseenter', () => { - document.querySelectorAll('.quick-stats').forEach(el => { - el.style.opacity = '1'; - }); - }); - } - - async loadDashboardData() { - try { - // Load vault status (store but don't show on central orb) - if (window.emmaAPI && window.emmaAPI.vault) { - try { - const status = await window.emmaAPI.vault.status(); - window.currentVaultStatus = status || { isUnlocked: false }; - } catch (e) { - console.warn('⚠️ Vault status unavailable:', e.message); - // CRITICAL FIX: Don't force lock status - preserve existing state - if (!window.currentVaultStatus) { - window.currentVaultStatus = { isUnlocked: false }; - } - } - - // Load stats safely - try { - const stats = await window.emmaAPI.vault.stats(); - this.updateStats({ - memories: (stats && stats.totalMemories) || 42, - people: (stats && stats.totalPeople) || 12, - today: (stats && stats.todayMemories) || 3 - }); - } catch (e) { - console.warn('⚠️ Vault stats unavailable:', e.message); - this.updateStats({ memories: 42, people: 12, today: 3 }); - } - } else { - // Demo data when API not available - console.log('🎭 Using demo data - Emma API not available'); - // CRITICAL FIX: Don't force lock status - preserve existing state - if (!window.currentVaultStatus) { - window.currentVaultStatus = { isUnlocked: false }; - } - this.updateStats({ memories: 42, people: 12, today: 3 }); - } - } catch (error) { - console.error('🚨 Dashboard data load error:', error); - // Fallback to demo data - // CRITICAL FIX: Don't force lock status - preserve existing state - if (!window.currentVaultStatus) { - window.currentVaultStatus = { isUnlocked: false }; - } - this.updateStats({ memories: 42, people: 12, today: 3 }); - } - } - - updateStats(newStats) { - Object.assign(this.stats, newStats); - - // Animate number changes - Object.keys(newStats).forEach(key => { - const element = document.getElementById(`stat-${key}`); - if (element) { - const start = parseInt(element.textContent) || 0; - const end = this.stats[key]; - const duration = 1000; - const startTime = Date.now(); - - const animate = () => { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - const current = Math.floor(start + (end - start) * progress); - element.textContent = current; - - if (progress < 1) { - requestAnimationFrame(animate); - } - }; - animate(); - } - }); - } - - showContextualPanels() { - const hour = new Date().getHours(); - - // Show all panels for demo - setTimeout(() => { - // Show all available panels - if (this.panels.dailyBrief) this.panels.dailyBrief.classList.add('active'); - if (this.panels.aiInsights) this.panels.aiInsights.classList.add('active'); - if (this.panels.quickActions) this.panels.quickActions.classList.add('active'); - }, 1000); - } - - startAnimations() { - // Implement remaining animation methods... - console.log('🎬 Starting background animations'); - } - - // Add simplified versions of other methods for CSP compliance - initEmmaOrb() { - try { - const orbContainer = document.getElementById('webgl-orb-container'); - if (!orbContainer) { - console.warn('🌟 WebGL orb container not found'); - return; - } - - if (window.EmmaOrb) { - // Create WebGL Emma Orb with Emma's signature purple-pink hue - this.webglOrb = new window.EmmaOrb(orbContainer, { - hue: 270, // Purple-pink Emma colors - hoverIntensity: 0.35, - rotateOnHover: true, - forceHoverState: false - }); - console.log('🌟 WebGL Emma Orb initialized successfully'); - } else { - console.warn('🌟 EmmaOrb class not available, using fallback'); - // Fallback to simple gradient if EmmaOrb class not loaded - orbContainer.style.background = 'radial-gradient(circle at 30% 30%, #8658ff, #4f46e5)'; - orbContainer.style.borderRadius = '50%'; - orbContainer.style.width = '100%'; - orbContainer.style.height = '100%'; - } - } catch (error) { - console.error('🚨 Error initializing Emma Orb:', error); - // Ensure fallback is applied - const orbContainer = document.getElementById('webgl-orb-container'); - if (orbContainer) { - orbContainer.style.background = 'radial-gradient(circle at 30% 30%, #8658ff, #4f46e5)'; - orbContainer.style.borderRadius = '50%'; - orbContainer.style.width = '100%'; - orbContainer.style.height = '100%'; - } - } - } - - toggleRadialMenu() { - this.isMenuOpen = !this.isMenuOpen; - console.log('🎯 Toggle menu:', this.isMenuOpen); - - if (this.isMenuOpen) { - this.radialMenu.classList.add('active'); - document.body.classList.add('menu-active'); - } else { - this.radialMenu.classList.remove('active'); - document.body.classList.remove('menu-active'); - } - } - - handleRadialAction(action) { - console.log('🎯 Radial action:', action); - this.toggleRadialMenu(); // Close menu after action - - switch (action) { - case 'voice': - this.startVoiceCapture(); - break; - case 'chat': - this.openEmmaChatExperience(); - break; - case 'memories': - this.openMemoryGallery(); - break; - case 'vault': - this.openVaultModal(); - break; - case 'share': - this.openShareExperience(); - break; - case 'search': - this.showToast('🔍 Search coming soon!', 'info'); - break; - default: - this.showToast(`Action: ${action}`, 'info'); - } - } - - async startVoiceCapture() { - console.log('🎤 Starting voice capture...'); - try { - if (window.VoiceCaptureExperience) { - this.voiceCaptureExperience = new VoiceCaptureExperience({ - source: 'dashboard', - mode: 'voice_only' - }); - await this.voiceCaptureExperience.show(); - } else { - this.showToast('🎤 Voice capture system loading...', 'info'); - } - } catch (error) { - console.error('🎤 Voice capture error:', error); - this.showToast('🎤 Voice capture unavailable', 'error'); - } - } - - openEmmaChatExperience() { - console.log('🤖 Opening Emma Chat...'); - try { - if (window.EmmaChatExperience) { - this.emmaChatExperience = new EmmaChatExperience(); - this.emmaChatExperience.show(); - } else { - this.showToast('🤖 Chat system loading...', 'info'); - } - } catch (error) { - console.error('🤖 Chat error:', error); - this.showToast('🤖 Chat unavailable', 'error'); - } - } - - openMemoryGallery() { - console.log('💎 Opening memory gallery...'); - window.location.href = '../memories-gallery.html'; - } - - openVaultModal() { - const modal = document.getElementById('vault-modal'); - if (modal) { - modal.style.display = 'flex'; - setTimeout(() => modal.classList.add('show'), 10); - } - } - - closeVaultModal() { - const modal = document.getElementById('vault-modal'); - if (modal) { - modal.classList.remove('show'); - setTimeout(() => modal.style.display = 'none', 300); - } - } - - showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast ${type}`; - const span = document.createElement('span'); - span.textContent = message; // Safe - prevents HTML injection - toast.appendChild(span); - document.body.appendChild(toast); - - setTimeout(() => toast.classList.add('show'), 10); - - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 400); - }, 3000); - } -} - -// Initialize dashboard when DOM is ready -document.addEventListener('DOMContentLoaded', () => { - window.emmaDashboard = new EmmaDashboard(); - console.log('✨ Emma Dashboard 2.0 initialized'); - - // Setup vault modal event listeners - const dashboard = window.emmaDashboard; - - // Close button - const closeBtn = document.getElementById('close-vault-modal'); - if (closeBtn) { - closeBtn.addEventListener('click', () => { - dashboard.closeVaultModal(); - }); - } - - // Click outside to close - const modal = document.getElementById('vault-modal'); - if (modal) { - modal.addEventListener('click', (e) => { - if (e.target.id === 'vault-modal') { - dashboard.closeVaultModal(); - } - }); - } - - // Other event listeners... - const unlockBtn = document.getElementById('unlock-btn'); - if (unlockBtn) { - unlockBtn.addEventListener('click', () => { - dashboard.unlockVault(); - }); - } -}); diff --git a/js/emma-browser-client.js b/js/emma-browser-client.js index 84009e37..1726806b 100644 --- a/js/emma-browser-client.js +++ b/js/emma-browser-client.js @@ -32,6 +32,17 @@ class EmmaBrowserClient { this.synth = window.speechSynthesis || null; this.audioWorkletNode = null; this.audioContext = null; + this.mediaStream = null; + this._audioSinkNode = null; + this._audioWorkletModuleUrl = null; + this._realtimeStream = null; + this._audioSourceNode = null; + this._sinkConnected = false; + this._realtimeProcessor = null; + this._realtimeSource = null; + this._realtimeAudioContext = null; + this._realtimeSinkNode = null; + this._disableRecognition = false; this.lastEmmaText = ''; } @@ -40,6 +51,7 @@ class EmmaBrowserClient { */ async startVoiceSession() { try { + this._disableRecognition = false; this.setState('connecting'); // Connect to Emma backend @@ -74,12 +86,44 @@ class EmmaBrowserClient { */ async setupPcmStreaming(mediaStream) { try { - if (!window.AudioWorkletNode || !window.AudioContext) return; + if (!mediaStream) { + return; + } + + this.mediaStream = mediaStream; + + if (!window.AudioWorkletNode || !window.AudioContext) { + return; + } + this.audioContext = this.audioContext || new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 }); - const source = this.audioContext.createMediaStreamSource(mediaStream); + if (!this.audioContext) { + return; + } - // Inline processor via AudioWorklet - const processorCode = ` + if (this._audioSourceNode) { + try { + this._audioSourceNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._audioSourceNode = null; + } + + if (this.audioWorkletNode) { + try { + this.audioWorkletNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this.audioWorkletNode = null; + } + + this._audioSourceNode = this.audioContext.createMediaStreamSource(mediaStream); + + if (!this._audioWorkletModuleUrl) { + // Inline processor via AudioWorklet (loaded once per context) + const processorCode = ` class EmmaPcmProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -130,17 +174,31 @@ class EmmaBrowserClient { registerProcessor('emma-pcm', EmmaPcmProcessor); `; - const blob = new Blob([processorCode], { type: 'application/javascript' }); - const url = URL.createObjectURL(blob); - await this.audioContext.audioWorklet.addModule(url); + const blob = new Blob([processorCode], { type: 'application/javascript' }); + this._audioWorkletModuleUrl = URL.createObjectURL(blob); + await this.audioContext.audioWorklet.addModule(this._audioWorkletModuleUrl); + } + this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'emma-pcm'); this.audioWorkletNode.port.onmessage = (e) => { if (e.data?.type === 'chunk') { this.sendToAgent({ type: 'user_audio_chunk', chunk: e.data.data }); } }; - source.connect(this.audioWorkletNode); - this.audioWorkletNode.connect(this.audioContext.destination); + + this._audioSinkNode = this._audioSinkNode || this.audioContext.createGain(); + if (this._audioSinkNode) { + this._audioSinkNode.gain.value = 0; + } + + this._audioSourceNode.connect(this.audioWorkletNode); + if (this._audioSinkNode) { + this.audioWorkletNode.connect(this._audioSinkNode); + if (!this._sinkConnected) { + this._audioSinkNode.connect(this.audioContext.destination); + this._sinkConnected = true; + } + } } catch (e) { console.warn('🎙️ PCM streaming not available:', e?.message || e); } @@ -545,11 +603,124 @@ class EmmaBrowserClient { */ async stopVoiceSession() { try { + this._disableRecognition = true; this.isConnected = false; this.setState('idle'); // Send stop message to agent this.sendToAgent({ type: 'stop_session' }); + + if (this.recognition) { + try { + this.recognition.onend = null; + this.recognition.onerror = null; + this.recognition.onresult = null; + this.recognition.onstart = null; + this.recognition.stop(); + } catch (e) { + // Ignore recognition stop errors + } + this.recognition = null; + } + this.isListening = false; + + if (this.mediaStream) { + try { + this.mediaStream.getTracks().forEach(track => track.stop()); + } catch (e) { + // Ignore track stop errors + } + this.mediaStream = null; + } + + if (this._audioSourceNode) { + try { + this._audioSourceNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._audioSourceNode = null; + } + + if (this.audioWorkletNode) { + try { + this.audioWorkletNode.port.onmessage = null; + this.audioWorkletNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this.audioWorkletNode = null; + } + + if (this._audioSinkNode) { + try { + this._audioSinkNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._audioSinkNode = null; + this._sinkConnected = false; + } + + if (this.audioContext) { + try { + this.audioContext.close(); + } catch (e) { + // Ignore close errors + } + this.audioContext = null; + } + + if (this._audioWorkletModuleUrl) { + URL.revokeObjectURL(this._audioWorkletModuleUrl); + this._audioWorkletModuleUrl = null; + } + + if (this._realtimeProcessor) { + try { + this._realtimeProcessor.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._realtimeProcessor.onaudioprocess = null; + this._realtimeProcessor = null; + } + + if (this._realtimeSource) { + try { + this._realtimeSource.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._realtimeSource = null; + } + + if (this._realtimeSinkNode) { + try { + this._realtimeSinkNode.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._realtimeSinkNode = null; + } + + if (this._realtimeAudioContext) { + try { + this._realtimeAudioContext.close(); + } catch (e) { + // Ignore close errors + } + this._realtimeAudioContext = null; + } + + if (this._realtimeStream) { + try { + this._realtimeStream.getTracks().forEach(track => track.stop()); + } catch (e) { + // Ignore track stop errors + } + this._realtimeStream = null; + } // Close WebSocket if (this.websocket) { @@ -610,14 +781,38 @@ class EmmaBrowserClient { } }); + this._realtimeStream = stream; + // Create audio context for real-time processing - this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); - const source = this.audioContext.createMediaStreamSource(stream); + this._realtimeAudioContext = this._realtimeAudioContext || new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); + if (!this._realtimeAudioContext) { + return; + } + + if (this._realtimeSource) { + try { + this._realtimeSource.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._realtimeSource = null; + } + + if (this._realtimeProcessor) { + try { + this._realtimeProcessor.disconnect(); + } catch (e) { + // Ignore disconnect errors + } + this._realtimeProcessor = null; + } + + this._realtimeSource = this._realtimeAudioContext.createMediaStreamSource(stream); // Create a script processor for real-time audio chunks - const processor = this.audioContext.createScriptProcessor(4096, 1, 1); + this._realtimeProcessor = this._realtimeAudioContext.createScriptProcessor(4096, 1, 1); - processor.onaudioprocess = (event) => { + this._realtimeProcessor.onaudioprocess = (event) => { if (!this.isConnected) return; const inputBuffer = event.inputBuffer; @@ -645,8 +840,16 @@ class EmmaBrowserClient { }); }; - source.connect(processor); - processor.connect(this.audioContext.destination); + this._realtimeSource.connect(this._realtimeProcessor); + + this._realtimeSinkNode = this._realtimeSinkNode || this._realtimeAudioContext.createGain(); + if (this._realtimeSinkNode) { + this._realtimeSinkNode.gain.value = 0; + this._realtimeProcessor.connect(this._realtimeSinkNode); + this._realtimeSinkNode.connect(this._realtimeAudioContext.destination); + } else { + this._realtimeProcessor.connect(this._realtimeAudioContext.destination); + } console.log('✅ Real-time audio streaming active'); diff --git a/js/emma-chat-experience.js b/js/emma-chat-experience.js index f47fd0c7..9718cc91 100644 --- a/js/emma-chat-experience.js +++ b/js/emma-chat-experience.js @@ -8,6 +8,46 @@ // Emma Chat Experience - Production Ready +window.metadataIncludesPerson = window.metadataIncludesPerson || ((peopleList, person) => { + if (!Array.isArray(peopleList) || !person) return false; + + const targetId = person.id ? String(person.id).trim() : ''; + if (!targetId) return false; + + const resolveEntryId = (entry) => { + if (!entry) return null; + if (typeof entry === 'string' || typeof entry === 'number') { + const trimmed = String(entry).trim(); + return trimmed || null; + } + if (typeof entry === 'object') { + const candidate = entry.id ?? entry.personId ?? entry.personID ?? entry.person_id ?? entry.uuid ?? entry.guid; + if (candidate === undefined || candidate === null) return null; + const trimmed = String(candidate).trim(); + return trimmed || null; + } + return null; + }; + + return peopleList.some(entry => { + const entryId = resolveEntryId(entry); + return entryId && entryId === targetId; + }); +}); + +window.contentMentionsPerson = window.contentMentionsPerson || ((personName, content) => { + if (!personName || !content) return false; + + const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const words = personName.trim().split(/\s+/).filter(Boolean).map(escapeRegex); + if (words.length === 0) return false; + + const pattern = `\\b${words.join('\\s+')}\\b(?:'s)?`; + const regex = new RegExp(pattern, 'i'); + + return regex.test(content); +}); + class EmmaChatExperience extends ExperiencePopup { constructor(position, settings = {}) { super(position, settings); @@ -20,6 +60,8 @@ class EmmaChatExperience extends ExperiencePopup { this.messageContainer = null; this.inputField = null; this.sendButton = null; + this.voiceIntegrationInitialized = false; + this.voiceIntegrationInitializing = false; // 🧠 Vectorless AI Engine Integration this.vectorlessEngine = null; @@ -167,6 +209,12 @@ class EmmaChatExperience extends ExperiencePopup { * Transforms chat into voice-first multimodal experience */ async initializeVoiceIntegration() { + if (this.voiceIntegrationInitialized || this.voiceIntegrationInitializing) { + return; + } + + this.voiceIntegrationInitializing = true; + try { console.log('🎙️ Initializing voice-first chat experience...'); @@ -193,6 +241,8 @@ class EmmaChatExperience extends ExperiencePopup { // Connect voice button to Emma's voice system this.setupVoiceButton(); + this.voiceIntegrationInitialized = true; + // Set up voice event handlers for chat integration (if needed) if (this.setupVoiceEventHandlers) { this.setupVoiceEventHandlers(); @@ -205,9 +255,11 @@ class EmmaChatExperience extends ExperiencePopup { this.voiceButton.title = 'Voice system not available'; } } catch (error) { - console.error('❌ Voice integration failed:', error); + console.error('??O Voice integration failed:', error); this.voiceButton.style.opacity = '0.5'; this.voiceButton.title = 'Voice system error: ' + error.message; + } finally { + this.voiceIntegrationInitializing = false; } } @@ -1228,26 +1280,17 @@ class EmmaChatExperience extends ExperiencePopup { if (window.emmaWebVault?.vaultData?.content?.memories) { const vaultMemories = window.emmaWebVault.vaultData.content.memories; + const personForMatching = { id: person.id, name: person.name }; for (const [memoryId, memory] of Object.entries(vaultMemories)) { - // Check if person is in memory's people metadata - if (memory.metadata && memory.metadata.people && - Array.isArray(memory.metadata.people) && - memory.metadata.people.includes(person.id)) { - connectedMemories.push({ ...memory, id: memoryId }); + const normalizedMemory = { ...memory, id: memory.id || memoryId }; + + if (window.metadataIncludesPerson(memory.metadata?.people, personForMatching)) { + connectedMemories.push(normalizedMemory); + continue; } - // Also check if person's name is mentioned in content - if (person.name && memory.content) { - const personName = person.name.toLowerCase(); - const memoryContent = memory.content.toLowerCase(); - if (memoryContent.includes(personName)) { - // Avoid duplicates - if (!connectedMemories.find(m => m.id === memoryId)) { - connectedMemories.push({ ...memory, id: memoryId }); - } - } - } + // Content-based inference removed; rely on explicit metadata associations only } } @@ -2257,8 +2300,12 @@ class EmmaChatExperience extends ExperiencePopup { } formatMessageContent(content) { - // Basic formatting for Emma's responses - return content + // Escape HTML, then apply basic formatting for Emma's responses + const esc = (s) => (window.escapeHtml ? window.escapeHtml(s) : String(s) + .replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/g, ''')); + const safe = esc(content || ''); + return safe .replace(/\*\*(.*?)\*\*/g, '$1') // Bold .replace(/\*(.*?)\*/g, '$1') // Italic .replace(/\n/g, '
'); // Line breaks @@ -5495,6 +5542,7 @@ RULES: * Show memory preview dialog */ showMemoryPreviewDialog(memory) { + const esc = (s) => (window.escapeHtml ? window.escapeHtml(String(s)) : String(s).replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/g, ''')); // 🎯 CRITICAL FIX: Store memory in temporary storage for editing this.temporaryMemories.set(memory.id, memory); console.log('🎯 MOBILE PREVIEW: Stored temporary memory for editing:', memory.id); @@ -5508,14 +5556,14 @@ RULES:
-

${memory.title || 'Beautiful Memory'}

+

${esc(memory.title || 'Beautiful Memory')}

-
${memory.metadata?.date || 'Saturday, August 23, 2025'}
+
${esc(memory.metadata?.date || 'Saturday, August 23, 2025')}
@@ -5526,7 +5574,7 @@ RULES: .filter(att => att.type?.startsWith('image/')) .slice(0, 5) .map((image, index) => ` -
+
`).join('')} @@ -5548,7 +5596,7 @@ RULES: ${peopleList.length > 0 ? `

👥 People in this memory

-
+
@@ -5557,7 +5605,7 @@ RULES:
-

${memory.content}

+

${esc(memory.content || '')}

${memory.metadata?.emotions?.length > 0 ? ` @@ -5565,7 +5613,7 @@ RULES: 💭
${memory.metadata.emotions.map(emotion => ` - ${emotion} + ${esc(emotion)} `).join('')}
@@ -5574,7 +5622,7 @@ RULES: ${memory.metadata?.location ? `
📍 - ${memory.metadata.location} + ${esc(memory.metadata.location)}
` : ''}
@@ -5587,15 +5635,15 @@ RULES: ${memory.attachments.map((attachment, index) => `
${attachment.type?.startsWith('image/') ? ` - ${attachment.name} + ${esc(attachment.name || '')} ` : attachment.type?.startsWith('video/') ? ` -
@@ -5995,7 +6043,7 @@ RULES: .filter(att => att.type?.startsWith('image/')) .slice(0, 5) .map((image, index) => ` -
+
`).join('')} @@ -6017,7 +6065,7 @@ RULES: ${peopleList.length > 0 ? `

👥 People in this memory

-
+
@@ -6026,7 +6074,7 @@ RULES:
-

${memory.content}

+

${esc(memory.content || '')}

${memory.metadata?.emotions?.length > 0 ? ` @@ -6034,7 +6082,7 @@ RULES: 💭
${memory.metadata.emotions.map(emotion => ` - ${emotion} + ${esc(emotion)} `).join('')}
@@ -6043,7 +6091,7 @@ RULES: ${memory.metadata?.location ? `
📍 - ${memory.metadata.location} + ${esc(memory.metadata.location)}
` : ''}
@@ -6851,21 +6899,29 @@ RULES: }, { passive: false, capture: false }); // 📱 MOBILE: Handle touch events for mobile/tablet scrolling - let touchStartY = 0; - - editModal.addEventListener('touchstart', (e) => { - touchStartY = e.touches[0].clientY; - }, { passive: true }); - + const isTouchWithinContent = (node) => { + if (!content || !node) return false; + if (node === content) return true; + if (node.nodeType !== 1) { + return isTouchWithinContent(node.parentNode); + } + return node.closest('.edit-modal-content') === content; + }; + editModal.addEventListener('touchmove', (e) => { - // If touch is on content area, allow normal scrolling - if (content && (e.target === content || content.contains(e.target))) { - return; // Allow normal touch scrolling on content + // Allow natural scrolling when the gesture originates inside the modal + if (isTouchWithinContent(e.target)) { + return; } - + // Otherwise, prevent background scrolling e.preventDefault(); }, { passive: false }); + + content.addEventListener('touchmove', (e) => { + // Keep scroll gestures inside the modal body + e.stopPropagation(); + }, { passive: true }); // Block body scroll completely while modal is open const originalOverflow = document.body.style.overflow; @@ -7000,7 +7056,18 @@ RULES: // Update memory object memory.metadata = memory.metadata || {}; memory.metadata.title = title; - memory.metadata.people = selectedPeople; + const selectedIds = Array.from(new Set(selectedPeople + .map(person => person.id ? person.id.trim() : '') + .filter(Boolean))); + const selectedNames = Array.from(new Set(selectedPeople + .map(person => person.name ? person.name.trim() : '') + .filter(Boolean))); + memory.metadata.people = selectedIds; + if (selectedNames.length > 0) { + memory.metadata.peopleNames = selectedNames; + } else if (memory.metadata.peopleNames) { + delete memory.metadata.peopleNames; + } memory.title = title; memory.content = content; @@ -7147,7 +7214,22 @@ RULES: } // Get currently selected people - const selectedPeopleIds = (memory.metadata?.people || []).map(p => p.id); + const selectedPeopleIds = (memory.metadata?.people || []) + .map(personRef => { + if (!personRef) return null; + if (typeof personRef === 'string' || typeof personRef === 'number') { + const trimmed = String(personRef).trim(); + return trimmed || null; + } + if (typeof personRef === 'object') { + const candidate = personRef.id ?? personRef.personId ?? personRef.personID ?? personRef.person_id; + if (candidate === undefined || candidate === null) return null; + const trimmed = String(candidate).trim(); + return trimmed || null; + } + return null; + }) + .filter(Boolean); if (people.length === 0) { loadingText.textContent = 'No people in vault yet. Add people first!'; @@ -7670,11 +7752,21 @@ RULES: // Auto-select the new person in the memory memory.metadata = memory.metadata || {}; - memory.metadata.people = memory.metadata.people || []; - memory.metadata.people.push({ - id: personId, - name: name - }); + memory.metadata.people = Array.isArray(memory.metadata.people) ? memory.metadata.people : []; + const trimmedPersonId = personId ? String(personId).trim() : ''; + if (trimmedPersonId && !memory.metadata.people.includes(trimmedPersonId)) { + memory.metadata.people.push(trimmedPersonId); + } + + if (name) { + const trimmedName = name.trim(); + if (trimmedName) { + memory.metadata.peopleNames = Array.isArray(memory.metadata.peopleNames) ? memory.metadata.peopleNames : []; + if (!memory.metadata.peopleNames.includes(trimmedName)) { + memory.metadata.peopleNames.push(trimmedName); + } + } + } this.showToast(`✨ ${name} added to vault and memory!`, 'success'); closeDialog(); @@ -7958,11 +8050,22 @@ RULES: if (newPerson) { // Replace temp ID with real ID const tempId = `temp_${personName.toLowerCase()}`; - const peopleIds = state.memory.metadata.people || []; + const peopleIds = Array.isArray(state.memory.metadata.people) ? state.memory.metadata.people : []; const updatedPeopleIds = peopleIds.map(id => id === tempId ? newPerson.id : id); state.memory.metadata.people = updatedPeopleIds; + const existingNames = Array.isArray(state.memory.metadata.peopleNames) ? state.memory.metadata.peopleNames : []; + const filteredNames = existingNames.filter(name => name.toLowerCase() !== personName.toLowerCase()); + if (newPerson.name) { + filteredNames.push(newPerson.name); + } + if (filteredNames.length > 0) { + state.memory.metadata.peopleNames = Array.from(new Set(filteredNames)); + } else if (state.memory.metadata.peopleNames) { + delete state.memory.metadata.peopleNames; + } + // Remove from newPeopleDetected state.memory.metadata.newPeopleDetected = (state.memory.metadata.newPeopleDetected || []) .filter(name => name.toLowerCase() !== personName.toLowerCase()); @@ -9621,7 +9724,7 @@ Just the question:`; if (people.length === 0) { peopleGrid.innerHTML = `
- No people in vault yet. + No people in vault yet.
`; return; @@ -10188,3 +10291,5 @@ Analyze the user's intent and respond with JSON: // Export for use in other modules window.EmmaChatExperience = EmmaChatExperience; console.log('💬 Emma Chat Experience: Module loaded successfully'); + + diff --git a/js/emma-input-modal.js b/js/emma-input-modal.js index d4233344..e3ba8963 100644 --- a/js/emma-input-modal.js +++ b/js/emma-input-modal.js @@ -35,6 +35,10 @@ class EmmaInputModal { defaultValue = "" } = options; + const esc = (s) => (window.escapeHtml ? window.escapeHtml(s) : String(s) + .replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/g, ''')); + this.currentResolve = resolve; this.currentReject = reject; this.isOpen = true; @@ -44,20 +48,20 @@ class EmmaInputModal {
-

${title}

+

${esc(title)}

-

${message}

+

${esc(message)}

diff --git a/js/emma-logger.js b/js/emma-logger.js index c4d02705..09fd022e 100644 --- a/js/emma-logger.js +++ b/js/emma-logger.js @@ -8,112 +8,81 @@ class EmmaLogger { constructor() { - // Use existing environment detection or fallback this.isProduction = window.EMMA_ENV === 'production'; this.debugEnabled = window.EMMA_DEBUG === true; - - // Log levels: error, warn, info, debug this.logLevel = this.isProduction ? 'warn' : 'debug'; - - console.log(`🔒 Emma Logger initialized: ${this.isProduction ? 'PRODUCTION' : 'DEVELOPMENT'} mode`); } - /** - * Only logs errors (always enabled) - */ + // Always log errors error(message, ...args) { - console.error(`❌ ${message}`, ...args); + console.error(`[ERR] ${message}`, ...args); } - /** - * Logs warnings (enabled in dev and prod) - */ + // Warnings (enabled in dev and prod) warn(message, ...args) { - console.warn(`⚠️ ${message}`, ...args); + console.warn(`[WARN] ${message}`, ...args); } - /** - * Logs info (disabled in production) - */ + // Info (disabled in production) info(message, ...args) { if (!this.isProduction) { - console.log(`ℹ️ ${message}`, ...args); + console.log(`[INFO] ${message}`, ...args); } } - /** - * Debug logs (only in development with debug flag) - */ + // Debug (only if explicitly enabled) debug(message, ...args) { if (this.debugEnabled) { - console.log(`🐛 ${message}`, ...args); + console.log(`[DEBUG] ${message}`, ...args); } } - /** - * Security-sensitive logs (never in production) - */ + // Security-sensitive logs (never detailed in production) security(message, ...args) { if (!this.isProduction) { - console.log(`🔒 SECURITY: ${message}`, ...args); + console.log(`[SECURITY] ${message}`, ...args); } else { - // In production, only log that a security event occurred console.warn('🔒 Security event logged (details suppressed in production)'); } } - /** - * Memory/vault operations (privacy-sensitive) - */ + // Vault operations (privacy-sensitive) vault(message, ...args) { if (!this.isProduction) { - console.log(`🗃️ VAULT: ${message}`, ...args); + console.log(`[VAULT] ${message}`, ...args); } } - /** - * User data processing (highly sensitive) - */ + // User data processing (highly sensitive) userData(message, ...args) { if (!this.isProduction) { - console.log(`👤 USER: ${message}`, ...args); + console.log(`[USER] ${message}`, ...args); } } - /** - * Production-safe status logging - */ + // Production-safe status logging status(message, ...args) { - // Always log status but sanitize in production if (this.isProduction) { - // Remove potentially sensitive details in production - const sanitized = message.replace(/passphrase|password|key|token/gi, '[REDACTED]'); - console.log(`📊 ${sanitized}`); + const sanitized = String(message).replace(/passphrase|password|key|token/gi, '[REDACTED]'); + console.log(`[STATUS] ${sanitized}`); } else { - console.log(`📊 ${message}`, ...args); + console.log(`[STATUS] ${message}`, ...args); } } - /** - * Wrap existing console methods for backwards compatibility - */ + // Optional: wrap console for backwards compatibility static wrapConsole() { if (window.EMMA_ENV === 'production') { const originalLog = console.log; const originalInfo = console.info; - - // In production, suppress console.log and console.info console.log = function(...args) { - // Only allow if explicitly marked as production-safe if (args[0] && typeof args[0] === 'string' && args[0].includes('[PROD-SAFE]')) { originalLog.apply(console, args); } }; - - console.info = function(...args) { + console.info = function(..._args) { // Suppress info logs in production }; - console.warn('🔒 Console logging suppressed in production mode'); } } @@ -122,9 +91,8 @@ // Create global logger instance window.EmmaLogger = new EmmaLogger(); - // Optionally wrap console for backwards compatibility if (window.EMMA_ENV === 'production') { EmmaLogger.wrapConsole(); } - })(); + diff --git a/js/emma-performance-optimizer.js b/js/emma-performance-optimizer.js index 96ec3df3..230fe9f0 100644 --- a/js/emma-performance-optimizer.js +++ b/js/emma-performance-optimizer.js @@ -13,9 +13,16 @@ class EmmaPerformanceOptimizer { this.performanceBudget = { maxInitialJS: 500, // KB maxImageSize: 5, // MB - maxConcurrentRequests: 6 + maxConcurrentRequests: 6, + maxMemoryMB: 120 }; - + this.memoryPressureActive = false; + this.memoryWarningIssued = false; + this.lastMemoryWarning = 0; + this.lastMemoryUsage = 0; + this.memoryCheckInterval = null; + this.voiceShutdownRequested = false; + this.init(); } @@ -450,18 +457,99 @@ class EmmaPerformanceOptimizer { } // Memory usage monitoring (if available) - if (performance.memory) { - setInterval(() => { + if (performance && performance.memory) { + if (this.memoryCheckInterval) { + clearInterval(this.memoryCheckInterval); + } + + this.memoryCheckInterval = setInterval(() => { const memoryInfo = performance.memory; const usedMB = memoryInfo.usedJSHeapSize / (1024 * 1024); - - if (usedMB > 100) { // Alert if using more than 100MB - console.warn(`⚠️ High memory usage: ${usedMB.toFixed(1)}MB`); + this.lastMemoryUsage = usedMB; + + const threshold = this.performanceBudget.maxMemoryMB || 120; + if (usedMB >= threshold) { + this.handleMemoryPressure(usedMB); + } else if (this.memoryPressureActive && usedMB < threshold * 0.75) { + this.releaseMemoryPressure(); + } else if (usedMB < threshold * 0.8) { + this.memoryWarningIssued = false; } }, 30000); // Check every 30 seconds } } + /** + * Handle high memory usage by shedding optional workloads + */ + handleMemoryPressure(currentMB) { + const threshold = this.performanceBudget.maxMemoryMB || 120; + const now = Date.now(); + + if (!this.memoryWarningIssued || (now - this.lastMemoryWarning) > 60000) { + console.warn(`[Emma] High memory usage: ${currentMB.toFixed(1)}MB (budget ${threshold}MB)`); + this.memoryWarningIssued = true; + this.lastMemoryWarning = now; + } + + if (typeof window !== 'undefined' && window.dispatchEvent) { + window.dispatchEvent(new CustomEvent('emma:memoryPressure', { + detail: { + usageMB: currentMB, + thresholdMB: threshold, + active: this.memoryPressureActive + } + })); + } + + if (this.memoryPressureActive) { + return; + } + + this.memoryPressureActive = true; + document.body.classList.add('emma-memory-pressure'); + + if (!this.isReducedMotion) { + document.body.classList.add('reduced-motion'); + } + + if (window.emmaChatExperience?.isVisible && window.emmaChatExperience.close) { + try { + window.emmaChatExperience.close(); + } catch (error) { + console.warn('[Emma] Unable to close chat during memory pressure:', error); + } + } + + if (!this.voiceShutdownRequested && window.emmaChatExperience?.emmaVoice?.isConnected) { + this.voiceShutdownRequested = true; + Promise.resolve(window.emmaChatExperience.emmaVoice.stopVoiceSession?.()) + .catch((error) => console.warn('[Emma] Failed to end voice session during memory pressure:', error)) + .finally(() => { + this.voiceShutdownRequested = false; + }); + } + } + + /** + * Clear memory pressure state once usage returns to normal levels + */ + releaseMemoryPressure() { + if (!this.memoryPressureActive) { + return; + } + + this.memoryPressureActive = false; + document.body.classList.remove('emma-memory-pressure'); + this.voiceShutdownRequested = false; + + if (typeof window !== 'undefined' && window.dispatchEvent) { + window.dispatchEvent(new CustomEvent('emma:memoryPressureRecovered', { + detail: { usageMB: this.lastMemoryUsage } + })); + } + } + /** * Public API for manual optimization */ diff --git a/js/emma-web-vault.js b/js/emma-web-vault.js index 399c47e6..6af49418 100644 --- a/js/emma-web-vault.js +++ b/js/emma-web-vault.js @@ -1,9 +1,9 @@ -/** +/** * Emma Web Vault System * Browser-compatible .emma file system with Web Crypto API * Preserves ALL desktop functionality while working in any browser * - * 💜 Built with love for preserving precious memories + * Built with love for preserving precious memories */ class EmmaWebVault { @@ -40,12 +40,12 @@ class EmmaWebVault { // SECURITY: Passphrase no longer stored in sessionStorage for security // User will be prompted for passphrase when needed - console.log('🔒 CONSTRUCTOR: Vault restored from IndexedDB - passphrase required for operations'); + console.log(' CONSTRUCTOR: Vault restored from IndexedDB - passphrase required for operations'); - console.log('✅ CONSTRUCTOR: Vault data restored from IndexedDB with', + console.log(' CONSTRUCTOR: Vault data restored from IndexedDB with', Object.keys(vaultData.content?.memories || {}).length, 'memories'); } else { - console.warn('⚠️ CONSTRUCTOR: No vault data in IndexedDB - creating minimal structure'); + console.warn(' CONSTRUCTOR: No vault data in IndexedDB - creating minimal structure'); // Fallback to minimal structure this.vaultData = { content: { memories: {}, people: {}, media: {} }, @@ -54,7 +54,7 @@ class EmmaWebVault { }; } } catch (error) { - console.error('❌ CONSTRUCTOR: Failed to restore vault data:', error); + console.error(' CONSTRUCTOR: Failed to restore vault data:', error); // Fallback to minimal structure this.vaultData = { content: { memories: {}, people: {}, media: {} }, @@ -128,7 +128,7 @@ class EmmaWebVault { // CRITICAL FIX: Remove automatic session expiry - vault stays unlocked until user locks it localStorage.removeItem('emmaVaultSessionExpiry'); // Remove any existing expiry - console.log('✅ Session storage set - new vault active AND unlocked (no expiry - user controlled)!'); + console.log(' Session storage set - new vault active AND unlocked (no expiry - user controlled)!'); // Save to IndexedDB for persistence await this.saveToIndexedDB(); const originalAutoDownload = this.vaultData.settings?.autoDownload; @@ -143,7 +143,7 @@ class EmmaWebVault { return { success: true, name: name }; } catch (error) { - console.error('❌ Failed to create vault:', error); + console.error(' Failed to create vault:', error); throw error; } } @@ -230,7 +230,7 @@ class EmmaWebVault { // CRITICAL FIX: Remove automatic session expiry - vault stays unlocked until user locks it localStorage.removeItem('emmaVaultSessionExpiry'); // Remove any existing expiry - console.log('✅ Session storage set - vault active AND unlocked (no expiry - user controlled)!'); + console.log(' Session storage set - vault active AND unlocked (no expiry - user controlled)!'); // Save to IndexedDB as backup AFTER loading from file await this.saveToIndexedDB(); @@ -238,7 +238,7 @@ class EmmaWebVault { return { success: true, stats: this.getStats() }; } catch (error) { - console.error('❌ Failed to open vault:', error); + console.error(' Failed to open vault:', error); throw error; } } @@ -249,7 +249,7 @@ class EmmaWebVault { async addMemory({ content, metadata = {}, attachments = [] }) { // WEBAPP-FIRST: All memory operations go through webapp vault - console.log('🚀 WEBAPP-FIRST: Adding memory to webapp vault:', { + console.log(' WEBAPP-FIRST: Adding memory to webapp vault:', { isOpen: this.isOpen, hasPassphrase: !!this.passphrase, sessionActive: sessionStorage.getItem('emmaVaultActive'), @@ -271,12 +271,12 @@ class EmmaWebVault { if (!this.passphrase) { // FIRST: Try to restore passphrase from session storage // SECURITY: Passphrase no longer stored in sessionStorage for security - console.log('🔐 EMERGENCY FIX: Checking passphrase for memory save'); + console.log(' EMERGENCY FIX: Checking passphrase for memory save'); if (this.passphrase) { - console.log('✅ EMERGENCY FIX: Passphrase available in memory'); + console.log(' EMERGENCY FIX: Passphrase available in memory'); } else { - console.error('🔐 CRITICAL: No passphrase available - emergency re-authentication required'); + console.error(' CRITICAL: No passphrase available - emergency re-authentication required'); // Emergency prompt - ALWAYS show for missing passphrase if (window.cleanSecurePasswordModal) { @@ -290,10 +290,10 @@ class EmmaWebVault { // Restore sessionStorage after emergency unlock if (this.passphrase) { // SECURITY: Never store passphrase in web storage - console.log('✅ EMERGENCY FIX: Passphrase restored and cached'); + console.log(' EMERGENCY FIX: Passphrase restored and cached'); } } catch (error) { - console.error('🔐 Secure modal failed:', error); + console.error(' Secure modal failed:', error); throw new Error('Vault access required to save memories. Please unlock your vault first.'); } } else { @@ -333,7 +333,7 @@ class EmmaWebVault { for (const attachment of attachments) { - console.log('🔥 ATTACHMENT DEBUG: Processing attachment:', { + console.log(' ATTACHMENT DEBUG: Processing attachment:', { name: attachment.name, type: attachment.type, hasData: !!attachment.data, @@ -345,7 +345,7 @@ class EmmaWebVault { const attachmentData = attachment.data || attachment.dataUrl; if (!attachmentData) { - console.error('❌ ATTACHMENT MISSING DATA:', attachment); + console.error(' ATTACHMENT MISSING DATA:', attachment); throw new Error('Attachment missing data - file may not have been uploaded properly'); } @@ -390,12 +390,12 @@ class EmmaWebVault { delete this.vaultData.content.memories[memoryId]; this.vaultData.stats.memoryCount--; - console.error('❌ Memory save failed - rolled back:', saveError); + console.error(' Memory save failed - rolled back:', saveError); throw new Error(`Failed to save memory: ${saveError.message}`); } } catch (error) { - console.error('❌ Failed to add memory:', error); + console.error(' Failed to add memory:', error); throw error; } } @@ -404,7 +404,7 @@ class EmmaWebVault { * Update memory (CRITICAL: Missing method causing data loss!) */ async updateMemory(memoryId, updates) { - console.log('🔄 VAULT: Updating memory:', memoryId, updates); + console.log(' VAULT: Updating memory:', memoryId, updates); // Extension mode: Route through extension if (this.extensionAvailable) { @@ -451,15 +451,15 @@ class EmmaWebVault { // Always update the timestamp memory.updated = timestamp; - console.log('✅ VAULT: Memory updated successfully:', memoryId); - console.log('📊 VAULT: Updated metadata.people:', memory.metadata?.people); + console.log(' VAULT: Memory updated successfully:', memoryId); + console.log(' VAULT: Updated metadata.people:', memory.metadata?.people); // Auto-save the vault await this.autoSave(); return { success: true, memory: memory }; } catch (error) { - console.error('❌ VAULT: Failed to update memory:', error); + console.error(' VAULT: Failed to update memory:', error); throw error; } } @@ -575,7 +575,7 @@ class EmmaWebVault { resolve({ success: true }); } else { - console.error('🗑️ EXTENSION DELETE: Failed:', event.data.error); + console.error(' EXTENSION DELETE: Failed:', event.data.error); reject(new Error(event.data.error || 'Delete failed')); } } @@ -681,7 +681,7 @@ class EmmaWebVault { }); } } catch (avatarErr) { - console.warn('⚠️ Failed to process avatar, continuing without:', avatarErr); + console.warn(' Failed to process avatar, continuing without:', avatarErr); } } @@ -698,12 +698,12 @@ class EmmaWebVault { delete this.vaultData.content.people[personId]; this.vaultData.stats.peopleCount--; - console.error('❌ Person save failed - rolled back:', saveError); + console.error(' Person save failed - rolled back:', saveError); throw new Error(`Failed to save person: ${saveError.message}`); } } catch (error) { - console.error('❌ Failed to add person:', error); + console.error(' Failed to add person:', error); throw error; } } @@ -736,7 +736,7 @@ class EmmaWebVault { }); } } catch (avatarErr) { - console.warn('⚠️ Failed to update avatar:', avatarErr); + console.warn(' Failed to update avatar:', avatarErr); } } @@ -744,7 +744,7 @@ class EmmaWebVault { await this.autoSave(); return { success: true, person: existing }; } catch (saveError) { - console.error('❌ Person update save failed:', saveError); + console.error(' Person update save failed:', saveError); throw new Error(`Failed to save person update: ${saveError.message}`); } } @@ -807,7 +807,7 @@ class EmmaWebVault { */ async addMedia({ type, data, name, file }) { - console.log('🔥 ADD_MEDIA DEBUG: Received parameters:', { + console.log(' ADD_MEDIA DEBUG: Received parameters:', { type, name, hasFile: !!file, @@ -858,7 +858,7 @@ class EmmaWebVault { const mediaId = this.generateId('media'); let mediaData; - console.log('🔥 MEDIA DATA TYPE CHECK:', { + console.log(' MEDIA DATA TYPE CHECK:', { hasFile: !!file, hasData: !!data, dataType: typeof data, @@ -875,10 +875,10 @@ class EmmaWebVault { mediaData = data; } else if (typeof data === 'string') { - console.log('📝 Using string data (base64/dataURL)'); + console.log(' Using string data (base64/dataURL)'); // SPECIAL CASE: For extension captures, store data URLs directly without encryption if (data.startsWith('data:')) { - console.log('🔧 EXTENSION CAPTURE: Storing data URL directly without encryption'); + console.log(' EXTENSION CAPTURE: Storing data URL directly without encryption'); const media = { id: mediaId, name: name, @@ -903,7 +903,7 @@ class EmmaWebVault { // Base64 or data URL - convert to ArrayBuffer for encryption mediaData = this.dataURLToArrayBuffer(data); } else { - console.error('❌ INVALID DATA FORMAT - DETAILED ANALYSIS:', { + console.error(' INVALID DATA FORMAT - DETAILED ANALYSIS:', { hasFile: !!file, hasData: !!data, dataType: typeof data, @@ -940,7 +940,7 @@ class EmmaWebVault { return mediaId; } catch (error) { - console.error('❌ Failed to add media:', error); + console.error(' Failed to add media:', error); throw error; } } @@ -972,7 +972,7 @@ class EmmaWebVault { // CRITICAL FIX: Handle both encrypted and unencrypted media if (media.encrypted === false || typeof media.data === 'string') { // Unencrypted data (base64 data URL) - return directly - console.log('📸 MEDIA: Returning unencrypted media data:', mediaId); + console.log(' MEDIA: Returning unencrypted media data:', mediaId); if (media.data.startsWith('data:')) { // Already a data URL - return as is @@ -983,7 +983,7 @@ class EmmaWebVault { } } else { // Encrypted data - decrypt first - console.log('🔐 MEDIA: Decrypting encrypted media data:', mediaId); + console.log(' MEDIA: Decrypting encrypted media data:', mediaId); let salt = this.vaultData.encryption.salt; if (salt && typeof salt === 'object' && !(salt instanceof Uint8Array)) { @@ -1000,11 +1000,11 @@ class EmmaWebVault { } } catch (error) { - console.error('❌ Failed to get media:', error); + console.error(' Failed to get media:', error); // Fallback: if decryption fails, try treating as unencrypted const media = this.vaultData.content.media[mediaId]; if (media && typeof media.data === 'string') { - console.log('🔄 MEDIA: Decryption failed, trying as unencrypted data'); + console.log(' MEDIA: Decryption failed, trying as unencrypted data'); if (media.data.startsWith('data:')) { return media.data; } else { @@ -1067,7 +1067,7 @@ class EmmaWebVault { await this.autoSave(); return { success: true }; } catch (error) { - console.error('❌ Failed to remove media:', error); + console.error(' Failed to remove media:', error); return { success: false, error: error.message }; } } @@ -1105,12 +1105,12 @@ class EmmaWebVault { */ async autoSave() { - // 🔥 CRITICAL: .emma file MUST be updated or operation fails + // CRITICAL: .emma file MUST be updated or operation fails const hasFileSystemAccess = 'showOpenFilePicker' in window; if (hasFileSystemAccess && this.fileHandle) { - // ✅ PREFERRED: Direct save to original file - console.log('💾 VAULT: Saving directly to .emma file'); + // PREFERRED: Direct save to original file + console.log(' VAULT: Saving directly to .emma file'); // Show sync status if (window.emmaSyncStatus) { @@ -1124,9 +1124,9 @@ class EmmaWebVault { if (this.extensionAvailable && this.extensionSyncEnabled) { try { await this.syncToExtension(); - console.log('✅ VAULT: Extension sync completed'); + console.log(' VAULT: Extension sync completed'); } catch (extensionError) { - console.warn('⚠️ EXTENSION: Sync failed (non-critical):', extensionError); + console.warn(' EXTENSION: Sync failed (non-critical):', extensionError); } } @@ -1134,17 +1134,17 @@ class EmmaWebVault { try { await this.saveToIndexedDB(); } catch (error) { - console.warn('⚠️ BACKUP: IndexedDB save failed (non-critical):', error); + console.warn(' BACKUP: IndexedDB save failed (non-critical):', error); } } else if (hasFileSystemAccess && !this.fileHandle) { - // 🔄 GRACEFUL: No file handle - save to IndexedDB and defer file save - console.warn('⚠️ GRACEFUL: No file handle - saving to IndexedDB, will prompt for file access on next user interaction'); + // GRACEFUL: No file handle - save to IndexedDB and defer file save + console.warn(' GRACEFUL: No file handle - saving to IndexedDB, will prompt for file access on next user interaction'); // Save to IndexedDB immediately as backup try { await this.saveToIndexedDB(); - console.log('✅ GRACEFUL: Saved to IndexedDB backup'); + console.log(' GRACEFUL: Saved to IndexedDB backup'); // Mark that we need file re-authentication this.needsFileReauth = true; @@ -1158,7 +1158,7 @@ class EmmaWebVault { return; } catch (indexedDBError) { - console.error('❌ GRACEFUL: IndexedDB backup also failed:', indexedDBError); + console.error(' GRACEFUL: IndexedDB backup also failed:', indexedDBError); // Only now show error since both methods failed if (window.emmaSyncStatus) { @@ -1168,8 +1168,8 @@ class EmmaWebVault { } } else { - // 🔄 FALLBACK: File System Access API not supported - console.log('📥 FALLBACK: Using download method for .emma file'); + // FALLBACK: File System Access API not supported + console.log(' FALLBACK: Using download method for .emma file'); // Show fallback sync status if (window.emmaSyncStatus) { @@ -1193,22 +1193,22 @@ class EmmaWebVault { clearTimeout(this.saveDebounceTimer); } - // 🔥 CRITICAL: Reduced debounce to 500ms for Emma's precious memories + // CRITICAL: Reduced debounce to 500ms for Emma's precious memories // Balance between data safety and file performance this.saveDebounceTimer = setTimeout(async () => { if (this.pendingChanges) { - console.log('💾 EMMA: Performing immediate .emma file save'); + console.log(' EMMA: Performing immediate .emma file save'); try { await this.performElegantSave(); this.pendingChanges = false; - console.log('✅ EMMA: .emma file saved successfully'); + console.log(' EMMA: .emma file saved successfully'); // Show success status if (window.emmaSyncStatus) { window.emmaSyncStatus.show('success', '.emma file updated successfully'); } } catch (error) { - console.error('❌ EMMA: Critical .emma file save failed:', error); + console.error(' EMMA: Critical .emma file save failed:', error); // Show error status if (window.emmaSyncStatus) { @@ -1231,7 +1231,7 @@ class EmmaWebVault { // Direct-save-only: Must have file handle if (!this.fileHandle || !('createWritable' in this.fileHandle)) { - console.error('⛔ DIRECT-SAVE: No file handle available'); + console.error(' DIRECT-SAVE: No file handle available'); this.showDirectSaveAffordance && this.showDirectSaveAffordance(); throw new Error('Direct save required: no file access available'); } @@ -1240,7 +1240,7 @@ class EmmaWebVault { await this.atomicFileUpdate(); } catch (error) { - console.error('❌ DIRECT-SAVE: Save failed:', error); + console.error(' DIRECT-SAVE: Save failed:', error); throw error; // Propagate error to caller } } @@ -1263,18 +1263,18 @@ class EmmaWebVault { try { // Test handle validity by attempting to get file info await this.fileHandle.getFile(); - console.log('✅ ATOMIC: File handle preserved after write'); + console.log(' ATOMIC: File handle preserved after write'); } catch (handleError) { - console.warn('⚠️ ATOMIC: File handle invalidated after write, will need re-auth:', handleError); + console.warn(' ATOMIC: File handle invalidated after write, will need re-auth:', handleError); // Don't throw - just log for debugging } } catch (error) { - console.error('❌ ATOMIC: File update failed:', error); + console.error(' ATOMIC: File update failed:', error); // If write failed, check if it's a handle issue if (error.name === 'NotAllowedError' || error.message.includes('permission')) { - console.warn('⚠️ ATOMIC: Permission denied - file handle may be invalid'); + console.warn(' ATOMIC: Permission denied - file handle may be invalid'); this.fileHandle = null; // Clear invalid handle } @@ -1319,7 +1319,7 @@ class EmmaWebVault { // Direct-save-only: Must have file handle for final save if (!this.fileHandle || !('createWritable' in this.fileHandle)) { - console.error('⛔ DIRECT-SAVE: Cannot lock - no file access'); + console.error(' DIRECT-SAVE: Cannot lock - no file access'); this.showDirectSaveAffordance && this.showDirectSaveAffordance(); throw new Error('Direct save required to lock vault'); } @@ -1327,7 +1327,7 @@ class EmmaWebVault { // Perform final atomic update await this.atomicFileUpdate(); - // 🔥 CRITICAL: Preserve filename for file access restoration + // CRITICAL: Preserve filename for file access restoration const preservedFileName = this.originalFileName; // Clear vault state @@ -1346,16 +1346,16 @@ class EmmaWebVault { localStorage.removeItem('emmaVaultActive'); localStorage.removeItem('emmaVaultName'); - // 🔄 PRESERVE: Keep filename in localStorage for file access restoration + // PRESERVE: Keep filename in localStorage for file access restoration if (preservedFileName) { localStorage.setItem('emmaVaultOriginalFileName', preservedFileName); - console.log('💾 LOCK: Preserved filename for restoration:', preservedFileName); + console.log(' LOCK: Preserved filename for restoration:', preservedFileName); } return { success: true }; } catch (error) { - console.error('❌ DIRECT-SAVE: Failed to lock vault:', error); + console.error(' DIRECT-SAVE: Failed to lock vault:', error); throw error; } } @@ -1390,7 +1390,7 @@ class EmmaWebVault { // Check if object store exists if (!db.objectStoreNames.contains('vaults')) { - console.warn('⚠️ IndexedDB: Object store "vaults" not found - returning null'); + console.warn(' IndexedDB: Object store "vaults" not found - returning null'); resolve(null); return; } @@ -1414,7 +1414,7 @@ class EmmaWebVault { }; }); } catch (error) { - console.error('❌ Failed to load from IndexedDB:', error); + console.error(' Failed to load from IndexedDB:', error); return null; } } @@ -1459,19 +1459,19 @@ class EmmaWebVault { this.originalFileName = sessionStorage.getItem('emmaVaultOriginalFileName') || localStorage.getItem('emmaVaultOriginalFileName'); - // 🔥 CRITICAL: Try to restore file access for .emma file sync + // CRITICAL: Try to restore file access for .emma file sync if (this.originalFileName && 'showOpenFilePicker' in window) { - console.log('🔄 RESTORE: Attempting to restore file access for:', this.originalFileName); + console.log(' RESTORE: Attempting to restore file access for:', this.originalFileName); try { // Try silent restoration first (may work if browser remembers permission) const restored = await this.reEstablishFileAccess(); if (restored) { - console.log('✅ RESTORE: File access restored automatically'); + console.log(' RESTORE: File access restored automatically'); } else { - console.warn('⚠️ RESTORE: File access not restored - will prompt on first save'); + console.warn(' RESTORE: File access not restored - will prompt on first save'); } } catch (error) { - console.warn('⚠️ RESTORE: File access restoration failed:', error); + console.warn(' RESTORE: File access restoration failed:', error); // Not critical - user will be prompted on first save } } @@ -1483,7 +1483,7 @@ class EmmaWebVault { } } catch (error) { - console.error('❌ ELEGANT: Failed to restore vault state:', error); + console.error(' ELEGANT: Failed to restore vault state:', error); return null; } } @@ -1506,7 +1506,7 @@ class EmmaWebVault { db.deleteObjectStore(storeName); } catch (e) { - console.warn('⚠️ IndexedDB: Could not delete store:', storeName); + console.warn(' IndexedDB: Could not delete store:', storeName); } }); @@ -1547,7 +1547,7 @@ class EmmaWebVault { try { if (!this.vaultData) { - console.error('❌ No vault data available for download'); + console.error(' No vault data available for download'); return; } @@ -1587,11 +1587,11 @@ class EmmaWebVault { // Show user message about file update if (typeof showToast === 'function') { - showToast(`📥 Updated ${baseName}.emma downloaded - replace your original file`, 'info'); + showToast(` Updated ${baseName}.emma downloaded - replace your original file`, 'info'); } } catch (error) { - console.error('❌ Failed to download vault:', error); + console.error(' Failed to download vault:', error); throw error; } } @@ -1633,7 +1633,7 @@ class EmmaWebVault { throw new Error('Invalid vault salt format'); } if (salt.length !== 32) { - console.warn('⚠️ SALT: Unexpected salt length:', salt.length, '- attempting to proceed with normalized value'); + console.warn(' SALT: Unexpected salt length:', salt.length, '- attempting to proceed with normalized value'); } // Persist normalized salt back in memory to avoid future issues try { this.vaultData.encryption.salt = salt; } catch (_) {} @@ -1661,8 +1661,8 @@ class EmmaWebVault { return result; } catch (error) { - console.error('❌ Failed to encrypt vault data:', error); - console.error('❌ Error details:', error.stack); + console.error(' Failed to encrypt vault data:', error); + console.error(' Error details:', error.stack); throw error; } } @@ -1711,7 +1711,7 @@ class EmmaWebVault { return vaultData; } catch (error) { - console.error('❌ Failed to decrypt vault file:', error); + console.error(' Failed to decrypt vault file:', error); throw new Error('Failed to decrypt vault. Please check your passphrase.'); } } @@ -1729,7 +1729,7 @@ class EmmaWebVault { showDirectSaveAffordance() { try { - console.log('🔍 AFFORDANCE: isFallbackMode?', this.isFallbackMode()); if (!this.isFallbackMode()) { + console.log(' AFFORDANCE: isFallbackMode?', this.isFallbackMode()); if (!this.isFallbackMode()) { return; } @@ -1741,7 +1741,7 @@ class EmmaWebVault { const btn = document.createElement('button'); btn.id = 'emma-direct-save-btn'; - btn.innerHTML = '🔓 Enable Direct Save'; + btn.innerHTML = ' Enable Direct Save'; btn.style.cssText = ` position: fixed; bottom: 20px; @@ -1773,15 +1773,15 @@ class EmmaWebVault { const enable = async () => { try { btn.disabled = true; - btn.innerHTML = '🔄 Requesting access...'; + btn.innerHTML = ' Requesting access...'; const ok = await this.reEstablishFileAccess(); if (ok && this.fileHandle) { - btn.innerHTML = '✅ Direct save enabled!'; + btn.innerHTML = ' Direct save enabled!'; btn.style.background = 'linear-gradient(135deg, #10b981 0%, #059669 100%)'; setTimeout(() => btn.remove(), 2000); - if (typeof showToast === 'function') showToast('✅ Direct save enabled - all changes now save automatically!', 'success'); + if (typeof showToast === 'function') showToast(' Direct save enabled - all changes now save automatically!', 'success'); } else { - btn.innerHTML = '🔓 Enable Direct Save'; + btn.innerHTML = ' Enable Direct Save'; btn.disabled = false; } } catch (e) { @@ -1869,8 +1869,8 @@ class EmmaWebVault { return result.buffer; } catch (error) { - console.error('❌ Encryption failed:', error); - console.error('❌ Error details:', error.stack); + console.error(' Encryption failed:', error); + console.error(' Error details:', error.stack); throw error; } } @@ -1927,7 +1927,7 @@ class EmmaWebVault { return decryptedData; } catch (error) { - console.error('❌ Decryption failed:', error); + console.error(' Decryption failed:', error); throw error; } } @@ -2016,14 +2016,14 @@ class EmmaWebVault { // Return emoji based on memory content or metadata const emotion = memory.metadata?.emotion || 'neutral'; const emojis = { - happy: '😊', - sad: '😢', - love: '💕', - excited: '🎉', - peaceful: '🌸', - neutral: '📝' + happy: '', + sad: '', + love: '', + excited: '', + peaceful: '', + neutral: '' }; - return emojis[emotion] || '📝'; + return emojis[emotion] || ''; } calculateMemorySize(memory) { @@ -2096,7 +2096,7 @@ class EmmaWebVault { break; case 'SYNC_ERROR': - console.error('❌ Sync error:', message.error); + console.error(' Sync error:', message.error); this.showSyncNotification('error', 'Sync failed: ' + message.error); break; @@ -2215,7 +2215,7 @@ class EmmaWebVault { } /** - * 🚨 REMOVED DUPLICATE autoSave - was overriding .emma file saving! + * REMOVED DUPLICATE autoSave - was overriding .emma file saving! * Original autoSave (line ~1106) handles proper .emma file persistence. */ @@ -2276,33 +2276,42 @@ class EmmaWebVault { /** * EXACT WORKING CRYPTO: Copied from extension background.js */ - async exactWorkingDecrypt(fileData, passphrase) { + async exactWorkingDecrypt(fileData, passphrase) { try { - - // Parse .emma file format: EMMA + version + salt + iv + encrypted data const data = new Uint8Array(fileData); - // Check magic bytes const magic = new TextDecoder().decode(data.slice(0, 4)); if (magic !== 'EMMA') { throw new Error('Invalid .emma file format'); } - // Extract components (EXACT format from working extension) - const version = data.slice(4, 6); - const salt = data.slice(6, 38); // 32 bytes - const iv = data.slice(38, 50); // 12 bytes - const encrypted = data.slice(50); + let offset = 4; + let version = null; + if (data.length >= offset + 2) { + const candidate = data.slice(offset, offset + 2); + const [major, minor] = candidate; + const looksLikeVersion = (major === 1 && minor <= 10) || (major === 0 && minor <= 10); + const bytesRemaining = data.length - (offset + 2 + 32 + 12); + if (looksLikeVersion && bytesRemaining >= 0) { + version = candidate; + offset += 2; + } + } + + const salt = data.slice(offset, offset + 32); + offset += 32; + const iv = data.slice(offset, offset + 12); + offset += 12; + const encrypted = data.slice(offset); - console.log('🔓 EXACT WORKING: Extracted vault components:', { + console.log('EXACT WORKING: Extracted vault components:', { magic, - version: Array.from(version), + version: version ? Array.from(version) : null, saltLength: salt.length, ivLength: iv.length, encryptedLength: encrypted.length }); - // Derive key from passphrase using PBKDF2 (EXACT parameters) const keyMaterial = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(passphrase), @@ -2311,45 +2320,62 @@ class EmmaWebVault { ['deriveBits', 'deriveKey'] ); - const key = await crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: salt, - iterations: 250000, // EXACT same as extension - hash: 'SHA-256' - }, - keyMaterial, - { name: 'AES-GCM', length: 256 }, - false, - ['decrypt'] - ); + const iterationCandidates = [250000, 100000]; + let decrypted = null; + let chosenIterations = null; + let lastError = null; - // Decrypt the data (EXACT same as extension) - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv: iv }, - key, - encrypted - ); + for (const iterations of iterationCandidates) { + try { + const key = await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['decrypt'] + ); + + decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + key, + encrypted + ); + chosenIterations = iterations; + break; + } catch (error) { + lastError = error; + } + } + + if (!decrypted) { + throw lastError || new Error('Failed to derive Emma vault key.'); + } - // Parse JSON const jsonString = new TextDecoder().decode(decrypted); const vaultData = JSON.parse(jsonString); - console.log('✅ EXACT WORKING: Vault decrypted successfully! Memories:', + vaultData.encryption = vaultData.encryption || {}; + vaultData.encryption.salt = new Uint8Array(salt); + if (chosenIterations) { + vaultData.encryption.iterations = chosenIterations; + } + + console.log('EXACT WORKING: Vault decrypted successfully! Memories:', Object.keys(vaultData.content?.memories || {}).length); return vaultData; } catch (error) { - console.error('❌ EXACT WORKING: Decryption failed:', error); + console.error('EXACT WORKING: Decryption failed:', error); throw new Error('Failed to decrypt vault: ' + error.message); } } - - /** - * PURE WEB APP: Native vault encryption (no extension needed) - */ - async nativeEncryptVault(vaultData, passphrase) { +async nativeEncryptVault(vaultData, passphrase) { try { // Generate encryption salt and IV @@ -2406,7 +2432,7 @@ class EmmaWebVault { return fileData; } catch (error) { - console.error('❌ NATIVE CRYPTO: Encryption failed:', error); + console.error(' NATIVE CRYPTO: Encryption failed:', error); throw error; } } @@ -2447,10 +2473,10 @@ window.emmaAPI = { sessionStorage.setItem('emmaVaultName', decryptedData.metadata?.name || decryptedData.name || 'Web Vault'); // SECURITY: Never store passphrase in web storage - console.log('✅ Vault unlocked successfully via API!'); + console.log(' Vault unlocked successfully via API!'); return { success: true, stats: window.emmaWebVault.getStats() }; } catch (error) { - console.error('❌ Failed to decrypt vault with provided passphrase:', error); + console.error(' Failed to decrypt vault with provided passphrase:', error); return { success: false, error: 'Incorrect passphrase or corrupted vault data' }; } } else { @@ -2465,11 +2491,11 @@ window.emmaAPI = { sessionStorage.setItem('emmaVaultActive', 'true'); sessionStorage.setItem('emmaVaultName', vaultData.metadata?.name || vaultData.name || 'Web Vault'); - console.log('✅ Vault opened successfully (unencrypted data)!'); + console.log(' Vault opened successfully (unencrypted data)!'); return { success: true, stats: window.emmaWebVault.getStats() }; } } catch (error) { - console.error('❌ Failed to unlock vault:', error); + console.error(' Failed to unlock vault:', error); return { success: false, error: error.message }; } }, @@ -2508,7 +2534,7 @@ window.emmaAPI = { return { success: true, id: mediaId }; } catch (error) { - console.error('❌ Failed to add attachment:', error); + console.error(' Failed to add attachment:', error); return { success: false, error: error.message }; } }, @@ -2517,7 +2543,7 @@ window.emmaAPI = { const res = await window.emmaWebVault.removeMedia(mediaId); return { success: res.success }; } catch (error) { - console.error('❌ Failed to remove attachment:', error); + console.error(' Failed to remove attachment:', error); return { success: false, error: error.message }; } } @@ -2538,7 +2564,7 @@ window.emmaAPI = { const res = await window.emmaWebVault.deleteMemory(memoryId); return { success: res.success }; } catch (error) { - console.error('❌ Failed to delete memory:', error); + console.error(' Failed to delete memory:', error); return { success: false, error: error.message }; } } @@ -2553,7 +2579,7 @@ window.emmaAPI = { const res = await window.emmaWebVault.updatePerson(person); return { success: true, person: res.person }; } catch (error) { - console.error('❌ Failed to update person:', error); + console.error(' Failed to update person:', error); return { success: false, error: error.message }; } }, @@ -2562,7 +2588,7 @@ window.emmaAPI = { const res = await window.emmaWebVault.deletePerson(personId); return { success: res.success }; } catch (error) { - console.error('❌ Failed to delete person:', error); + console.error(' Failed to delete person:', error); return { success: false, error: error.message }; } }, @@ -2571,21 +2597,21 @@ window.emmaAPI = { try { // CLEAN: Direct vault access since vault should exist on all pages now if (!window.emmaWebVault) { - console.warn('⚠️ EmmaWebVault not available, returning empty people list'); + console.warn(' EmmaWebVault not available, returning empty people list'); return { success: true, items: [] }; } if (typeof window.emmaWebVault.listPeople !== 'function') { - console.warn('⚠️ listPeople method not available, returning empty people list'); + console.warn(' listPeople method not available, returning empty people list'); return { success: true, items: [] }; } - console.log('👥 API: Calling vault.listPeople()...'); + console.log(' API: Calling vault.listPeople()...'); const people = await window.emmaWebVault.listPeople(); - console.log(`👥 API: Retrieved ${people?.length || 0} people from vault`); + console.log(` API: Retrieved ${people?.length || 0} people from vault`); return { success: true, items: people || [] }; } catch (error) { - console.error('❌ Failed to list people:', error); + console.error(' Failed to list people:', error); return { success: false, error: error.message, items: [] }; } }, @@ -2621,7 +2647,7 @@ window.emmaAPI = { // Fallback for non-extension mode return await window.emmaWebVault.updatePerson(personData); } catch (error) { - console.error('❌ Failed to update person:', error); + console.error(' Failed to update person:', error); return { success: false, error: error.message }; } } @@ -2639,7 +2665,7 @@ window.emmaAPI = { * EMMA ETHOS: Never silently fail - always inform the user */ EmmaWebVault.prototype.showFileSyncError = function() { - console.error('🚨 CRITICAL: .emma file sync broken - showing user error'); + console.error(' CRITICAL: .emma file sync broken - showing user error'); // Remove any existing sync error modal const existing = document.querySelector('.emma-sync-error-modal'); @@ -2672,7 +2698,7 @@ EmmaWebVault.prototype.showFileSyncError = function() { text-align: center; box-shadow: 0 20px 40px rgba(0,0,0,0.3); "> -
🚨
+

CRITICAL: .emma File Access Lost

Your memories cannot be saved to your .emma file right now. This means your memories @@ -2689,7 +2715,7 @@ EmmaWebVault.prototype.showFileSyncError = function() { font-weight: 600; cursor: pointer; margin-right: 15px; - ">🔗 Restore File Access + "> Restore File Access - -

- -
-

- ${escapeHtml(personName)} wants to share their vault - "${escapeHtml(invitation.vaultName || 'Memory Vault')}" with you. -

- -
-

Permissions:

-

${permissionList}

-
- -
-

🔒 End-to-end encrypted

-

📅 Expires: ${new Date(invitation.expires).toLocaleDateString()}

-

🆔 From: ${escapeHtml(invitation.issuerFingerprint.substring(0, 16))}...

-
-
- -
- - - -
- `; - - modal.appendChild(modalContent); - document.body.appendChild(modal); - - // Event listeners - modal.querySelector('.close-invitation-modal').addEventListener('click', () => modal.remove()); - modal.querySelector('.decline-invitation').addEventListener('click', () => modal.remove()); - - modal.querySelector('.accept-invitation').addEventListener('click', async () => { - try { - const acceptBtn = modal.querySelector('.accept-invitation'); - acceptBtn.textContent = 'Connecting...'; - acceptBtn.disabled = true; - - // Accept the share via P2P - await p2pManager.acceptShare(invitation); - - showNotification(`Successfully connected to ${personName}'s vault!`, 'success'); - modal.remove(); - - // Refresh the page to show new shared vault - setTimeout(() => location.reload(), 1000); - - } catch (error) { - console.error('Failed to accept share:', error); - showNotification('Failed to accept share: ' + error.message, 'error'); - modal.remove(); - } - }); - - // Click outside to close - modal.addEventListener('click', (e) => { - if (e.target === modal) modal.remove(); - }); -} - -// People Management JavaScript -let allPeople = []; -let filteredPeople = []; - -// Initialize the page -document.addEventListener('DOMContentLoaded', async function() { - - await initializeCrypto(); - loadPeople(); - handleUrlParameters(); - setupEventListeners(); -}); - -// Setup all event listeners -function setupEventListeners() { - - // Add person buttons - const addPersonBtn = document.getElementById('addPersonBtn'); - const addPersonBtnEmpty = document.getElementById('addPersonBtnEmpty'); - - if (addPersonBtn) { - addPersonBtn.addEventListener('click', openAddPersonModal); - } - - if (addPersonBtnEmpty) { - addPersonBtnEmpty.addEventListener('click', openAddPersonModal); - } - - // Search functionality - const searchInput = document.getElementById('searchInput'); - const relationshipFilter = document.getElementById('relationshipFilter'); - - if (searchInput) { - searchInput.addEventListener('input', searchPeople); - } - - if (relationshipFilter) { - relationshipFilter.addEventListener('change', searchPeople); - } - - // Modal controls - const closeModalBtn = document.getElementById('closeModalBtn'); - const cancelBtn = document.getElementById('cancelBtn'); - const addPersonForm = document.getElementById('addPersonForm'); - - if (closeModalBtn) { - closeModalBtn.addEventListener('click', closeAddPersonModal); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', closeAddPersonModal); - } - - if (addPersonForm) { - addPersonForm.addEventListener('submit', addPerson); - } - - // Close modal when clicking outside - const modalOverlay = document.getElementById('addPersonModal'); - if (modalOverlay) { - modalOverlay.addEventListener('click', function(e) { - if (e.target === modalOverlay) { - closeAddPersonModal(); - } - }); - } - - // Event delegation for person cards (since they're dynamically created) - const peopleGrid = document.getElementById('peopleGrid'); - if (peopleGrid) { - peopleGrid.addEventListener('click', function(e) { - // Find the person card element - const personCard = e.target.closest('.person-card'); - if (personCard) { - const personId = parseInt(personCard.dataset.personId); - - // Check if it's a share button click - if (e.target.classList.contains('share-vault-btn') || e.target.closest('.share-vault-btn')) { - e.stopPropagation(); - showShareVaultModal(personId); - } else { - // Regular person card click - viewPerson(personId); - } - } - }); - } - - // Event delegation for identity modal buttons (dynamically created) - document.addEventListener('click', function(e) { - if (e.target.classList.contains('identity-export-btn')) { - const personId = e.target.dataset.personId; - if (personId) { - exportIdentity(personId); - } - } - - if (e.target.classList.contains('identity-copy-btn')) { - const fingerprint = e.target.dataset.fingerprint; - if (fingerprint) { - copyFingerprint(fingerprint); - } - } - - if (e.target.classList.contains('identity-share-btn')) { - const fingerprint = e.target.dataset.fingerprint; - if (fingerprint) { - shareIdentity(fingerprint); - } - } - - // Edit person button - if (e.target.classList.contains('edit-person-btn')) { - const personId = e.target.dataset.personId; - if (personId) { - enablePersonEdit(personId); - } - } - - // Save person button - if (e.target.classList.contains('save-person-btn')) { - const personId = e.target.dataset.personId; - if (personId) { - savePersonEdit(personId); - } - } - - // Cancel edit button - if (e.target.classList.contains('cancel-edit-btn')) { - cancelPersonEdit(); - } - - // Share vault button in modal - if (e.target.classList.contains('share-vault-btn-modal')) { - const personId = e.target.dataset.personId; - if (personId) { - showShareVaultModal(parseInt(personId)); - } - } - - // Add person modal buttons - if (e.target.classList.contains('close-add-person-modal')) { - closeAddPersonModal(); - } - - if (e.target.classList.contains('cancel-add-person-btn')) { - closeAddPersonModal(); - } - - // Person detail modal close button - if (e.target.classList.contains('close-person-detail-modal')) { - const modal = e.target.closest('.modal-overlay'); - if (modal) { - modal.remove(); - } - } - - if (e.target.classList.contains('submit-add-person-btn')) { - const form = e.target.closest('form'); - if (form) { - const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); - form.dispatchEvent(submitEvent); - } - } - }); - - // Event listener for form submission in add person modal - document.addEventListener('submit', function(e) { - if (e.target.id === 'addPersonFormModal') { - addPerson(e); - } - }); - -} - -// Load people data -async function loadPeople() { - try { - showLoading(true); - - // Get people from storage (try chrome.storage first, fallback to localStorage) - let people = []; - try { - const result = await chrome.storage.local.get(['emma_people']); - people = result.emma_people || []; - - } catch (error) { - console.warn('⚠️ Chrome storage failed, trying localStorage:', error); - people = JSON.parse(localStorage.getItem('emma_people') || '[]'); - - } - - // Also check old format for migration - if (people.length === 0) { - const oldPeople = localStorage.getItem('emma-people'); - if (oldPeople) { - people = JSON.parse(oldPeople); - - // Save in new format - try { - await chrome.storage.local.set({ emma_people: people }); - } catch (error) { - localStorage.setItem('emma_people', JSON.stringify(people)); - } - // Remove old format - localStorage.removeItem('emma-people'); - } - } - - allPeople = people; - - // If no people, create some sample data - if (allPeople.length === 0) { - allPeople = createSamplePeople(); - // Save sample data using new storage system - try { - await chrome.storage.local.set({ emma_people: allPeople }); - } catch (error) { - localStorage.setItem('emma_people', JSON.stringify(allPeople)); - } - } - - filteredPeople = [...allPeople]; - displayPeople(); - updateStats(); - - } catch (error) { - console.error('Failed to load people:', error); - } finally { - showLoading(false); - } -} - -// Create sample people data -function createSamplePeople() { - return [ - { - id: 1, - name: "Sarah Johnson", - email: "sarah@example.com", - phone: "+1 (555) 123-4567", - relationship: "family", - notes: "My sister, lives in Portland. Loves hiking and photography.", - dateAdded: new Date().toISOString() - }, - { - id: 2, - name: "Mike Chen", - email: "mike.chen@example.com", - phone: "+1 (555) 987-6543", - relationship: "colleague", - notes: "Frontend developer on our team. Great at React and design.", - dateAdded: new Date().toISOString() - }, - { - id: 3, - name: "Emma Wilson", - email: "emma.w@example.com", - phone: "+1 (555) 456-7890", - relationship: "best_friend", - notes: "College roommate. Now works in marketing. Always up for coffee!", - dateAdded: new Date().toISOString() - } - ]; -} - -// Display people -function displayPeople() { - const peopleGrid = document.getElementById('peopleGrid'); - const emptyState = document.getElementById('emptyState'); - - if (filteredPeople.length === 0) { - peopleGrid.style.display = 'none'; - emptyState.style.display = 'block'; - - const searchTerm = document.getElementById('searchInput').value; - const relationshipFilter = document.getElementById('relationshipFilter').value; - - if (searchTerm || relationshipFilter !== 'all') { - document.getElementById('emptyStateMessage').textContent = 'No people match your current filters. Try adjusting your search or filters.'; - } else { - document.getElementById('emptyStateMessage').textContent = 'Start by adding your first person to begin building your network.'; - } - return; - } - - peopleGrid.style.display = 'grid'; - emptyState.style.display = 'none'; - - peopleGrid.innerHTML = filteredPeople.map(person => createPersonCard(person)).join(''); -} - -// Create person card HTML -function createPersonCard(person) { - const initials = person.name.split(' ').map(n => n[0]).join('').toUpperCase(); - const relationshipEmoji = getRelationshipEmoji(person.relationship); - const relationshipLabel = getRelationshipLabel(person.relationship); - - // Create avatar (profile picture or initials) - const avatarContent = person.profilePicture - ? `${escapeHtml(person.name)}` - : initials; - - return ` -
-
${avatarContent}
-
-

${escapeHtml(person.name)}

-
- ${relationshipEmoji} ${relationshipLabel} -
-

${person.notes ? escapeHtml(person.notes) : 'No notes added yet.'}

- ${person.keyFingerprint ? ` -
-
- 🔐 - Identity: - ${person.keyFingerprint.substring(0, 16)}... - ${person.verificationStatus === 'verified' ? '' : - person.verificationStatus === 'trusted' ? '' : - ''} -
-
- ` : ''} -
- ${person.keyFingerprint ? ` - - ` : ''} -
- `; -} - -// Get relationship emoji -function getRelationshipEmoji(relationship) { - const emojis = { - self: '🧑', - family: '👨‍👩‍👧‍👦', - friend: '👫', - best_friend: '💝', - romantic: '💕', - colleague: '💼' - }; - return emojis[relationship] || '👤'; -} - -// Get relationship label -function getRelationshipLabel(relationship) { - const labels = { - self: 'Myself', - family: 'Family', - friend: 'Friend', - best_friend: 'Best Friend', - romantic: 'Romantic', - colleague: 'Colleague' - }; - return labels[relationship] || 'Other'; -} - -// Search functionality -function searchPeople() { - const searchTerm = document.getElementById('searchInput').value.toLowerCase(); - const relationshipFilter = document.getElementById('relationshipFilter').value; - - filteredPeople = allPeople.filter(person => { - const matchesSearch = searchTerm === '' || - person.name.toLowerCase().includes(searchTerm) || - person.email.toLowerCase().includes(searchTerm) || - person.notes.toLowerCase().includes(searchTerm); - - const matchesRelationship = relationshipFilter === 'all' || - person.relationship === relationshipFilter; - - return matchesSearch && matchesRelationship; - }); - - displayPeople(); -} - -// Update stats -function updateStats() { - const totalCount = allPeople.length; - const familyCount = allPeople.filter(p => p.relationship === 'family').length; - const colleagueCount = allPeople.filter(p => p.relationship === 'colleague').length; - - document.getElementById('totalCount').textContent = totalCount; - document.getElementById('familyCount').textContent = familyCount; - document.getElementById('colleagueCount').textContent = colleagueCount; -} - -// Show loading state -function showLoading(show) { - const loader = document.querySelector('.loading'); - if (loader) { - loader.style.display = show ? 'block' : 'none'; - } -} - -// Handle URL parameters -function handleUrlParameters() { - const urlParams = new URLSearchParams(window.location.search); - const shouldAdd = urlParams.get('add'); - - if (shouldAdd === 'true') { - setTimeout(() => { - openAddPersonModal(); - }, 500); - } -} - -// Modal functions - open beautiful add person modal -function openAddPersonModal() { - - // Remove existing modal if it exists - const existingModal = document.querySelector('.add-person-modal-overlay'); - if (existingModal) { - existingModal.remove(); - } - - // Create new modal with the beautiful design - const modal = document.createElement('div'); - modal.className = 'modal-overlay active add-person-modal-overlay'; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); - - // Add profile picture preview functionality - const profilePicInput = modal.querySelector('#modalPersonProfilePic'); - const profilePicPreview = modal.querySelector('.profile-pic-preview'); - - if (profilePicInput && profilePicPreview) { - profilePicInput.addEventListener('change', function(e) { - const file = e.target.files[0]; - if (file) { - // Validate file size (5MB limit) - if (file.size > 5 * 1024 * 1024) { - showNotification('Image must be smaller than 5MB', 'error'); - e.target.value = ''; - return; - } - - // Validate file type - if (!file.type.startsWith('image/')) { - showNotification('Please select an image file', 'error'); - e.target.value = ''; - return; - } - - // Preview the image - const reader = new FileReader(); - reader.onload = function(e) { - profilePicPreview.innerHTML = ``; - }; - reader.readAsDataURL(file); - } else { - // Reset to default - profilePicPreview.innerHTML = '?'; - } - }); - } - - // Focus on first input - setTimeout(() => { - const firstInput = modal.querySelector('#modalPersonName'); - if (firstInput) firstInput.focus(); - }, 100); -} - -function closeAddPersonModal() { - const modal = document.querySelector('.add-person-modal-overlay'); - if (modal) { - modal.remove(); - } -} - -// Add person - updated for new modal -async function addPerson(event) { - - event.preventDefault(); - - const modal = document.querySelector('.add-person-modal-overlay'); - if (!modal) { - console.error('❌ Add person modal not found'); - return; - } - - try { - // Show loading state - const submitButton = modal.querySelector('.submit-add-person-btn'); - const originalText = submitButton?.textContent || 'Add Person'; - if (submitButton) { - submitButton.textContent = 'Generating identity...'; - submitButton.disabled = true; - } - - // Handle profile picture - let profilePicture = null; - const profilePicInput = modal.querySelector('#modalPersonProfilePic'); - if (profilePicInput?.files?.[0]) { - const file = profilePicInput.files[0]; - const reader = new FileReader(); - profilePicture = await new Promise((resolve) => { - reader.onload = (e) => resolve(e.target.result); - reader.readAsDataURL(file); - }); - } - - const newPerson = { - id: Date.now(), - name: modal.querySelector('#modalPersonName')?.value?.trim() || '', - email: modal.querySelector('#modalPersonEmail')?.value?.trim() || '', - phone: modal.querySelector('#modalPersonPhone')?.value?.trim() || '', - relationship: modal.querySelector('#modalPersonRelationship')?.value || '', - notes: modal.querySelector('#modalPersonNotes')?.value?.trim() || '', - profilePicture: profilePicture, - dateAdded: new Date().toISOString() - }; - - if (!newPerson.name || !newPerson.relationship) { - showNotification('Please fill in all required fields.', 'error'); - if (submitButton) { - submitButton.textContent = originalText; - submitButton.disabled = false; - } - return; - } - - // Generate cryptographic identity - - let identity = null; - try { - identity = await generateIdentity(); - console.log('✅ Identity generated:', identity?.fingerprint?.substring(0, 20) + '...'); - - // Add crypto fields to person - newPerson.publicSigningKey = identity.signing.publicKey; - newPerson.publicEncryptionKey = identity.encryption.publicKey; - newPerson.keyFingerprint = identity.fingerprint; - newPerson.keyCreatedAt = identity.createdAt; - newPerson.verificationStatus = 'unverified'; - - // Store private keys for self - if (newPerson.relationship === 'self') { - await storeOwnIdentity(newPerson.id, identity); - } - } catch (identityError) { - console.warn('⚠️ Identity generation failed, continuing without crypto:', identityError); - } - - // Get existing people - let people = []; - try { - const result = await chrome.storage.local.get(['emma_people']); - people = result.emma_people || JSON.parse(localStorage.getItem('emma_people') || '[]'); - } catch (error) { - people = JSON.parse(localStorage.getItem('emma_people') || '[]'); - } - - // Add new person - people.push(newPerson); - - // Save to storage - try { - await chrome.storage.local.set({ emma_people: people }); - } catch (error) { - localStorage.setItem('emma_people', JSON.stringify(people)); - } - - // Update global variable - allPeople = people; - - // Show success message in modal - const successMessage = modal.querySelector('#addPersonSuccessMessage'); - if (successMessage) { - successMessage.innerHTML = ` -

✅ ${newPerson.name} added successfully!

- ${newPerson.relationship === 'self' ? ` -
-

🔐 Cryptographic Identity Created

-

Fingerprint: ${identity?.fingerprint?.substring(0, 20) || 'N/A'}...

-
- ` : ''} - `; - successMessage.style.display = 'block'; - } - - // Reset form - const form = modal.querySelector('#addPersonFormModal'); - if (form) form.reset(); - - // Restore button - if (submitButton) { - submitButton.textContent = originalText; - submitButton.disabled = false; - } - - // Refresh display - displayPeople(); - - // 🌟 CRITICAL: Refresh constellation if user came from there - setTimeout(() => { - refreshConstellationAfterPersonAdd(); - }, 500); - - // Close modal after delay - setTimeout(() => { - closeAddPersonModal(); - }, 2000); - - // Show identity card for self - if (newPerson.relationship === 'self' && identity) { - setTimeout(() => { - showIdentityCard(newPerson, identity); - }, 2500); - } - - } catch (error) { - console.error('❌ Failed to add person:', error); - showNotification('Failed to add person: ' + error.message, 'error'); - - // Restore button - const submitButton = modal.querySelector('.submit-add-person-btn'); - if (submitButton) { - submitButton.textContent = 'Add Person'; - submitButton.disabled = false; - } - } -} - -/** - * 🌟 REFRESH CONSTELLATION AFTER PERSON ADD - * Ensure new person appears immediately in constellation - */ -function refreshConstellationAfterPersonAdd() { - console.log('🌟 PEOPLE PAGE: Refreshing constellation after person add'); - - // Method 1: If constellation view function is available, call it - if (window.loadConstellationView && typeof window.loadConstellationView === 'function') { - console.log('🌟 PEOPLE PAGE: Calling loadConstellationView() after person add'); - window.loadConstellationView(); - } - // Method 2: Dispatch event for constellation to listen - else { - console.log('🌟 PEOPLE PAGE: Dispatching person added event for constellation'); - window.dispatchEvent(new CustomEvent('emmaPersonAdded', { - detail: { - action: 'refresh_constellation', - source: 'people_page', - timestamp: new Date().toISOString() - } - })); - } -} - -// Store own identity keys securely -async function storeOwnIdentity(personId, identity) { - try { - // Store in chrome.storage.local for persistence - const key = `emma_identity_${personId}`; - await chrome.storage.local.set({ - [key]: { - signing: { - privateKey: identity.signing.privateKey, - publicKey: identity.signing.publicKey - }, - encryption: { - privateKey: identity.encryption.privateKey, - publicKey: identity.encryption.publicKey - }, - fingerprint: identity.fingerprint, - createdAt: identity.createdAt - } - }); - } catch (error) { - console.error('Failed to store identity:', error); - // Fallback to localStorage if chrome.storage not available - localStorage.setItem(`emma_identity_${personId}`, JSON.stringify(identity)); - } -} - -// Show identity card for the user's own identity -function showIdentityCard(person, identity) { - const card = generateIdentityCard(person, identity); - - // Create a modal to show the identity card - const modal = document.createElement('div'); - modal.className = 'modal-overlay active'; - modal.innerHTML = ` - - `; - document.body.appendChild(modal); -} - -// Show person detail modal with edit capability -async function showPersonDetailModal(person, identity) { - - // Load existing sharing records for this person (to display shared memories) - const sharingRecords = await getSharingRecordsForPerson(person.id); - let sharedMemories = []; - try { - // Flatten records into a single list of { memoryId, permission } - const seen = new Set(); - for (const rec of sharingRecords) { - for (const m of rec.memories || []) { - if (!seen.has(m.memoryId)) { - sharedMemories.push({ memoryId: m.memoryId, permission: m.permission }); - seen.add(m.memoryId); - } - } - } - } catch (e) { - console.warn('Failed to process sharing records:', e); - sharedMemories = []; - } - - // Resolve memory display metadata for shared items - let allCapsules = []; - if (sharedMemories.length > 0) { - try { - allCapsules = await loadMemoryCapsules(); - } catch (e) { - console.warn('Could not load memory capsules for display:', e); - } - } - - const sharedListHtml = sharedMemories.length === 0 - ? `
No shared memory capsules yet.
` - : `
- ${sharedMemories.map((entry) => { - const mem = resolveMemoryById(allCapsules, entry.memoryId) || { title: 'Unknown memory', core: { type: 'text' }, header: { created: null } }; - const title = getMemoryTitle(mem); - const type = getMemoryType(mem); - const date = getMemoryDate(mem); - const icon = getMemoryIcon(type); - const permChip = `${entry.permission}`; - return `
- ${icon} -
-
- ${escapeHtml(title)} - ${escapeHtml(date)} -
-
${escapeHtml(type)}
-
- ${permChip} -
`; - }).join('')} -
`; - - // Create modal - const modal = document.createElement('div'); - modal.className = 'modal-overlay active person-detail-modal'; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); -} - -// Fetch sharing records for a specific person -async function getSharingRecordsForPerson(personId) { - try { - let records = []; - try { - const result = await chrome.storage.local.get(['emma_vault_sharing']); - records = result.emma_vault_sharing || []; - } catch (e) { - records = JSON.parse(localStorage.getItem('emma_vault_sharing') || '[]'); - } - return records.filter(r => parseInt(r.personId) === parseInt(personId) && r.status !== 'revoked'); - } catch (e) { - console.warn('Failed to load sharing records:', e); - return []; - } -} - -// Resolve a memory capsule by ID across various storage shapes -function resolveMemoryById(allCapsules, memoryId) { - if (!allCapsules || allCapsules.length === 0) return null; - const byId = allCapsules.find(m => String(m.id) === String(memoryId)); - if (byId) return byId; - const byHeaderId = allCapsules.find(m => String(m.header?.id) === String(memoryId)); - return byHeaderId || null; -} - -// Enable editing mode for person details -function enablePersonEdit(personId) { - - const modal = document.querySelector('.person-detail-modal'); - if (!modal) return; - - // Enable all form inputs - const nameInput = modal.querySelector('#edit-name'); - const emailInput = modal.querySelector('#edit-email'); - const phoneInput = modal.querySelector('#edit-phone'); - const relationshipSelect = modal.querySelector('#edit-relationship'); - const notesTextarea = modal.querySelector('#edit-notes'); - const profilePicInput = modal.querySelector('#edit-profile-pic'); - const identityFingerprintInput = modal.querySelector('#edit-identity-fingerprint'); - - [nameInput, emailInput, phoneInput, notesTextarea].forEach(input => { - if (input) { - input.removeAttribute('readonly'); - input.style.background = 'rgba(255, 255, 255, 0.98)'; - input.style.border = '2px solid rgba(134, 88, 255, 0.4)'; - } - }); - - if (profilePicInput) { - profilePicInput.removeAttribute('disabled'); - profilePicInput.style.background = 'rgba(255, 255, 255, 0.98)'; - profilePicInput.style.border = '2px solid rgba(134, 88, 255, 0.4)'; - - // Add preview functionality for edit mode - profilePicInput.addEventListener('change', function(e) { - const file = e.target.files[0]; - const preview = modal.querySelector('.edit-profile-pic-preview'); - if (file && preview) { - if (file.size > 5 * 1024 * 1024) { - showNotification('Image must be smaller than 5MB', 'error'); - e.target.value = ''; - return; - } - - if (!file.type.startsWith('image/')) { - showNotification('Please select an image file', 'error'); - e.target.value = ''; - return; - } - - const reader = new FileReader(); - reader.onload = function(e) { - preview.innerHTML = `Preview`; - }; - reader.readAsDataURL(file); - } - }); - } - - if (relationshipSelect) { - relationshipSelect.removeAttribute('disabled'); - relationshipSelect.style.background = 'rgba(255, 255, 255, 0.98)'; - relationshipSelect.style.border = '2px solid rgba(134, 88, 255, 0.4)'; - } - - // Enable identity fingerprint editing - if (identityFingerprintInput) { - identityFingerprintInput.removeAttribute('readonly'); - identityFingerprintInput.style.background = 'rgba(134, 88, 255, 0.15)'; - identityFingerprintInput.style.border = '2px solid rgba(134, 88, 255, 0.4)'; - } - - // Toggle display between read-only and editable identity fingerprint - const fingerprintDisplay = modal.querySelectorAll('.identity-fingerprint-display'); - const fingerprintEdit = modal.querySelectorAll('.identity-fingerprint-edit'); - - fingerprintDisplay.forEach(display => { - if (display) display.style.display = 'none'; - }); - - fingerprintEdit.forEach(edit => { - if (edit) edit.style.display = 'block'; - }); - - // Show/hide buttons - const editBtn = modal.querySelector('.edit-person-btn'); - const saveBtn = modal.querySelector('.save-person-btn'); - const cancelBtn = modal.querySelector('.cancel-edit-btn'); - - if (editBtn) { - editBtn.style.display = 'none'; // Hide edit icon in header - } - if (saveBtn) saveBtn.style.display = 'block'; - if (cancelBtn) cancelBtn.style.display = 'block'; - - // Store original values for cancel functionality - modal.dataset.originalName = nameInput?.value || ''; - modal.dataset.originalEmail = emailInput?.value || ''; - modal.dataset.originalPhone = phoneInput?.value || ''; - modal.dataset.originalRelationship = relationshipSelect?.value || ''; - modal.dataset.originalNotes = notesTextarea?.value || ''; - modal.dataset.originalIdentityFingerprint = identityFingerprintInput?.value || ''; - - showNotification('Edit mode enabled. Make your changes and click Save.', 'info'); -} - -// Save person edit changes -async function savePersonEdit(personId) { - - const modal = document.querySelector('.person-detail-modal'); - if (!modal) return; - - try { - // Get form values - const name = modal.querySelector('#edit-name')?.value?.trim(); - const email = modal.querySelector('#edit-email')?.value?.trim(); - const phone = modal.querySelector('#edit-phone')?.value?.trim(); - const relationship = modal.querySelector('#edit-relationship')?.value; - const notes = modal.querySelector('#edit-notes')?.value?.trim(); - const identityFingerprint = modal.querySelector('#edit-identity-fingerprint')?.value?.trim(); - - // Validate required fields - if (!name) { - showNotification('Name is required', 'error'); - return; - } - - // Validate identity fingerprint format if provided - if (identityFingerprint && identityFingerprint.length > 0) { - if (identityFingerprint.length < 20) { - showNotification('Identity fingerprint must be at least 20 characters', 'error'); - return; - } - // Basic hex validation - if (!/^[a-fA-F0-9:]+$/.test(identityFingerprint)) { - showNotification('Identity fingerprint must contain only hexadecimal characters and colons', 'error'); - return; - } - } - - // Get current people data - let people = []; - try { - const result = await chrome.storage.local.get(['emma_people']); - people = result.emma_people || JSON.parse(localStorage.getItem('emma_people') || '[]'); - } catch (error) { - people = JSON.parse(localStorage.getItem('emma_people') || '[]'); - } - - // Find and update the person - const personIndex = people.findIndex(p => p.id === parseInt(personId)); - - if (personIndex === -1) { - showNotification('Person not found', 'error'); - return; - } - - // Handle profile picture update - let profilePicture = people[personIndex].profilePicture; // Keep existing by default - const profilePicInput = modal.querySelector('#edit-profile-pic'); - if (profilePicInput?.files?.[0]) { - const file = profilePicInput.files[0]; - const reader = new FileReader(); - profilePicture = await new Promise((resolve) => { - reader.onload = (e) => resolve(e.target.result); - reader.readAsDataURL(file); - }); - } - - // Update person data - people[personIndex] = { - ...people[personIndex], - name, - email: email || null, - phone: phone || null, - relationship, - notes: notes || null, - profilePicture: profilePicture, - lastModified: new Date().toISOString() - }; - - // Check if identity fingerprint is being updated - const hasIdentityUpdate = identityFingerprint && identityFingerprint !== people[personIndex].keyFingerprint; - - // Update identity fingerprint if provided and different - if (hasIdentityUpdate) { - people[personIndex].keyFingerprint = identityFingerprint; - people[personIndex].keyUpdatedAt = Date.now(); - - } - - // Save to storage - try { - await chrome.storage.local.set({ emma_people: people }); - } catch (error) { - localStorage.setItem('emma_people', JSON.stringify(people)); - } - - // Update allPeople global variable - allPeople = people; - - // Refresh the display - displayPeople(); - - // Close modal and show success - modal.remove(); - showNotification(`Person updated successfully!${hasIdentityUpdate ? ' Identity fingerprint also updated.' : ''}`, 'success'); - - } catch (error) { - console.error('❌ Failed to save person:', error); - showNotification('Failed to save changes: ' + error.message, 'error'); - } -} - -// Cancel person edit changes -function cancelPersonEdit() { - - const modal = document.querySelector('.person-detail-modal'); - if (!modal) return; - - // Restore original values - const nameInput = modal.querySelector('#edit-name'); - const emailInput = modal.querySelector('#edit-email'); - const phoneInput = modal.querySelector('#edit-phone'); - const relationshipSelect = modal.querySelector('#edit-relationship'); - const notesTextarea = modal.querySelector('#edit-notes'); - const identityFingerprintInput = modal.querySelector('#edit-identity-fingerprint'); - - if (nameInput) nameInput.value = modal.dataset.originalName || ''; - if (emailInput) emailInput.value = modal.dataset.originalEmail || ''; - if (phoneInput) phoneInput.value = modal.dataset.originalPhone || ''; - if (relationshipSelect) relationshipSelect.value = modal.dataset.originalRelationship || ''; - if (notesTextarea) notesTextarea.value = modal.dataset.originalNotes || ''; - if (identityFingerprintInput) identityFingerprintInput.value = modal.dataset.originalIdentityFingerprint || ''; - - // Disable editing - [nameInput, emailInput, phoneInput, notesTextarea, identityFingerprintInput].forEach(input => { - if (input) { - input.setAttribute('readonly', 'readonly'); - input.style.background = 'rgba(255, 255, 255, 0.95)'; - input.style.border = '1px solid rgba(134, 88, 255, 0.2)'; - } - }); - - // Restore identity fingerprint display/edit toggle - const fingerprintDisplay = modal.querySelectorAll('.identity-fingerprint-display'); - const fingerprintEdit = modal.querySelectorAll('.identity-fingerprint-edit'); - - fingerprintDisplay.forEach(display => { - if (display) display.style.display = 'block'; - }); - - fingerprintEdit.forEach(edit => { - if (edit) edit.style.display = 'none'; - }); - - if (relationshipSelect) { - relationshipSelect.setAttribute('disabled', 'disabled'); - relationshipSelect.style.background = 'rgba(255, 255, 255, 0.95)'; - relationshipSelect.style.border = '1px solid rgba(134, 88, 255, 0.2)'; - } - - const profilePicInput = modal.querySelector('#edit-profile-pic'); - if (profilePicInput) { - profilePicInput.setAttribute('disabled', 'disabled'); - profilePicInput.style.background = 'rgba(255, 255, 255, 0.95)'; - profilePicInput.style.border = '1px solid rgba(134, 88, 255, 0.2)'; - profilePicInput.value = ''; // Clear file input - } - - // Show/hide buttons - const editBtn = modal.querySelector('.edit-person-btn'); - const saveBtn = modal.querySelector('.save-person-btn'); - const cancelBtn = modal.querySelector('.cancel-edit-btn'); - - if (editBtn) editBtn.style.display = 'flex'; // Show edit icon in header again - if (saveBtn) saveBtn.style.display = 'none'; - if (cancelBtn) cancelBtn.style.display = 'none'; - - showNotification('Changes cancelled', 'info'); -} - -// Export identity for backup -async function exportIdentity(personId) { - try { - - let identity = null; - - // Try chrome.storage first - try { - const result = await chrome.storage.local.get([`emma_identity_${personId}`]); - identity = result[`emma_identity_${personId}`]; - - } catch (chromeError) { - console.warn('⚠️ Chrome storage failed, trying localStorage:', chromeError); - } - - // Fallback to localStorage - if (!identity) { - try { - const stored = localStorage.getItem(`emma_identity_${personId}`); - if (stored) { - identity = JSON.parse(stored); - - } - } catch (localError) { - console.warn('⚠️ LocalStorage failed:', localError); - } - } - - if (!identity) { - console.error('❌ No identity found for person:', personId); - showNotification('Identity not found. Make sure this is your own identity.', 'error'); - return; - } - - const backup = { - version: '1.0', - type: 'emma-identity-backup', - personId, - createdAt: new Date().toISOString(), - identity - }; - - const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `emma-identity-${personId}-backup.json`; - a.style.display = 'none'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - showNotification('Identity exported successfully!', 'success'); - } catch (error) { - console.error('❌ Failed to export identity:', error); - showNotification('Failed to export identity: ' + error.message, 'error'); - } -} - -// Copy fingerprint to clipboard -function copyFingerprint(fingerprint) { - navigator.clipboard.writeText(fingerprint).then(() => { - showNotification('Fingerprint copied to clipboard', 'success'); - }).catch(() => { - showNotification('Failed to copy fingerprint', 'error'); - }); -} - -// Share identity fingerprint -function shareIdentity(fingerprint) { - const shareText = `Emma Identity Fingerprint:\n${fingerprint}\n\nThis cryptographic fingerprint can be used to verify my identity in secure communications.`; - - if (navigator.share) { - navigator.share({ - title: 'Emma Identity Fingerprint', - text: shareText, - }).then(() => { - showNotification('Identity shared successfully', 'success'); - }).catch((error) => { - - // Fallback to copy - copyToClipboard(shareText); - }); - } else { - // Fallback for browsers that don't support Web Share API - copyToClipboard(shareText); - } -} - -function copyToClipboard(text) { - navigator.clipboard.writeText(text).then(() => { - showNotification('Identity information copied to clipboard', 'success'); - }).catch(err => { - console.error('Failed to copy:', err); - showNotification('Failed to copy identity information', 'error'); - }); -} - -// Show share vault modal -async function showShareVaultModal(personId) { - const person = allPeople.find(p => p.id === personId); - if (!person) return; - - // Load memory capsules - let memories = []; - try { - memories = await loadMemoryCapsules(); - } catch (error) { - console.error('Failed to load memories:', error); - showNotification('Failed to load memory capsules', 'error'); - return; - } - - // Create share vault modal - const modal = document.createElement('div'); - modal.className = 'modal-overlay active share-vault-modal'; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); - - // Add event listeners for the modal - setupShareVaultModalListeners(modal, personId); -} - -// Load memory capsules from storage - use the same logic as memories.js -async function loadMemoryCapsules() { - - try { - // Try vault first (same as memories.js) - try { - const vaultList = await chrome.runtime.sendMessage({ action: 'vault.listCapsules', limit: 200 }); - if (vaultList && vaultList.success && vaultList.items && vaultList.items.length > 0) { - const memories = vaultList.items.map(h => ({ - id: h.id, - title: h.title || '(Encrypted Capsule)', - timestamp: h.ts || Date.now(), - role: h.role || 'assistant', - source: h.source || 'unknown', - type: 'vault' - })); - - return memories; - } - } catch {} - - // Try background next (same as memories.js) - try { - const response = await chrome.runtime.sendMessage({ action: 'getAllMemories', limit: 1000, offset: 0 }); - if (response && response.success && response.memories && response.memories.length > 0) { - - return response.memories; - } - } catch {} - - // Fallback to chrome storage (same as memories.js) - const result = await chrome.storage.local.get(['emma_memories']); - const memories = result.emma_memories || []; - - return memories; - - } catch (error) { - console.error('❌ Failed to load memory capsules:', error); - return []; - } -} - -// Create memory selection item for the share modal -function createMemorySelectionItem(memory) { - const memoryTitle = getMemoryTitle(memory); - const memoryType = getMemoryType(memory); - const memoryDate = getMemoryDate(memory); - const memoryPreview = getMemoryPreview(memory); - - return ` -
- -
- - - - -
-
- `; -} - -// Helper functions for memory display -function getMemoryTitle(memory) { - if (memory.title) return memory.title; - if (memory.core?.content) { - const content = memory.core.content; - if (typeof content === 'string') { - return content.substring(0, 50) + (content.length > 50 ? '...' : ''); - } - } - if (memory.content) { - return memory.content.substring(0, 50) + (memory.content.length > 50 ? '...' : ''); - } - return 'Untitled Memory'; -} - -function getMemoryType(memory) { - if (memory.core?.type) return memory.core.type; - if (memory.type) return memory.type; - if (memory.source) return memory.source; - return 'text'; -} - -function getMemoryDate(memory) { - const date = memory.header?.created || memory.created || memory.timestamp || memory.dateAdded; - if (date) { - return new Date(date).toLocaleDateString(); - } - return 'Unknown date'; -} - -function getMemoryPreview(memory) { - if (memory.semantic?.summary) return memory.semantic.summary; - if (memory.summary) return memory.summary; - if (memory.core?.content) { - const content = memory.core.content; - if (typeof content === 'string') { - return content.substring(0, 100) + (content.length > 100 ? '...' : ''); - } - } - if (memory.content) { - return memory.content.substring(0, 100) + (memory.content.length > 100 ? '...' : ''); - } - return 'No preview available'; -} - -function getMemoryIcon(type) { - const iconMap = { - 'text': '📝', - 'image': '🖼️', - 'video': '🎥', - 'audio': '🎵', - 'document': '📄', - 'conversation': '💬', - 'note': '📋', - 'web': '🌐', - 'social': '📱', - 'email': '📧' - }; - return iconMap[type] || '📝'; -} - -// Setup event listeners for share vault modal -function setupShareVaultModalListeners(modal, personId) { - // Permission selection - const permissionOptions = modal.querySelectorAll('.permission-option'); - permissionOptions.forEach(option => { - option.addEventListener('click', function() { - // Remove active state from all options - permissionOptions.forEach(opt => { - opt.style.background = 'rgba(255, 255, 255, 0.05)'; - opt.style.border = '2px solid rgba(255, 255, 255, 0.1)'; - }); - - // Add active state to clicked option - this.style.background = 'rgba(134, 88, 255, 0.1)'; - this.style.border = '2px solid rgba(134, 88, 255, 0.3)'; - - // Update all memory items to this permission - const permission = this.dataset.permission; - updateAllMemoryPermissions(permission); - }); - }); - - // Select all/none buttons - modal.querySelector('.select-all-memories')?.addEventListener('click', function() { - const checkboxes = modal.querySelectorAll('.memory-checkbox'); - checkboxes.forEach(cb => cb.checked = true); - }); - - modal.querySelector('.select-none-memories')?.addEventListener('click', function() { - const checkboxes = modal.querySelectorAll('.memory-checkbox'); - checkboxes.forEach(cb => cb.checked = false); - }); - - // Individual memory permission buttons - modal.addEventListener('click', function(e) { - if (e.target.classList.contains('permission-mini')) { - const memoryItem = e.target.closest('.memory-item'); - const permission = e.target.dataset.permission; - updateMemoryPermission(memoryItem, permission); - } - }); - - // Close modal - modal.querySelector('.close-share-vault-modal')?.addEventListener('click', function() { - modal.remove(); - }); - - modal.querySelector('.cancel-share-vault')?.addEventListener('click', function() { - modal.remove(); - }); - - // Confirm sharing - modal.querySelector('.confirm-share-vault')?.addEventListener('click', function() { - confirmVaultSharing(personId, modal); - }); -} - -// Update all memory permissions to a specific level -function updateAllMemoryPermissions(permission) { - const modal = document.querySelector('.share-vault-modal'); - if (!modal) return; - - const memoryItems = modal.querySelectorAll('.memory-item'); - memoryItems.forEach(item => updateMemoryPermission(item, permission)); -} - -// Update permission for a specific memory -function updateMemoryPermission(memoryItem, permission) { - const permissionButtons = memoryItem.querySelectorAll('.permission-mini'); - - // Reset all buttons - permissionButtons.forEach(btn => { - btn.style.background = 'rgba(255, 255, 255, 0.05)'; - btn.style.border = '1px solid rgba(255, 255, 255, 0.2)'; - btn.style.color = '#cccccc'; - }); - - // Highlight selected permission - const selectedBtn = memoryItem.querySelector(`[data-permission="${permission}"]`); - if (selectedBtn) { - selectedBtn.style.background = 'rgba(134, 88, 255, 0.1)'; - selectedBtn.style.border = '1px solid rgba(134, 88, 255, 0.3)'; - selectedBtn.style.color = '#8658ff'; - } -} - -// Confirm and process vault sharing -async function confirmVaultSharing(personId, modal) { - try { - - // Get selected memories and their permissions - const selectedMemories = []; - const memoryItems = modal.querySelectorAll('.memory-item'); - - memoryItems.forEach(item => { - const checkbox = item.querySelector('.memory-checkbox'); - if (checkbox?.checked) { - const memoryId = item.dataset.memoryId; - const activePermission = item.querySelector('.permission-mini[style*="rgb(134, 88, 255)"]'); - const permission = activePermission?.dataset.permission || 'view'; - - selectedMemories.push({ - memoryId, - permission - }); - } - }); - - if (selectedMemories.length === 0) { - showNotification('Please select at least one memory to share', 'error'); - return; - } - - // Get person details - const person = allPeople.find(p => p.id === parseInt(personId)); - if (!person) { - showNotification('Person not found', 'error'); - return; - } - - // Check if person has crypto identity - if (!person.keyFingerprint) { - showNotification('This person does not have a cryptographic identity yet', 'error'); - return; - } - - // Create sharing record - const sharingRecord = { - id: `share_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - personId: parseInt(personId), - personName: person.name, - personFingerprint: person.keyFingerprint, - memories: selectedMemories, - createdAt: new Date().toISOString(), - status: 'active' - }; - - // Store sharing record locally - await storeVaultSharingRecord(sharingRecord); - - // Get vault ID (for now, use default vault) - const vaultId = await getCurrentVaultId(); - - // Determine overall permissions (highest permission level) - const permissions = { - read: true, - write: selectedMemories.some(m => ['contribute', 'edit'].includes(m.permission)), - delete: selectedMemories.some(m => m.permission === 'edit'), - share: false, // For now, don't allow re-sharing - admin: false - }; - - // Initiate P2P sharing if available - if (p2pManager && person.keyFingerprint) { - try { - - // Add person to monitoring for incoming shares - await p2pManager.addPeerToMonitor(person.keyFingerprint); - - // Share vault via P2P - const shareId = await p2pManager.shareVault( - vaultId, - person.keyFingerprint, - permissions - ); - - showNotification(`HML Sync: Vault sharing initiated with ${person.name}! They will receive a notification when online.`, 'success'); - - } catch (p2pError) { - console.error('⚠️ HML Sync sharing failed, but local record saved:', p2pError); - showNotification(`Vault access recorded locally. HML Sync will retry when ${person.name} is online.`, 'info'); - } - } else { - // Fallback: local only - showNotification(`Vault access shared with ${person.name}! ${selectedMemories.length} memories shared locally.`, 'success'); - } - - modal.remove(); - - } catch (error) { - console.error('❌ Failed to share vault:', error); - showNotification('Failed to share vault access: ' + error.message, 'error'); - } -} - -// Get current vault ID -async function getCurrentVaultId() { - try { - const result = await chrome.storage.local.get(['emma_current_vault']); - return result.emma_current_vault || 'default'; - } catch (error) { - return 'default'; - } -} - -// Store vault sharing record -async function storeVaultSharingRecord(sharingRecord) { - try { - // Get existing sharing records - let sharingRecords = []; - try { - const result = await chrome.storage.local.get(['emma_vault_sharing']); - sharingRecords = result.emma_vault_sharing || []; - } catch (error) { - sharingRecords = JSON.parse(localStorage.getItem('emma_vault_sharing') || '[]'); - } - - // Add new record - sharingRecords.push(sharingRecord); - - // Save back to storage - try { - await chrome.storage.local.set({ emma_vault_sharing: sharingRecords }); - } catch (error) { - localStorage.setItem('emma_vault_sharing', JSON.stringify(sharingRecords)); - } - - } catch (error) { - console.error('❌ Failed to store sharing record:', error); - throw error; - } -} - -// View person details -async function viewPerson(personId) { - - try { - // Get person data - let people = []; - try { - const result = await chrome.storage.local.get(['emma_people']); - people = result.emma_people || JSON.parse(localStorage.getItem('emma_people') || '[]'); - } catch (error) { - people = JSON.parse(localStorage.getItem('emma_people') || '[]'); - } - - const person = people.find(p => p.id === personId); - if (!person) { - showNotification('Person not found', 'error'); - return; - } - - // Get identity data if it exists - let identity = null; - try { - const result = await chrome.storage.local.get([`emma_identity_${personId}`]); - identity = result[`emma_identity_${personId}`] || JSON.parse(localStorage.getItem(`emma_identity_${personId}`) || 'null'); - } catch (error) { - identity = JSON.parse(localStorage.getItem(`emma_identity_${personId}`) || 'null'); - } - - showPersonDetailModal(person, identity); - } catch (error) { - console.error('❌ Failed to load person details:', error); - showNotification('Failed to load person details', 'error'); - } -} - -// Save people to localStorage -async function savePeople() { - try { - await chrome.storage.local.set({ emma_people: allPeople }); - - } catch (error) { - localStorage.setItem('emma_people', JSON.stringify(allPeople)); - - } -} - -// Utility functions -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -function showNotification(message, type = 'info') { - // Simple notification - you can enhance this - const notification = document.createElement('div'); - notification.textContent = message; - notification.className = `notification ${type}`; - notification.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - padding: 15px 20px; - border-radius: 8px; - color: white; - z-index: 10000; - max-width: 300px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#17a2b8'}; - `; - - document.body.appendChild(notification); - - // Remove after 3 seconds - setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } - }, 3000); -} - -// Expose functions to global scope for HTML event handlers -window.addPerson = addPerson; -window.openAddPersonModal = openAddPersonModal; -window.closeAddPersonModal = closeAddPersonModal; -window.searchPeople = searchPeople; -window.viewPerson = viewPerson; -window.showShareVaultModal = showShareVaultModal; -window.exportIdentity = exportIdentity; -window.copyFingerprint = copyFingerprint; - -// Test that the function is properly exposed - -// Add universal navigation handler -function setupUniversalNavigation() { - const backBtn = document.getElementById('people-back-btn'); - if (backBtn) { - - backBtn.style.pointerEvents = 'auto'; - backBtn.style.position = 'relative'; - backBtn.style.zIndex = '9999999'; - backBtn.style.border = '2px solid red'; // Red debug border - - backBtn.addEventListener('click', (e) => { - - e.preventDefault(); - e.stopPropagation(); - - try { - if (window.location && window.location.href.includes('people.html')) { - - window.location.href = 'welcome.html'; - } else { - - window.history.back(); - } - } catch (error) { - console.error('🔥 PEOPLE: Navigation error:', error); - try { - - window.location.href = 'welcome.html'; - } catch {} - } - }); - } else { - console.error('🔥 PEOPLE: No back button found!'); - } -} - -// Set up navigation when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupUniversalNavigation); -} else { - setupUniversalNavigation(); -} diff --git a/js/popup.js b/js/popup.js index f54659e6..7fbd339d 100644 --- a/js/popup.js +++ b/js/popup.js @@ -277,7 +277,7 @@ function attachEventListeners() { } if (elements.peopleBtn) { - elements.peopleBtn.addEventListener('click', () => { try { openPeople(); } catch { try { window.location.href='people.html'; } catch {} } }); + elements.peopleBtn.addEventListener('click', () => { try { openPeople(); } catch { try { window.location.href='pages/people-emma.html'; } catch {} } }); } if (elements.relationshipsBtn) { @@ -1024,7 +1024,7 @@ function openPeople() { return; } } catch {} - try { window.location.href = 'people.html'; } catch {} + try { window.location.href = 'pages/people-emma.html'; } catch {} } function openRelationships() { diff --git a/js/sanitize.js b/js/sanitize.js new file mode 100644 index 00000000..45a077e3 --- /dev/null +++ b/js/sanitize.js @@ -0,0 +1,16 @@ +// Minimal global HTML sanitizer for interpolations +// Use escapeHtml(value) for any user-provided or dynamic content +(function(){ + function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + if (typeof window !== 'undefined') { + window.escapeHtml = escapeHtml; + } +})(); + diff --git a/js/secure-password-modal.js b/js/secure-password-modal.js index f9114dae..56ed09a6 100644 --- a/js/secure-password-modal.js +++ b/js/secure-password-modal.js @@ -36,6 +36,10 @@ class SecurePasswordModal { confirmPassword = false } = options; + const esc = (s) => (window.escapeHtml ? window.escapeHtml(s) : String(s) + .replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/'/g, ''')); + this.currentResolve = resolve; this.currentReject = reject; this.isOpen = true; @@ -45,19 +49,19 @@ class SecurePasswordModal {
-

${title}

+

${esc(title)}

-

${message}

+

${esc(message)}

diff --git a/js/theme-manager.js b/js/theme-manager.js new file mode 100644 index 00000000..78e932f6 --- /dev/null +++ b/js/theme-manager.js @@ -0,0 +1,617 @@ +(function createEmmaThemeManager() { + const STORAGE_KEY = 'emma.theme.selection'; + const STORAGE_BG_KEY = 'emma.theme.background'; + const EVENT_APPLIED = 'emmaThemeApplied'; + const EVENT_CHANGED = 'emmaThemeChanged'; + const DEFAULT_THEME_ID = 'aurora-classic'; + + const docEl = document.documentElement; + + const themeLookup = new Map(); + const backgroundLookup = new Map(); + let appliedVariables = []; + let activeTheme = null; + let initialized = false; + + let themeDefaultGradient = null; + let themeDefaultAurora = null; + + const DEFAULT_THEME_CATALOG = [ + { + id: 'aurora-classic', + name: 'Aurora Classic', + description: 'Signature Emma gradient with deep cosmos ambiance.', + preview: { primary: '#7c3aed', secondary: '#f093fb', surface: '#0a0a0f' }, + cssVars: { + '--emma-surface-primary': '#0a0a0f', + '--emma-surface-muted': 'rgba(255, 255, 255, 0.04)', + '--emma-surface-inverse': 'rgba(255, 255, 255, 0.92)', + '--emma-gradient-primary': 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)', + '--emma-gradient-secondary': 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%)', + '--emma-gradient-aurora': 'linear-gradient(135deg, #667eea20 0%, #764ba240 25%, #f093fb20 50%, #4dabf720 75%, #667eea20 100%)', + '--emma-border-subtle': 'rgba(255, 255, 255, 0.1)', + '--emma-border-strong': 'rgba(255, 255, 255, 0.2)', + '--emma-text-strong': '#ffffff', + '--emma-text-standard': 'rgba(255, 255, 255, 0.85)', + '--emma-text-muted': 'rgba(255, 255, 255, 0.6)', + '--emma-text-on-accent': '#ffffff', + '--emma-neutral-rgb': '255, 255, 255', + '--emma-neutral-inverse-rgb': '10, 10, 15', + '--emma-accent-primary': '#8b5cf6', + '--emma-accent-secondary': '#ec4899', + '--emma-accent-primary-rgb': '139, 92, 246', + '--emma-accent-secondary-rgb': '236, 72, 153', + '--emma-accent-surface': 'rgba(139, 92, 246, 0.2)', + '--emma-accent-surface-strong': 'rgba(139, 92, 246, 0.35)', + '--emma-accent-border': 'rgba(139, 92, 246, 0.4)', + '--emma-success': '#4ade80', + '--emma-warning': '#fbbf24', + '--emma-danger': '#f87171', + '--emma-chat-user-bubble': 'linear-gradient(135deg, #8b5cf6, #f093fb)', + '--emma-chat-user-text': '#ffffff', + '--emma-chat-emma-bubble': 'rgba(37, 38, 65, 0.85)', + '--emma-chat-emma-text': 'rgba(255, 255, 255, 0.92)', + '--emma-chat-user-meta': 'rgba(255, 255, 255, 0.7)', + '--emma-chat-emma-meta': 'rgba(255, 255, 255, 0.6)', + '--emma-chat-surface': 'rgba(255, 255, 255, 0.05)', + '--emma-chat-surface-strong': 'rgba(255, 255, 255, 0.1)', + '--emma-chat-border': 'rgba(255, 255, 255, 0.2)', + '--emma-chat-muted': 'rgba(255, 255, 255, 0.7)', + '--emma-chat-strong': 'rgba(255, 255, 255, 0.9)', + '--emma-chat-error': '#f87171', + '--emma-chat-success': '#10b981', + '--emma-nav-highlight': 'rgba(139, 92, 246, 0.2)', + '--emma-constellation-node': 'rgba(134, 88, 255, 0.6)', + '--emma-constellation-glow': 'rgba(134, 88, 255, 0.85)', + '--emma-orb-hue': '265', + '--emma-orb-hover-intensity': '0.25', + '--emma-glass': 'rgba(255, 255, 255, 0.05)', + '--emma-card-bg': 'rgba(255, 255, 255, 0.04)', + '--emma-button-secondary-bg': 'rgba(255, 255, 255, 0.1)', + '--emma-button-secondary-border': 'rgba(255, 255, 255, 0.2)', + '--emma-button-secondary-text': 'rgba(255, 255, 255, 0.85)', + '--emma-button-secondary-bg-hover': 'rgba(255, 255, 255, 0.2)', + '--emma-bg-gradient': 'linear-gradient(135deg, #0a0a0f 0%, #1a1033 50%, #0f0c29 100%)' + }, + background: { + id: 'aurora', + name: 'Aurora Field', + description: 'Soft cosmic gradients with Emma\'s signature glow.', + intensity: 0.35 + }, + flags: { + highContrast: false, + animatedBackground: true + } + }, + { + id: 'sunrise-glow', + name: 'Sunrise Glow', + description: 'Warm peach and coral palette with soft morning light.', + preview: { primary: '#ff8c68', secondary: '#ffd26f', surface: '#fff6eb' }, + cssVars: { + '--emma-surface-primary': '#fff6eb', + '--emma-surface-muted': '#ffe9d6', + '--emma-surface-inverse': '#1f1a24', + '--emma-gradient-primary': 'linear-gradient(135deg, #ff9a8b 0%, #ff6a88 50%, #ff99ac 100%)', + '--emma-gradient-secondary': 'linear-gradient(135deg, #ffecd6 0%, #ffcba4 50%, #ff9a8b 100%)', + '--emma-gradient-aurora': 'linear-gradient(135deg, rgba(255, 206, 170, 0.4) 0%, rgba(255, 145, 171, 0.4) 50%, rgba(255, 233, 214, 0.4) 100%)', + '--emma-border-subtle': 'rgba(31, 26, 36, 0.1)', + '--emma-border-strong': 'rgba(31, 26, 36, 0.2)', + '--emma-text-strong': '#1f1a24', + '--emma-text-standard': 'rgba(31, 26, 36, 0.85)', + '--emma-text-muted': 'rgba(31, 26, 36, 0.6)', + '--emma-text-on-accent': '#1f1a24', + '--emma-neutral-rgb': '31, 26, 36', + '--emma-neutral-inverse-rgb': '240, 224, 205', + '--emma-accent-primary': '#ff8c68', + '--emma-accent-secondary': '#ffb347', + '--emma-accent-primary-rgb': '255, 140, 104', + '--emma-accent-secondary-rgb': '255, 179, 71', + '--emma-accent-surface': 'rgba(255, 176, 124, 0.18)', + '--emma-accent-surface-strong': 'rgba(255, 176, 124, 0.3)', + '--emma-accent-border': 'rgba(255, 176, 124, 0.35)', + '--emma-success': '#3aa76d', + '--emma-warning': '#f59e0b', + '--emma-danger': '#e4566e', + '--emma-chat-user-bubble': 'linear-gradient(135deg, #ffb07c, #ff8c68)', + '--emma-chat-user-text': '#1f1a24', + '--emma-chat-emma-bubble': 'rgba(255, 233, 214, 0.95)', + '--emma-chat-emma-text': '#1f1a24', + '--emma-chat-user-meta': 'rgba(31, 26, 36, 0.6)', + '--emma-chat-emma-meta': 'rgba(31, 26, 36, 0.5)', + '--emma-chat-surface': 'rgba(31, 26, 36, 0.08)', + '--emma-chat-surface-strong': 'rgba(31, 26, 36, 0.12)', + '--emma-chat-border': 'rgba(31, 26, 36, 0.18)', + '--emma-chat-muted': 'rgba(31, 26, 36, 0.6)', + '--emma-chat-strong': 'rgba(31, 26, 36, 0.9)', + '--emma-chat-error': '#e4566e', + '--emma-chat-success': '#3aa76d', + '--emma-nav-highlight': 'rgba(255, 176, 124, 0.25)', + '--emma-constellation-node': 'rgba(255, 176, 124, 0.7)', + '--emma-constellation-glow': 'rgba(255, 176, 124, 0.9)', + '--emma-orb-hue': '24', + '--emma-orb-hover-intensity': '0.2', + '--emma-glass': 'rgba(255, 255, 255, 0.7)', + '--emma-card-bg': 'rgba(255, 255, 255, 0.85)', + '--emma-button-secondary-bg': 'rgba(31, 26, 36, 0.08)', + '--emma-button-secondary-border': 'rgba(31, 26, 36, 0.12)', + '--emma-button-secondary-text': 'rgba(31, 26, 36, 0.72)', + '--emma-button-secondary-bg-hover': 'rgba(31, 26, 36, 0.14)', + '--emma-bg-gradient': 'linear-gradient(135deg, #fff6eb 0%, #ffe3cc 50%, #ffd2b0 100%)' + }, + background: { + id: 'sunrise', + name: 'Sunrise Veil', + description: 'Warm morning light with gentle amber motion.', + intensity: 0.2 + }, + flags: { + highContrast: false, + animatedBackground: true + } + }, + { + id: 'midnight-forest', + name: 'Midnight Forest', + description: 'Deep greens and teals with luminous accents for focus.', + preview: { primary: '#1c3d3a', secondary: '#3ae8b3', surface: '#061b1a' }, + cssVars: { + '--emma-surface-primary': '#061b1a', + '--emma-surface-muted': 'rgba(10, 58, 52, 0.55)', + '--emma-surface-inverse': '#e9fff9', + '--emma-gradient-primary': 'linear-gradient(135deg, #0f4251 0%, #166c54 50%, #3ae8b3 100%)', + '--emma-gradient-secondary': 'linear-gradient(135deg, #2d9486 0%, #1f745f 50%, #3ae8b3 100%)', + '--emma-gradient-aurora': 'linear-gradient(135deg, rgba(61, 214, 190, 0.2) 0%, rgba(26, 95, 91, 0.35) 50%, rgba(73, 164, 164, 0.2) 100%)', + '--emma-border-subtle': 'rgba(58, 149, 138, 0.32)', + '--emma-border-strong': 'rgba(58, 149, 138, 0.5)', + '--emma-text-strong': '#e9fff9', + '--emma-text-standard': 'rgba(233, 255, 249, 0.86)', + '--emma-text-muted': 'rgba(233, 255, 249, 0.65)', + '--emma-text-on-accent': '#032221', + '--emma-neutral-rgb': '233, 255, 249', + '--emma-neutral-inverse-rgb': '12, 64, 60', + '--emma-accent-primary': '#3ae8b3', + '--emma-accent-secondary': '#5adeff', + '--emma-accent-primary-rgb': '58, 232, 179', + '--emma-accent-secondary-rgb': '90, 222, 255', + '--emma-accent-surface': 'rgba(58, 149, 138, 0.18)', + '--emma-accent-surface-strong': 'rgba(58, 149, 138, 0.32)', + '--emma-accent-border': 'rgba(58, 149, 138, 0.45)', + '--emma-success': '#4ade80', + '--emma-warning': '#fbbf24', + '--emma-danger': '#f87171', + '--emma-chat-user-bubble': 'linear-gradient(135deg, #3ae8b3, #5adeff)', + '--emma-chat-user-text': '#0b2320', + '--emma-chat-emma-bubble': 'rgba(14, 52, 54, 0.88)', + '--emma-chat-emma-text': 'rgba(233, 255, 249, 0.92)', + '--emma-chat-user-meta': 'rgba(11, 35, 33, 0.6)', + '--emma-chat-emma-meta': 'rgba(233, 255, 249, 0.65)', + '--emma-chat-surface': 'rgba(12, 64, 60, 0.35)', + '--emma-chat-surface-strong': 'rgba(12, 64, 60, 0.5)', + '--emma-chat-border': 'rgba(58, 149, 138, 0.45)', + '--emma-chat-muted': 'rgba(233, 255, 249, 0.7)', + '--emma-chat-strong': 'rgba(233, 255, 249, 0.92)', + '--emma-chat-error': '#f87171', + '--emma-chat-success': '#3ae8b3', + '--emma-nav-highlight': 'rgba(58, 149, 138, 0.3)', + '--emma-constellation-node': 'rgba(58, 149, 138, 0.7)', + '--emma-constellation-glow': 'rgba(58, 149, 138, 0.95)', + '--emma-orb-hue': '160', + '--emma-orb-hover-intensity': '0.25', + '--emma-glass': 'rgba(8, 43, 43, 0.6)', + '--emma-card-bg': 'rgba(8, 43, 43, 0.55)', + '--emma-button-secondary-bg': 'rgba(12, 64, 60, 0.6)', + '--emma-button-secondary-border': 'rgba(58, 149, 138, 0.45)', + '--emma-button-secondary-text': 'rgba(233, 255, 249, 0.88)', + '--emma-button-secondary-bg-hover': 'rgba(12, 64, 60, 0.75)', + '--emma-bg-gradient': 'linear-gradient(135deg, #031010 0%, #0f2f2b 50%, #041b19 100%)' + }, + background: { + id: 'forest', + name: 'Forest Night', + description: 'Luminous particles drifting through deep forest hues.', + intensity: 0.45 + }, + flags: { + highContrast: true, + animatedBackground: false + } + } + ]; + + const externalCatalog = Array.isArray(window.EMMA_THEMES) ? window.EMMA_THEMES : null; + const catalog = externalCatalog && externalCatalog.length ? externalCatalog : DEFAULT_THEME_CATALOG; + if (!externalCatalog || externalCatalog.length === 0) { + console.warn('Emma theme manager: falling back to built-in theme catalog'); + window.EMMA_THEMES = catalog; + } + catalog.forEach(theme => { + if (theme && theme.id) { + themeLookup.set(theme.id, theme); + if (theme.background && theme.background.id) { + const existing = backgroundLookup.get(theme.background.id) || { + ...theme.background, + themeIds: [] + }; + if (!existing.themeIds.includes(theme.id)) { + existing.themeIds.push(theme.id); + } + backgroundLookup.set(theme.background.id, existing); + } + } + }); + + function getThemeByBackgroundId(backgroundId) { + if (!backgroundId) return null; + return catalog.find(theme => theme.background && theme.background.id === backgroundId) || null; + } + + function getFallbackTheme() { + return themeLookup.get(DEFAULT_THEME_ID) || catalog[0] || null; + } + + function readPersistedThemeId() { + try { + const stored = localStorage.getItem(STORAGE_KEY); + console.log('[ThemeManager] read persisted theme:', stored); + return stored; + } catch (error) { + console.warn('Emma theme manager: unable to read persisted theme', error); + return null; + } + } + + function persistThemeSelection(themeId) { + try { + localStorage.setItem(STORAGE_KEY, themeId); + console.log('[ThemeManager] persisted theme selection:', themeId); + } catch (error) { + console.warn('Emma theme manager: unable to persist selection', error); + } + } + + function readPersistedBackgroundId() { + try { + const stored = localStorage.getItem(STORAGE_BG_KEY); + return stored || null; + } catch (error) { + console.warn('Emma theme manager: unable to read persisted background', error); + return null; + } + } + + function persistBackgroundId(backgroundId) { + try { + if (backgroundId) { + localStorage.setItem(STORAGE_BG_KEY, backgroundId); + } else { + localStorage.removeItem(STORAGE_BG_KEY); + } + } catch (error) { + console.warn('Emma theme manager: unable to persist background choice', error); + } + } + + function applyBackgroundState(backgroundId) { + const resolvedBackgroundId = backgroundId || (activeTheme && activeTheme.background && activeTheme.background.id) || null; + + if (resolvedBackgroundId) { + docEl.dataset.background = resolvedBackgroundId; + } else { + delete docEl.dataset.background; + } + + const backgroundTheme = getThemeByBackgroundId(resolvedBackgroundId); + + if (backgroundTheme && backgroundTheme.cssVars) { + const gradientOverride = backgroundTheme.cssVars['--emma-bg-gradient'] || themeDefaultGradient; + if (gradientOverride) { + docEl.style.setProperty('--emma-bg-gradient', gradientOverride); + } else if (themeDefaultGradient) { + docEl.style.setProperty('--emma-bg-gradient', themeDefaultGradient); + } else { + docEl.style.removeProperty('--emma-bg-gradient'); + } + + const auroraOverride = backgroundTheme.cssVars['--emma-gradient-aurora'] || backgroundTheme.cssVars['--emma-aurora']; + if (auroraOverride) { + docEl.style.setProperty('--emma-aurora', auroraOverride); + } else if (themeDefaultAurora) { + docEl.style.setProperty('--emma-aurora', themeDefaultAurora); + } else { + docEl.style.removeProperty('--emma-aurora'); + } + } else { + if (themeDefaultGradient) { + docEl.style.setProperty('--emma-bg-gradient', themeDefaultGradient); + } else { + docEl.style.removeProperty('--emma-bg-gradient'); + } + + if (themeDefaultAurora) { + docEl.style.setProperty('--emma-aurora', themeDefaultAurora); + } else { + docEl.style.removeProperty('--emma-aurora'); + } + } + } + + function dispatchBackgroundChange(backgroundId, previousBackgroundId) { + const detail = { + backgroundId: backgroundId || null, + previousBackgroundId: previousBackgroundId || null, + timestamp: Date.now() + }; + window.dispatchEvent(new CustomEvent('emmaBackgroundChanged', { detail })); + } + + function lookupBackground(backgroundId) { + return backgroundLookup.get(backgroundId) || null; + } + + function buildEventPayload(theme, previousTheme) { + return { + theme, + previousTheme, + timestamp: Date.now() + }; + } + + function broadcast(theme, previousTheme, eventName) { + if (!theme) return; + const detail = buildEventPayload(theme, previousTheme); + window.dispatchEvent(new CustomEvent(eventName, { detail })); + if (eventName !== EVENT_APPLIED) { + window.dispatchEvent(new CustomEvent(EVENT_APPLIED, { detail })); + } + } + + function applyCssVariables(theme) { + if (!theme || !theme.cssVars) return; + // Remove previously applied inline properties to avoid stale values + appliedVariables.forEach(key => { + docEl.style.removeProperty(key); + }); + + themeDefaultGradient = null; + themeDefaultAurora = null; + + const keys = Object.keys(theme.cssVars); + keys.forEach(key => { + docEl.style.setProperty(key, theme.cssVars[key]); + }); + + docEl.style.setProperty('--emma-theme-id', `'${theme.id}'`); + if (theme.cssVars && theme.cssVars['--emma-bg-gradient']) { + themeDefaultGradient = theme.cssVars['--emma-bg-gradient']; + docEl.style.setProperty('--emma-bg-gradient', themeDefaultGradient); + } else if (!themeDefaultGradient) { + const computed = getComputedStyle(docEl).getPropertyValue('--emma-bg-gradient'); + themeDefaultGradient = computed && computed.trim() ? computed.trim() : null; + } + appliedVariables = keys.concat('--emma-theme-id'); + + if (theme.cssVars && theme.cssVars['--emma-gradient-aurora']) { + themeDefaultAurora = theme.cssVars['--emma-gradient-aurora']; + docEl.style.setProperty('--emma-aurora', themeDefaultAurora); + } else if (!themeDefaultAurora) { + const computedAurora = getComputedStyle(docEl).getPropertyValue('--emma-aurora'); + themeDefaultAurora = computedAurora && computedAurora.trim() ? computedAurora.trim() : null; + } + } + + function applyBackgroundMetadata(theme, options = {}) { + if (!theme) return; + + const stored = readPersistedBackgroundId(); + const respectStored = options.respectStored !== false; + const previousBackground = docEl.dataset.background || null; + + let targetBackgroundId = null; + + if (respectStored && stored && lookupBackground(stored)) { + targetBackgroundId = stored; + } else if (theme.background && theme.background.id) { + targetBackgroundId = theme.background.id; + if (options.persist !== false) { + persistBackgroundId(theme.background.id); + } + } else if (options.persist !== false) { + persistBackgroundId(null); + } + + applyBackgroundState(targetBackgroundId); + + if (previousBackground !== targetBackgroundId) { + dispatchBackgroundChange(targetBackgroundId, previousBackground); + } + } + + async function syncThemeToVault(theme) { + const vault = window.emmaWebVault; + if (!vault || !vault.vaultData) return; + + try { + vault.vaultData.content = vault.vaultData.content || {}; + vault.vaultData.content.settings = vault.vaultData.content.settings || {}; + + vault.vaultData.content.settings.theme = { + id: theme.id, + appliedAt: Date.now() + }; + + if (typeof vault.persistSettings === 'function') { + await vault.persistSettings('theme'); + } else if (typeof vault.saveToIndexedDB === 'function') { + await vault.saveToIndexedDB(); + } else { + // Fallback: flag pending save + vault.pendingChanges = true; + } + } catch (error) { + console.warn('Emma theme manager: unable to sync theme to vault', error); + } + } + + function applyTheme(themeId, options = {}) { + const previousTheme = activeTheme; + + let theme = themeLookup.get(themeId); + if (!theme) { + theme = getFallbackTheme(); + } + + if (!theme) { + console.error('Emma theme manager: no theme available to apply'); + return null; + } + + if (previousTheme && previousTheme.id === theme.id && options.force !== true) { + return activeTheme; + } + + docEl.dataset.theme = theme.id; + applyCssVariables(theme); + applyBackgroundMetadata(theme, { + respectStored: options.keepBackground !== false, + persist: options.persist !== false + }); + + activeTheme = theme; + + if (options.persist !== false) { + persistThemeSelection(theme.id); + syncThemeToVault(theme); + } + + broadcast(theme, previousTheme, options.silent ? EVENT_APPLIED : EVENT_CHANGED); + return theme; + } + + function detectInitialTheme() { + const persisted = readPersistedThemeId(); + if (persisted && themeLookup.has(persisted)) { + console.log('[ThemeManager] using persisted theme', persisted); + return persisted; + } + + // Attempt to map system preference to a matching theme flag + try { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDark) { + const darkCandidate = catalog.find(theme => theme.flags && theme.flags.darkPreferred); + if (darkCandidate) return darkCandidate.id; + } else { + const lightCandidate = catalog.find(theme => theme.flags && theme.flags.lightPreferred); + if (lightCandidate) return lightCandidate.id; + } + } catch (error) { + // No-op: fallback handled below + } + + return getFallbackTheme()?.id || null; + } + + function initialize() { + if (initialized) return; + initialized = true; + + const initialThemeId = detectInitialTheme(); + console.log('[ThemeManager] initialize -> applying', initialThemeId || DEFAULT_THEME_ID); + applyTheme(initialThemeId || DEFAULT_THEME_ID, { persist: false, silent: true }); + + if (window.matchMedia) { + try { + const media = window.matchMedia('(prefers-color-scheme: dark)'); + media.addEventListener('change', () => { + const stored = readPersistedThemeId(); + if (!stored) { + const fallbackId = detectInitialTheme(); + applyTheme(fallbackId, { persist: false }); + } + }); + } catch (error) { + // Older browsers may not support addEventListener on media query lists + } + } + } + function updateBackground(backgroundId, options = {}) { + if (backgroundId && !lookupBackground(backgroundId)) { + console.warn('Emma theme manager: unknown background id', backgroundId); + } + + const previous = docEl.dataset.background || null; + applyBackgroundState(backgroundId || null); + + if (options.persist !== false) { + persistBackgroundId(backgroundId || null); + } + + if (previous !== (backgroundId || null)) { + dispatchBackgroundChange(backgroundId || null, previous); + } + } + const themeManager = { + init: initialize, + applyTheme, + setTheme(themeId, options = {}) { + return applyTheme(themeId, options); + }, + getTheme(themeId) { + return themeLookup.get(themeId) || null; + }, + getThemes() { + return [...catalog]; + }, + getActiveTheme() { + return activeTheme; + }, + setBackground(backgroundId, options = {}) { + updateBackground(backgroundId, options); + }, + getBackground() { + return docEl.dataset.background || null; + }, + getBackgrounds() { + return [...backgroundLookup.values()]; + }, + getBackgroundInfo(backgroundId) { + return lookupBackground(backgroundId); + }, + refresh() { + if (activeTheme) { + applyTheme(activeTheme.id, { force: true, persist: false, silent: true }); + } + } + }; + + window.emmaThemeManager = themeManager; + initialize(); +})(); + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/theme-settings-ui.js b/js/theme-settings-ui.js new file mode 100644 index 00000000..1fc47362 --- /dev/null +++ b/js/theme-settings-ui.js @@ -0,0 +1,386 @@ +(function(window) { + if (!window) return; + + const instances = new Set(); + let previewOriginal = null; + let listenersBound = false; + + function init(config = {}) { + console.log('[EmmaThemeUI] init called with', config); + if (!window.emmaThemeManager) { + console.warn('EmmaThemeUI: theme manager not available yet.'); + setTimeout(() => init(config), 200); + return; + } + + const themeContainer = config.themeContainerId + ? document.getElementById(config.themeContainerId) + : null; + if (!themeContainer) { + console.warn('EmmaThemeUI: theme container not found for id', config.themeContainerId); + return; + } + + const backgroundContainer = config.backgroundContainerId + ? document.getElementById(config.backgroundContainerId) + : null; + + const instance = { + themeContainer, + backgroundContainer + }; + instances.add(instance); + + renderThemeCards(instance); + if (backgroundContainer) { + renderBackgroundOptions(instance); + } + + if (!listenersBound) { + window.addEventListener('emmaThemeApplied', handleThemeApplied); + window.addEventListener('emmaBackgroundChanged', handleBackgroundChanged); + listenersBound = true; + } + + ensureThemeConsistency(); + const activeTheme = window.emmaThemeManager.getActiveTheme(); + if (activeTheme) { + updateThemeActiveState(activeTheme.id); + } + updateBackgroundActiveState(window.emmaThemeManager.getBackground()); + } + + function renderThemeCards(instance) { + const container = instance.themeContainer; + if (!container) return; + + container.innerHTML = ''; + const themes = window.emmaThemeManager.getThemes(); + if (!Array.isArray(themes) || themes.length === 0) { + console.warn('EmmaThemeUI: no themes available to render, retrying shortly...'); + setTimeout(() => renderThemeCards(instance), 200); + if (!container.dataset.pendingMessage) { + container.dataset.pendingMessage = 'true'; + container.innerHTML = ` +
+ Loading themes… +
+ `; + } + return; + } + console.log('[EmmaThemeUI] rendering themes:', themes.length); + delete container.dataset.pendingMessage; + const activeThemeId = window.emmaThemeManager.getActiveTheme()?.id || null; + + themes.forEach(theme => { + const card = document.createElement('button'); + card.type = 'button'; + card.className = 'theme-card'; + card.dataset.themeId = theme.id; + card.setAttribute('role', 'listitem'); + card.setAttribute('aria-label', `${theme.name} theme`); + + const preview = document.createElement('div'); + preview.className = 'theme-card__preview'; + const gradientPrimary = theme.cssVars?.['--emma-gradient-primary'] || theme.preview?.primary; + if (gradientPrimary) { + preview.style.background = gradientPrimary; + } + + const swatches = document.createElement('div'); + swatches.className = 'theme-card__swatches'; + ['primary', 'secondary', 'surface'].forEach(key => { + const value = theme.preview?.[key]; + if (!value) return; + const swatch = document.createElement('div'); + swatch.className = 'theme-card__swatch'; + swatch.style.background = value; + swatches.appendChild(swatch); + }); + preview.appendChild(swatches); + + const info = document.createElement('div'); + info.className = 'theme-card__info'; + + const nameEl = document.createElement('div'); + nameEl.className = 'theme-card__name'; + nameEl.textContent = theme.name; + + const descEl = document.createElement('div'); + descEl.className = 'theme-card__description'; + descEl.textContent = theme.description || 'Custom Emma theme.'; + + info.appendChild(nameEl); + info.appendChild(descEl); + + const badgeRow = document.createElement('div'); + badgeRow.className = 'theme-card__badges'; + if (theme.flags?.highContrast) { + const badge = document.createElement('span'); + badge.className = 'theme-card__badge'; + badge.textContent = 'High Contrast'; + badgeRow.appendChild(badge); + } + if (theme.flags?.animatedBackground) { + const badge = document.createElement('span'); + badge.className = 'theme-card__badge'; + badge.textContent = 'Animated'; + badgeRow.appendChild(badge); + } + + card.append(preview, info); + if (badgeRow.childElementCount > 0) { + card.appendChild(badgeRow); + } + + card.addEventListener('click', () => applyThemeSelection(theme.id)); + card.addEventListener('mouseenter', () => previewTheme(theme.id)); + card.addEventListener('mouseleave', cancelThemePreview); + card.addEventListener('focus', () => previewTheme(theme.id)); + card.addEventListener('blur', cancelThemePreview); + + if (theme.id === activeThemeId) { + card.classList.add('theme-card--active'); + card.setAttribute('aria-current', 'true'); + } + + container.appendChild(card); + }); + } + + function renderBackgroundOptions(instance) { + const container = instance.backgroundContainer; + if (!container) return; + + container.innerHTML = ''; + + const defaultButton = document.createElement('button'); + defaultButton.type = 'button'; + defaultButton.className = 'background-option'; + defaultButton.dataset.backgroundId = ''; + defaultButton.setAttribute('role', 'listitem'); + + const defaultPreview = document.createElement('div'); + defaultPreview.className = 'background-option__preview'; + defaultPreview.style.background = 'var(--emma-gradient-secondary)'; + + const defaultInfo = document.createElement('div'); + defaultInfo.className = 'background-option__info'; + + const defaultName = document.createElement('div'); + defaultName.className = 'background-option__name'; + defaultName.textContent = 'Follow Theme'; + + const defaultDescription = document.createElement('div'); + defaultDescription.className = 'background-option__description'; + defaultDescription.textContent = 'Automatically uses the background recommended by each theme.'; + + defaultInfo.append(defaultName, defaultDescription); + defaultButton.append(defaultPreview, defaultInfo); + defaultButton.addEventListener('click', () => { + updateBackground(null, { persist: true }); + try { + localStorage.removeItem('emma.theme.background'); + } catch (error) { + console.warn('EmmaThemeUI: could not clear stored background', error); + } + updateBackgroundActiveState(null); + }); + container.appendChild(defaultButton); + + const backgroundsRaw = window.emmaThemeManager.getBackgrounds() || []; + if (!Array.isArray(backgroundsRaw) || backgroundsRaw.length === 0) { + console.warn('EmmaThemeUI: no backgrounds available to render, retrying shortly...'); + setTimeout(() => renderBackgroundOptions(instance), 200); + container.innerHTML = ` +
+ Loading backgrounds… +
+ `; + return; + } + const backgrounds = backgroundsRaw.sort((a, b) => { + const nameA = (a.name || a.id).toLowerCase(); + const nameB = (b.name || b.id).toLowerCase(); + return nameA.localeCompare(nameB); + }); + console.log('[EmmaThemeUI] rendering backgrounds:', backgrounds.length); + + backgrounds.forEach(bg => { + const option = document.createElement('button'); + option.type = 'button'; + option.className = 'background-option'; + option.dataset.backgroundId = bg.id; + option.setAttribute('role', 'listitem'); + option.setAttribute('aria-label', `${bg.name || bg.id} background`); + + const preview = document.createElement('div'); + preview.className = 'background-option__preview'; + const firstThemeId = Array.isArray(bg.themeIds) ? bg.themeIds[0] : null; + if (firstThemeId) { + const theme = window.emmaThemeManager.getTheme(firstThemeId); + const gradient = theme?.cssVars?.['--emma-gradient-primary']; + if (gradient) { + preview.style.background = gradient; + } + } + + const info = document.createElement('div'); + info.className = 'background-option__info'; + + const nameEl = document.createElement('div'); + nameEl.className = 'background-option__name'; + nameEl.textContent = bg.name || bg.id; + + const descriptionEl = document.createElement('div'); + descriptionEl.className = 'background-option__description'; + descriptionEl.textContent = bg.description || 'Applies a thematic animated background.'; + + info.append(nameEl, descriptionEl); + option.append(preview, info); + + option.addEventListener('click', () => { + console.log('[EmmaThemeUI] selecting background', bg.id); + updateBackground(bg.id, { persist: true }); + try { + localStorage.setItem('emma.theme.background', bg.id); + } catch (error) { + console.warn('EmmaThemeUI: could not persist background directly', error); + } + updateBackgroundActiveState(bg.id); + }); + + container.appendChild(option); + }); + } + + function applyThemeSelection(themeId) { + if (!window.emmaThemeManager) return; + console.log('[EmmaThemeUI] applying theme via selection', themeId); + previewOriginal = null; + const currentBackground = window.emmaThemeManager.getBackground(); + const keepBackground = !!currentBackground; + window.emmaThemeManager.setTheme(themeId, { keepBackground }); + try { + localStorage.setItem('emma.theme.selection', themeId); + } catch (error) { + console.warn('EmmaThemeUI: could not persist theme selection directly', error); + } + updateThemeActiveState(themeId); + } + + function previewTheme(themeId) { + if (!window.emmaThemeManager) return; + const activeTheme = window.emmaThemeManager.getActiveTheme(); + if (activeTheme && activeTheme.id === themeId) { + return; + } + if (!previewOriginal) { + previewOriginal = activeTheme ? activeTheme.id : null; + } + window.emmaThemeManager.setTheme(themeId, { persist: false, silent: true, keepBackground: true }); + updateThemeActiveState(themeId); + } + + function cancelThemePreview() { + if (!window.emmaThemeManager) return; + if (!previewOriginal) return; + const current = window.emmaThemeManager.getActiveTheme(); + if (current && current.id !== previewOriginal) { + window.emmaThemeManager.setTheme(previewOriginal, { persist: false, silent: true, keepBackground: true }); + updateThemeActiveState(previewOriginal); + } + previewOriginal = null; + } + + function updateThemeActiveState(activeThemeId) { + document.querySelectorAll('.theme-card').forEach(card => { + const isActive = card.dataset.themeId === activeThemeId; + card.classList.toggle('theme-card--active', isActive); + if (isActive) { + card.setAttribute('aria-current', 'true'); + } else { + card.removeAttribute('aria-current'); + } + }); + } + + function updateBackgroundActiveState(backgroundId) { + document.querySelectorAll('.background-option').forEach(option => { + const optionId = option.dataset.backgroundId || ''; + const isActive = (!backgroundId && !optionId) || optionId === backgroundId; + option.classList.toggle('background-option--active', isActive); + if (isActive) { + option.setAttribute('aria-current', 'true'); + } else { + option.removeAttribute('aria-current'); + } + }); + } + + function updateBackground(backgroundId, options = {}) { + if (!window.emmaThemeManager) return; + window.emmaThemeManager.setBackground(backgroundId, options); + } + + function handleThemeApplied(event) { + const themeId = event?.detail?.theme?.id; + if (themeId) { + updateThemeActiveState(themeId); + updateBackgroundActiveState(window.emmaThemeManager.getBackground()); + } + } + + function handleBackgroundChanged(event) { + updateBackgroundActiveState(event?.detail?.backgroundId || null); + } + + window.EmmaThemeUI = { + init, + refresh() { + ensureThemeConsistency(); + const activeTheme = window.emmaThemeManager?.getActiveTheme()?.id; + if (activeTheme) { + updateThemeActiveState(activeTheme); + } + updateBackgroundActiveState(window.emmaThemeManager?.getBackground() || null); + instances.forEach(instance => { + renderThemeCards(instance); + if (instance.backgroundContainer) { + renderBackgroundOptions(instance); + } + }); + } + }; + + function ensureThemeConsistency() { + if (!window.emmaThemeManager) return; + try { + const storedThemeId = localStorage.getItem('emma.theme.selection'); + const activeTheme = window.emmaThemeManager.getActiveTheme(); + if (storedThemeId && (!activeTheme || activeTheme.id !== storedThemeId)) { + const themeExists = window.emmaThemeManager.getTheme(storedThemeId); + if (themeExists) { + const currentBg = window.emmaThemeManager.getBackground(); + const keepBackground = !!currentBg; + window.emmaThemeManager.setTheme(storedThemeId, { + persist: false, + silent: true, + keepBackground + }); + } + } + + const storedBackground = localStorage.getItem('emma.theme.background'); + const activeBackground = window.emmaThemeManager.getBackground(); + if (storedBackground && activeBackground !== storedBackground) { + window.emmaThemeManager.setBackground(storedBackground, { persist: false }); + } else if (!storedBackground && activeBackground) { + window.emmaThemeManager.setBackground(null, { persist: false }); + } + } catch (error) { + console.warn('EmmaThemeUI: unable to sync theme from storage', error); + } + } +})(window); diff --git a/js/voice-capture-experience.js b/js/voice-capture-experience.js index 54300548..6f4e96c1 100644 --- a/js/voice-capture-experience.js +++ b/js/voice-capture-experience.js @@ -307,11 +307,15 @@ class VoiceCaptureExperience extends ExperiencePopup { const container = document.getElementById('topic-nodes'); if (!container) return; + const esc = (s) => (window.escapeHtml ? window.escapeHtml(s) : String(s) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''')); + container.innerHTML = suggestions.map((suggestion, index) => ` -
+
-
${suggestion.icon}
-
${suggestion.text}
+
${esc(suggestion.icon)}
+
${esc(suggestion.text)}
@@ -542,14 +546,19 @@ class VoiceCaptureExperience extends ExperiencePopup { } formatTranscriptText(text) { - // Add subtle formatting for better readability - return text - .split(/(\.|!|\?)\s+/) - .map((segment, index) => { - if (segment.match(/[.!?]/)) { - return `${segment}`; + // Escape HTML, then wrap sentence-ending punctuation for readability + const esc = (s) => (window.escapeHtml ? window.escapeHtml(s) : String(s) + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''')); + const safe = esc(text); + return safe + .split(/([.!?]\s+)/) + .map((seg) => { + const m = seg.match(/^([.!?])(\s+)$/); + if (m) { + return `${m[1]}${m[2]}`; } - return segment; + return seg; }) .join(''); } diff --git a/js/webapp-extension-bridge.js b/js/webapp-extension-bridge.js index ae695f54..02387a0b 100644 --- a/js/webapp-extension-bridge.js +++ b/js/webapp-extension-bridge.js @@ -15,6 +15,8 @@ class EmmaWebappExtensionBridge { setupListeners() { // Listen for messages from extension window.addEventListener('message', (event) => { + // Only accept messages from same origin for safety + if (event.origin !== window.location.origin) return; if (event.data?.source === 'emma-extension') { this.handleExtensionMessage(event.data); } @@ -153,7 +155,7 @@ class EmmaWebappExtensionBridge { type: 'EMMA_RESPONSE', messageId: messageId, ...response - }, '*'); + }, window.location.origin); } } diff --git a/js/welcome.js b/js/welcome.js index e2722a3d..6ff3191e 100644 --- a/js/welcome.js +++ b/js/welcome.js @@ -58,11 +58,11 @@ async function init() { } else if (!status.isUnlocked) { console.log('🔍 Welcome: Vault exists but locked, redirecting to dashboard with unlock prompt'); // Redirect to dashboard which will handle vault unlock - window.location.href = '../pages/dashboard-new.html'; + window.location.href = '../dashboard.html'; } else { console.log('🔍 Welcome: Vault exists and unlocked, redirecting to dashboard'); // Vault exists and is unlocked, redirect to dashboard - window.location.href = '../pages/dashboard-new.html'; + window.location.href = '../dashboard.html'; } } else { // Fallback to Chrome extension API @@ -192,7 +192,7 @@ async function createVault() { // Redirect to dashboard after 2 seconds setTimeout(() => { console.log('🔐 Welcome: Vault created successfully, redirecting to dashboard'); - window.location.href = '../pages/dashboard-new.html'; + window.location.href = '../dashboard.html'; }, 2000); } else { showStatus(response.error || 'Failed to create vault', 'error'); diff --git a/legacy/README.md b/legacy/README.md new file mode 100644 index 00000000..7b36f4be --- /dev/null +++ b/legacy/README.md @@ -0,0 +1,3 @@ +# Legacy Assets + +The files under legacy/unused/ are retained only as historical references. They are not loaded by the production build and can be removed once their contents are no longer needed for future migrations. diff --git a/js/aurora.js b/legacy/unused/js/aurora.js similarity index 100% rename from js/aurora.js rename to legacy/unused/js/aurora.js diff --git a/js/chat/emma-chat-core.js b/legacy/unused/js/chat/emma-chat-core.js similarity index 100% rename from js/chat/emma-chat-core.js rename to legacy/unused/js/chat/emma-chat-core.js diff --git a/js/chat/emma-dementia-companion.js b/legacy/unused/js/chat/emma-dementia-companion.js similarity index 100% rename from js/chat/emma-dementia-companion.js rename to legacy/unused/js/chat/emma-dementia-companion.js diff --git a/js/chat/emma-intent-classifier.js b/legacy/unused/js/chat/emma-intent-classifier.js similarity index 100% rename from js/chat/emma-intent-classifier.js rename to legacy/unused/js/chat/emma-intent-classifier.js diff --git a/js/chat/emma-memory-operations.js b/legacy/unused/js/chat/emma-memory-operations.js similarity index 100% rename from js/chat/emma-memory-operations.js rename to legacy/unused/js/chat/emma-memory-operations.js diff --git a/js/chat/emma-person-handler.js b/legacy/unused/js/chat/emma-person-handler.js similarity index 100% rename from js/chat/emma-person-handler.js rename to legacy/unused/js/chat/emma-person-handler.js diff --git a/js/chat/emma-photo-manager.js b/legacy/unused/js/chat/emma-photo-manager.js similarity index 100% rename from js/chat/emma-photo-manager.js rename to legacy/unused/js/chat/emma-photo-manager.js diff --git a/js/chat/index.js b/legacy/unused/js/chat/index.js similarity index 100% rename from js/chat/index.js rename to legacy/unused/js/chat/index.js diff --git a/js/dementia-companion-init.js b/legacy/unused/js/dementia-companion-init.js similarity index 100% rename from js/dementia-companion-init.js rename to legacy/unused/js/dementia-companion-init.js diff --git a/js/memories-gallery.js b/legacy/unused/js/memories-gallery.js similarity index 100% rename from js/memories-gallery.js rename to legacy/unused/js/memories-gallery.js diff --git a/js/memory-gallery-clean.js b/legacy/unused/js/memory-gallery-clean.js similarity index 100% rename from js/memory-gallery-clean.js rename to legacy/unused/js/memory-gallery-clean.js diff --git a/js/screenshot-photo-capture.js b/legacy/unused/js/screenshot-photo-capture.js similarity index 100% rename from js/screenshot-photo-capture.js rename to legacy/unused/js/screenshot-photo-capture.js diff --git a/js/universal-orb-injection.js b/legacy/unused/js/universal-orb-injection.js similarity index 100% rename from js/universal-orb-injection.js rename to legacy/unused/js/universal-orb-injection.js diff --git a/js/web-memories.js b/legacy/unused/js/web-memories.js similarity index 100% rename from js/web-memories.js rename to legacy/unused/js/web-memories.js diff --git a/pages/dashboard-new.html b/pages/dashboard-new.html deleted file mode 100644 index c1e49c48..00000000 --- a/pages/dashboard-new.html +++ /dev/null @@ -1,5294 +0,0 @@ - - - - - - Emma - Your Intelligent Memory Companion - - - - -
-
-
- - - - - -
-

emma

-

Your Intelligent Memory Companion

-
- - -
- -
- - -
-
- -
-
- -
-
-
-
- - -
-
-
🎤
-
Capture
-
-
-
🔒
-
Vault
-
-
-
💝
-
Memories
-
-
-
👥
-
People
-
-
-
🔍
-
Search
-
-
-
📱
-
Share
-
-
-
💬
-
Chat
-
-
-
⚙️
-
Settings
-
-
- - -
-
-
0
-
Memories
-
-
-
0
-
People
-
- - -
-
🌟
-
Upgrade to .emma
-
NEW
-
-
-
0
-
Today
-
-
-
-
- - -
-
-
📅
-
Daily Brief
-
-
-

Good morning! You have 3 memories from this day last year.

-

- • Meeting with Sarah about the project
- • Family dinner at Marina Bay
- • Completed the meditation course -

-
-
- -
-
-
💡
-
AI Insights
-
-
-

I noticed you've been capturing more creative moments lately. Your photography memories have increased by 40% this month!

-
-
- -
-
-
-
Quick Actions
-
-
-

Press Space to activate voice capture
- Press Cmd+K to search memories
- Say "Hey Emma" to start

-
-
- - -
-
-
-
-
-
-
-
- Listening... -
- - -
-
-
-

Loading Emma...

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pages/dashboard-secure.html b/pages/dashboard-secure.html deleted file mode 100644 index 32e0945d..00000000 --- a/pages/dashboard-secure.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - Emma - Your Intelligent Memory Companion - - - - - - - - - -
-
-
- - - - - -
-

emma

-

Your Intelligent Memory Companion

-
- - -
- -
- - -
-
- -
-
- -
-
-
-
- - -
-
-
🎤
-
Voice
-
- -
-
💬
-
Chat
-
- -
-
💎
-
Memories
-
- -
-
🔒
-
Vault
-
- -
-
📱
-
Share
-
- -
-
🔍
-
Search
-
-
- - -
-
-
- - -
-
- -

Emma Vault

- -
- 🔒 Vault Status Loading... -
- -
-
- - -
- -
- - -
-
- -
- - -
-
-
- - - - - - - - - - - - - - diff --git a/pages/dashboard.html b/pages/dashboard.html deleted file mode 100644 index ddb2292d..00000000 --- a/pages/dashboard.html +++ /dev/null @@ -1,6662 +0,0 @@ - - - - - - Emma - Your Intelligent Memory Companion - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - - -
-

emma

-

Your Intelligent Memory Companion

-
- - -
- -
- - -
-
- -
-
- -
-
-
-
- - -
-
-
- - - - -
-
Memories
-
-
-
- - - - -
-
People
-
-
-
- - - -
-
Chat
-
-
- - -
- -
- - - - -
-
- - -
-
-
0
-
Memories
-
-
-
0
-
People
-
- - -
-
🌟
-
Upgrade to .emma
-
NEW
-
-
-
0
-
Today
-
-
-
-
- - - - -
-
-
📅
-
Daily Brief
-
-
-

Good morning! You have 3 memories from this day last year.

-

- • Meeting with Sarah about the project
- • Family dinner at Marina Bay
- • Completed the meditation course -

-
-
- -
-
-
💡
-
AI Insights
-
-
-

I noticed you've been capturing more creative moments lately. Your photography memories have increased by 40% this month!

-
-
- -
-
-
-
Quick Actions
-
-
-

Press Space to activate voice capture
- Press Cmd+K to search memories
- Say "Hey Emma" to start

-
-
- - -
-
-
-
-
-
-
-
- Listening... -
- - -
-
-
-

Loading Emma...

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pages/emma-settings-redesigned.html b/pages/emma-settings-redesigned.html index bb02df95..f93b4a91 100644 --- a/pages/emma-settings-redesigned.html +++ b/pages/emma-settings-redesigned.html @@ -4,30 +4,30 @@ Emma Settings - Your Intelligent Memory Companion + - - - + + + + + + - - - - - + + -
- -
-
-
-

People

-

Manage your family, friends, and connections

-
- -
- - -
-
-
- - - - - -
- -
-
-
- - -
-
-
👥
-
-

Total People

-

0

-
-
-
-
❤️
-
-

Family & Close

-

0

-
-
-
-
💼
-
-

Colleagues

-

0

-
-
-
- - -
- -
- - -
-
-
- - - -
- - -
- -
- - -