diff --git a/.cursor/scratchpad.md b/.cursor/scratchpad.md index 3aab0b0e..f128fa87 100644 --- a/.cursor/scratchpad.md +++ b/.cursor/scratchpad.md @@ -1843,7 +1843,7 @@ class EmmaQuestionEngine { #### **EMMA BRANDING PRINCIPLES** ๐Ÿ“ 1. **Color Palette**: - - Primary: `--emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)` + - Primary: `--emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%)` - Background: `--emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%)` - Cards: `rgba(255, 255, 255, 0.05)` with `backdrop-filter: blur(20px)` 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 465bbe09..00000000 Binary files a/.render-deploy-trigger and /dev/null differ 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..e51ce98d 100644 --- a/add-person.html +++ b/add-person.html @@ -4,7 +4,11 @@ Add Person - Emma + + + + + + +
+
+
+

Welcome to Emma Chat

+

Start a conversation below.

+
+
+
+ +
+
+ + +
+
+ + + + + + + diff --git a/css/content.css b/css/content.css deleted file mode 100644 index cc843d21..00000000 --- a/css/content.css +++ /dev/null @@ -1,349 +0,0 @@ -/* Emma Lite - Content Script Styles */ - -/* Emma Brand Colors */ -:root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); - --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); - --emma-purple: #764ba2; - --emma-pink: #f093fb; - --emma-dark: #1a1033; - --emma-glass: rgba(26, 16, 51, 0.9); - --emma-border: rgba(255, 255, 255, 0.1); - --emma-text: #ffffff; - --emma-text-secondary: rgba(255, 255, 255, 0.7); -} - -/* Emma Capture Button (Floating) */ -.emma-capture-button { - position: fixed; - bottom: 24px; - right: 24px; - width: 56px; - height: 56px; - background: var(--emma-gradient-1); - border: none; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - color: white; - box-shadow: 0 8px 32px rgba(118, 75, 162, 0.4); - transition: all 0.3s ease; - z-index: 9999; - overflow: hidden; -} - -.emma-capture-button::before { - content: ''; - position: absolute; - inset: 0; - background: rgba(255, 255, 255, 0.2); - transform: scale(0); - border-radius: 50%; - transition: transform 0.6s ease; -} - -.emma-capture-button:hover { - transform: translateY(-4px); - box-shadow: 0 12px 40px rgba(118, 75, 162, 0.5); -} - -.emma-capture-button:hover::before { - transform: scale(1); -} - -.emma-capture-button:active { - transform: translateY(-2px); -} - -/* Emma Badge (Shows capture count) */ -.emma-badge { - position: absolute; - top: -4px; - right: -4px; - background: #f093fb; - color: white; - font-size: 11px; - font-weight: 600; - padding: 2px 6px; - border-radius: 10px; - min-width: 20px; - text-align: center; - box-shadow: 0 2px 8px rgba(240, 147, 251, 0.4); -} - -/* Emma Notification */ -.emma-notification { - position: fixed; - top: 24px; - right: 24px; - max-width: 360px; - background: var(--emma-glass); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--emma-border); - border-radius: 16px; - padding: 20px; - color: var(--emma-text); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - font-size: 14px; - line-height: 1.5; - box-shadow: 0 12px 48px rgba(26, 16, 51, 0.6); - transform: translateX(400px); - opacity: 0; - transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); - z-index: 10000; -} - -.emma-notification::before { - content: ''; - position: absolute; - inset: -1px; - background: var(--emma-gradient-1); - border-radius: 16px; - opacity: 0.5; - z-index: -1; - filter: blur(20px); -} - -.emma-notification-show { - transform: translateX(0); - opacity: 1; -} - -.emma-notification-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.emma-notification-icon { - width: 40px; - height: 40px; - background: var(--emma-gradient-1); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; - flex-shrink: 0; -} - -.emma-notification-title { - font-size: 16px; - font-weight: 600; - background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.8) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.emma-notification-message { - color: var(--emma-text-secondary); - margin-bottom: 16px; -} - -.emma-notification-actions { - display: flex; - gap: 12px; -} - -.emma-notification-button { - flex: 1; - background: rgba(255, 255, 255, 0.1); - border: 1px solid var(--emma-border); - border-radius: 8px; - padding: 8px 16px; - color: var(--emma-text); - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - backdrop-filter: blur(10px); -} - -.emma-notification-button:hover { - background: rgba(255, 255, 255, 0.15); - transform: translateY(-1px); -} - -.emma-notification-button-primary { - background: var(--emma-gradient-1); - border: none; - color: white; -} - -.emma-notification-button-primary:hover { - box-shadow: 0 4px 16px rgba(118, 75, 162, 0.4); -} - -/* Emma Inline UI (Settings, Status) */ -.emma-inline-ui { - position: fixed; - top: 80px; - right: 24px; - width: 280px; - background: var(--emma-glass); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--emma-border); - border-radius: 16px; - padding: 20px; - box-shadow: 0 12px 48px rgba(26, 16, 51, 0.6); - transform: translateX(320px); - opacity: 0; - transition: all 0.4s ease; - z-index: 9998; -} - -.emma-inline-ui-visible { - transform: translateX(0); - opacity: 1; -} - -.emma-inline-ui-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; -} - -.emma-inline-ui-title { - font-size: 16px; - font-weight: 600; - color: var(--emma-text); -} - -.emma-inline-ui-close { - width: 24px; - height: 24px; - background: rgba(255, 255, 255, 0.1); - border: none; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: var(--emma-text-secondary); - transition: all 0.3s ease; -} - -.emma-inline-ui-close:hover { - background: rgba(255, 255, 255, 0.2); - color: var(--emma-text); -} - -/* Emma Stats Display */ -.emma-stats { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 16px; -} - -.emma-stat { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--emma-border); - border-radius: 12px; - padding: 12px; - text-align: center; -} - -.emma-stat-value { - font-size: 20px; - font-weight: 600; - background: var(--emma-gradient-1); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - display: block; -} - -.emma-stat-label { - font-size: 11px; - color: var(--emma-text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Emma Progress Bar */ -.emma-progress { - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - height: 6px; - overflow: hidden; - margin-top: 8px; -} - -.emma-progress-bar { - height: 100%; - background: var(--emma-gradient-1); - border-radius: 8px; - transition: width 0.3s ease; -} - -/* Emma Loading Animation */ -.emma-loading { - display: inline-block; - width: 16px; - height: 16px; - border: 2px solid var(--emma-border); - border-top-color: var(--emma-purple); - border-radius: 50%; - animation: emma-spin 1s linear infinite; -} - -@keyframes emma-spin { - to { transform: rotate(360deg); } -} - -/* Emma Tooltip */ -.emma-tooltip { - position: absolute; - background: var(--emma-glass); - backdrop-filter: blur(10px); - border: 1px solid var(--emma-border); - border-radius: 8px; - padding: 8px 12px; - color: var(--emma-text); - font-size: 12px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transform: translateY(4px); - transition: all 0.3s ease; - z-index: 10001; -} - -.emma-tooltip-visible { - opacity: 1; - transform: translateY(0); -} - -/* Animations */ -@keyframes emma-pulse { - 0% { - box-shadow: 0 0 0 0 rgba(118, 75, 162, 0.4); - } - 70% { - box-shadow: 0 0 0 20px rgba(118, 75, 162, 0); - } - 100% { - box-shadow: 0 0 0 0 rgba(118, 75, 162, 0); - } -} - -.emma-pulse { - animation: emma-pulse 2s infinite; -} - -/* Dark mode adjustments for light-themed sites */ -@media (prefers-color-scheme: light) { - .emma-notification, - .emma-inline-ui { - box-shadow: 0 12px 48px rgba(26, 16, 51, 0.8); - } -} \ No newline at end of file diff --git a/css/dashboard-base.css b/css/dashboard-base.css new file mode 100644 index 00000000..1fd6f47b --- /dev/null +++ b/css/dashboard-base.css @@ -0,0 +1,1766 @@ +๏ปฟ* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: var(--emma-bg-gradient); + color: var(--emma-text); + min-height: 100vh; + overflow-x: hidden; + overflow-y: auto; + position: relative; + padding-bottom: env(safe-area-inset-bottom, 0); +} + +body.modal-open { + overflow: hidden; +} + +/* Aurora Background Effect */ +.aurora-bg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.3; + z-index: 1; + pointer-events: none; +} + +.aurora { + position: absolute; + width: 200%; + height: 200%; + background: var(--emma-aurora); + animation: aurora 20s ease-in-out infinite; + filter: blur(60px); +} + +:root[data-background="aurora"] .aurora { + background: var(--emma-aurora); +} + +:root[data-background="sunrise"] .aurora { + background: var(--emma-background-sunrise); +} + +:root[data-background="forest"] .aurora { + background: var(--emma-background-forest); +} +:root[data-background="ocean"] .aurora { + background: var(--emma-background-ocean); +} + +:root[data-background="ember"] .aurora { + background: var(--emma-background-ember); +} + +:root[data-background="blossom"] .aurora { + background: var(--emma-background-blossom); +} + +:root[data-background="zen"] .aurora { + background: var(--emma-background-zen); +} +@keyframes aurora { + 0%, 100% { transform: translate(-50%, -50%) rotate(0deg) scale(1); } + 33% { transform: translate(-30%, -60%) rotate(120deg) scale(1.1); } + 66% { transform: translate(-70%, -40%) rotate(240deg) scale(0.9); } +} + +body.performance-lite .aurora-bg, +body.performance-lite .welcome-text { + display: none; +} + +body.performance-lite .aurora { + animation: none; +} + +body.performance-lite .neural-network { + opacity: 0; +} + +/* Main Container */ +.dashboard { + position: relative; + width: 100%; + min-height: 100vh; + min-height: 100dvh; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + padding: 0 env(safe-area-inset-right, 0) env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0); +} + +/* Central Command Orb Container */ +.command-center { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +/* Emma Orb */ +.emma-orb-container { + position: relative; + width: 200px; + height: 200px; + cursor: pointer; + z-index: 6; + transition: transform 0.3s ease; +} + +.emma-orb-container:hover { + transform: scale(1.05); +} + +.emma-orb-webgl { + width: 100%; + height: 100%; + border-radius: 50%; + box-shadow: var(--emma-glow), inset 0 0 40px rgba(var(--emma-neutral-rgb), 0.1); + position: relative; + overflow: hidden; +} + +.emma-orb-webgl canvas { + border-radius: 50%; + width: 100% !important; + height: 100% !important; +} + +@keyframes orb-pulse { + 0%, 100% { transform: scale(1); box-shadow: var(--emma-glow), inset 0 0 40px rgba(var(--emma-neutral-rgb), 0.1); } + 50% { transform: scale(1.02); box-shadow: 0 0 60px rgba(var(--emma-accent-primary-rgb), 0.8), inset 0 0 40px rgba(var(--emma-neutral-rgb), 0.2); } +} + +@keyframes vault-unlock-pulse { + 0% { transform: scale(1); } + 30% { transform: scale(1.1); box-shadow: 0 0 80px rgba(16, 185, 129, 0.8); } + 60% { transform: scale(0.95); } + 100% { transform: scale(1); } +} + +/* Orb status removed - clean WebGL orb only */ + +.orb-icon { + font-size: 48px; + margin-bottom: 8px; + filter: drop-shadow(0 0 10px rgba(var(--emma-neutral-rgb), 0.5)); +} + +/* Orb text removed - clean WebGL orb only */ + +/* Neural Network Menu */ +.neural-network { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; /* CRITICAL FIX: Lower z-index so nodes appear above connections */ +} + +.neural-canvas { + width: 100%; + height: 100%; +} + +.radial-menu { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + opacity: 0; + transition: opacity 0.6s ease; + z-index: 5; +} + +.radial-menu.active { + opacity: 1; +} + +.radial-menu .radial-item { + pointer-events: none; +} + +.radial-menu.active .radial-item { + pointer-events: auto; +} + +.radial-item { + position: absolute; + width: 90px; + height: 90px; + background: var(--emma-glass); + backdrop-filter: blur(20px); + border: 2px solid rgba(var(--emma-accent-primary-rgb), 0.3); + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: opacity 0.4s ease, transform 0.2s ease; + opacity: 0; + box-shadow: 0 0 18px rgba(var(--emma-accent-primary-rgb), 0.25); + z-index: 10; +} + +.radial-menu.active .radial-item { + opacity: 1; +} + +/* Elegant add node glow animation */ +@keyframes addNodeGlow { + 0%, 100% { + box-shadow: + 0 0 14px rgba(16, 185, 129, 0.35), + 0 0 26px rgba(16, 185, 129, 0.18), + inset 0 0 14px rgba(16, 185, 129, 0.1); + } + 50% { + box-shadow: + 0 0 20px rgba(16, 185, 129, 0.45), + 0 0 34px rgba(16, 185, 129, 0.22), + inset 0 0 18px rgba(16, 185, 129, 0.14); + } +} + +/* Subtle heartbeat animation for people nodes */ +@keyframes heartbeat { + 0%, 100% { + box-shadow: + 0 6px 22px rgba(111, 99, 217, 0.35), + 0 0 16px rgba(239, 68, 68, 0.25), + 0 0 26px rgba(239, 68, 68, 0.1); + } + 25% { + box-shadow: + 0 6px 22px rgba(111, 99, 217, 0.35), + 0 0 20px rgba(239, 68, 68, 0.32), + 0 0 30px rgba(239, 68, 68, 0.14); + } + 50% { + box-shadow: + 0 6px 22px rgba(111, 99, 217, 0.35), + 0 0 16px rgba(239, 68, 68, 0.25), + 0 0 26px rgba(239, 68, 68, 0.1); + } + 75% { + box-shadow: + 0 6px 22px rgba(111, 99, 217, 0.35), + 0 0 18px rgba(239, 68, 68, 0.28), + 0 0 28px rgba(239, 68, 68, 0.12); + } +} + +/* Memory Constellation Styles - Star-like nodes */ +.memory-node { + font-size: 12px; + color: var(--emma-text-on-accent); + backdrop-filter: blur(15px); + box-shadow: + 0 0 18px rgba(var(--emma-accent-primary-rgb), 0.35), + 0 0 32px rgba(var(--emma-accent-primary-rgb), 0.18), + inset 0 0 12px rgba(var(--emma-neutral-rgb), 0.12); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); + position: relative; +} + +/* Star-like glow effect */ +.memory-node::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: 50%; + background: linear-gradient(45deg, rgba(var(--emma-accent-primary-rgb), 0.3), rgba(var(--emma-accent-secondary-rgb), 0.3)); + z-index: -1; + opacity: 0.7; + animation: star-pulse 3s ease-in-out infinite; +} + +@keyframes star-pulse { + 0%, 100% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(1.1); opacity: 1; } +} + +.memory-node .memory-thumbnail { + font-size: 28px; + margin-bottom: 2px; + filter: drop-shadow(0 0 8px rgba(var(--emma-neutral-rgb), 0.5)); + text-shadow: 0 0 10px rgba(var(--emma-accent-primary-rgb), 0.8); +} + +.memory-node .memory-label { + font-size: 11px; + opacity: 0; + position: absolute; + top: 75px; + left: 50%; + transform: translateX(-50%); + background: rgba(var(--emma-neutral-inverse-rgb), 0.95); + padding: 6px 10px; + border-radius: 6px; + white-space: nowrap; + transition: all 0.3s ease; + pointer-events: none; + z-index: 1001; + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); + backdrop-filter: blur(10px); + font-weight: 500; +} + +.memory-node:hover .memory-label { + opacity: 1; + transform: translateX(-50%) translateY(-5px); +} + +/* Theme-based colors */ +.memory-node[data-theme="family"] { + box-shadow: + 0 0 30px rgba(255, 105, 135, 0.4), + 0 0 60px rgba(255, 105, 135, 0.2); +} + +.memory-node[data-theme="travel"] { + box-shadow: + 0 0 30px rgba(52, 211, 153, 0.4), + 0 0 60px rgba(52, 211, 153, 0.2); +} + +.memory-node[data-theme="recent"] { + box-shadow: + 0 0 30px rgba(96, 165, 250, 0.4), + 0 0 60px rgba(96, 165, 250, 0.2); +} + +.memory-node[data-theme="special"] { + box-shadow: + 0 0 30px rgba(251, 191, 36, 0.4), + 0 0 60px rgba(251, 191, 36, 0.2); +} + +.constellation-controls { + color: var(--emma-text-strong); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.constellation-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.constellation-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.constellation-close { + background: none; + border: none; + color: var(--emma-text-standard); + font-size: 18px; + cursor: pointer; + padding: 5px; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.3s ease; +} + +.constellation-close:hover { + background: rgba(var(--emma-neutral-rgb), 0.1); +} + +.constellation-themes { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.theme-filter { + background: var(--emma-accent-surface); + border: 1px solid var(--emma-accent-border); + color: var(--emma-text-on-accent); + padding: 8px 12px; + border-radius: 20px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s ease; +} + +.theme-filter.active { + background: var(--emma-accent-surface-strong); + border-color: var(--emma-accent-border); +} + +.theme-filter.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.theme-filter:hover:not(.disabled) { + background: var(--emma-accent-surface-strong); + transform: translateY(-2px); +} + +/* Memory Dialog Styles */ +.memory-dialog { + background: rgba(var(--emma-neutral-inverse-rgb), 0.95); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); + border-radius: 16px; + padding: 0; + max-width: 800px; + width: 90%; + max-height: 80vh; + overflow: hidden; + backdrop-filter: blur(20px); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + transform: scale(0.9); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.memory-dialog-overlay[style*="opacity: 1"] .memory-dialog { + transform: scale(1); +} + +.memory-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px 32px; + border-bottom: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.1), rgba(var(--emma-accent-secondary-rgb), 0.1)); +} + +.memory-title { + margin: 0; + font-size: 24px; + font-weight: 600; + color: var(--emma-text-on-accent); + text-shadow: 0 0 10px rgba(var(--emma-accent-primary-rgb), 0.5); +} + +.memory-close { + background: none; + border: none; + color: var(--emma-text-standard); + font-size: 24px; + cursor: pointer; + padding: 8px; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.3s ease; +} + +.memory-close:hover { + background: rgba(var(--emma-neutral-rgb), 0.1); +} + +.memory-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + padding: 32px; +} + +.memory-slideshow { + display: flex; + flex-direction: column; + gap: 16px; +} + +.slideshow-container { + aspect-ratio: 16/9; + border-radius: 12px; + overflow: hidden; + background: rgba(var(--emma-accent-primary-rgb), 0.1); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); +} + +.memory-slide { + width: 100%; + height: 100%; + display: none; + align-items: center; + justify-content: center; +} + +.memory-slide.active { + display: flex; +} + +.media-placeholder { + text-align: center; + color: var(--emma-text-standard); +} + +.media-icon { + font-size: 48px; + display: block; + margin-bottom: 16px; +} + +.slideshow-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; +} + +.slide-btn { + background: var(--emma-accent-surface); + border: 1px solid var(--emma-accent-border); + color: var(--emma-text-on-accent); + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + font-size: 18px; + transition: all 0.3s ease; +} + +.slide-btn:hover { + background: var(--emma-accent-surface-strong); + transform: scale(1.05); +} + +.slide-indicator { + color: var(--emma-text-standard); + font-size: 14px; + font-weight: 500; +} + +.memory-details { + display: flex; + flex-direction: column; + gap: 24px; +} + +.memory-summary h3 { + margin: 0 0 12px 0; + font-size: 18px; + font-weight: 600; + color: var(--emma-text-strong); +} + +.memory-summary p { + margin: 0; + line-height: 1.6; + color: var(--emma-text-standard); +} + +.memory-metadata { + display: flex; + flex-direction: column; + gap: 12px; +} + +.metadata-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid rgba(var(--emma-accent-primary-rgb), 0.1); +} + +.metadata-item .label { + font-weight: 500; + color: var(--emma-text-standard); +} + +.metadata-item .value { + font-weight: 600; + color: var(--emma-text-strong); +} + +.value.theme-family { color: rgba(255, 105, 135, 1); } +.value.theme-travel { color: rgba(52, 211, 153, 1); } +.value.theme-recent { color: rgba(96, 165, 250, 1); } +.value.theme-special { color: rgba(251, 191, 36, 1); } + +.memory-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 24px 32px; + border-top: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.05); +} + +.action-btn { + padding: 12px 24px; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + border: none; +} + +.action-btn.secondary { + background: var(--emma-accent-surface); + color: var(--emma-text-on-accent); + border: 1px solid var(--emma-accent-border); +} + +.action-btn.secondary:hover { + background: var(--emma-accent-surface-strong); + transform: translateY(-2px); +} + +.action-btn.primary { + background: var(--emma-gradient-primary); + color: var(--emma-text-on-accent); + border: none; +} + +.action-btn.primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(var(--emma-accent-primary-rgb), 0.4); +} + +/* Simplified fade-in timing for menu items */ +.radial-item:nth-child(1) { transition-delay: 0.1s; } +.radial-item:nth-child(2) { transition-delay: 0.15s; } +.radial-item:nth-child(3) { transition-delay: 0.2s; } +.radial-item:nth-child(4) { transition-delay: 0.25s; } + +.radial-item:hover { + background: rgba(var(--emma-accent-primary-rgb), 0.3); + border-color: var(--emma-purple); + transform: scale(1.05); + box-shadow: 0 0 40px rgba(var(--emma-accent-primary-rgb), 0.4); +} + +.radial-item.pulsing { + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + box-shadow: 0 0 30px rgba(var(--emma-accent-primary-rgb), 0.2); + transform: scale(1); + } + 50% { + box-shadow: 0 0 50px rgba(var(--emma-accent-primary-rgb), 0.5); + transform: scale(1.05); + } +} + +.radial-item-icon { + margin-bottom: 4px; + filter: drop-shadow(0 0 5px rgba(var(--emma-neutral-rgb), 0.3)); +} + +.radial-item-label { + font-size: 12px; + text-align: center; + opacity: 0.9; + font-weight: 500; +} + +.node-connection { + stroke: rgba(var(--emma-accent-primary-rgb), 0.3); + stroke-width: 2; + fill: none; + stroke-dasharray: 5, 5; + animation: dash 20s linear infinite; +} + +@keyframes dash { + to { stroke-dashoffset: -100; } +} + +/* Info Panels */ +.info-panel { + position: fixed; + background: var(--emma-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--emma-border); + border-radius: 20px; + padding: 24px; + min-width: 280px; + opacity: 0; + transform: translateY(20px); + transition: all 0.4s ease; + z-index: 10; +} + +.info-panel.active { + opacity: 1; + transform: translateY(0); +} + +/* Fade panels and header text when neural menu is active */ +body.menu-active .info-panel { + opacity: 0; + transform: translateY(20px) scale(0.9); + filter: blur(2px); + pointer-events: none; +} + +body.menu-active .welcome-text { + opacity: 0 !important; + transform: translateY(-20px) scale(0.9); + filter: blur(3px); + pointer-events: none; + visibility: hidden; +} + +body.menu-active .voice-indicator { + opacity: 0; + transform: translateX(-50%) translateY(20px) scale(0.9); + filter: blur(2px); + pointer-events: none; +} + +body.menu-active .constellation-canvas { + opacity: 0; + filter: blur(3px); + pointer-events: none; +} + +/* Constellation Mode - Hide ALL UI except constellation */ +body.constellation-active .info-panel, +body.constellation-active .welcome-text, +body.constellation-active .voice-indicator, +body.constellation-active .constellation-canvas, +body.constellation-active .quick-stats { + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + transition: all 0.5s ease; +} + +/* Keep only constellation controls and radial menu visible */ +body.constellation-active .radial-menu { + opacity: 1 !important; + visibility: visible !important; + pointer-events: none !important; +} + +body.constellation-active .radial-menu.active { + pointer-events: none !important; +} + +body.constellation-active .radial-menu.active .radial-item { + pointer-events: auto !important; +} + +body.constellation-active .constellation-controls { + opacity: 1 !important; + visibility: visible !important; +} + +/* Hide panels in constellation mode */ +body.constellation-active .info-panel.right-stack, +body.constellation-active .utility-icons { + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; +} + +/* Bottom Left Utility Icons */ +/* Clean Icons - No backgrounds */ +.clean-icons { + position: fixed; + bottom: 24px; + left: 24px; + display: flex; + gap: 16px; + z-index: 1000; +} + +.clean-icons > .emma-floating-button { + color: var(--emma-accent-primary, #6f63d9); + transition: color 180ms ease, transform 180ms ease; +} + +.clean-icons > .emma-floating-button:hover, +.clean-icons > .emma-floating-button:focus-visible { + color: var(--emma-text-strong, #ffffff); +} + +/* Hide old utility icons completely */ +.utility-icons { + display: none !important; +} + +.utility-item { + display: none !important; +} + +.utility-icon { + display: none !important; +} + +.utility-label { + display: none !important; +} + +/* Burger menu CSS removed for clean interface */ + +/* Right side panel stack - hidden by default */ +.info-panel.right-stack { + position: fixed; + right: -360px; /* Hidden off-screen */ + width: 320px; + min-width: 320px; + max-width: 320px; + height: 180px; + min-height: 180px; + max-height: 180px; + transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 999; +} + +/* Show panels when menu is active */ +.panels-visible .info-panel.right-stack { + right: 24px; +} + +#daily-brief.right-stack { + top: 24px; +} + +#ai-insights.right-stack { + top: calc(24px + 180px + 16px); +} + +#quick-actions.right-stack { + top: calc(24px + 180px + 16px + 180px + 16px); +} + +/* Legacy positioning for any remaining panels */ +.info-panel.top-left { + top: 24px; + left: 24px; +} + +.info-panel.top-right { + top: 24px; + right: 24px; +} + +.info-panel.bottom-left { + bottom: 24px; + left: 24px; +} + +.info-panel.bottom-right { + bottom: 24px; + right: 24px; +} + +.panel-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(var(--emma-neutral-rgb), 0.1); +} + +/* Adjust content for right stack panels */ +.info-panel.right-stack .panel-content { + overflow: hidden; + height: calc(100% - 70px); /* Account for header height */ +} + +.info-panel.right-stack .panel-content p { + font-size: 14px; + line-height: 1.4; + margin-bottom: 8px; +} + +.panel-icon { + font-size: 24px; +} + +.panel-title { + font-size: 18px; + font-weight: 600; +} + +.panel-content { + font-size: 14px; + line-height: 1.6; + color: var(--emma-text-secondary); +} + +/* Memory Constellation Background */ +.constellation-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + opacity: 0.3; + pointer-events: none; +} + +/* Voice Activation Indicator */ +.voice-indicator { + position: fixed; + bottom: 40px; + left: 50%; + transform: translateX(-50%); + background: var(--emma-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--emma-border); + border-radius: 30px; + padding: 12px 24px; + display: flex; + align-items: center; + gap: 12px; + opacity: 0; + transform: translateX(-50%) translateY(20px); + transition: all 0.3s ease; + z-index: 10; +} + +.voice-indicator.active { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* Minimal landing state - hide advanced controls until expanded */ +body.dashboard-minimal .quick-stats, +body.dashboard-minimal .info-panel, +body.dashboard-minimal .voice-indicator { + display: none !important; +} + +.voice-waves { + display: flex; + gap: 3px; + align-items: center; +} + +.voice-wave { + width: 3px; + height: 20px; + background: var(--emma-purple); + border-radius: 3px; + animation: wave 1s ease-in-out infinite; +} + +.voice-wave:nth-child(2) { animation-delay: 0.1s; height: 30px; } +.voice-wave:nth-child(3) { animation-delay: 0.2s; height: 25px; } +.voice-wave:nth-child(4) { animation-delay: 0.3s; height: 35px; } +.voice-wave:nth-child(5) { animation-delay: 0.4s; height: 20px; } + +@keyframes wave { + 0%, 100% { transform: scaleY(0.5); opacity: 0.5; } + 50% { transform: scaleY(1); opacity: 1; } +} + +/* Quick Stats */ +.quick-stats { + position: absolute; + bottom: -80px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 32px; + opacity: 0; + transition: opacity 0.4s ease; + pointer-events: none; +} + +.command-center:hover .quick-stats { + opacity: 1; +} + +.stat-item { + text-align: center; + cursor: pointer; + transition: transform 0.2s ease; +} + +.stat-item:hover { + transform: translateY(-4px); +} + +.stat-value { + font-size: 28px; + font-weight: 700; + background: var(--emma-gradient-1); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.stat-label { + font-size: 12px; + color: var(--emma-text-tertiary); + text-transform: uppercase; + letter-spacing: 1px; + margin-top: 4px; +} + +/* Notification Toast */ +.toast { + position: fixed; + top: 24px; + right: 24px; + background: var(--emma-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--emma-border); + border-radius: 12px; + padding: 16px 24px; + display: flex; + align-items: center; + gap: 12px; + opacity: 0; + transform: translateX(400px); + transition: all 0.4s ease; + z-index: 1000; +} + +.toast.show { + opacity: 1; + transform: translateX(0); +} + +.toast.success { border-color: var(--emma-success); } +.toast.error { border-color: var(--emma-error); } +.toast.info { border-color: var(--emma-blue); } + +/* Loading State */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.loading-overlay.active { + opacity: 1; + pointer-events: all; +} + +.loading-content { + text-align: center; +} + +.loading-orb { + width: 80px; + height: 80px; + margin: 0 auto 20px; + position: relative; + /* Emma's orb will be rendered here via WebGL */ +} + +/* Fallback for when WebGL orb isn't available */ +.loading-orb-fallback { + width: 100%; + height: 100%; + background: var(--emma-gradient-primary); + border-radius: 50%; + animation: loading-pulse 1.5s ease-in-out infinite; + box-shadow: var(--emma-glow-strong); +} + +@keyframes loading-pulse { + 0%, 100% { transform: scale(1); opacity: 0.8; } + 50% { transform: scale(1.2); opacity: 1; } +} + +/* Welcome Text */ +.welcome-text { + position: fixed; + top: 40px; + left: 50%; + transform: translateX(-50%); + text-align: center; + opacity: 0; + animation: fade-in 1s ease-out 0.5s forwards; +} + +.welcome-title { + font-size: 48px; + font-weight: 200; + margin-bottom: 8px; + background: var(--emma-gradient-1); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.welcome-subtitle { + font-size: 18px; + color: var(--emma-text-secondary); + font-weight: 400; +} + +@keyframes fade-in { + to { opacity: 1; } +} + +/* Vault Modal Styles */ +.vault-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + opacity: 0; + transition: opacity 0.3s ease; +} + +.vault-modal.show { + opacity: 1; +} + +.vault-modal-content { + background: linear-gradient(135deg, rgba(26, 16, 51, 0.98), rgba(45, 27, 105, 0.98)); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); + border-radius: 24px; + max-width: 600px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + transform: scale(0.9); + transition: transform 0.3s ease; +} + +.vault-modal.show .vault-modal-content { + transform: scale(1); +} + +.vault-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32px 32px 24px; + border-bottom: 1px solid rgba(var(--emma-neutral-rgb), 0.1); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.1), rgba(var(--emma-accent-secondary-rgb), 0.1)); +} + +.vault-header-content { + display: flex; + align-items: center; + gap: 20px; +} + +.vault-icon { + width: 64px; + height: 64px; + background: var(--emma-gradient-primary); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; +} + +.vault-title h2 { + margin: 0 0 4px 0; + font-size: 28px; + font-weight: 700; + color: var(--emma-text); +} + +.vault-title p { + margin: 0; + color: var(--emma-text-secondary); + font-size: 16px; +} + +.vault-close { + width: 48px; + height: 48px; + border: none; + background: rgba(var(--emma-neutral-rgb), 0.1); + color: var(--emma-text); + border-radius: 12px; + font-size: 24px; + cursor: pointer; + transition: all 0.2s ease; +} + +.vault-close:hover { + background: rgba(var(--emma-neutral-rgb), 0.2); +} + +.vault-modal-body { + padding: 32px; +} + +.vault-status-section { + margin-bottom: 32px; +} + +.vault-status-card { + display: flex; + align-items: center; + gap: 20px; + padding: 24px; + background: rgba(var(--emma-neutral-rgb), 0.05); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.1); + border-radius: 16px; + transition: all 0.3s ease; +} + +.vault-status-card.unlocked { + border-color: rgba(74, 222, 128, 0.3); + background: rgba(74, 222, 128, 0.1); +} + +.vault-btn { + padding: 12px 24px; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.vault-btn.primary { + background: var(--emma-gradient-primary); + color: var(--emma-text-on-accent); +} + +.vault-btn.primary:hover { + transform: translateY(-2px); + box-shadow: var(--emma-shadow-raised); +} + +.vault-btn.secondary { + background: var(--emma-button-secondary-bg); + color: var(--emma-button-secondary-text); + border: 1px solid var(--emma-button-secondary-border); +} + +.vault-btn.secondary:hover { + background: var(--emma-button-secondary-bg-hover); +} + +.vault-btn.small { + padding: 8px 16px; + font-size: 14px; +} + +.vault-unlock-section { + margin-bottom: 32px; + padding: 24px; + background: rgba(var(--emma-neutral-rgb), 0.03); + border-radius: 16px; + border: 1px solid rgba(var(--emma-neutral-rgb), 0.1); +} + +.unlock-form label { + display: block; + margin-bottom: 8px; + color: var(--emma-text); + font-weight: 600; +} + +.unlock-form input { + width: 100%; + padding: 16px 20px; + background: rgba(var(--emma-neutral-rgb), 0.1); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); + border-radius: 12px; + color: var(--emma-text); + font-size: 16px; + margin-bottom: 20px; + text-align: center; + letter-spacing: 2px; +} + +.unlock-form input:focus { + outline: none; + border-color: var(--emma-purple); + box-shadow: 0 0 0 3px rgba(var(--emma-accent-primary-rgb), 0.2); +} + +.unlock-actions { + display: flex; + gap: 12px; + justify-content: center; +} + +/* Create Vault Section */ +.vault-create-section { + margin-bottom: 32px; + padding: 24px; + background: rgba(16, 185, 129, 0.1); + border-radius: 16px; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.create-vault-card { + text-align: center; +} + +.create-vault-icon { + font-size: 48px; + margin-bottom: 16px; +} + +.create-vault-content h3 { + color: var(--emma-text); + margin-bottom: 8px; + font-size: 20px; +} + +.create-vault-content p { + color: var(--emma-text-secondary); + margin-bottom: 20px; + font-size: 16px; +} + +.vault-qr-section h3 { + margin: 0 0 8px 0; + color: var(--emma-text); + font-size: 20px; +} + +.vault-qr-section p { + margin: 0 0 20px 0; + color: var(--emma-text-secondary); +} + +.qr-option-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: rgba(var(--emma-neutral-rgb), 0.05); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.1); + border-radius: 12px; + transition: all 0.3s ease; + cursor: pointer; +} + +.qr-option-card:hover { + border-color: rgba(var(--emma-accent-primary-rgb), 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.1); +} + +.qr-option-icon { + font-size: 32px; + min-width: 32px; +} + +.qr-option-content { + flex: 1; +} + +.qr-option-content h4 { + margin: 0 0 4px 0; + color: var(--emma-text); + font-size: 16px; +} + +.qr-option-content p { + margin: 0; + color: var(--emma-text-secondary); + font-size: 14px; +} + +.vault-actions-section { + border-top: 1px solid rgba(var(--emma-neutral-rgb), 0.1); + padding-top: 24px; +} + +/* Responsive Design */ +/* Mobile-First Responsive Design */ +@media (max-width: 768px) { + .radial-menu { + width: 100vw; + height: 100vh; + } + + .radial-item { + /* ๐ŸŒŒ UNIFIED: Same size for all devices - no more mobile/desktop differences */ + width: 90px !important; + height: 90px !important; + font-size: 12px; + /* ๐ŸŽฏ TOUCH FRIENDLY: Large enough for all devices */ + min-width: 90px; + min-height: 90px; + /* ๐Ÿ”— BOUND TO ORB: Unified positioning */ + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .radial-item-icon { + font-size: 28px; + } + + .radial-item-label { + font-size: 11px; + margin-top: 3px; + font-weight: 500; + } + + /* ๐Ÿ“ฑ MOBILE OPTIMIZATION: Better visual feedback */ + .radial-item:active { + transform: scale(0.95) !important; + background: rgba(var(--emma-accent-primary-rgb), 0.3) !important; + } + + .emma-orb-container { + width: 120px !important; + height: 120px !important; + } + + .welcome-title { + font-size: 2.5rem; + } + + .welcome-subtitle { + font-size: 0.9rem; + } + + /* Mobile constellation filters */ + .constellation-burger-menu { + top: 10px !important; + left: 10px !important; + } + + .constellation-menu-panel { + width: 280px !important; + max-height: 70vh !important; + overflow-y: auto; + } + + .info-panel, + .info-panel.right-stack { + position: fixed; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + top: auto !important; + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; + height: auto !important; + min-height: auto !important; + max-height: 50vh !important; + border-radius: 20px 20px 0 0; + overflow-y: auto; + } + + /* Stack panels vertically on mobile */ + #daily-brief.right-stack { transform: translateY(0); } + #ai-insights.right-stack { transform: translateY(0); } + #quick-actions.right-stack { transform: translateY(0); } +} + +/* Tablet optimizations */ +@media (max-width: 1024px) and (min-width: 769px) { + .radial-item { + /* ๐ŸŒŒ UNIFIED: Same 90px size for tablets too */ + width: 90px; + height: 90px; + } + + .emma-orb-container { + width: 160px; + height: 160px; + } +} + +/* Import Voice Capture Premium Styles */ + +/* Person Summary Modal Styles */ +.person-summary-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.person-summary-modal-overlay.active { + opacity: 1; + visibility: visible; +} + +.person-summary-modal { + /* ๐ŸŽจ EMMA GLASSMORPHISM: Match chat interface aesthetic */ + background: rgba(26, 16, 51, 0.92); + backdrop-filter: blur(25px) saturate(150%); + -webkit-backdrop-filter: blur(25px) saturate(150%); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.12); + border-radius: 24px; + padding: 32px; + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + transform: scale(0.9) translateY(20px); + transition: transform 0.3s ease; + position: relative; + box-shadow: + 0 25px 80px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(var(--emma-neutral-rgb), 0.08), + inset 0 1px 0 rgba(var(--emma-neutral-rgb), 0.12); +} + +.person-summary-modal .modal-close { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(var(--emma-neutral-rgb), 0.1); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); + color: var(--emma-text-standard); + font-size: 18px; + font-weight: 300; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.person-summary-modal .modal-close:hover { + background: rgba(var(--emma-neutral-rgb), 0.2); + border-color: rgba(var(--emma-neutral-rgb), 0.4); + transform: scale(1.1); +} + +.person-summary-modal-overlay.active .person-summary-modal { + transform: scale(1) translateY(0); +} + +.person-summary-header { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 32px; + padding-bottom: 20px; + border-bottom: 1px solid rgba(var(--emma-neutral-rgb), 0.1); +} + +.person-summary-avatar { + width: 80px; + height: 80px; + border-radius: 50%; + background: var(--emma-gradient-secondary); + display: flex; + align-items: center; + justify-content: center; + color: var(--emma-text-on-accent); + font-weight: 700; + font-size: 32px; + overflow: hidden; +} + +.person-summary-info { + flex: 1; +} + +.person-summary-name { + font-size: 1.8rem; + font-weight: 600; + color: var(--emma-text-strong); + margin: 0 0 8px 0; +} + +.person-summary-relation { + font-size: 1rem; + color: var(--emma-text-standard); + margin: 0 0 4px 0; + text-transform: capitalize; +} + +.person-summary-contact { + font-size: 0.9rem; + color: var(--emma-text-muted); + margin: 0; +} + +.summary-section-title { + font-size: 1.2rem; + font-weight: 600; + color: var(--emma-text-strong); + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.summary-section-title::before { + content: ''; + width: 3px; + height: 20px; + background: var(--emma-gradient-secondary); + border-radius: 2px; +} + +.connected-memories { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; +} + +.connected-memory-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: rgba(var(--emma-neutral-rgb), 0.05); + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; +} + +.connected-memory-item:hover { + background: rgba(var(--emma-neutral-rgb), 0.1); + transform: translateY(-2px); +} + +.memory-preview { + width: 50px; + height: 50px; + border-radius: 8px; + background: rgba(var(--emma-neutral-rgb), 0.1); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + font-size: 20px; +} + +.memory-preview img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.memory-info { + flex: 1; +} + +.memory-title { + font-size: 1rem; + font-weight: 500; + color: var(--emma-text-strong); + margin-bottom: 4px; +} + +.memory-date { + font-size: 0.85rem; + color: var(--emma-text-muted); +} + +.no-memories { + text-align: center; + padding: 40px 20px; + color: var(--emma-text-muted); + font-style: italic; +} + +.person-summary-actions { + display: flex; + gap: 12px; +} + +.person-summary-actions .btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 16px; + border: none; + border-radius: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.person-summary-actions .btn-primary { + background: var(--emma-gradient-primary); + color: var(--emma-text-on-accent); +} + +.person-summary-actions .btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--emma-shadow-raised); +} + +.person-summary-actions .btn-secondary { + background: rgba(var(--emma-neutral-rgb), 0.1); + color: var(--emma-text-standard); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); +} + +.person-summary-actions .btn-secondary:hover { + background: rgba(var(--emma-neutral-rgb), 0.2); + color: var(--emma-text-strong); +} + + + + + + + + diff --git a/css/dashboard-secure.css b/css/dashboard-secure.css deleted file mode 100644 index c6e28164..00000000 --- a/css/dashboard-secure.css +++ /dev/null @@ -1,426 +0,0 @@ -/* app/css/dashboard-secure.css - Extracted styles for CSP compliance */ - -/* Emma Brand Variables */ -:root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); - --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); - --emma-aurora: linear-gradient(135deg, #667eea20 0%, #764ba240 25%, #f093fb20 50%, #4dabf720 75%, #667eea20 100%); - --emma-bg-gradient: linear-gradient(135deg, #0a0a0f 0%, #1a1033 50%, #0f0c29 100%); - --emma-purple: #764ba2; - --emma-pink: #f093fb; - --emma-blue: #667eea; - --emma-dark: #0a0a0f; - --emma-card-bg: rgba(255, 255, 255, 0.03); - --emma-glass: rgba(255, 255, 255, 0.05); - --emma-border: rgba(255, 255, 255, 0.1); - --emma-text: #ffffff; - --emma-text-secondary: rgba(255, 255, 255, 0.8); - --emma-text-tertiary: rgba(255, 255, 255, 0.6); - --emma-success: #4ade80; - --emma-error: #f87171; - --emma-warning: #fbbf24; - --emma-glow: 0 0 40px rgba(134, 88, 255, 0.6); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - background: var(--emma-bg-gradient); - color: var(--emma-text); - min-height: 100vh; - overflow: hidden; - position: relative; -} - -/* Aurora Background Effect */ -.aurora-bg { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.3; - z-index: 1; - pointer-events: none; -} - -.aurora { - position: absolute; - width: 200%; - height: 200%; - background: var(--emma-aurora); - animation: aurora 20s ease-in-out infinite; - filter: blur(60px); -} - -@keyframes aurora { - 0%, 100% { transform: translate(-50%, -50%) rotate(0deg) scale(1); } - 33% { transform: translate(-30%, -60%) rotate(120deg) scale(1.1); } - 66% { transform: translate(-70%, -40%) rotate(240deg) scale(0.9); } -} - -/* Main Container */ -.dashboard { - position: relative; - width: 100%; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - z-index: 2; -} - -/* Central Command Orb Container */ -.command-center { - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -/* Emma Orb */ -.emma-orb-container { - position: relative; - width: 200px; - height: 200px; - cursor: pointer; - transition: transform 0.3s ease; -} - -.emma-orb-container:hover { - transform: scale(1.05); -} - -.emma-orb-webgl { - width: 100%; - height: 100%; - border-radius: 50%; - box-shadow: var(--emma-glow), inset 0 0 40px rgba(255, 255, 255, 0.1); - position: relative; - overflow: hidden; -} - -.emma-orb-webgl canvas { - border-radius: 50%; - width: 100% !important; - height: 100% !important; -} - -@keyframes orb-pulse { - 0%, 100% { transform: scale(1); box-shadow: var(--emma-glow), inset 0 0 40px rgba(255, 255, 255, 0.1); } - 50% { transform: scale(1.02); box-shadow: 0 0 60px rgba(134, 88, 255, 0.8), inset 0 0 40px rgba(255, 255, 255, 0.2); } -} - -/* Neural Network Menu */ -.neural-network { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 8; -} - -.neural-canvas { - width: 100%; - height: 100%; -} - -/* Constellation Canvas */ -.constellation-canvas { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; -} - -/* Welcome Text */ -.welcome-text { - position: fixed; - top: 40px; - left: 50%; - transform: translateX(-50%); - text-align: center; - z-index: 3; -} - -.welcome-title { - font-size: 48px; - font-weight: 200; - background: var(--emma-gradient-1); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin-bottom: 8px; -} - -.welcome-subtitle { - font-size: 16px; - color: var(--emma-text-secondary); - font-weight: 300; -} - -/* Radial Menu Styles */ -.radial-menu { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 10; - opacity: 0; - transition: opacity 0.3s ease; -} - -.radial-menu.active { - opacity: 1; - pointer-events: all; -} - -.radial-item { - position: absolute; - width: 90px; - height: 90px; - background: var(--emma-glass); - border: 1px solid var(--emma-border); - border-radius: 50%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.3s ease; - backdrop-filter: blur(20px); - transform: scale(0); - opacity: 0; -} - -.radial-item:hover { - background: rgba(255, 255, 255, 0.1); - transform: scale(1.1) !important; - box-shadow: 0 0 40px rgba(134, 88, 255, 0.6); -} - -.radial-item-icon { - font-size: 24px; - margin-bottom: 4px; -} - -.radial-item-label { - font-size: 11px; - color: var(--emma-text-secondary); - text-align: center; - font-weight: 500; -} - -/* Menu Active State */ -body.menu-active .welcome-text, -body.menu-active .constellation-canvas { - opacity: 0.2; - transition: opacity 0.3s ease; -} - -/* Toast Notifications */ -.toast { - position: fixed; - bottom: 20px; - right: 20px; - background: var(--emma-glass); - color: var(--emma-text); - padding: 12px 20px; - border-radius: 8px; - border: 1px solid var(--emma-border); - backdrop-filter: blur(20px); - font-size: 14px; - font-weight: 500; - z-index: 1000; - transform: translateX(100%); - opacity: 0; - transition: all 0.3s ease; -} - -.toast.show { - transform: translateX(0); - opacity: 1; -} - -.toast.success { - border-color: var(--emma-success); - background: rgba(74, 222, 128, 0.1); -} - -.toast.error { - border-color: var(--emma-error); - background: rgba(248, 113, 113, 0.1); -} - -.toast.warning { - border-color: var(--emma-warning); - background: rgba(251, 191, 36, 0.1); -} - -/* Loading Overlay */ -.loading { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--emma-bg-gradient); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; -} - -.loading.active { - opacity: 1; - visibility: visible; -} - -/* Vault Modal Styles */ -.vault-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - display: none; - align-items: center; - justify-content: center; - z-index: 1000; - opacity: 0; - transition: opacity 0.3s ease; -} - -.vault-modal.show { - opacity: 1; -} - -.vault-modal-content { - background: var(--emma-glass); - border: 1px solid var(--emma-border); - border-radius: 16px; - padding: 32px; - max-width: 500px; - width: 90%; - backdrop-filter: blur(20px); - position: relative; -} - -.vault-modal-title { - font-size: 24px; - font-weight: 600; - margin-bottom: 16px; - text-align: center; - background: var(--emma-gradient-1); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.vault-status { - padding: 16px; - background: var(--emma-card-bg); - border-radius: 8px; - text-align: center; - margin-bottom: 20px; -} - -/* Form Styles */ -.unlock-section { - display: none; - margin-top: 20px; -} - -.form-group { - margin-bottom: 16px; -} - -.form-label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--emma-text-secondary); -} - -.form-input { - width: 100%; - padding: 12px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--emma-border); - border-radius: 8px; - color: var(--emma-text); - font-size: 16px; -} - -.form-input:focus { - outline: none; - border-color: var(--emma-purple); - box-shadow: 0 0 0 2px rgba(118, 75, 162, 0.2); -} - -/* Button Styles */ -.btn { - padding: 12px 24px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - background: var(--emma-glass); - color: var(--emma-text); - border: 1px solid var(--emma-border); -} - -.btn:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateY(-2px); -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-primary { - background: var(--emma-gradient-1); - border: none; - color: white; -} - -.btn-secondary { - background: var(--emma-card-bg); - border: 1px solid var(--emma-border); -} - -.btn-close { - position: absolute; - top: 16px; - right: 16px; - background: none; - border: none; - font-size: 24px; - color: var(--emma-text-secondary); - cursor: pointer; - padding: 4px; -} - -.btn-close:hover { - color: var(--emma-text); -} diff --git a/css/emma-chat-clean.css b/css/emma-chat-clean.css index 762db8c0..aaacb2c5 100644 --- a/css/emma-chat-clean.css +++ b/css/emma-chat-clean.css @@ -1,4 +1,4 @@ -/** +๏ปฟ/** * Emma Chat Experience - Clean Redesigned Interface * CTO-approved perfect responsive design for Debbe's demo * @@ -22,9 +22,9 @@ width: 40px; height: 40px; border: none; - background: rgba(255, 255, 255, 0.1); + background: var(--emma-chat-surface-strong); border-radius: 50%; - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-muted); cursor: pointer; transition: all 0.3s ease; display: flex; @@ -34,11 +34,52 @@ } .chat-close-btn:hover { - background: rgba(255, 255, 255, 0.2); - color: white; + background: rgba(var(--emma-neutral-rgb), 0.2); + color: var(--emma-chat-strong); transform: scale(1.1); } +/* ================================ + CHAT TOOLBAR / VOICE TOGGLE + ================================ */ + +.voice-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + border-radius: 999px; + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.18); + color: white; + font-size: 13px; + font-weight: 600; + padding: 6px 14px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 6px 20px rgba(var(--emma-accent-primary-rgb), 0.25); +} + +.voice-toggle:hover { + transform: translateY(-1px); + box-shadow: 0 10px 24px rgba(var(--emma-accent-primary-rgb), 0.35); +} + +.voice-toggle.is-muted { + background: rgba(var(--emma-neutral-rgb), 0.15); + border-color: rgba(var(--emma-neutral-rgb), 0.3); + color: rgba(255, 255, 255, 0.8); + box-shadow: none; +} + +.voice-toggle-icon { + font-size: 16px; + line-height: 1; +} + +.voice-toggle-label { + line-height: 1.2; +} + /* ================================ CHAT MESSAGES - Clean & Responsive ================================ */ @@ -59,17 +100,17 @@ } .emma-chat-messages::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); + background: var(--emma-chat-surface); border-radius: 3px; } .emma-chat-messages::-webkit-scrollbar-thumb { - background: rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.4); border-radius: 3px; } .emma-chat-messages::-webkit-scrollbar-thumb:hover { - background: rgba(139, 92, 246, 0.6); + background: rgba(var(--emma-accent-primary-rgb), 0.6); } /* Emma Orb Avatar - High-Definition Real Orb */ @@ -99,16 +140,16 @@ } .user-message .message-bubble { - background: linear-gradient(135deg, #8B5CF6, #F093FB); + background: var(--emma-chat-user-bubble); border-radius: 18px 18px 4px 18px; padding: 12px 16px; max-width: 70%; word-wrap: break-word; - box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3); + box-shadow: 0 4px 16px rgba(var(--emma-accent-primary-rgb), 0.3); } .user-message .message-text { - color: white; + color: var(--emma-chat-user-text); font-size: 15px; line-height: 1.4; margin: 0 0 4px 0; @@ -116,7 +157,7 @@ .user-message .message-time { font-size: 11px; - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-user-meta); font-weight: 500; } @@ -129,8 +170,8 @@ } .emma-message .message-content { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(139, 92, 246, 0.3); + background: var(--emma-chat-emma-bubble); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); border-radius: 4px 18px 18px 18px; padding: 12px 16px; max-width: 70%; @@ -139,7 +180,7 @@ } .emma-message .message-content p { - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-emma-text); font-size: 15px; line-height: 1.4; margin: 0 0 6px 0; @@ -147,7 +188,7 @@ .emma-message .message-content .message-time { font-size: 11px; - color: rgba(255, 255, 255, 0.5); + color: var(--emma-chat-emma-meta); font-weight: 500; display: block; } @@ -159,23 +200,24 @@ .emma-chat-input { flex-shrink: 0; padding: 20px 20px 24px 20px; /* PADDING: Equal left/right padding to match top/bottom */ - border-top: 2px solid rgba(139, 92, 246, 0.2); + border-top: 2px solid rgba(var(--emma-accent-primary-rgb), 0.2); } .input-wrapper { display: flex; - align-items: flex-end; + align-items: center; + flex-wrap: nowrap; gap: 12px; - background: rgba(255, 255, 255, 0.05); - border: 2px solid rgba(139, 92, 246, 0.3); + background: var(--emma-chat-surface); + border: 2px solid rgba(var(--emma-accent-primary-rgb), 0.3); border-radius: 16px; padding: 12px; transition: all 0.3s ease; } .input-wrapper:focus-within { - border-color: rgba(139, 92, 246, 0.6); - box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); + border-color: rgba(var(--emma-accent-primary-rgb), 0.6); + box-shadow: 0 0 20px rgba(var(--emma-accent-primary-rgb), 0.3); } .voice-btn, .settings-btn, .send-btn { @@ -189,12 +231,22 @@ align-items: center; justify-content: center; flex-shrink: 0; + appearance: none; +} + +.voice-btn svg, +.settings-btn svg, +.send-btn svg { + width: 20px; + height: 20px; + display: block; + stroke: currentColor; } .voice-btn { background: rgba(16, 185, 129, 0.2); border: 2px solid rgba(16, 185, 129, 0.4); - color: #10b981; + color: var(--emma-chat-success); } .voice-btn:hover { @@ -205,7 +257,7 @@ .settings-btn { background: rgba(156, 163, 175, 0.2); border: 2px solid rgba(156, 163, 175, 0.4); - color: #9ca3af; + color: var(--emma-chat-muted); } .settings-btn:hover { @@ -214,13 +266,13 @@ } .send-btn { - background: linear-gradient(135deg, #8B5CF6, #F093FB); - border: 2px solid rgba(139, 92, 246, 0.6); - color: white; + background: var(--emma-gradient-primary); + border: 2px solid rgba(var(--emma-accent-primary-rgb), 0.6); + color: var(--emma-chat-strong); } .send-btn:hover { - background: linear-gradient(135deg, #7C3AED, #E879F9); + background: var(--emma-gradient-secondary); transform: scale(1.1); } @@ -232,11 +284,12 @@ .chat-textarea { flex: 1; + min-width: 0; min-height: 40px; max-height: 120px; border: none; background: transparent; - color: white; + color: var(--emma-chat-strong); font-size: 15px; font-family: inherit; resize: none; @@ -246,7 +299,7 @@ } .chat-textarea::placeholder { - color: rgba(255, 255, 255, 0.5); + color: var(--emma-chat-muted); } .input-hints { @@ -254,7 +307,7 @@ margin-bottom: 8px; /* PADDING: Extra bottom margin to prevent cutoff */ text-align: center; font-size: 12px; - color: rgba(255, 255, 255, 0.5); + color: var(--emma-chat-muted); font-weight: 500; padding-bottom: 4px; /* PADDING: Additional bottom padding */ } @@ -268,7 +321,7 @@ align-items: center; gap: 12px; padding: 16px 0 0 0; - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-muted); font-size: 14px; font-weight: 500; } @@ -282,7 +335,7 @@ width: 8px; height: 8px; border-radius: 50%; - background: rgba(139, 92, 246, 0.6); + background: rgba(var(--emma-accent-primary-rgb), 0.6); animation: typing-bounce 1.4s ease-in-out infinite both; } @@ -394,7 +447,7 @@ .settings-btn:focus, .send-btn:focus, .chat-close-btn:focus { - outline: 3px solid rgba(139, 92, 246, 0.6); + outline: 3px solid rgba(var(--emma-accent-primary-rgb), 0.6); outline-offset: 2px; } @@ -405,12 +458,12 @@ /* High contrast mode support */ @media (prefers-contrast: high) { .input-wrapper { - border-color: rgba(139, 92, 246, 0.8); + border-color: rgba(var(--emma-accent-primary-rgb), 0.8); } .emma-chat-title { - color: white; - -webkit-text-fill-color: white; + color: var(--emma-chat-strong); + -webkit-text-fill-color: var(--emma-chat-strong); } } @@ -427,6 +480,19 @@ .voice-btn, .settings-btn, .send-btn, .chat-close-btn { transition: none; } + + .emma-settings-modal { + transition: none; + } + + .settings-content { + transition: none; + transform: none; + } + + .emma-settings-modal.show .settings-content { + transform: none; + } } /* ================================ @@ -439,104 +505,248 @@ left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(15px); + background: + radial-gradient(circle at 15% 12%, rgba(var(--emma-accent-primary-rgb), 0.2) 0%, transparent 55%), + radial-gradient(circle at 82% 18%, rgba(var(--emma-accent-secondary-rgb), 0.16) 0%, transparent 60%), + rgba(0, 0, 0, 0.82); + backdrop-filter: blur(18px) saturate(160%); z-index: 10001; display: flex; align-items: center; justify-content: center; opacity: 0; - transition: opacity 0.3s ease; + transition: opacity 280ms ease; + overflow: hidden; +} + +.emma-settings-modal::before { + content: ''; + position: absolute; + inset: -40% -20% 35% -20%; + background: + radial-gradient(circle at 20% 20%, rgba(var(--emma-accent-primary-rgb), 0.35), transparent 58%), + linear-gradient(135deg, rgba(255, 255, 255, 0.08), transparent); + opacity: 0; + filter: blur(70px); + transform: scale(0.8); + transition: opacity 420ms ease, transform 420ms ease; + pointer-events: none; } .emma-settings-modal.show { opacity: 1; } +.emma-settings-modal.show::before { + opacity: 1; + transform: scale(1); +} + .settings-content { - background: linear-gradient(145deg, rgba(139, 92, 246, 0.15), rgba(240, 147, 251, 0.10)); - border: 2px solid rgba(139, 92, 246, 0.3); - border-radius: 24px; - padding: 32px; - max-width: 500px; - width: 90%; + position: relative; + background: linear-gradient(175deg, rgba(28, 26, 48, 0.95) 0%, rgba(14, 12, 32, 0.98) 58%, rgba(10, 8, 26, 0.98) 100%); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.45); + border-radius: 32px; + padding: clamp(28px, 3vw, 40px); + max-width: 880px; + width: min(880px, 92vw); max-height: 80vh; overflow-y: auto; - backdrop-filter: blur(20px); - box-shadow: 0 24px 80px rgba(139, 92, 246, 0.4); - transform: scale(0.9); - transition: transform 0.3s ease; + backdrop-filter: blur(28px) saturate(140%); + box-shadow: + 0 46px 120px rgba(6, 4, 22, 0.72), + 0 0 0 1px rgba(255, 255, 255, 0.06); + transform: translateY(26px) scale(0.92) rotateX(6deg); + transition: + transform 420ms cubic-bezier(0.22, 1, 0.32, 1), + box-shadow 320ms ease; + isolation: isolate; + display: grid; + grid-template-columns: minmax(0, 260px) minmax(0, 1fr); + gap: clamp(24px, 3vw, 36px); +} + +.settings-content::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1.6px; + background: linear-gradient(140deg, rgba(var(--emma-accent-primary-rgb), 0.9) 0%, rgba(var(--emma-accent-secondary-rgb), 0.65) 45%, rgba(96, 199, 255, 0.4) 100%); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + opacity: 0.85; + pointer-events: none; +} + +.settings-content::after { + content: ''; + position: absolute; + inset: 12% -30% -45% -30%; + background: + radial-gradient(circle at 30% -10%, rgba(var(--emma-accent-primary-rgb), 0.45), transparent 65%), + radial-gradient(circle at 85% 0%, rgba(var(--emma-accent-secondary-rgb), 0.28), transparent 60%); + opacity: 0.55; + filter: blur(60px); + pointer-events: none; + z-index: 0; } .emma-settings-modal.show .settings-content { - transform: scale(1); + transform: translateY(0) scale(1) rotateX(0deg); + box-shadow: + 0 48px 110px rgba(6, 4, 22, 0.78), + 0 0 0 1px rgba(255, 255, 255, 0.08); } .settings-header { - text-align: center; - margin-bottom: 24px; - padding-bottom: 16px; - border-bottom: 2px solid rgba(139, 92, 246, 0.2); + grid-column: 1; + grid-row: 1 / span 10; + display: grid; + gap: 18px; + text-align: left; + padding-right: clamp(12px, 1vw, 18px); + border-right: 1px solid rgba(255, 255, 255, 0.12); + position: sticky; + top: clamp(28px, 3vw, 42px); + align-self: start; } .settings-title { - font-size: 24px; + font-size: clamp(2rem, 2.6vw, 2.4rem); font-weight: 700; margin: 0; - background: linear-gradient(135deg, #8B5CF6, #F093FB); + background: var(--emma-gradient-primary); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + letter-spacing: 0.04em; + text-transform: lowercase; } -.settings-section { - margin-bottom: 24px; - padding: 20px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(139, 92, 246, 0.2); +.settings-subtitle { + font-size: 0.95rem; + color: rgba(220, 225, 255, 0.78); + line-height: 1.7; + margin: 0; +} + +.settings-insight { + display: grid; + gap: 6px; + padding: 14px 18px; border-radius: 16px; + background: rgba(26, 30, 60, 0.7); + border: 1px solid rgba(255, 255, 255, 0.08); + color: rgba(214, 218, 255, 0.85); + font-size: 0.9rem; + line-height: 1.6; +} + +.settings-insight strong { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: rgba(197, 205, 255, 0.72); +} + +.settings-content { + counter-reset: settings-section; +} + +.settings-section { + grid-column: 2; + position: relative; + margin: 0; + padding: 28px 28px 26px; + border-radius: 24px; + background: linear-gradient(155deg, rgba(24, 26, 56, 0.78), rgba(18, 18, 40, 0.88)); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: + 0 22px 60px rgba(4, 4, 12, 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + display: grid; + gap: 16px; + overflow: hidden; + counter-increment: settings-section; +} + +.settings-section::before { + content: 'Section ' counter(settings-section, decimal-leading-zero); + position: absolute; + top: 22px; + left: 22px; + font-size: 0.72rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: rgba(198, 205, 250, 0.32); + z-index: 0; +} + +.settings-section::after { + content: ''; + position: absolute; + inset: -48% -10% 72% -45%; + background: radial-gradient(circle at 0% 50%, rgba(var(--emma-accent-primary-rgb), 0.22), transparent 58%); + opacity: 0.55; + pointer-events: none; + z-index: 0; } .settings-section h4 { - margin: 0 0 12px 0; - font-size: 16px; + margin: 28px 0 12px 0; + font-size: 1.1rem; font-weight: 600; - color: white; + color: var(--emma-text-strong); + letter-spacing: 0.02em; + position: relative; + z-index: 1; } .settings-description { margin: 0 0 16px 0; - font-size: 14px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.7); + font-size: 0.95rem; + line-height: 1.7; + color: rgba(214, 218, 255, 0.78); + position: relative; + z-index: 1; } .settings-input { width: 100%; - padding: 12px 16px; - border: 2px solid rgba(139, 92, 246, 0.3); - border-radius: 12px; - background: rgba(255, 255, 255, 0.05); - color: white; - font-size: 14px; - transition: all 0.3s ease; + padding: 14px 18px; + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); + border-radius: 14px; + background: rgba(20, 24, 58, 0.65); + color: var(--emma-chat-strong); + font-size: 0.95rem; + letter-spacing: 0.01em; + transition: border-color 0.3s ease, box-shadow 0.3s ease; } .settings-input:focus { outline: none; - border-color: rgba(139, 92, 246, 0.6); - box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); + border-color: rgba(var(--emma-accent-primary-rgb), 0.7); + box-shadow: + 0 0 0 2px rgba(var(--emma-accent-primary-rgb), 0.18), + 0 0 28px rgba(var(--emma-accent-primary-rgb), 0.25); } .settings-input::placeholder { - color: rgba(255, 255, 255, 0.5); + color: rgba(188, 194, 235, 0.55); } .input-help { display: block; margin-top: 8px; font-size: 12px; - color: rgba(255, 255, 255, 0.6); + color: var(--emma-chat-muted); } /* Toggle Switches */ @@ -548,93 +758,129 @@ .toggle-switch { position: relative; - width: 50px; - height: 26px; + display: inline-flex; + align-items: center; + width: 64px; + height: 32px; cursor: pointer; } .toggle-switch input { - display: none; + position: absolute; + inset: 0; + margin: 0; + opacity: 0; + cursor: pointer; } .toggle-slider { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.2); - border-radius: 26px; - transition: all 0.3s ease; + inset: 0; + display: block; + background: linear-gradient(125deg, rgba(24, 24, 52, 0.9), rgba(18, 20, 46, 0.78)); + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.08); + transition: all 0.35s cubic-bezier(0.3, 0.7, 0.4, 1.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); } -.toggle-slider:before { +.toggle-slider::before { content: ''; position: absolute; - height: 20px; - width: 20px; - left: 3px; - bottom: 3px; - background: white; + top: 3px; + left: 4px; + width: 26px; + height: 26px; border-radius: 50%; - transition: all 0.3s ease; + background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.95), rgba(220, 225, 255, 0.6)); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.6); + transition: all 0.35s cubic-bezier(0.3, 0.7, 0.4, 1.1); +} + +.toggle-slider::after { + content: ''; + position: absolute; + inset: 4px; + border-radius: 999px; + background: linear-gradient(120deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)); + pointer-events: none; + transition: opacity 0.35s ease; } .toggle-switch input:checked + .toggle-slider { - background: linear-gradient(135deg, #8B5CF6, #F093FB); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.7), rgba(var(--emma-accent-secondary-rgb), 0.55)); + border-color: rgba(var(--emma-accent-primary-rgb), 0.5); + box-shadow: + 0 0 22px rgba(var(--emma-accent-primary-rgb), 0.32), + inset 0 1px 0 rgba(255, 255, 255, 0.18); } -.toggle-switch input:checked + .toggle-slider:before { - transform: translateX(24px); +.toggle-switch input:checked + .toggle-slider::before { + transform: translateX(32px); + background: radial-gradient(circle at 30% 30%, #ffffff, rgba(255, 255, 255, 0.78)); + box-shadow: + 0 4px 18px rgba(var(--emma-accent-primary-rgb), 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.85); } .toggle-label { font-size: 14px; - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-strong); font-weight: 500; } /* Settings Buttons */ .settings-footer { + grid-column: 2; display: flex; - gap: 12px; + gap: 14px; justify-content: flex-end; - padding-top: 20px; - border-top: 2px solid rgba(139, 92, 246, 0.2); + padding-top: 22px; + border-top: 1px solid rgba(255, 255, 255, 0.12); } .settings-btn-secondary { padding: 12px 24px; - border: 2px solid rgba(255, 255, 255, 0.3); - border-radius: 12px; - background: rgba(255, 255, 255, 0.1); - color: white; - font-size: 14px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 14px; + background: rgba(24, 28, 64, 0.65); + color: rgba(224, 228, 255, 0.85); + font-size: 0.95rem; font-weight: 600; + letter-spacing: 0.02em; cursor: pointer; - transition: all 0.3s ease; + transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease; } .settings-btn-secondary:hover { - background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.28); + box-shadow: 0 12px 28px rgba(8, 8, 22, 0.45); transform: translateY(-2px); } .settings-btn-primary { - padding: 12px 24px; + padding: 12px 28px; border: none; - border-radius: 12px; - background: linear-gradient(135deg, #8B5CF6, #F093FB); - color: white; - font-size: 14px; - font-weight: 600; + border-radius: 16px; + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.92), rgba(var(--emma-accent-secondary-rgb), 0.68)); + color: #0f1024; + font-size: 0.96rem; + font-weight: 700; + letter-spacing: 0.03em; cursor: pointer; - transition: all 0.3s ease; + transition: transform 0.3s ease, box-shadow 0.3s ease; + box-shadow: + 0 18px 42px rgba(var(--emma-accent-primary-rgb), 0.38), + inset 0 1px 0 rgba(255, 255, 255, 0.65); } .settings-btn-primary:hover { - background: linear-gradient(135deg, #7C3AED, #E879F9); transform: translateY(-2px); + box-shadow: + 0 26px 60px rgba(var(--emma-accent-primary-rgb), 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.75); } /* ================================ @@ -676,6 +922,39 @@ } } +@media (max-width: 900px) { + .settings-content { + grid-template-columns: 1fr; + gap: 20px; + } + + .settings-header { + position: static; + border-right: 0; + padding-right: 0; + text-align: center; + grid-row: auto; + } + + .settings-title { + text-transform: none; + } + + .settings-subtitle { + font-size: 0.92rem; + } + + .settings-section { + grid-column: 1; + } + + .settings-footer { + grid-column: 1; + justify-content: center; + flex-wrap: wrap; + } +} + /* Large screens (1400px and up) */ @media (min-width: 1400px) { .emma-chat-header { @@ -706,8 +985,8 @@ align-items: center; gap: 12px; padding: 12px 16px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(139, 92, 246, 0.2); + background: var(--emma-chat-surface); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); border-radius: 12px; } @@ -719,7 +998,7 @@ #status-text { font-size: 14px; - color: rgba(255, 255, 255, 0.8); + color: var(--emma-chat-strong); font-weight: 500; } @@ -730,8 +1009,8 @@ .memory-detection-indicator { margin-top: 12px; padding: 12px 16px; - background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(236, 72, 153, 0.1)); - border: 1px solid rgba(139, 92, 246, 0.3); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.1), rgba(var(--emma-accent-secondary-rgb), 0.1)); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); border-radius: 12px; backdrop-filter: blur(10px); animation: memoryDetectionSlideIn 0.4s ease; @@ -747,30 +1026,30 @@ .pulse-dot { width: 8px; height: 8px; - background: #8B5CF6; + background: var(--emma-accent-primary); border-radius: 50%; animation: emmaPulse 2s infinite; } .detection-text { - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-strong); font-size: 13px; font-weight: 500; flex: 1; } .confidence { - color: rgba(139, 92, 246, 0.8); + color: rgba(var(--emma-accent-primary-rgb), 0.8); font-size: 12px; font-weight: 600; - background: rgba(139, 92, 246, 0.15); + background: rgba(var(--emma-accent-primary-rgb), 0.15); padding: 4px 8px; border-radius: 6px; } .save-memory-btn { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - color: white; + background: var(--emma-gradient-primary); + color: var(--emma-chat-strong); border: none; border-radius: 8px; padding: 8px 16px; @@ -778,18 +1057,18 @@ font-weight: 600; cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3); + box-shadow: 0 2px 8px rgba(var(--emma-accent-primary-rgb), 0.3); } .save-memory-btn:hover { transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); - background: linear-gradient(135deg, #9333EA, #F472B6); + box-shadow: 0 4px 12px rgba(var(--emma-accent-primary-rgb), 0.4); + background: var(--emma-gradient-secondary); } .save-memory-btn:active { transform: translateY(0); - box-shadow: 0 2px 6px rgba(139, 92, 246, 0.3); + box-shadow: 0 2px 6px rgba(var(--emma-accent-primary-rgb), 0.3); } @keyframes emmaPulse { @@ -817,9 +1096,9 @@ } .suggestion-action { - background: rgba(139, 92, 246, 0.2); - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + color: var(--emma-chat-strong); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); border-radius: 8px; padding: 6px 12px; font-size: 12px; @@ -829,18 +1108,18 @@ } .suggestion-action.primary { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - border: 1px solid rgba(139, 92, 246, 0.6); - color: white; + background: var(--emma-gradient-primary); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.6); + color: var(--emma-chat-strong); } .suggestion-action:hover { transform: translateY(-1px); - background: rgba(139, 92, 246, 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.3); } .suggestion-action.primary:hover { - background: linear-gradient(135deg, #9333EA, #F472B6); + background: var(--emma-gradient-secondary); } /* ================================ @@ -850,8 +1129,8 @@ .emma-file-upload, .emma-simple-upload { margin-top: 16px; padding: 20px; - background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(236, 72, 153, 0.08)); - border: 2px dashed rgba(139, 92, 246, 0.4); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.08), rgba(var(--emma-accent-secondary-rgb), 0.08)); + border: 2px dashed rgba(var(--emma-accent-primary-rgb), 0.4); border-radius: 16px; text-align: center; transition: all 0.3s ease; @@ -867,8 +1146,8 @@ } .emma-file-btn { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - color: white; + background: var(--emma-gradient-primary); + color: var(--emma-chat-strong); border: none; padding: 12px 24px; border-radius: 12px; @@ -876,18 +1155,18 @@ font-weight: 600; cursor: pointer; transition: all 0.3s ease; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); + box-shadow: 0 4px 12px rgba(var(--emma-accent-primary-rgb), 0.3); } .emma-file-btn:hover { transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); + box-shadow: 0 6px 20px rgba(var(--emma-accent-primary-rgb), 0.4); } .emma-skip-btn { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); + background: var(--emma-chat-surface-strong); + color: var(--emma-chat-strong); + border: 1px solid var(--emma-chat-border); padding: 8px 16px; border-radius: 8px; font-size: 14px; @@ -897,8 +1176,8 @@ } .emma-skip-btn:hover { - background: rgba(255, 255, 255, 0.15); - color: white; + background: rgba(var(--emma-neutral-rgb), 0.15); + color: var(--emma-chat-strong); transform: translateY(-1px); } @@ -908,7 +1187,7 @@ } .upload-success { - color: #10B981; + color: var(--emma-chat-success); font-weight: 600; padding: 8px 16px; background: rgba(16, 185, 129, 0.1); @@ -917,39 +1196,39 @@ } .emma-file-upload:hover { - border-color: rgba(139, 92, 246, 0.6); - background: linear-gradient(135deg, rgba(139, 92, 246, 0.12), rgba(236, 72, 153, 0.12)); + border-color: rgba(var(--emma-accent-primary-rgb), 0.6); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.12), rgba(var(--emma-accent-secondary-rgb), 0.12)); transform: translateY(-2px); } .emma-file-upload.dragover { - border-color: #8B5CF6; - background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2)); + border-color: var(--emma-accent-primary); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.2), rgba(var(--emma-accent-secondary-rgb), 0.2)); transform: scale(1.02); } .upload-icon { font-size: 32px; margin-bottom: 12px; - color: #8B5CF6; + color: var(--emma-accent-primary); display: block; } .upload-text { - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-strong); font-size: 16px; font-weight: 600; margin-bottom: 8px; } .upload-hint { - color: rgba(255, 255, 255, 0.6); + color: var(--emma-chat-muted); font-size: 13px; font-weight: 500; } .upload-formats { - color: rgba(139, 92, 246, 0.8); + color: rgba(var(--emma-accent-primary-rgb), 0.8); font-size: 12px; margin-top: 8px; font-weight: 500; @@ -974,8 +1253,8 @@ align-items: center; gap: 12px; padding: 12px; - background: rgba(139, 92, 246, 0.1); - border: 1px solid rgba(139, 92, 246, 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.1); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); border-radius: 10px; margin-bottom: 8px; } @@ -985,7 +1264,7 @@ height: 40px; border-radius: 8px; object-fit: cover; - background: rgba(139, 92, 246, 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.2); display: flex; align-items: center; justify-content: center; @@ -997,20 +1276,20 @@ } .file-preview-name { - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-strong); font-size: 14px; font-weight: 600; margin-bottom: 4px; } .file-preview-size { - color: rgba(255, 255, 255, 0.6); + color: var(--emma-chat-muted); font-size: 12px; } .file-remove-btn { background: rgba(244, 67, 54, 0.2); - color: #F44336; + color: var(--emma-chat-error); border: 1px solid rgba(244, 67, 54, 0.4); border-radius: 6px; padding: 6px 10px; @@ -1033,8 +1312,8 @@ } .upload-action-btn { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - color: white; + background: var(--emma-gradient-primary); + color: var(--emma-chat-strong); border: none; border-radius: 10px; padding: 10px 20px; @@ -1042,24 +1321,24 @@ font-weight: 600; cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 3px 10px rgba(139, 92, 246, 0.3); + box-shadow: 0 3px 10px rgba(var(--emma-accent-primary-rgb), 0.3); } .upload-action-btn:hover { transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(139, 92, 246, 0.4); - background: linear-gradient(135deg, #9333EA, #F472B6); + box-shadow: 0 5px 15px rgba(var(--emma-accent-primary-rgb), 0.4); + background: var(--emma-gradient-secondary); } .upload-action-btn.secondary { - background: rgba(139, 92, 246, 0.2); - border: 1px solid rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); box-shadow: none; } .upload-action-btn.secondary:hover { - background: rgba(139, 92, 246, 0.3); - box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.3); + box-shadow: 0 2px 8px rgba(var(--emma-accent-primary-rgb), 0.2); } /* ================================ @@ -1093,7 +1372,7 @@ .dialog-content { position: relative; background: linear-gradient(135deg, rgba(30, 30, 30, 0.95), rgba(20, 20, 20, 0.95)); - border: 1px solid rgba(139, 92, 246, 0.3); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); border-radius: 20px; padding: 0; max-width: 600px; @@ -1107,8 +1386,8 @@ } .dialog-header { - background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2)); - border-bottom: 1px solid rgba(139, 92, 246, 0.3); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.2), rgba(var(--emma-accent-secondary-rgb), 0.2)); + border-bottom: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); padding: 20px 24px; display: flex; align-items: center; @@ -1116,7 +1395,7 @@ } .dialog-header h3 { - color: white; + color: var(--emma-chat-strong); font-size: 20px; font-weight: 700; margin: 0; @@ -1124,10 +1403,10 @@ } .dialog-close { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); + background: var(--emma-chat-surface-strong); + border: 1px solid var(--emma-chat-border); border-radius: 8px; - color: white; + color: var(--emma-chat-strong); width: 32px; height: 32px; display: flex; @@ -1152,14 +1431,14 @@ } .memory-preview h4 { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 18px; font-weight: 600; margin: 0 0 16px 0; } .memory-preview p { - color: rgba(255, 255, 255, 0.8); + color: var(--emma-chat-strong); font-size: 16px; line-height: 1.6; margin: 0 0 20px 0; @@ -1173,18 +1452,18 @@ } .meta-item { - background: rgba(139, 92, 246, 0.15); - color: rgba(255, 255, 255, 0.9); + background: rgba(var(--emma-accent-primary-rgb), 0.15); + color: var(--emma-chat-strong); padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; - border: 1px solid rgba(139, 92, 246, 0.3); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); } .dialog-footer { - background: rgba(139, 92, 246, 0.1); - border-top: 1px solid rgba(139, 92, 246, 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.1); + border-top: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); padding: 20px 24px; display: flex; gap: 12px; @@ -1192,8 +1471,8 @@ } .button-primary { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - color: white; + background: var(--emma-gradient-primary); + color: var(--emma-chat-strong); border: none; border-radius: 10px; padding: 12px 24px; @@ -1201,19 +1480,19 @@ font-weight: 600; cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 3px 10px rgba(139, 92, 246, 0.3); + box-shadow: 0 3px 10px rgba(var(--emma-accent-primary-rgb), 0.3); } .button-primary:hover { transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(139, 92, 246, 0.4); - background: linear-gradient(135deg, #9333EA, #F472B6); + box-shadow: 0 5px 15px rgba(var(--emma-accent-primary-rgb), 0.4); + background: var(--emma-gradient-secondary); } .button-secondary { - background: rgba(139, 92, 246, 0.2); - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + color: var(--emma-chat-strong); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); border-radius: 10px; padding: 12px 20px; font-size: 14px; @@ -1223,7 +1502,7 @@ } .button-secondary:hover { - background: rgba(139, 92, 246, 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.3); transform: translateY(-1px); } @@ -1240,8 +1519,8 @@ ================================ */ .memory-capsule-preview { - background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(236, 72, 153, 0.1)); - border: 1px solid rgba(139, 92, 246, 0.3); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.1), rgba(var(--emma-accent-secondary-rgb), 0.1)); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.3); border-radius: 16px; padding: 20px; margin: 12px 0; @@ -1255,23 +1534,23 @@ gap: 12px; margin-bottom: 16px; padding-bottom: 12px; - border-bottom: 1px solid rgba(139, 92, 246, 0.2); + border-bottom: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); } .capsule-icon { font-size: 24px; - filter: drop-shadow(0 2px 4px rgba(139, 92, 246, 0.3)); + filter: drop-shadow(0 2px 4px rgba(var(--emma-accent-primary-rgb), 0.3)); } .capsule-title { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 18px; font-weight: 700; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .memory-title { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 20px; font-weight: 600; margin: 0 0 12px 0; @@ -1279,7 +1558,7 @@ } .memory-story { - color: rgba(255, 255, 255, 0.85); + color: var(--emma-chat-strong); font-size: 16px; line-height: 1.6; margin: 0 0 16px 0; @@ -1294,14 +1573,14 @@ } .detail-label { - color: rgba(139, 92, 246, 0.9); + color: rgba(111, 99, 217, 0.9); font-size: 14px; font-weight: 600; min-width: 80px; } .detail-value { - color: rgba(255, 255, 255, 0.8); + color: var(--emma-chat-strong); font-size: 14px; font-weight: 500; } @@ -1319,15 +1598,15 @@ aspect-ratio: 1; border-radius: 10px; overflow: hidden; - background: rgba(139, 92, 246, 0.1); - border: 1px solid rgba(139, 92, 246, 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.1); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); position: relative; transition: all 0.2s ease; } .media-thumbnail:hover { transform: scale(1.05); - border-color: rgba(139, 92, 246, 0.4); + border-color: rgba(var(--emma-accent-primary-rgb), 0.4); } .media-thumbnail img { @@ -1348,7 +1627,7 @@ left: 50%; transform: translate(-50%, -50%); font-size: 20px; - color: white; + color: var(--emma-chat-strong); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.7); } @@ -1368,7 +1647,7 @@ .file-name { font-size: 10px; - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-muted); text-align: center; overflow: hidden; text-overflow: ellipsis; @@ -1381,19 +1660,19 @@ flex-direction: column; align-items: center; justify-content: center; - background: rgba(139, 92, 246, 0.2); - border: 1px dashed rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + border: 1px dashed rgba(var(--emma-accent-primary-rgb), 0.4); } .more-count { font-size: 18px; font-weight: 700; - color: rgba(139, 92, 246, 0.9); + color: rgba(111, 99, 217, 0.9); } .more-text { font-size: 11px; - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-muted); font-weight: 500; } @@ -1403,7 +1682,7 @@ gap: 12px; margin-top: 20px; padding-top: 16px; - border-top: 1px solid rgba(139, 92, 246, 0.2); + border-top: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); } .capsule-btn { @@ -1418,25 +1697,25 @@ } .capsule-btn.primary { - background: linear-gradient(135deg, #8B5CF6, #EC4899); - color: white; - box-shadow: 0 3px 10px rgba(139, 92, 246, 0.3); + background: var(--emma-gradient-primary); + color: var(--emma-chat-strong); + box-shadow: 0 3px 10px rgba(var(--emma-accent-primary-rgb), 0.3); } .capsule-btn.primary:hover { transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(139, 92, 246, 0.4); - background: linear-gradient(135deg, #9333EA, #F472B6); + box-shadow: 0 5px 15px rgba(var(--emma-accent-primary-rgb), 0.4); + background: var(--emma-gradient-secondary); } .capsule-btn.secondary { - background: rgba(139, 92, 246, 0.2); - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + color: var(--emma-chat-strong); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); } .capsule-btn.secondary:hover { - background: rgba(139, 92, 246, 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.3); transform: translateY(-1px); } @@ -1460,7 +1739,7 @@ } .response-text { - color: rgba(255, 255, 255, 0.9); + color: var(--emma-chat-strong); font-size: 16px; line-height: 1.6; margin: 0 0 16px 0; @@ -1468,8 +1747,8 @@ /* Memory Results Grid */ .emma-memory-results { - background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(236, 72, 153, 0.08)); - border: 1px solid rgba(139, 92, 246, 0.25); + background: linear-gradient(135deg, rgba(var(--emma-accent-primary-rgb), 0.08), rgba(var(--emma-accent-secondary-rgb), 0.08)); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.25); border-radius: 12px; padding: 16px; margin: 12px 0; @@ -1481,7 +1760,7 @@ gap: 8px; margin-bottom: 12px; padding-bottom: 8px; - border-bottom: 1px solid rgba(139, 92, 246, 0.2); + border-bottom: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); } .results-icon { @@ -1489,7 +1768,7 @@ } .results-title { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 14px; font-weight: 600; } @@ -1501,8 +1780,8 @@ } .memory-card { - background: rgba(139, 92, 246, 0.1); - border: 1px solid rgba(139, 92, 246, 0.2); + background: rgba(var(--emma-accent-primary-rgb), 0.1); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.2); border-radius: 10px; padding: 12px; cursor: pointer; @@ -1511,8 +1790,8 @@ .memory-card:hover { transform: translateY(-2px); - border-color: rgba(139, 92, 246, 0.4); - background: rgba(139, 92, 246, 0.15); + border-color: rgba(var(--emma-accent-primary-rgb), 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.15); } .memory-thumbnail { @@ -1522,7 +1801,7 @@ overflow: hidden; margin-bottom: 8px; position: relative; - background: rgba(139, 92, 246, 0.1); + background: rgba(var(--emma-accent-primary-rgb), 0.1); } .memory-thumbnail img { @@ -1539,7 +1818,7 @@ .memory-icon { font-size: 24px; - color: rgba(139, 92, 246, 0.6); + color: rgba(var(--emma-accent-primary-rgb), 0.6); } .media-count { @@ -1547,7 +1826,7 @@ top: 4px; right: 4px; background: rgba(0, 0, 0, 0.7); - color: white; + color: var(--emma-chat-strong); font-size: 10px; padding: 2px 6px; border-radius: 4px; @@ -1559,7 +1838,7 @@ } .memory-title { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 13px; font-weight: 600; margin-bottom: 4px; @@ -1567,14 +1846,14 @@ } .memory-snippet { - color: rgba(255, 255, 255, 0.7); + color: var(--emma-chat-muted); font-size: 12px; line-height: 1.4; margin-bottom: 6px; } .memory-people { - color: rgba(139, 92, 246, 0.8); + color: rgba(var(--emma-accent-primary-rgb), 0.8); font-size: 11px; font-weight: 500; } @@ -1585,9 +1864,9 @@ } .show-more-btn { - background: rgba(139, 92, 246, 0.2); - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(139, 92, 246, 0.4); + background: rgba(var(--emma-accent-primary-rgb), 0.2); + color: var(--emma-chat-strong); + border: 1px solid rgba(var(--emma-accent-primary-rgb), 0.4); border-radius: 8px; padding: 8px 16px; font-size: 12px; @@ -1597,14 +1876,14 @@ } .show-more-btn:hover { - background: rgba(139, 92, 246, 0.3); + background: rgba(var(--emma-accent-primary-rgb), 0.3); transform: translateY(-1px); } /* Emma Suggestions */ .emma-suggestions { - background: linear-gradient(135deg, rgba(236, 72, 153, 0.08), rgba(139, 92, 246, 0.08)); - border: 1px solid rgba(236, 72, 153, 0.25); + background: linear-gradient(135deg, rgba(var(--emma-accent-secondary-rgb), 0.08), rgba(var(--emma-accent-primary-rgb), 0.08)); + border: 1px solid rgba(var(--emma-accent-secondary-rgb), 0.25); border-radius: 12px; padding: 16px; margin: 12px 0; @@ -1622,7 +1901,7 @@ } .suggestions-title { - color: rgba(255, 255, 255, 0.95); + color: var(--emma-chat-strong); font-size: 14px; font-weight: 600; } @@ -1634,9 +1913,9 @@ } .emma-suggestion-btn { - background: rgba(236, 72, 153, 0.2); - color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(236, 72, 153, 0.4); + background: rgba(var(--emma-accent-secondary-rgb), 0.2); + color: var(--emma-chat-strong); + border: 1px solid rgba(var(--emma-accent-secondary-rgb), 0.4); border-radius: 8px; padding: 8px 12px; font-size: 12px; @@ -1648,7 +1927,341 @@ } .emma-suggestion-btn:hover { - background: rgba(236, 72, 153, 0.3); + background: rgba(var(--emma-accent-secondary-rgb), 0.3); transform: translateY(-1px); - border-color: rgba(236, 72, 153, 0.6); + border-color: rgba(var(--emma-accent-secondary-rgb), 0.6); +} + + + + + + + + + + + + + +.settings-section > * { + position: relative; + z-index: 1; +} + +:root { + --memory-modal-background: var(--emma-bg-gradient); +} + +:root[data-background="aurora"] { + --memory-modal-background: var(--emma-background-aurora); +} + +:root[data-background="sunrise"] { + --memory-modal-background: var(--emma-background-sunrise); +} + +:root[data-background="forest"] { + --memory-modal-background: var(--emma-background-forest); +} + +:root[data-background="ocean"] { + --memory-modal-background: var(--emma-background-ocean); +} + +:root[data-background="ember"] { + --memory-modal-background: var(--emma-background-ember); +} + +:root[data-background="blossom"] { + --memory-modal-background: var(--emma-background-blossom); +} + +:root[data-background="zen"] { + --memory-modal-background: var(--emma-background-zen); +} + +/* Edit Memory Modal */ +.memory-edit-modal { + isolation: isolate; +} + +.memory-edit-modal .edit-modal-content { + position: relative; + border-radius: 28px; + padding: clamp(28px, 5vw, 40px); + width: min(640px, 92vw); + max-height: min(820px, 85vh); + overflow-y: auto; + color: var(--emma-text-strong, #ffffff); + border: 1px solid rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.22); + background: linear-gradient(155deg, rgba(8, 10, 26, 0.78), rgba(5, 6, 18, 0.94)), var(--memory-modal-background); + background-blend-mode: overlay; + box-shadow: + 0 32px 90px rgba(3, 2, 12, 0.65), + 0 0 0 1px rgba(255, 255, 255, 0.04); + backdrop-filter: blur(28px); + scroll-behavior: smooth; +} + +.memory-edit-modal .edit-modal-content::before { + content: ''; + position: absolute; + inset: 16px; + border-radius: 24px; + background: + radial-gradient(circle at 18% 15%, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.25), transparent 55%), + radial-gradient(circle at 75% 12%, rgba(var(--emma-accent-secondary-rgb, 208, 111, 168), 0.2), transparent 60%); + filter: blur(55px); + opacity: 0.7; + pointer-events: none; +} + +.memory-edit-modal .edit-modal-content > * { + position: relative; + z-index: 1; +} + +.memory-edit-modal .edit-modal-content::-webkit-scrollbar { + width: 6px; +} + +.memory-edit-modal .edit-modal-content::-webkit-scrollbar-thumb { + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.35); + border-radius: 999px; +} + +.memory-edit-modal .edit-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.memory-edit-modal .edit-modal-header h2 { + margin: 0; + font-size: 24px; + font-weight: 600; + color: var(--emma-text-strong, #ffffff); +} + +.memory-edit-modal .close-edit-btn { + width: 40px; + height: 40px; + border-radius: 999px; + border: 1px solid var(--edit-modal-border); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.12); + color: var(--emma-text-strong, #ffffff); + cursor: pointer; + font-size: 22px; + transition: transform 0.2s ease, background 0.2s ease; +} + +.memory-edit-modal .close-edit-btn:hover { + transform: scale(1.05); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.2); +} + +.memory-edit-modal .edit-modal-field, +.memory-edit-modal .edit-modal-section { + margin-bottom: 22px; +} + +.memory-edit-modal .edit-modal-label { + display: block; + margin-bottom: 10px; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--emma-text-standard, rgba(255, 255, 255, 0.85)); + text-transform: uppercase; + opacity: 0.85; +} + +.memory-edit-modal .edit-title-input, +.memory-edit-modal .edit-content-textarea { + width: 100%; + border-radius: 14px; + border: 1px solid rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.28); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.08); + color: var(--emma-text-strong, #ffffff); + font-size: 15px; + padding: 14px 16px; + box-sizing: border-box; + transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.memory-edit-modal .edit-content-textarea { + min-height: 140px; + resize: vertical; + line-height: 1.4; +} + +.memory-edit-modal .edit-title-input:focus, +.memory-edit-modal .edit-content-textarea:focus { + outline: none; + border-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.85); + box-shadow: 0 0 0 3px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.25); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.15); +} + +.memory-edit-modal .people-picker-container { + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.06); + border: 1px solid rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.12); + border-radius: 22px; + padding: 20px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.memory-edit-modal .people-picker-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 16px; + margin-bottom: 12px; +} + +.memory-edit-modal .people-picker-loading { + text-align: center; + font-size: 13px; + color: var(--emma-text-muted, rgba(255, 255, 255, 0.65)); +} + +.memory-edit-modal .edit-attachments-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: 14px; +} + +.memory-edit-modal .attachment-card { + position: relative; + aspect-ratio: 1; + border-radius: 16px; + overflow: hidden; + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.08); + border: 1px dashed rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.25); + display: flex; + align-items: center; + justify-content: center; +} + +.memory-edit-modal .attachment-card img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.memory-edit-modal .attachment-fallback { + font-size: 28px; + color: var(--emma-text-muted, rgba(255, 255, 255, 0.7)); +} + +.memory-edit-modal .remove-attachment-btn { + position: absolute; + top: 6px; + right: 6px; + width: 24px; + height: 24px; + border-radius: 999px; + border: none; + background: var(--emma-danger, #f87171); + color: #fff; + font-size: 14px; + cursor: pointer; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); + transition: transform 0.2s ease; +} + +.memory-edit-modal .remove-attachment-btn:hover { + transform: scale(1.05); +} + +.memory-edit-modal .add-media-btn { + aspect-ratio: 1; + border-radius: 16px; + border: 2px dashed rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.6); + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.15); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease; + color: var(--emma-text-strong, #ffffff); + font-weight: 600; +} + +.memory-edit-modal .add-media-btn:hover, +.memory-edit-modal .add-media-btn:focus-visible { + outline: none; + transform: translateY(-2px); + border-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.9); + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.25); +} + +.memory-edit-modal .add-media-icon { + font-size: 28px; +} + +.memory-edit-modal .add-media-label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.memory-edit-modal .edit-modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; + margin-top: 32px; +} + +.memory-edit-modal .edit-modal-actions-right { + display: inline-flex; + gap: 12px; + flex-wrap: wrap; +} + +.memory-edit-modal .delete-memory-btn, +.memory-edit-modal .cancel-edit-btn, +.memory-edit-modal .save-edit-btn { + border-radius: 999px; + padding: 12px 26px; + font-weight: 600; + border: 1px solid transparent; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.memory-edit-modal .delete-memory-btn { + border-color: rgba(248, 113, 113, 0.6); + background: rgba(248, 113, 113, 0.18); + color: var(--emma-danger, #f87171); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2); +} + +.memory-edit-modal .cancel-edit-btn { + border-color: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.25); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.12); + color: var(--emma-text-strong, #ffffff); +} + +.memory-edit-modal .save-edit-btn { + background: linear-gradient(120deg, + var(--emma-accent-primary, #6f63d9), + var(--emma-accent-secondary, #d06fa8)); + color: var(--emma-text-on-accent, #ffffff); + box-shadow: 0 18px 40px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.4); +} + +.memory-edit-modal .delete-memory-btn:hover, +.memory-edit-modal .cancel-edit-btn:hover, +.memory-edit-modal .save-edit-btn:hover { + transform: translateY(-1px); +} + +.memory-edit-modal .save-edit-btn:active { + transform: translateY(0); } diff --git a/css/emma-chat.css b/css/emma-chat.css deleted file mode 100644 index aa0bd15f..00000000 --- a/css/emma-chat.css +++ /dev/null @@ -1,1265 +0,0 @@ -/** - * Emma Chat Experience - Premium Clean Interface - * CTO-approved design matching voice capture aesthetic - */ - -/* ================================ - CHAT FOUNDATION - ================================ */ - -.emma-chat-studio { - position: relative; - padding: 20px 20px 20px 20px; /* PADDING: Equal padding on all sides for balanced spacing */ - margin: 0; - background: rgba(255, 255, 255, 0.02); - backdrop-filter: blur(20px); - width: 100%; - height: 100%; - min-height: 600px; - max-height: 80vh; - overflow: hidden; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - color: var(--emma-text, #ffffff); - display: flex; - flex-direction: column; - gap: 12px; -} - -/* ================================ - EMMA ANCHOR (matching voice capture) - ================================ */ - -.emma-anchor { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - text-align: center; - padding: 0 8px 0 8px; /* SPACING: Add subtle left/right padding for Emma orb breathing room */ - flex-shrink: 0; -} - -.webgl-orb-container { - position: relative; - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Fallback styling for when WebGL orb isn't available */ -.webgl-orb-container:empty { - background: radial-gradient( - circle at 30% 30%, - rgba(138, 94, 250, 0.95) 0%, - rgba(118, 75, 162, 0.9) 40%, - rgba(240, 147, 251, 0.95) 100% - ); - box-shadow: - 0 0 25px rgba(118, 75, 162, 0.4), - 0 0 50px rgba(240, 147, 251, 0.2); -} - -.emma-hint { - font-size: 14px; - color: rgba(255, 255, 255, 0.9); - margin: 0; - transition: all 0.4s ease; - line-height: 1.4; - max-width: 280px; - font-weight: 600; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); -} - -/* ================================ - CHAT MESSAGES AREA - ================================ */ - -.chat-messages-area { - flex: 1 1 auto; - overflow-y: auto; - overflow-x: hidden; - padding: 0 4px; - margin: 0 -4px; - scroll-behavior: smooth; - min-height: 0; /* Critical for flex shrinking */ -} - -.chat-messages-area::-webkit-scrollbar { - width: 6px; -} - -.chat-messages-area::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 3px; -} - -.chat-messages-area::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; -} - -.chat-messages-area::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); -} - -.messages-container { - display: flex; - flex-direction: column; - gap: 16px; - padding: 8px 0; - min-height: 100%; -} - -/* ================================ - MESSAGE BUBBLES - ================================ */ - -.message { - display: flex; - gap: 12px; - max-width: 85%; - animation: messageAppear 0.3s ease; -} - -.user-message { - align-self: flex-end; - flex-direction: row-reverse; -} - -.emma-message { - align-self: flex-start; -} - -@keyframes messageAppear { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.message-avatar { - width: 32px; - height: 32px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; -} - -.emma-avatar-mini { - width: 32px; - height: 32px; - border-radius: 50%; - background: radial-gradient( - circle at 30% 30%, - rgba(138, 94, 250, 0.9) 0%, - rgba(118, 75, 162, 0.8) 40%, - rgba(240, 147, 251, 0.9) 100% - ); - box-shadow: 0 2px 8px rgba(118, 75, 162, 0.3); -} - -.message-content { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; -} - -.message-text { - padding: 12px 16px; - border-radius: 18px; - font-size: 14px; - line-height: 1.4; - word-wrap: break-word; - position: relative; -} - -.user-message .message-text { - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.8) 0%, - rgba(240, 147, 251, 0.8) 100% - ); - color: white; - border-bottom-right-radius: 6px; -} - -.emma-message .message-text { - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.04) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.95); - border-bottom-left-radius: 6px; -} - -.message-time { - font-size: 11px; - color: rgba(255, 255, 255, 0.5); - padding: 0 4px; - align-self: flex-end; -} - -.user-message .message-time { - align-self: flex-start; -} - -/* ================================ - TYPING INDICATOR - ================================ */ - -.typing-indicator { - display: flex; - align-items: center; - gap: 12px; - padding: 8px 16px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 18px; - max-width: 200px; - margin-bottom: 8px; -} - -.typing-dots { - display: flex; - gap: 4px; -} - -.typing-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: rgba(240, 147, 251, 0.8); - animation: typingPulse 1.4s ease-in-out infinite; -} - -.typing-dot:nth-child(2) { - animation-delay: 0.2s; -} - -.typing-dot:nth-child(3) { - animation-delay: 0.4s; -} - -@keyframes typingPulse { - 0%, 60%, 100% { - opacity: 0.3; - transform: scale(0.8); - } - 30% { - opacity: 1; - transform: scale(1); - } -} - -.typing-text { - font-size: 12px; - color: rgba(255, 255, 255, 0.6); - font-style: italic; -} - -/* ================================ - CHAT INPUT AREA - ================================ */ - -.chat-input-area { - flex: 0 0 auto; /* Never grow, never shrink, auto size */ - display: flex; - flex-direction: column; - gap: 8px; - padding: 8px 0; - min-height: 80px; /* Ensure minimum space */ - border-top: 1px solid rgba(255, 255, 255, 0.08); -} - -.input-container { - display: flex; - gap: 8px; - align-items: flex-end; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 24px; - padding: 8px 12px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - min-height: 44px; - max-height: 120px; -} - -.voice-input-button { - width: 44px; - height: 44px; - border-radius: 12px; - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.06) 0%, - rgba(255, 255, 255, 0.02) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.12); - color: rgba(255, 255, 255, 0.7); - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.voice-input-button:hover { - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.04) 100% - ); - border-color: rgba(240, 147, 251, 0.3); - color: rgba(255, 255, 255, 0.9); - transform: translateY(-1px); -} - -.voice-input-button.listening { - background: linear-gradient( - 135deg, - rgba(240, 67, 54, 0.8) 0%, - rgba(244, 67, 54, 0.6) 100% - ); - border-color: rgba(244, 67, 54, 0.4); - color: white; - animation: voicePulse 1.5s ease-in-out infinite; -} - -.voice-input-button:disabled { - opacity: 0.4; - cursor: not-allowed; - transform: none; -} - -@keyframes voicePulse { - 0%, 100% { - box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); - } - 50% { - box-shadow: 0 0 0 8px rgba(244, 67, 54, 0); - } -} - -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes slideOutRight { - from { - transform: translateX(0); - opacity: 1; - } - to { - transform: translateX(100%); - opacity: 0; - } -} - -.chat-input { - flex: 1 1 auto; - background: transparent; - border: none; - border-radius: 16px; - padding: 8px 0; - color: rgba(255, 255, 255, 0.95); - font-size: 14px; - line-height: 1.4; - resize: none; - outline: none; - transition: all 0.3s ease; - min-height: 28px; - max-height: 80px; - font-family: inherit; -} - -.chat-input::placeholder { - color: rgba(255, 255, 255, 0.5); -} - -.chat-input:focus { - border-color: rgba(240, 147, 251, 0.4); - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.04) 100% - ); - box-shadow: 0 0 0 2px rgba(240, 147, 251, 0.2); -} - -.send-button { - width: 44px; - height: 44px; - border-radius: 12px; - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.8) 0%, - rgba(240, 147, 251, 0.8) 100% - ); - border: 1px solid rgba(240, 147, 251, 0.3); - color: white; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.send-button:hover { - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.9) 0%, - rgba(240, 147, 251, 0.9) 100% - ); - transform: translateY(-1px); - box-shadow: - 0 4px 12px rgba(118, 75, 162, 0.3), - 0 2px 6px rgba(0, 0, 0, 0.2); -} - -.send-button:active { - transform: translateY(0); -} - -.send-button:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -.input-hints { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 11px; - color: rgba(255, 255, 255, 0.4); -} - -.hint-item { - display: flex; - align-items: center; -} - -.hint-separator { - opacity: 0.5; -} - -/* ================================ - RESPONSIVE DESIGN - ================================ */ - -@media (max-width: 600px) { - .emma-chat-studio { - padding: 12px 16px; - gap: 10px; - min-height: 100vh; - max-height: 100vh; - } - - .emma-anchor { - gap: 8px; - } - - .webgl-orb-container { - width: 60px; - height: 60px; - } - - .chat-messages-area { - padding: 0 2px; - margin: 0 -2px; - } - - .message { - max-width: 95%; - } - - .message-text { - padding: 10px 14px; - font-size: 13px; - } - - .chat-input-area { - min-height: 70px; - padding: 6px 0; - } - - .input-container { - padding: 6px 10px; - gap: 6px; - } - - .chat-input { - font-size: 13px; - padding: 6px 0; - max-height: 60px; - } - - .voice-input-button, - .vectorless-settings-button, - .send-button { - width: 36px; - height: 36px; - } - - .input-hints { - font-size: 10px; - gap: 6px; - } -} - -/* ================================ - FOCUS MODE (matching voice capture) - ================================ */ - -body.chat-focus .info-panel, -body.chat-focus .welcome-text, -body.chat-focus .quick-stats, -body.chat-focus .radial-menu, -body.chat-focus .constellation-canvas { - opacity: 0.12 !important; - filter: blur(4px) !important; - pointer-events: none !important; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; -} - -/* ================================ - ACCESSIBILITY - ================================ */ - -.chat-input:focus, -.send-button:focus { - outline: 2px solid rgba(240, 147, 251, 0.8); - outline-offset: 2px; -} - -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* ================================ - MESSAGE FORMATTING - ================================ */ - -.message-text strong { - font-weight: 600; - color: rgba(255, 255, 255, 1); -} - -.message-text em { - font-style: italic; - color: rgba(240, 147, 251, 0.9); -} - -.message-text br { - line-height: 1.6; -} - -/* ================================ - VECTORLESS AI SETTINGS MODAL - ================================ */ - -.vectorless-settings-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 99998; - display: none; - align-items: center; - justify-content: center; -} - -.modal-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(8px); -} - -.modal-content { - position: relative; - background: linear-gradient(135deg, rgba(118, 75, 162, 0.95) 0%, rgba(138, 94, 250, 0.95) 100%); - backdrop-filter: blur(20px); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); - width: 90%; - max-width: 500px; - max-height: 80vh; - overflow: hidden; - color: white; -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 24px 24px 0; - margin-bottom: 20px; -} - -.modal-header h3 { - margin: 0; - font-size: 20px; - font-weight: 700; - color: white; -} - -.modal-close { - background: none; - border: none; - color: white; - font-size: 28px; - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: background 0.3s ease; -} - -.modal-close:hover { - background: rgba(255, 255, 255, 0.1); -} - -.modal-body { - padding: 0 24px; - max-height: 50vh; - overflow-y: auto; -} - -.modal-body::-webkit-scrollbar { - width: 6px; -} - -.modal-body::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; -} - -.modal-body::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; -} - -.settings-section { - margin-bottom: 24px; -} - -.settings-section h4 { - margin: 0 0 8px 0; - font-size: 16px; - font-weight: 600; - color: white; -} - -.settings-description { - margin: 0 0 16px 0; - font-size: 14px; - color: rgba(255, 255, 255, 0.8); - line-height: 1.4; -} - -.input-group { - margin-bottom: 16px; -} - -.input-group label { - display: block; - margin-bottom: 8px; - font-size: 14px; - font-weight: 500; - color: white; -} - -.settings-input { - width: 100%; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 10px; - color: white; - font-size: 14px; - transition: all 0.3s ease; -} - -.settings-input:focus { - outline: none; - border-color: rgba(255, 255, 255, 0.4); - background: rgba(255, 255, 255, 0.15); -} - -.settings-input::placeholder { - color: rgba(255, 255, 255, 0.5); -} - -.input-help { - display: block; - margin-top: 6px; - font-size: 12px; - color: rgba(255, 255, 255, 0.6); - font-style: italic; -} - -.toggle-group { - display: flex; - align-items: center; - gap: 12px; -} - -.toggle-switch { - position: relative; - display: inline-block; - width: 50px; - height: 26px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.2); - transition: 0.3s; - border-radius: 26px; -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 3px; - bottom: 3px; - background: white; - transition: 0.3s; - border-radius: 50%; -} - -input:checked + .toggle-slider { - background: rgba(255, 255, 255, 0.4); -} - -input:checked + .toggle-slider:before { - transform: translateX(24px); -} - -.toggle-label { - font-size: 14px; - color: white; - font-weight: 500; -} - -.vectorless-status { - display: flex; - align-items: center; - gap: 12px; - padding: 16px; - background: rgba(255, 255, 255, 0.1); - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.status-indicator { - font-size: 18px; -} - -#status-text { - font-size: 14px; - color: rgba(255, 255, 255, 0.9); - font-weight: 500; -} - -.modal-footer { - display: flex; - gap: 12px; - padding: 20px 24px 24px; - justify-content: flex-end; -} - -.button-primary, -.button-secondary { - padding: 12px 24px; - border: none; - border-radius: 10px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.button-primary { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.button-primary:hover { - background: rgba(255, 255, 255, 0.3); - transform: translateY(-1px); -} - -.button-secondary { - background: transparent; - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.button-secondary:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -/* ================================ - VECTORLESS SETTINGS BUTTON - ================================ */ - -.vectorless-settings-button { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 50%; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.3s ease; - color: rgba(255, 255, 255, 0.8); -} - -.vectorless-settings-button:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.4); - color: white; - transform: rotate(90deg); -} - -/* ================================ - VECTORLESS MESSAGE STYLING - ================================ */ - -.vectorless-message { - border-left: 3px solid rgba(138, 94, 250, 0.6); -} - -.vectorless-avatar { - background: linear-gradient(135deg, #8A5EFA, #764ba2); - position: relative; -} - -.vectorless-avatar::after { - content: '๐Ÿง '; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 14px; -} - -.vectorless-badge { - background: rgba(138, 94, 250, 0.3); - color: rgba(255, 255, 255, 0.9); - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; - margin-left: 8px; -} - -.message-citations { - margin-top: 12px; - padding: 12px; - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.message-citations h5 { - margin: 0 0 8px 0; - font-size: 12px; - color: rgba(255, 255, 255, 0.8); - font-weight: 600; -} - -.citation-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.citation-item:last-child { - border-bottom: none; -} - -.citation-title { - font-size: 13px; - color: rgba(255, 255, 255, 0.9); - font-weight: 500; -} - -.citation-relevance { - font-size: 12px; - color: rgba(138, 94, 250, 0.8); - font-weight: 600; -} - -.message-suggestions { - margin-top: 12px; -} - -.suggestions-label { - font-size: 12px; - color: rgba(255, 255, 255, 0.8); - margin-bottom: 8px; - font-weight: 600; -} - -.suggestion-buttons { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.suggestion-button { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.9); - padding: 6px 12px; - border-radius: 15px; - font-size: 12px; - cursor: pointer; - transition: all 0.3s ease; -} - -.suggestion-button:hover { - background: rgba(138, 94, 250, 0.3); - border-color: rgba(138, 94, 250, 0.5); - color: white; -} - -/* ================================ - INTELLIGENT MEMORY CAPTURE - ================================ */ - -.memory-detection-indicator { - margin-top: 12px; - padding: 12px 16px; - background: linear-gradient(135deg, rgba(138, 94, 250, 0.1) 0%, rgba(240, 147, 251, 0.1) 100%); - border: 1px solid rgba(138, 94, 250, 0.3); - border-radius: 12px; - transition: all 0.3s ease; -} - -.memory-detection-indicator:hover { - background: linear-gradient(135deg, rgba(138, 94, 250, 0.15) 0%, rgba(240, 147, 251, 0.15) 100%); - border-color: rgba(138, 94, 250, 0.5); - transform: translateY(-1px); -} - -.detection-content { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -.pulse-dot { - width: 8px; - height: 8px; - background: rgba(138, 94, 250, 0.8); - border-radius: 50%; - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.5); - opacity: 0.5; - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -.detection-text { - color: rgba(255, 255, 255, 0.9); - font-size: 13px; - font-weight: 600; -} - -.confidence { - color: rgba(255, 255, 255, 0.9); - background: rgba(138, 94, 250, 0.3); - padding: 2px 8px; - border-radius: 10px; - font-size: 12px; - font-weight: 600; -} - -.save-memory-btn { - margin-left: auto; - padding: 6px 14px; - background: rgba(138, 94, 250, 0.2); - border: 1px solid rgba(138, 94, 250, 0.4); - color: white; - border-radius: 15px; - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.save-memory-btn:hover { - background: rgba(138, 94, 250, 0.3); - border-color: rgba(138, 94, 250, 0.6); - transform: translateY(-1px); -} - -/* Memory Suggestion Actions */ -.memory-suggestion-actions { - display: flex; - gap: 10px; - margin-top: 12px; - flex-wrap: wrap; -} - -.suggestion-action { - padding: 8px 16px; - border: none; - border-radius: 20px; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; -} - -.suggestion-action.primary { - background: linear-gradient(135deg, rgba(138, 94, 250, 0.8) 0%, rgba(240, 147, 251, 0.8) 100%); - color: white; -} - -.suggestion-action.primary:hover { - background: linear-gradient(135deg, rgba(138, 94, 250, 0.9) 0%, rgba(240, 147, 251, 0.9) 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(138, 94, 250, 0.3); -} - -.suggestion-action.secondary { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.suggestion-action.secondary:hover { - background: rgba(255, 255, 255, 0.15); - color: white; -} - -/* Memory Preview Dialog */ -.memory-preview-dialog { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 99999; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transition: opacity 0.3s ease; -} - -.memory-preview-dialog.show { - opacity: 1; -} - -.memory-preview-dialog .dialog-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(8px); -} - -.memory-preview-dialog .dialog-content { - position: relative; - background: linear-gradient(135deg, rgba(118, 75, 162, 0.95) 0%, rgba(138, 94, 250, 0.95) 100%); - backdrop-filter: blur(20px); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); - width: 90%; - max-width: 600px; - max-height: 80vh; - overflow: hidden; - color: white; - transform: scale(0.9); - transition: transform 0.3s ease; -} - -.memory-preview-dialog.show .dialog-content { - transform: scale(1); -} - -.memory-preview-dialog .dialog-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 24px 24px 0; - margin-bottom: 20px; -} - -.memory-preview-dialog .dialog-header h3 { - margin: 0; - font-size: 20px; - font-weight: 700; - color: white; -} - -.memory-preview-dialog .dialog-close { - background: none; - border: none; - color: white; - font-size: 28px; - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: background 0.3s ease; -} - -.memory-preview-dialog .dialog-close:hover { - background: rgba(255, 255, 255, 0.1); -} - -.memory-preview-dialog .dialog-body { - padding: 0 24px 24px; - max-height: 50vh; - overflow-y: auto; -} - -.memory-preview { - background: rgba(255, 255, 255, 0.1); - border-radius: 16px; - padding: 20px; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.memory-preview h4 { - margin: 0 0 12px 0; - font-size: 18px; - font-weight: 700; - color: white; -} - -.memory-preview p { - margin: 0 0 16px 0; - line-height: 1.6; - color: rgba(255, 255, 255, 0.9); -} - -.memory-metadata { - display: flex; - flex-wrap: wrap; - gap: 12px; -} - -.meta-item { - background: rgba(255, 255, 255, 0.2); - padding: 4px 12px; - border-radius: 15px; - font-size: 12px; - color: rgba(255, 255, 255, 0.9); - display: inline-flex; - align-items: center; - gap: 4px; -} - -.memory-preview-dialog .dialog-footer { - display: flex; - gap: 12px; - padding: 0 24px 24px; - justify-content: flex-end; -} - -/* Responsive adjustments for memory capture */ -@media (max-width: 600px) { - .detection-content { - font-size: 12px; - } - - .save-memory-btn { - margin-left: 0; - margin-top: 8px; - width: 100%; - } - - .memory-suggestion-actions { - flex-direction: column; - } - - .suggestion-action { - width: 100%; - } - - .memory-preview-dialog .dialog-content { - margin: 20px; - max-height: calc(100vh - 40px); - } -} \ No newline at end of file diff --git a/css/emma-components.css b/css/emma-components.css new file mode 100644 index 00000000..e0f8f037 --- /dev/null +++ b/css/emma-components.css @@ -0,0 +1,194 @@ +/* ============================================================ + Emma Design System โ€“ Interactive Components + ============================================================ */ + +.emma-button { + --emma-button-gradient-start: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.92); + --emma-button-gradient-end: rgba(var(--emma-accent-secondary-rgb, 208, 111, 168), 0.72); + --emma-button-shadow: 0 14px 34px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); + --emma-button-border: rgba(255, 255, 255, 0.14); + --emma-button-ink: var(--emma-text-on-accent, #0f1024); + + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 0.95rem 2.2rem; + min-height: 3.2rem; + font-size: 0.96rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--emma-button-ink); + background: linear-gradient(135deg, var(--emma-button-gradient-start), var(--emma-button-gradient-end)); + border-radius: 999px; + border: 1px solid var(--emma-button-border); + box-shadow: var(--emma-button-shadow); + cursor: pointer; + text-decoration: none; + transition: transform 320ms cubic-bezier(0.18, 0.88, 0.26, 1), + box-shadow 320ms cubic-bezier(0.18, 0.88, 0.26, 1), + filter 320ms ease; + overflow: hidden; + isolation: isolate; +} + +.emma-button::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: + linear-gradient(120deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0)), + linear-gradient(120deg, rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0)); + mix-blend-mode: soft-light; + opacity: 0.4; + pointer-events: none; + transition: opacity 260ms ease, transform 320ms ease; +} + +.emma-button::after { + content: ''; + position: absolute; + inset: 1px; + border-radius: inherit; + border: 1px solid rgba(255, 255, 255, 0.22); + opacity: 0.9; + pointer-events: none; +} + +.emma-button span, +.emma-button .emma-button__label { + position: relative; + z-index: 1; +} + +.emma-button .emma-button__icon { + font-size: 1.2em; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.emma-button:hover, +.emma-button:focus-visible { + outline: none; + transform: translateY(-4px); + box-shadow: + 0 18px 42px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.32), + 0 10px 18px rgba(0, 0, 0, 0.35); + filter: saturate(1.04); +} + +.emma-button:hover::before, +.emma-button:focus-visible::before { + opacity: 0.6; + transform: translateY(-3px); +} + +.emma-button:active { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.32); +} + +.emma-button:disabled { + cursor: not-allowed; + opacity: 0.6; + transform: none; + box-shadow: none; +} + +.emma-button--primary, +.emma-button--neutral, +.emma-button--surface { + --emma-button-gradient-start: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.92); + --emma-button-gradient-end: rgba(var(--emma-accent-secondary-rgb, 208, 111, 168), 0.72); + --emma-button-shadow: 0 14px 34px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); + --emma-button-ink: var(--emma-text-on-accent, #0f1024); +} + +.emma-button--neutral { + --emma-button-gradient-start: rgba(16, 185, 129, 0.95); + --emma-button-gradient-end: rgba(56, 189, 248, 0.75); + --emma-button-ink: rgba(9, 35, 35, 0.92); + --emma-button-shadow: 0 16px 36px rgba(16, 185, 129, 0.28); +} + +.emma-button--affirmative, +.emma-button--success { + --emma-button-gradient-start: rgba(74, 222, 128, 0.92); + --emma-button-gradient-end: rgba(34, 197, 94, 0.78); + --emma-button-ink: rgba(9, 34, 25, 0.92); + --emma-button-shadow: 0 16px 36px rgba(52, 211, 153, 0.3); +} + +.emma-button--danger { + --emma-button-gradient-start: rgba(248, 113, 113, 0.92); + --emma-button-gradient-end: rgba(239, 68, 68, 0.78); + --emma-button-ink: rgba(62, 12, 12, 0.92); + --emma-button-shadow: 0 14px 34px rgba(239, 68, 68, 0.28); +} + +.emma-button--outline, +.emma-button.is-outline { + --emma-button-gradient-start: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.1); + --emma-button-gradient-end: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.02); + --emma-button-ink: rgba(232, 236, 255, 0.88); + --emma-button-shadow: 0 12px 26px rgba(8, 10, 24, 0.3); + --emma-button-border: rgba(255, 255, 255, 0.22); +} + +.emma-button--ghost, +.emma-button.is-ghost { + --emma-button-gradient-start: rgba(18, 20, 48, 0.62); + --emma-button-gradient-end: rgba(9, 12, 32, 0.8); + --emma-button-ink: rgba(220, 225, 255, 0.86); + --emma-button-shadow: 0 12px 24px rgba(0, 0, 0, 0.28); + --emma-button-border: rgba(255, 255, 255, 0.12); +} + +.emma-button--block { + width: 100%; +} + +.emma-button__pulse { + position: absolute; + inset: -2px; + border-radius: inherit; + background: radial-gradient( + circle at 50% 50%, + rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.35) 0%, + rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0) 65% + ); + opacity: 0; + transition: opacity 0.6s ease; + pointer-events: none; +} + +.emma-button[data-state="connected"] .emma-button__pulse { + opacity: 1; + animation: emmaButtonPulse 2.8s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .emma-button { + transition: none; + transform: none; + box-shadow: var(--emma-button-shadow); + } + + .emma-button::after { + transition: none; + } + + .emma-button:hover, + .emma-button:focus-visible, + .emma-button:active { + transform: none; + } + + .emma-button[data-state="connected"] .emma-button__pulse { + animation: none; + } +} diff --git a/css/emma-voice-chat.css b/css/emma-voice-chat.css index 088b5ac5..c4168f1e 100644 --- a/css/emma-voice-chat.css +++ b/css/emma-voice-chat.css @@ -56,7 +56,7 @@ width: 40px; height: 40px; border-radius: 50%; - background: linear-gradient(135deg, #8B5CF6, #F093FB); + background: linear-gradient(135deg, #6F63D9, #DEB3E4); display: flex; align-items: center; justify-content: center; @@ -159,8 +159,8 @@ } .emma-message[data-voice="true"] { - border-left: 4px solid #8b5cf6; - background: rgba(139, 92, 246, 0.1); + border-left: 4px solid #6f63d9; + background: rgba(111, 99, 217, 0.1); } .emma-message[data-voice="true"]::before { diff --git a/css/main.css b/css/main.css index 8128ba1a..15d2f0b2 100644 --- a/css/main.css +++ b/css/main.css @@ -1,22 +1,4 @@ -/* Emma Main CSS - Shared styles for all pages */ - -/* CSS Variables for Emma Brand Colors */ -:root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); - --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); - --emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%); - --emma-purple: #764ba2; - --emma-pink: #f093fb; - --emma-dark: #1a1033; - --emma-card-bg: rgba(255, 255, 255, 0.05); - --emma-border: rgba(255, 255, 255, 0.1); - --emma-text: #ffffff; - --emma-text-secondary: rgba(255, 255, 255, 0.7); - --emma-text-tertiary: rgba(255, 255, 255, 0.5); - --emma-success: #4ade80; - --emma-error: #f87171; - --emma-warning: #fbbf24; -} +๏ปฟ/* Emma Main CSS - Shared styles for all pages */ /* Reset and Base Styles */ * { @@ -45,12 +27,39 @@ body::before { width: 200%; height: 200%; background: radial-gradient(circle at 20% 20%, rgba(102, 126, 234, 0.3) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, rgba(240, 147, 251, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 80%, rgba(222, 179, 228, 0.3) 0%, transparent 50%), radial-gradient(circle at 40% 60%, rgba(118, 75, 162, 0.2) 0%, transparent 50%); animation: float 20s ease-in-out infinite; z-index: -1; } +:root[data-background="aurora"] body::before { + background: var(--emma-background-aurora); +} + +:root[data-background="sunrise"] body::before { + background: var(--emma-background-sunrise); +} + +:root[data-background="forest"] body::before { + background: var(--emma-background-forest); +} + +:root[data-background="ocean"] body::before { + background: var(--emma-background-ocean); +} + +:root[data-background="ember"] body::before { + background: var(--emma-background-ember); +} + +:root[data-background="blossom"] body::before { + background: var(--emma-background-blossom); +} + +:root[data-background="zen"] body::before { + background: var(--emma-background-zen); +} @keyframes float { 0%, 100% { transform: translate(0, 0) rotate(0deg); } 50% { transform: translate(-20px, -20px) rotate(180deg); } @@ -80,7 +89,7 @@ h1, h2, h3, h4, h5, h6 { font-weight: 300; line-height: 1.2; margin-bottom: 16px; - background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.8) 100%); + background: linear-gradient(135deg, rgba(var(--emma-neutral-rgb), 1) 0%, rgba(var(--emma-neutral-rgb), 0.8) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -104,6 +113,7 @@ a:hover { } /* Buttons */ + .btn { display: inline-flex; align-items: center; @@ -126,13 +136,13 @@ a:hover { } .btn-secondary { - background: rgba(255, 255, 255, 0.1); + background: rgba(var(--emma-neutral-rgb), 0.1); backdrop-filter: blur(10px); border: 1px solid var(--emma-border); } .btn-secondary:hover { - background: rgba(255, 255, 255, 0.15); + background: rgba(var(--emma-neutral-rgb), 0.15); } /* Logo */ @@ -263,16 +273,16 @@ a:hover { } ::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); + background: rgba(var(--emma-neutral-rgb), 0.05); } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); + background: rgba(var(--emma-neutral-rgb), 0.2); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.3); + background: rgba(var(--emma-neutral-rgb), 0.3); } /* Animations */ @@ -303,4 +313,122 @@ a:hover { .glass-card { padding: 24px; } -} \ No newline at end of file +} +.modal-overlay.invite-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.8); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.invite-modal__container { + background: linear-gradient(135deg, rgba(40, 30, 60, 0.98) 0%, rgba(30, 20, 50, 0.98) 100%); + border-radius: 16px; + padding: 32px; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + border: 1px solid rgba(111, 99, 217, 0.3); + animation: fadeIn 0.3s ease; +} + +.invite-modal__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.invite-modal__title { + margin: 0; + color: var(--emma-text-strong); + font-size: 24px; +} + +.invite-modal__close { + background: none; + border: none; + color: var(--emma-text-muted); + font-size: 24px; + cursor: pointer; +} + +.invite-modal__body { + margin-bottom: 24px; +} + +.invite-modal__description { + color: var(--emma-text-muted); + line-height: 1.6; + margin-bottom: 16px; +} + +.invite-modal__permissions { + background: rgba(111, 99, 217, 0.1); + border: 1px solid rgba(111, 99, 217, 0.3); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.invite-modal__permissions-title { + margin: 0 0 8px 0; + color: var(--emma-accent-primary); +} + +.invite-modal__permissions-list { + margin: 0; + color: var(--emma-text-standard); +} + +.invite-modal__meta { + color: var(--emma-text-muted); + font-size: 12px; +} + +.invite-modal__meta-item { + margin: 4px 0; +} + +.invite-modal__actions { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.invite-modal__button { + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + font-weight: 500; + transition: all 0.3s; +} + +.invite-modal__button--ghost { + background: rgba(var(--emma-neutral-rgb), 0.1); + border: 1px solid rgba(var(--emma-neutral-rgb), 0.2); + color: var(--emma-text-standard); +} + +.invite-modal__button--primary { + background: var(--emma-gradient-primary); + border: none; + color: var(--emma-text-on-accent); +} + +.text-white { + color: var(--emma-text-strong); +} + +.text-accent { + color: var(--emma-accent-primary); +} + + + + diff --git a/css/memories.css b/css/memories.css index bf7fca79..6fd94a75 100644 --- a/css/memories.css +++ b/css/memories.css @@ -2,11 +2,11 @@ /* CSS Variables */ :root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); --emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%); --emma-purple: #764ba2; - --emma-pink: #f093fb; + --emma-pink: #deb3e4; --emma-dark: #1a1033; --emma-card-bg: rgba(255, 255, 255, 0.05); --emma-border: rgba(255, 255, 255, 0.1); @@ -74,11 +74,11 @@ body { } .emma-onboarding-modal .onboarding-header { padding: 24px; - background: linear-gradient(135deg, rgba(102,126,234,0.18) 0%, rgba(118,75,162,0.45) 50%, rgba(240,147,251,0.24) 100%); + background: linear-gradient(135deg, rgba(102,126,234,0.18) 0%, rgba(118,75,162,0.45) 50%, rgba(222,179,228,0.24) 100%); } .emma-onboarding-modal .onboarding-header .orb { width: 44px; height: 44px; border-radius: 50%; - background: radial-gradient(circle at 30% 30%, #f093fb, #764ba2 60%, #1a1033 100%); + background: radial-gradient(circle at 30% 30%, #deb3e4, #764ba2 60%, #1a1033 100%); box-shadow: inset 0 0 20px rgba(255,255,255,0.25), 0 0 30px rgba(118,75,162,0.35); margin-bottom: 12px; } @@ -328,7 +328,7 @@ body { /* Memory Card Header */ .memory-card-header { height: 180px; - background: linear-gradient(135deg, #667eea20 0%, #764ba250 50%, #f093fb30 100%); + background: linear-gradient(135deg, #667eea20 0%, #764ba250 50%, #deb3e430 100%); display: flex; align-items: center; justify-content: center; @@ -623,14 +623,14 @@ body { .memory-capsule.has-media { background: linear-gradient(135deg, rgba(118, 75, 162, 0.1) 0%, - rgba(240, 147, 251, 0.1) 100%); + rgba(222, 179, 228, 0.1) 100%); border-color: rgba(118, 75, 162, 0.3); } .memory-capsule.has-media:hover { background: linear-gradient(135deg, rgba(118, 75, 162, 0.15) 0%, - rgba(240, 147, 251, 0.15) 100%); + rgba(222, 179, 228, 0.15) 100%); border-color: rgba(118, 75, 162, 0.5); } @@ -929,7 +929,7 @@ body { justify-content: center; background: linear-gradient(135deg, rgba(118, 75, 162, 0.2) 0%, - rgba(240, 147, 251, 0.2) 100%); + rgba(222, 179, 228, 0.2) 100%); border: 2px dashed var(--emma-purple); } @@ -1109,7 +1109,7 @@ body { .conversation-message.assistant { border-left-color: var(--emma-pink); - background: linear-gradient(135deg, rgba(118, 75, 162, 0.1) 0%, rgba(240, 147, 251, 0.1) 100%); + background: linear-gradient(135deg, rgba(118, 75, 162, 0.1) 0%, rgba(222, 179, 228, 0.1) 100%); } .message-header { diff --git a/css/memory-detail-modal.css b/css/memory-detail-modal.css index c406028d..988e4a95 100644 --- a/css/memory-detail-modal.css +++ b/css/memory-detail-modal.css @@ -88,7 +88,7 @@ #memory-title-input:focus { background: rgba(255, 255, 255, 0.1) !important; - border-color: rgba(139, 92, 246, 0.5) !important; + border-color: rgba(111, 99, 217, 0.5) !important; outline: none !important; } @@ -119,12 +119,12 @@ } #memory-save-btn { - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%) !important; + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%) !important; border: none !important; } #memory-save-btn:hover { - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4) !important; + box-shadow: 0 4px 12px rgba(111, 99, 217, 0.4) !important; } #memory-delete-btn:hover { @@ -218,7 +218,7 @@ left: 0 !important; right: 0 !important; height: 3px !important; - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%) !important; + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%) !important; } /* Tab Content */ @@ -327,7 +327,7 @@ content: ''; width: 3px; height: 16px; - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%); border-radius: 2px; } @@ -360,7 +360,7 @@ width: 24px; height: 24px; border-radius: 50%; - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%); display: flex; align-items: center; justify-content: center; @@ -452,11 +452,11 @@ .add-tag-input:focus { outline: none; background: rgba(255, 255, 255, 0.15); - border-color: rgba(139, 92, 246, 0.5); + border-color: rgba(111, 99, 217, 0.5); } .add-tag-btn { - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%); border: none; color: white; padding: 8px 16px; @@ -467,7 +467,7 @@ .add-tag-btn:hover { transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4); + box-shadow: 0 4px 12px rgba(111, 99, 217, 0.4); } /* Tag List */ @@ -483,8 +483,8 @@ align-items: center; gap: 6px; padding: 6px 12px; - background: rgba(139, 92, 246, 0.2); - border: 1px solid rgba(139, 92, 246, 0.3); + background: rgba(111, 99, 217, 0.2); + border: 1px solid rgba(111, 99, 217, 0.3); border-radius: 16px; font-size: 0.85rem; color: rgba(255, 255, 255, 0.9); @@ -492,8 +492,8 @@ } .tag-item:hover { - background: rgba(139, 92, 246, 0.3); - border-color: rgba(139, 92, 246, 0.5); + background: rgba(111, 99, 217, 0.3); + border-color: rgba(111, 99, 217, 0.5); } .tag-remove { diff --git a/css/popup.css b/css/popup.css index a7abd138..7db00158 100644 --- a/css/popup.css +++ b/css/popup.css @@ -2,11 +2,11 @@ /* CSS Variables for Emma Brand Colors */ :root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); --emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%); --emma-purple: #764ba2; - --emma-pink: #f093fb; + --emma-pink: #deb3e4; --emma-dark: #1a1033; --emma-card-bg: rgba(255, 255, 255, 0.05); --emma-border: rgba(255, 255, 255, 0.1); @@ -58,7 +58,7 @@ body.no-webgl::before { width: 200%; height: 200%; background: radial-gradient(circle at 20% 20%, rgba(147, 51, 234, 0.3) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, rgba(236, 72, 153, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 80%, rgba(208, 111, 168, 0.3) 0%, transparent 50%), radial-gradient(circle at 40% 60%, rgba(6, 182, 212, 0.2) 0%, transparent 50%); animation: float 20s ease-in-out infinite; z-index: -1; @@ -207,7 +207,7 @@ body.no-webgl::before { font-size: 24px; font-weight: 700; letter-spacing: 1px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -483,7 +483,7 @@ body.no-webgl::before { .btn.small { padding: 8px 10px; border-radius: 10px; font-size: 12px; } .btn.tiny { padding: 4px 8px; border-radius: 8px; font-size: 11px; } -.btn.primary { background: linear-gradient(135deg, #8658ff 0%, #6843cc 100%); color: #fff; border: 0; } +.btn.primary { background: linear-gradient(135deg, #6f63d9 0%, #6843cc 100%); color: #fff; border: 0; } /* HML Modal Redesign - Enhanced Styles */ .hml-modal-overlay { @@ -533,7 +533,7 @@ body.no-webgl::before { .hml-icon-bg { position: absolute; inset: 0; - background: linear-gradient(135deg, #6B5CFF 0%, #8658ff 100%); + background: linear-gradient(135deg, #6F63D9 0%, #6f63d9 100%); border-radius: 10px; opacity: 0.2; animation: pulse 2s ease-in-out infinite; @@ -546,7 +546,7 @@ body.no-webgl::before { .hml-title-icon { font-size: 18px; - color: #8658ff; + color: #6f63d9; position: relative; z-index: 1; } @@ -568,7 +568,7 @@ body.no-webgl::before { .hml-status-banner { margin: 0 18px 10px; padding: 10px 14px; - background: linear-gradient(135deg, rgba(107,92,255,0.1) 0%, rgba(134,88,255,0.05) 100%); + background: linear-gradient(135deg, rgba(107,92,255,0.1) 0%, rgba(111,99,217,0.05) 100%); border: 1px solid rgba(107,92,255,0.2); border-radius: 10px; display: flex; @@ -697,7 +697,7 @@ body.no-webgl::before { left: 0; right: 0; height: 2px; - background: linear-gradient(90deg, #6B5CFF 0%, #8658ff 100%); + background: linear-gradient(90deg, #6F63D9 0%, #6f63d9 100%); } .hml-tab-badge { @@ -767,7 +767,7 @@ body.no-webgl::before { .hml-stat-value { font-size: 20px; font-weight: 700; - color: #8658ff; + color: #6f63d9; margin-bottom: 2px; line-height: 1; } @@ -795,7 +795,7 @@ body.no-webgl::before { .hml-action-btn { background: rgba(107,92,255,0.2); border: 1px solid rgba(107,92,255,0.3); - color: #8658ff; + color: #6f63d9; padding: 6px 12px; border-radius: 8px; display: flex; @@ -861,7 +861,7 @@ body.no-webgl::before { /* Peers Tab Styles */ .hml-identity-card { - background: linear-gradient(135deg, rgba(107,92,255,0.1) 0%, rgba(134,88,255,0.05) 100%); + background: linear-gradient(135deg, rgba(107,92,255,0.1) 0%, rgba(111,99,217,0.05) 100%); border: 1px solid rgba(107,92,255,0.2); border-radius: 10px; padding: 10px; @@ -928,7 +928,7 @@ body.no-webgl::before { .hml-create-id-btn { width: 100%; - background: linear-gradient(135deg, #6B5CFF 0%, #8658ff 100%); + background: linear-gradient(135deg, #6F63D9 0%, #6f63d9 100%); border: none; color: #fff; padding: 12px 20px; @@ -1023,7 +1023,7 @@ body.no-webgl::before { } .hml-switch input:checked + .hml-switch-slider { - background: #6B5CFF; + background: #6F63D9; } .hml-switch input:checked + .hml-switch-slider::after { @@ -1083,7 +1083,7 @@ body.no-webgl::before { .hml-text-btn { background: none; border: none; - color: #8658ff; + color: #6f63d9; font-size: 13px; cursor: pointer; padding: 8px 0; @@ -1091,7 +1091,7 @@ body.no-webgl::before { } .hml-text-btn:hover { - color: #6B5CFF; + color: #6F63D9; text-decoration: underline; } @@ -1105,7 +1105,7 @@ body.no-webgl::before { width: 60px; height: 60px; margin: 0 auto 14px; - background: linear-gradient(135deg, rgba(107,92,255,0.2) 0%, rgba(134,88,255,0.1) 100%); + background: linear-gradient(135deg, rgba(107,92,255,0.2) 0%, rgba(111,99,217,0.1) 100%); border-radius: 16px; display: flex; align-items: center; @@ -1145,7 +1145,7 @@ body.no-webgl::before { .hml-feature i { font-size: 20px; - color: #8658ff; + color: #6f63d9; } .hml-feature span { @@ -1184,7 +1184,7 @@ body.no-webgl::before { top: -10px; left: 50%; transform: translateX(-50%); - background: linear-gradient(135deg, #6B5CFF 0%, #8658ff 100%); + background: linear-gradient(135deg, #6F63D9 0%, #6f63d9 100%); color: #fff; font-size: 10px; font-weight: 600; @@ -1204,7 +1204,7 @@ body.no-webgl::before { .hml-plan-price { font-size: 24px; font-weight: 700; - color: #8658ff; + color: #6f63d9; margin-bottom: 16px; } @@ -1241,7 +1241,7 @@ body.no-webgl::before { width: 100%; background: rgba(107,92,255,0.2); border: 1px solid rgba(107,92,255,0.3); - color: #8658ff; + color: #6f63d9; padding: 10px; border-radius: 10px; cursor: pointer; @@ -1257,7 +1257,7 @@ body.no-webgl::before { } .hml-cloud-cta { - background: linear-gradient(135deg, #6B5CFF 0%, #8658ff 100%); + background: linear-gradient(135deg, #6F63D9 0%, #6f63d9 100%); border: none; color: #fff; padding: 14px 28px; @@ -2419,7 +2419,7 @@ input:checked + .slider:before { } .action-card.chat { - border-left: 3px solid #8b5cf6; + border-left: 3px solid #6f63d9; } /* Inline Chat Panel */ @@ -2477,7 +2477,7 @@ input:checked + .slider:before { .message-inline { display: flex; gap: 8px; } .message-inline.user { justify-content: flex-end; } .bubble-inline { background: rgba(255,255,255,0.06); border: 1px solid var(--emma-border); padding: 8px 10px; border-radius: 12px; max-width: 85%; } -.message-inline.user .bubble-inline { background: rgba(139,92,246,0.18); border-color: rgba(139,92,246,0.4); } +.message-inline.user .bubble-inline { background: rgba(111,99,217,0.18); border-color: rgba(111,99,217,0.4); } /* Persona action row - tidy grouped buttons */ .persona-actions { display:flex; justify-content: space-between; align-items:center; gap:10px; padding: 10px 12px; border-top:1px solid var(--emma-border); } diff --git a/css/theme-tokens.css b/css/theme-tokens.css new file mode 100644 index 00000000..afbcc02d --- /dev/null +++ b/css/theme-tokens.css @@ -0,0 +1,97 @@ +๏ปฟ:root { + /* Theme metadata */ + --emma-theme-id: 'aurora-classic'; + --emma-background-style: aurora; + + /* Core surfaces */ + --emma-surface-primary: #0a0a0f; + --emma-surface-muted: rgba(255, 255, 255, 0.04); + --emma-surface-inverse: rgba(255, 255, 255, 0.9); + + /* Gradients and backgrounds */ + --emma-gradient-primary: linear-gradient(135deg, #5058b4 0%, #5a3f8f 50%, #a468a5 100%); + --emma-gradient-secondary: linear-gradient(135deg, #3f3f7d 0%, #5a3f8a 50%, #84528f 100%); + --emma-gradient-aurora: linear-gradient(135deg, rgba(80, 88, 180, 0.3) 0%, rgba(90, 63, 143, 0.28) 25%, rgba(164, 104, 165, 0.25) 50%, rgba(67, 132, 187, 0.18) 75%, rgba(80, 88, 180, 0.3) 100%); + + /* Text */ + --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: 15, 15, 23; + + /* Accents */ + --emma-accent-primary: #6f63d9; + --emma-accent-secondary: #d06fa8; + --emma-accent-primary-rgb: 111, 99, 217; + --emma-accent-secondary-rgb: 208, 111, 168; + --emma-success: #4ade80; + --emma-warning: #fbbf24; + --emma-danger: #f87171; + + /* Utility */ + --emma-border-subtle: rgba(255, 255, 255, 0.1); + --emma-border-strong: rgba(255, 255, 255, 0.2); + --emma-glow-strong: 0 0 22px rgba(111, 99, 217, 0.32); + --emma-shadow-raised: 0 16px 36px rgba(2, 6, 18, 0.35); + --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); + + /* Chat */ + --emma-chat-user-bubble: linear-gradient(135deg, #6f63d9, #d06fa8); + --emma-chat-user-text: #ffffff; + --emma-chat-emma-bubble: rgba(37, 38, 65, 0.85); + --emma-chat-emma-text: rgba(255, 255, 255, 0.9); + --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; + + /* Navigation / interactions */ + --emma-nav-highlight: rgba(var(--emma-accent-primary-rgb), 0.16); + --emma-background-aurora: linear-gradient(135deg, rgba(80, 88, 180, 0.25), rgba(90, 63, 143, 0.25)); + --emma-background-sunrise: linear-gradient(135deg, rgba(255, 176, 124, 0.35), rgba(255, 220, 186, 0.35)); + --emma-background-forest: linear-gradient(135deg, rgba(58, 149, 138, 0.4), rgba(14, 52, 54, 0.4)); + --emma-background-ocean: linear-gradient(135deg, rgba(77, 196, 255, 0.2), rgba(14, 84, 120, 0.4)); + --emma-background-ember: linear-gradient(135deg, rgba(249, 115, 22, 0.18), rgba(71, 21, 27, 0.45)); + --emma-background-blossom: linear-gradient(135deg, rgba(252, 179, 255, 0.24), rgba(241, 179, 239, 0.32)); + --emma-background-zen: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(17, 94, 89, 0.24)); + --emma-accent-surface: rgba(var(--emma-accent-primary-rgb), 0.14); + --emma-accent-surface-strong: rgba(var(--emma-accent-primary-rgb), 0.24); + --emma-accent-border: rgba(var(--emma-accent-primary-rgb), 0.3); + + /* Memory constellation */ + --emma-constellation-node: rgba(var(--emma-accent-primary-rgb), 0.45); + --emma-constellation-glow: rgba(var(--emma-accent-primary-rgb), 0.6); + + /* Orb */ + --emma-orb-hue: 265; + --emma-orb-hover-intensity: 0.25; + + /* Legacy aliases */ + --emma-gradient-1: var(--emma-gradient-primary); + --emma-gradient-2: var(--emma-gradient-secondary); + --emma-aurora: var(--emma-gradient-aurora); + --emma-bg-gradient: linear-gradient(135deg, var(--emma-surface-primary) 0%, #1a1033 50%, #0f0c29 100%); + --emma-purple: var(--emma-accent-primary); + --emma-pink: var(--emma-accent-secondary); + --emma-blue: #667eea; + --emma-dark: var(--emma-surface-primary); + --emma-card-bg: var(--emma-surface-muted); + --emma-glass: rgba(255, 255, 255, 0.05); + --emma-border: var(--emma-border-subtle); + --emma-text: var(--emma-text-strong); + --emma-text-secondary: var(--emma-text-standard); + --emma-text-tertiary: var(--emma-text-muted); +} + + + diff --git a/css/unified-memory-wizard.css b/css/unified-memory-wizard.css index db6c7028..55ae028e 100644 --- a/css/unified-memory-wizard.css +++ b/css/unified-memory-wizard.css @@ -8,11 +8,11 @@ /* Emma CSS Variables */ :root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); --emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%); --emma-purple: #764ba2; - --emma-pink: #f093fb; + --emma-pink: #deb3e4; --emma-dark: #1a1033; --emma-card-bg: rgba(255, 255, 255, 0.05); --emma-border: rgba(255, 255, 255, 0.1); @@ -232,7 +232,7 @@ .method-option:hover { background: rgba(255, 255, 255, 0.08); - border-color: rgba(240, 147, 251, 0.3); + border-color: rgba(222, 179, 228, 0.3); transform: translateY(-2px); box-shadow: 0 8px 32px rgba(118, 75, 162, 0.15); } @@ -620,7 +620,7 @@ left: 0; right: 0; bottom: 0; - background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 50%, rgba(240, 147, 251, 0.05) 100%); + background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 50%, rgba(222, 179, 228, 0.05) 100%); opacity: 0; transition: opacity 0.3s ease; } @@ -1148,7 +1148,7 @@ } .question-actions .wizard-btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); border: none; color: white; box-shadow: 0 4px 16px rgba(118, 75, 162, 0.3); diff --git a/css/vault-control-panel.css b/css/vault-control-panel.css new file mode 100644 index 00000000..30d9e121 --- /dev/null +++ b/css/vault-control-panel.css @@ -0,0 +1,890 @@ +.emma-floating-button { + width: 52px; + height: 52px; + border-radius: 50%; + border: 1px solid rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.4); + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.18); + color: var(--emma-accent-primary, #6f63d9); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 26px; + cursor: pointer; + transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease, border 180ms ease; + backdrop-filter: blur(12px); +} + +.emma-floating-button:hover, +.emma-floating-button:focus-visible { + transform: scale(1.08); + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); + border-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.55); + box-shadow: 0 8px 24px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); + color: var(--emma-text-strong, #ffffff); + outline: none; +} + +.emma-floating-button:focus-visible { + box-shadow: 0 0 0 2px rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.35), + 0 8px 24px rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); +} + +.emma-floating-button svg { + width: 24px; + height: 24px; + stroke: currentColor; +} + +.vault-control-shield { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 9999; +} + +:root { + --vault-modal-background: var(--emma-bg-gradient); +} + +:root[data-background="aurora"] { + --vault-modal-background: var(--emma-background-aurora); +} + +:root[data-background="sunrise"] { + --vault-modal-background: var(--emma-background-sunrise); +} + +:root[data-background="forest"] { + --vault-modal-background: var(--emma-background-forest); +} + +:root[data-background="ocean"] { + --vault-modal-background: var(--emma-background-ocean); +} + +:root[data-background="ember"] { + --vault-modal-background: var(--emma-background-ember); +} + +:root[data-background="blossom"] { + --vault-modal-background: var(--emma-background-blossom); +} + +:root[data-background="zen"] { + --vault-modal-background: var(--emma-background-zen); +} + +.vault-control-overlay { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: clamp(24px, 5vh, 48px) clamp(12px, 4vw, 40px); + backdrop-filter: blur(30px); + background: var(--vault-modal-background); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + opacity: 0; + pointer-events: none; + transition: opacity 260ms ease; + z-index: 10000; +} + +.vault-control-overlay::before { + content: ''; + position: absolute; + inset: clamp(18px, 3vw, 36px); + border-radius: 40px; + background: + radial-gradient(circle at 20% 18%, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.22), transparent 55%), + radial-gradient(circle at 80% 12%, rgba(var(--emma-accent-secondary-rgb, 208, 111, 168), 0.16), transparent 60%); + opacity: 0.5; + filter: blur(70px); + pointer-events: none; + transition: opacity 260ms ease; +} + +.vault-control-overlay.is-visible { + opacity: 1; + pointer-events: auto; +} + +.vault-control-dialog { + position: relative; + width: min(900px, calc(100vw - 48px)); + max-height: 90vh; + overflow-y: auto; + display: flex; + flex-direction: column; + -webkit-overflow-scrolling: touch; + padding: 0; + border-radius: 32px; + border: 1px solid rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.18); + background: linear-gradient(148deg, rgba(6, 8, 20, 0.52), rgba(6, 8, 18, 0.76)), var(--vault-modal-background); + background-blend-mode: overlay; + color: var(--emma-text-strong, #ffffff); + box-shadow: + 0 28px 80px rgba(3, 2, 12, 0.6), + 0 0 0 1px rgba(255, 255, 255, 0.04); + backdrop-filter: blur(26px); + transform: translateY(24px) scale(0.94); + opacity: 0; + transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1), opacity 260ms ease; + overflow: hidden; +} + +.vault-control-dialog::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: inherit; + border: 1px solid rgba(255, 255, 255, 0.04); + pointer-events: none; + z-index: 1; +} + +.vault-control-overlay.is-visible .vault-control-dialog { + transform: translateY(0) scale(1); + opacity: 1; +} + +.vault-control__dismiss { + position: absolute; + top: 20px; + right: 20px; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.35); + background: rgba(10, 14, 30, 0.68); + color: var(--emma-text-strong, #ffffff); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 3; + box-shadow: 0 12px 24px rgba(3, 5, 18, 0.4); + transition: + transform 0.22s ease, + box-shadow 0.22s ease, + background 0.22s ease, + border-color 0.22s ease, + color 0.22s ease; + backdrop-filter: blur(12px); + touch-action: manipulation; +} + +.vault-control__dismiss svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 1.8; +} + +.vault-control__dismiss:hover, +.vault-control__dismiss:focus-visible { + transform: translateY(-2px); + box-shadow: 0 10px 18px rgba(0, 0, 0, 0.32); + outline: none; + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.28); + border-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.6); +} + +body[data-surface="light"] .vault-control__dismiss, +body[data-theme="light"] .vault-control__dismiss { + background: rgba(255, 255, 255, 0.9); + color: var(--emma-neutral-inverse, #0f0f17); + border-color: rgba(var(--emma-neutral-inverse-rgb, 15, 15, 23), 0.18); + box-shadow: 0 12px 22px rgba(17, 24, 39, 0.2); +} + +body[data-surface="light"] .vault-control__dismiss:hover, +body[data-surface="light"] .vault-control__dismiss:focus-visible, +body[data-theme="light"] .vault-control__dismiss:hover, +body[data-theme="light"] .vault-control__dismiss:focus-visible { + color: var(--emma-accent-primary, #6f63d9); + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.16); + border-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.45); +} + +.vault-control { + position: relative; + display: grid; + gap: clamp(20px, 2.4vw, 32px); + padding: clamp(28px, 3vw, 44px); + flex: 1 1 auto; + min-height: 0; + color: rgba(239, 241, 255, 0.88); + z-index: 2; + max-height: clamp(60vh, 78vh, 86vh); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding-right: calc(clamp(28px, 3vw, 44px) + 6px); + align-content: start; + scrollbar-gutter: stable both-edges; +} + +.vault-control::after { + content: ''; + position: absolute; + inset: -18% -12% 35% -12%; + background: + radial-gradient(circle at 12% 12%, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.32), transparent 50%), + radial-gradient(circle at 78% 0%, rgba(110, 211, 255, 0.22), transparent 58%); + opacity: 0.7; + filter: blur(50px); + z-index: -1; + pointer-events: none; +} + +.vault-control__hero { + position: relative; + z-index: 1; +} + +.vault-hero { + position: relative; + display: grid; + gap: 20px; + padding: clamp(26px, 3vw, 36px); + border-radius: 28px; + border: 1px solid rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.32); + background: linear-gradient(160deg, rgba(11, 14, 32, 0.55), rgba(8, 10, 26, 0.78)), var(--vault-modal-background); + background-blend-mode: overlay; + box-shadow: + 0 20px 48px rgba(12, 8, 34, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.08); + overflow: hidden; + --vault-sync-accent: var(--emma-accent-primary, #6f63d9); +} + +.vault-hero__glow { + position: absolute; + inset: -35% -25% 45% -25%; + background: + radial-gradient(circle at 30% 20%, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.42), transparent 58%), + radial-gradient(circle at 82% -10%, rgba(96, 199, 255, 0.35), transparent 64%); + opacity: 0.9; + filter: blur(60px); + pointer-events: none; +} + +.vault-hero__signal { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 14px; + border-radius: 999px; + background: rgba(18, 20, 48, 0.7); + border: 1px solid rgba(255, 255, 255, 0.14); + width: fit-content; + font-size: 0.75rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(210, 217, 255, 0.78); + position: relative; + z-index: 1; +} + +.vault-hero__signal-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--vault-sync-accent, var(--emma-accent-primary, #6f63d9)); + box-shadow: 0 0 8px var(--vault-sync-accent, rgba(111, 99, 217, 0.6)); + animation: vaultSignalPulse 2.4s ease-in-out infinite; +} + +.vault-hero__signal-dot[data-state="dirty"] { + background: var(--emma-warning, #f59e0b); + box-shadow: 0 0 8px rgba(245, 158, 11, 0.6); +} + +.vault-hero__signal-dot[data-state="clean"] { + background: var(--emma-success, #34d399); + box-shadow: 0 0 8px rgba(52, 211, 153, 0.6); +} + +.vault-hero__core { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 18px; + align-items: center; + position: relative; + z-index: 1; +} + +.vault-hero__icon { + width: 72px; + height: 72px; + border-radius: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + background: linear-gradient(200deg, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.4), rgba(26, 22, 60, 0.85)); + box-shadow: + 0 12px 28px rgba(14, 8, 40, 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.18); +} + +.vault-hero__eyebrow { + display: inline-block; + font-size: 0.75rem; + letter-spacing: 0.26em; + text-transform: uppercase; + color: rgba(205, 211, 255, 0.6); + margin-bottom: 6px; +} + +.vault-hero__title { + font-size: clamp(1.8rem, 2.4vw, 2.3rem); + margin: 0; + letter-spacing: 0.015em; +} + +.vault-hero__subtitle { + margin: 10px 0 0; + font-size: 0.95rem; + color: rgba(214, 219, 255, 0.76); + max-width: 25ch; + line-height: 1.6; +} + +.vault-hero__quickstats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + position: relative; + z-index: 1; +} + +.vault-hero__stat { + border-radius: 16px; + padding: 14px 16px; + background: rgba(24, 22, 54, 0.65); + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; + gap: 6px; +} + +.vault-hero__stat span { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: rgba(184, 193, 240, 0.65); +} + +.vault-hero__stat strong { + font-size: clamp(1rem, 1.5vw, 1.35rem); + letter-spacing: 0.03em; +} + +.vault-hero__meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding-top: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + position: relative; + z-index: 1; +} + +.vault-hero__chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 0.78rem; + color: rgba(225, 229, 255, 0.78); + background: linear-gradient(120deg, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.38), rgba(18, 22, 58, 0.7)); + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.16em; +} + +.vault-hero__name { + font-size: 0.95rem; + color: rgba(238, 240, 255, 0.9); + letter-spacing: 0.04em; +} + +@keyframes vaultSignalPulse { + 0%, + 100% { + transform: scale(1); + opacity: 0.8; + } + 50% { + transform: scale(1.18); + opacity: 1; + } +} + +.vault-section { + position: relative; + border-radius: 24px; + padding: 28px 28px 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: linear-gradient(155deg, rgba(20, 18, 40, 0.82) 0%, rgba(17, 16, 34, 0.88) 60%, rgba(14, 13, 30, 0.94) 100%), var(--vault-modal-background); + background-blend-mode: overlay; + box-shadow: + 0 24px 60px rgba(0, 0, 0, 0.38), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + display: grid; + gap: 20px; + overflow: hidden; + isolation: isolate; +} + +.vault-section::after { + content: ''; + position: absolute; + inset: -55% -10% 70% -35%; + background: radial-gradient(circle at 0% 50%, rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.25), transparent 60%); + opacity: 0.4; + pointer-events: none; +} +.vault-section__header { + display: grid; + gap: 10px; + position: relative; + z-index: 1; + align-content: start; +} + +.vault-section__eyebrow { + font-size: 0.7rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: rgba(190, 198, 244, 0.52); +} + +.vault-section__headline { + display: flex; + align-items: center; + gap: 12px; +} + +.vault-section__icon { + font-size: 22px; +} + +.vault-section__title { + font-size: 19px; + font-weight: 600; + margin: 0; + color: var(--emma-text-strong, #ffffff); + letter-spacing: 0.02em; +} + +.vault-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 18px 22px; + position: relative; + z-index: 1; +} + +.vault-stats__item { + position: relative; + padding: 16px 18px; + border-radius: 18px; + background: linear-gradient(145deg, rgba(38, 34, 68, 0.55), rgba(22, 20, 48, 0.35)); + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04); + display: grid; + gap: 8px; +} + +.vault-stats__item dt { + font-size: 13px; + letter-spacing: 0.02em; + text-transform: uppercase; + color: rgba(165, 175, 214, 0.72); +} + +.vault-stats__item dd { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--emma-text-strong, #ffffff); +} + +.vault-sync { + display: grid; + gap: 16px; + position: relative; + z-index: 1; +} + +.vault-sync__row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + font-size: 15px; + padding: 14px 16px; + border-radius: 16px; + background: rgba(32, 30, 56, 0.6); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.vault-sync__label { + color: rgba(179, 189, 226, 0.72); + font-weight: 500; + letter-spacing: 0.01em; +} + +.vault-sync__value { + color: var(--emma-text-strong, #ffffff); + font-weight: 600; + letter-spacing: 0.01em; +} + +.vault-section--sync { + --vault-sync-accent: var(--emma-accent-primary, #6f63d9); + background: linear-gradient(160deg, rgba(24, 26, 56, 0.92) 0%, rgba(18, 19, 46, 0.88) 62%, rgba(12, 13, 38, 0.94) 100%); +} + +.vault-section--sync[data-sync-state="clean"] { + --vault-sync-accent: var(--emma-success, #34d399); +} + +.vault-section--sync[data-sync-state="dirty"] { + --vault-sync-accent: var(--emma-warning, #f59e0b); +} + +.vault-section--info { + background: linear-gradient(150deg, rgba(28, 28, 64, 0.78), rgba(18, 18, 44, 0.9)); +} + +.vault-section--controls { + background: linear-gradient(150deg, rgba(22, 24, 52, 0.85), rgba(16, 18, 42, 0.9)); + border-color: rgba(255, 255, 255, 0.06); +} + +.vault-section--controls .vault-section__header { + margin-bottom: 4px; +} + +.vault-section--controls .emma-button { + justify-content: space-between; +} + +.vault-sync__badge { + position: relative; + padding: 18px 22px 18px 28px; + border-radius: 18px; + border: 1px solid rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.12); + background: rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.04); + color: var(--emma-text-standard, rgba(255, 255, 255, 0.86)); + display: grid; + gap: 6px; + font-size: 0.96rem; + box-shadow: inset 0 0 0 1px rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.05); +} + +.vault-sync__badge strong { + font-weight: 700; + color: var(--emma-text-strong, #ffffff); + letter-spacing: 0.02em; +} + +.vault-sync__badge::before { + content: ''; + position: absolute; + left: 14px; + top: 14px; + bottom: 14px; + width: 6px; + border-radius: 999px; + background: var(--vault-sync-accent); +} + +.vault-actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 18px; + position: relative; + z-index: 1; +} + +.vault-button { + width: 100%; + justify-content: flex-start; + align-items: center; + gap: 1.2rem; + padding: 1.05rem 2rem; + font-size: 1rem; + letter-spacing: 0.02em; + text-align: left; +} + +.vault-button__icon { + font-size: 1.25em; + flex-shrink: 0; +} + +.vault-button__label { + white-space: nowrap; + flex: 1; +} + +.vault-button--neutral { + --emma-button-gradient-start: rgba(16, 185, 129, 0.92); + --emma-button-gradient-end: rgba(56, 189, 248, 0.78); + --emma-button-ink: rgba(8, 34, 36, 0.92); + --emma-button-shadow: 0 16px 36px rgba(16, 185, 129, 0.26); +} + +.vault-button--affirmative { + --emma-button-gradient-start: rgba(94, 234, 212, 0.92); + --emma-button-gradient-end: rgba(129, 161, 255, 0.78); + --emma-button-ink: rgba(10, 28, 46, 0.9); + --emma-button-shadow: 0 16px 36px rgba(79, 209, 197, 0.28); +} + +.vault-button--danger { + --emma-button-gradient-start: rgba(248, 113, 113, 0.92); + --emma-button-gradient-end: rgba(239, 68, 68, 0.78); + --emma-button-ink: rgba(62, 12, 12, 0.92); + --emma-button-shadow: 0 14px 34px rgba(239, 68, 68, 0.28); +} + +.vault-control { + scrollbar-width: thin; + scrollbar-color: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.5) rgba(20, 22, 40, 0.4); +} + +.vault-control::-webkit-scrollbar { + width: 8px; +} + +.vault-control::-webkit-scrollbar-track { + background: rgba(18, 20, 40, 0.6); + border-radius: 999px; +} + +.vault-control::-webkit-scrollbar-thumb { + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.5); + border-radius: 999px; +} + +.vault-control::-webkit-scrollbar-thumb:hover { + background: rgba(var(--emma-accent-primary-rgb, 111, 99, 217), 0.65); +} + +.vault-toast { + position: fixed; + top: calc(24px + (var(--vault-toast-index, 0) * 72px)); + right: 24px; + min-width: 260px; + max-width: 320px; + padding: 14px 18px 14px 22px; + border-radius: 16px; + border: 1px solid rgba(var(--emma-neutral-rgb, 255, 255, 255), 0.16); + background: rgba(var(--emma-neutral-inverse-rgb, 10, 10, 15), 0.82); + color: var(--emma-text-strong, #ffffff); + box-shadow: var(--emma-shadow-raised, 0 18px 48px rgba(0, 0, 0, 0.35)); + backdrop-filter: blur(14px); + transform: translateX(110%); + opacity: 0; + transition: transform 220ms ease, opacity 220ms ease; + z-index: 11000; + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 12px; +} + +.vault-toast::before { + content: ''; + width: 6px; + height: 100%; + border-radius: 4px; + background: var(--vault-toast-accent, var(--emma-accent-primary, #6f63d9)); + display: inline-block; +} + +.vault-toast.is-visible { + transform: translateX(0); + opacity: 1; +} + +.vault-toast--success { + --vault-toast-accent: var(--emma-success, #34d399); +} + +.vault-toast--error { + --vault-toast-accent: var(--emma-danger, #f87171); +} + +.vault-toast--info { + --vault-toast-accent: var(--emma-accent-primary, #6f63d9); +} + +@media (max-width: 720px) { + .vault-control-dialog { + max-height: 100vh; + border-radius: 26px; + padding: 0; + } + + .vault-control { + max-height: none; + padding: 24px 20px; + padding-right: 20px; + } + + .vault-section { + padding: 32px 22px 22px; + } + + .vault-actions { + grid-template-columns: 1fr; + } + + .vault-control-shield { + bottom: 16px; + right: 16px; + } + + .vault-toast { + left: 16px; + right: 16px; + min-width: auto; + } +} + +@media (max-width: 600px) { + .vault-control-overlay { + padding: 16px; + align-items: flex-start; + } + + .vault-control-dialog { + width: calc(100vw - 24px); + max-width: calc(100vw - 24px); + max-height: calc(100dvh - 24px); + margin: clamp(12px, 4vh, 24px) auto; + border-radius: 20px; + } + + .vault-control { + padding: 20px 18px; + padding-right: 18px; + max-height: none; + } + + .vault-control__dismiss { + top: 14px; + right: 14px; + } +} + +@media (max-width: 720px) { + .vault-control-overlay { + align-items: flex-start; + } + + .vault-control-dialog { + margin: clamp(16px, 6vh, 28px) auto; + } + + .vault-section { + border-radius: 20px; + padding: 24px 22px 20px; + gap: 16px; + } + + .vault-section__header { + gap: 8px; + } + + .vault-section__eyebrow { + letter-spacing: 0.12em; + } + + .vault-section__headline { + gap: 10px; + } + + .vault-section__title { + font-size: 1.05rem; + line-height: 1.2; + } + + .vault-stats { + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 14px 16px; + } + + .vault-sync__row { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .vault-sync__value { + margin-left: 0; + } + + .vault-actions { + grid-template-columns: minmax(0, 1fr); + } + + .vault-button__label { + white-space: normal; + } +} + +@media (min-width: 980px) { + .vault-control { + grid-template-columns: minmax(0, 320px) minmax(0, 1fr); + grid-template-areas: + "hero info" + "hero sync" + "hero controls"; + align-items: start; + } + + .vault-control__hero { + grid-area: hero; + height: 100%; + } + + .vault-section--info { + grid-area: info; + } + + .vault-section--sync { + grid-area: sync; + } + + .vault-section--controls { + grid-area: controls; + } + +} + +@media (prefers-reduced-motion: reduce) { + .vault-control-overlay, + .vault-control-dialog, + .vault-toast { + transition-duration: 0.01ms; + transition-delay: 0ms; + animation-duration: 0.01ms; + } + + .vault-hero__signal-dot { + animation: none; + } + + .vault-button:hover { + transform: none; + } +} diff --git a/css/vault-locked-overlay.css b/css/vault-locked-overlay.css index 43ee592a..6e4ad0d9 100644 --- a/css/vault-locked-overlay.css +++ b/css/vault-locked-overlay.css @@ -47,7 +47,7 @@ animation: emma-pulse 2s ease-in-out infinite; box-shadow: 0 0 40px rgba(118, 75, 162, 0.4), - 0 0 80px rgba(240, 147, 251, 0.2), + 0 0 80px rgba(222, 179, 228, 0.2), inset 0 0 20px rgba(255, 255, 255, 0.1); } @@ -144,7 +144,7 @@ transform: translateY(-1px); box-shadow: 0 8px 25px rgba(118, 75, 162, 0.3), - 0 4px 15px rgba(240, 147, 251, 0.2); + 0 4px 15px rgba(222, 179, 228, 0.2); } .vault-unlock-btn:active { @@ -248,14 +248,14 @@ transform: scale(1); box-shadow: 0 0 40px rgba(118, 75, 162, 0.4), - 0 0 80px rgba(240, 147, 251, 0.2), + 0 0 80px rgba(222, 179, 228, 0.2), inset 0 0 20px rgba(255, 255, 255, 0.1); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(118, 75, 162, 0.6), - 0 0 100px rgba(240, 147, 251, 0.3), + 0 0 100px rgba(222, 179, 228, 0.3), inset 0 0 25px rgba(255, 255, 255, 0.15); } } diff --git a/css/voice-capture-minimal.css b/css/voice-capture-minimal.css index dbfae724..8080cec4 100644 --- a/css/voice-capture-minimal.css +++ b/css/voice-capture-minimal.css @@ -54,11 +54,11 @@ circle at 30% 30%, rgba(138, 94, 250, 0.95) 0%, rgba(118, 75, 162, 0.9) 40%, - rgba(240, 147, 251, 0.95) 100% + rgba(222, 179, 228, 0.95) 100% ); box-shadow: 0 0 30px rgba(118, 75, 162, 0.5), - 0 0 60px rgba(240, 147, 251, 0.2); + 0 0 60px rgba(222, 179, 228, 0.2); } .emma-hint { @@ -239,7 +239,7 @@ background: linear-gradient( 135deg, rgba(118, 75, 162, 0.12) 0%, - rgba(240, 147, 251, 0.06) 100% + rgba(222, 179, 228, 0.06) 100% ); border-color: rgba(118, 75, 162, 0.2); transform: translateY(-1px); @@ -248,10 +248,10 @@ .topic-node.selected .node-content { background: linear-gradient( 135deg, - rgba(240, 147, 251, 0.15) 0%, + rgba(222, 179, 228, 0.15) 0%, rgba(118, 75, 162, 0.1) 100% ); - border-color: rgba(240, 147, 251, 0.3); + border-color: rgba(222, 179, 228, 0.3); transform: scale(1.02); } @@ -287,9 +287,9 @@ background: linear-gradient( 135deg, rgba(118, 75, 162, 0.8) 0%, - rgba(240, 147, 251, 0.8) 100% + rgba(222, 179, 228, 0.8) 100% ); - border: 1px solid rgba(240, 147, 251, 0.3); + border: 1px solid rgba(222, 179, 228, 0.3); border-radius: 16px; color: white; font-size: 15px; @@ -305,7 +305,7 @@ background: linear-gradient( 135deg, rgba(118, 75, 162, 0.9) 0%, - rgba(240, 147, 251, 0.9) 100% + rgba(222, 179, 228, 0.9) 100% ); transform: translateY(-2px); box-shadow: @@ -429,7 +429,7 @@ 90deg, #667eea 0%, #764ba2 50%, - #f093fb 100% + #deb3e4 100% ); border-radius: 2px; width: 0%; @@ -469,7 +469,7 @@ height: 8px; background: linear-gradient( 180deg, - rgba(240, 147, 251, 0.8) 0%, + rgba(222, 179, 228, 0.8) 0%, rgba(118, 75, 162, 0.6) 100% ); border-radius: 2px; @@ -524,7 +524,7 @@ .record-button:focus, .action-btn:focus, .topic-node:focus { - outline: 2px solid rgba(240, 147, 251, 0.8); + outline: 2px solid rgba(222, 179, 228, 0.8); outline-offset: 2px; } diff --git a/css/voice-capture-premium.css b/css/voice-capture-premium.css deleted file mode 100644 index 77277dd5..00000000 --- a/css/voice-capture-premium.css +++ /dev/null @@ -1,924 +0,0 @@ -/** - * Emma Voice Memory Studio - Premium Design System - * Sophisticated, on-brand voice capture experience - */ - -/* ================================ - EMMA VOICE STUDIO FOUNDATION - ================================ */ - -.emma-voice-studio { - position: relative; - padding: 0; - margin: 0; - background: transparent; - width: 100%; - height: 100%; - min-height: 600px; - overflow: hidden; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - color: var(--emma-text, #ffffff); -} - -/* ================================ - VOICE STUDIO HEADER - ================================ */ - -.voice-studio-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 24px 32px 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.03) 0%, - rgba(255, 255, 255, 0.01) 100% - ); -} - -.emma-presence { - display: flex; - align-items: center; - gap: 20px; -} - -.emma-neural-orb { - position: relative; - width: 64px; - height: 64px; - display: flex; - align-items: center; - justify-content: center; -} - -.neural-core { - width: 32px; - height: 32px; - border-radius: 50%; - background: radial-gradient( - circle at 30% 30%, - rgba(102, 126, 234, 0.9) 0%, - rgba(118, 75, 162, 0.8) 50%, - rgba(240, 147, 251, 0.9) 100% - ); - box-shadow: - 0 0 20px rgba(118, 75, 162, 0.4), - inset 0 2px 4px rgba(255, 255, 255, 0.2); - position: relative; - z-index: 3; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); -} - -.neural-ring { - position: absolute; - border-radius: 50%; - border: 1px solid rgba(118, 75, 162, 0.2); - animation: neuralPulse 3s ease-in-out infinite; -} - -.neural-ring.ring-1 { - width: 40px; - height: 40px; - animation-delay: 0s; -} - -.neural-ring.ring-2 { - width: 52px; - height: 52px; - animation-delay: 1s; -} - -.neural-ring.ring-3 { - width: 64px; - height: 64px; - animation-delay: 2s; -} - -@keyframes neuralPulse { - 0%, 100% { - opacity: 0.3; - transform: scale(1); - } - 50% { - opacity: 0.8; - transform: scale(1.1); - } -} - -.emma-neural-orb.listening .neural-core { - background: radial-gradient( - circle at 30% 30%, - rgba(240, 147, 251, 1) 0%, - rgba(118, 75, 162, 0.9) 50%, - rgba(102, 126, 234, 1) 100% - ); - box-shadow: - 0 0 30px rgba(240, 147, 251, 0.6), - 0 0 60px rgba(118, 75, 162, 0.3), - inset 0 2px 8px rgba(255, 255, 255, 0.3); - animation: listeningPulse 2s ease-in-out infinite; -} - -.emma-neural-orb.listening .neural-ring { - border-color: rgba(240, 147, 251, 0.4); - animation-duration: 2s; -} - -@keyframes listeningPulse { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.05); - } -} - -.emma-state { - flex: 1; -} - -.studio-title { - font-size: 24px; - font-weight: 600; - margin: 0 0 4px 0; - background: linear-gradient(135deg, #667eea, #764ba2, #f093fb); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - letter-spacing: -0.5px; -} - -.studio-subtitle { - font-size: 15px; - color: rgba(255, 255, 255, 0.7); - margin: 0; - transition: all 0.3s ease; - line-height: 1.4; -} - -.studio-status { - display: flex; - align-items: center; -} - -.status-indicator { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 20px; - transition: all 0.3s ease; -} - -.status-indicator.recording { - background: rgba(240, 147, 251, 0.1); - border-color: rgba(240, 147, 251, 0.3); -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: rgba(76, 175, 80, 0.8); - transition: all 0.3s ease; -} - -.status-indicator.recording .status-dot { - background: rgba(240, 147, 251, 1); - animation: recordingBlink 1.5s ease-in-out infinite; -} - -@keyframes recordingBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -.status-text { - font-size: 13px; - font-weight: 500; - color: rgba(255, 255, 255, 0.8); -} - -/* ================================ - TRANSCRIPTION CANVAS - ================================ */ - -.transcription-canvas { - margin: 20px 32px 24px; - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.02) 0%, - rgba(255, 255, 255, 0.01) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 16px; - overflow: hidden; - position: relative; -} - -.canvas-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 20px 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); - background: rgba(255, 255, 255, 0.02); -} - -.canvas-label { - font-size: 13px; - font-weight: 600; - color: rgba(255, 255, 255, 0.9); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.canvas-metrics { - display: flex; - align-items: center; - gap: 12px; - font-size: 12px; - color: rgba(255, 255, 255, 0.6); -} - -.metric { - font-weight: 500; -} - -.metric-separator { - opacity: 0.4; -} - -.transcription-viewport { - position: relative; - min-height: 120px; - max-height: 200px; - overflow-y: auto; - padding: 20px; -} - -.transcription-content { - position: relative; - z-index: 2; -} - -.transcription-prompt { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - height: 80px; - color: rgba(255, 255, 255, 0.5); - font-style: italic; -} - -.prompt-icon { - font-size: 20px; - opacity: 0.8; -} - -.prompt-text { - font-size: 15px; - font-weight: 400; -} - -.transcribed-text { - font-size: 16px; - line-height: 1.6; - color: rgba(255, 255, 255, 0.95); - animation: textAppear 0.3s ease; -} - -@keyframes textAppear { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.sentence-end { - opacity: 0.8; -} - -.transcription-gradient { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 30px; - background: linear-gradient( - to top, - rgba(26, 16, 51, 0.8) 0%, - transparent 100% - ); - pointer-events: none; -} - -/* Custom scrollbar for transcription */ -.transcription-viewport::-webkit-scrollbar { - width: 4px; -} - -.transcription-viewport::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 2px; -} - -.transcription-viewport::-webkit-scrollbar-thumb { - background: rgba(118, 75, 162, 0.6); - border-radius: 2px; -} - -/* ================================ - TOPIC CONSTELLATION - ================================ */ - -.topic-constellation { - margin: 0 32px 24px; -} - -.constellation-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; -} - -.constellation-title { - font-size: 14px; - font-weight: 600; - color: rgba(255, 255, 255, 0.8); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.constellation-refresh { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - cursor: pointer; - transition: all 0.3s ease; - color: rgba(255, 255, 255, 0.6); -} - -.constellation-refresh:hover { - background: rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.8); - transform: translateY(-1px); -} - -.constellation-refresh svg { - transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); -} - -.topic-nodes { - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -.topic-node { - position: relative; - cursor: pointer; - animation: nodeAppear 0.6s cubic-bezier(0.4, 0, 0.2, 1); - animation-fill-mode: both; - animation-delay: var(--node-delay, 0ms); -} - -@keyframes nodeAppear { - from { - opacity: 0; - transform: translateY(20px) scale(0.9); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.node-content { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - background: linear-gradient( - 135deg, - rgba(255, 255, 255, 0.06) 0%, - rgba(255, 255, 255, 0.02) 100% - ); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 12px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - z-index: 2; - backdrop-filter: blur(10px); -} - -.topic-node:hover .node-content { - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.15) 0%, - rgba(240, 147, 251, 0.08) 100% - ); - border-color: rgba(118, 75, 162, 0.3); - transform: translateY(-2px); -} - -.topic-node.selected .node-content { - background: linear-gradient( - 135deg, - rgba(240, 147, 251, 0.2) 0%, - rgba(118, 75, 162, 0.15) 100% - ); - border-color: rgba(240, 147, 251, 0.5); - transform: scale(1.05); -} - -.node-icon { - font-size: 16px; - opacity: 0.9; -} - -.node-text { - font-size: 13px; - font-weight: 500; - color: rgba(255, 255, 255, 0.85); - white-space: nowrap; -} - -.node-glow { - position: absolute; - inset: -2px; - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.3) 0%, - rgba(240, 147, 251, 0.3) 100% - ); - border-radius: 14px; - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1; - filter: blur(8px); -} - -.topic-node:hover .node-glow { - opacity: 0.6; -} - -.topic-node.selected .node-glow { - opacity: 1; - animation: selectedGlow 2s ease-in-out; -} - -@keyframes selectedGlow { - 0%, 100% { - opacity: 0.8; - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(1.1); - } -} - -/* ================================ - CONTROL PANEL - ================================ */ - -.control-panel { - display: flex; - align-items: center; - justify-content: space-between; - margin: 0 32px 24px; - gap: 20px; -} - -.primary-controls { - flex: 1; -} - -.emma-control-btn { - display: flex; - align-items: center; - gap: 16px; - padding: 16px 24px; - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.8) 0%, - rgba(240, 147, 251, 0.8) 100% - ); - border: 1px solid rgba(240, 147, 251, 0.4); - border-radius: 12px; - color: white; - font-size: 15px; - font-weight: 600; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; - width: 100%; - max-width: 280px; -} - -.emma-control-btn:hover { - background: linear-gradient( - 135deg, - rgba(118, 75, 162, 0.9) 0%, - rgba(240, 147, 251, 0.9) 100% - ); - transform: translateY(-2px); - box-shadow: - 0 8px 25px rgba(118, 75, 162, 0.3), - 0 4px 10px rgba(0, 0, 0, 0.2); -} - -.emma-control-btn:active { - transform: translateY(-1px); -} - -.emma-control-btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none; - box-shadow: none; -} - -.emma-control-btn.pause-btn { - background: linear-gradient( - 135deg, - rgba(255, 193, 7, 0.8) 0%, - rgba(255, 152, 0, 0.8) 100% - ); - border-color: rgba(255, 193, 7, 0.4); -} - -.emma-control-btn.pause-btn:hover { - background: linear-gradient( - 135deg, - rgba(255, 193, 7, 0.9) 0%, - rgba(255, 152, 0, 0.9) 100% - ); -} - -.btn-icon { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; -} - -.btn-content { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; -} - -.btn-label { - font-size: 15px; - font-weight: 600; - line-height: 1.2; -} - -.btn-shortcut { - font-size: 11px; - opacity: 0.8; - font-weight: 400; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.secondary-controls { - display: flex; - gap: 12px; -} - -.emma-action-btn { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 10px; - color: rgba(255, 255, 255, 0.8); - font-size: 13px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; -} - -.emma-action-btn:hover { - background: rgba(255, 255, 255, 0.08); - color: rgba(255, 255, 255, 0.95); - transform: translateY(-1px); -} - -.emma-action-btn.save-btn:hover { - background: rgba(76, 175, 80, 0.2); - border-color: rgba(76, 175, 80, 0.4); - color: rgba(76, 175, 80, 1); -} - -.emma-action-btn.discard-btn:hover { - background: rgba(244, 67, 54, 0.2); - border-color: rgba(244, 67, 54, 0.4); - color: rgba(244, 67, 54, 1); -} - -/* ================================ - PROGRESS SYSTEM - ================================ */ - -.progress-system { - margin: 0 32px 20px; -} - -.progress-track { - position: relative; - height: 4px; - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; - overflow: hidden; - margin-bottom: 12px; -} - -.progress-fill { - height: 100%; - background: linear-gradient( - 90deg, - #667eea 0%, - #764ba2 50%, - #f093fb 100% - ); - border-radius: 2px; - width: 0%; - transition: width 0.3s ease; - position: relative; -} - -.progress-fill::after { - content: ''; - position: absolute; - top: 0; - right: 0; - width: 20px; - height: 100%; - background: linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.4) 100% - ); - border-radius: 0 2px 2px 0; -} - -.progress-markers { - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; - pointer-events: none; -} - -.marker { - position: absolute; - top: 0; - width: 1px; - height: 100%; - background: rgba(255, 255, 255, 0.3); -} - -.progress-meta { - display: flex; - align-items: center; - justify-content: space-between; -} - -.time-display { - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - color: rgba(255, 255, 255, 0.7); - font-variant-numeric: tabular-nums; -} - -.time-separator { - opacity: 0.5; -} - -.max-duration { - opacity: 0.5; -} - -.quality-indicator { - display: flex; - align-items: center; - gap: 8px; -} - -.quality-bars { - display: flex; - gap: 2px; -} - -.quality-bars .bar { - width: 3px; - height: 12px; - background: rgba(255, 255, 255, 0.2); - border-radius: 1px; - transition: all 0.3s ease; -} - -.quality-bars .bar.active { - background: linear-gradient( - 180deg, - #4ade80 0%, - #22c55e 100% - ); -} - -.quality-label { - font-size: 11px; - color: rgba(255, 255, 255, 0.6); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* ================================ - NEURAL VISUALIZER - ================================ */ - -.neural-visualizer { - margin: 0 32px 20px; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transition: opacity 0.5s ease; -} - -.neural-visualizer.active { - opacity: 1; -} - -.visualizer-grid { - display: flex; - align-items: end; - justify-content: center; - gap: 4px; - height: 40px; -} - -.frequency-bar { - width: 3px; - height: 8px; - background: linear-gradient( - 180deg, - rgba(240, 147, 251, 0.8) 0%, - rgba(118, 75, 162, 0.6) 100% - ); - border-radius: 1px; - transform-origin: bottom; - transition: all 0.1s ease; - animation: frequencyDance 2s ease-in-out infinite; - animation-delay: var(--delay, 0ms); -} - -@keyframes frequencyDance { - 0%, 100% { - transform: scaleY(0.3); - opacity: 0.6; - } - 50% { - transform: scaleY(1); - opacity: 1; - } -} - -.neural-visualizer.active .frequency-bar { - animation-play-state: running; -} - -.neural-visualizer:not(.active) .frequency-bar { - animation-play-state: paused; - transform: scaleY(0.2); - opacity: 0.3; -} - -/* ================================ - RESPONSIVE DESIGN - ================================ */ - -@media (max-width: 600px) { - .voice-studio-header { - padding: 20px 24px 16px; - flex-direction: column; - gap: 16px; - align-items: stretch; - } - - .emma-presence { - justify-content: center; - text-align: center; - } - - .studio-status { - justify-content: center; - } - - .transcription-canvas, - .topic-constellation, - .control-panel, - .progress-system, - .neural-visualizer { - margin-left: 24px; - margin-right: 24px; - } - - .control-panel { - flex-direction: column; - gap: 16px; - } - - .emma-control-btn { - max-width: none; - } - - .secondary-controls { - justify-content: center; - } - - .topic-nodes { - justify-content: center; - } -} - -/* ================================ - ACCESSIBILITY & MOTION - ================================ */ - -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -.emma-control-btn:focus, -.emma-action-btn:focus, -.topic-node:focus, -.constellation-refresh:focus { - outline: 2px solid rgba(240, 147, 251, 0.8); - outline-offset: 2px; -} - -/* ================================ - FOCUS MODE INTEGRATION - ================================ */ - -body.voice-capture-focus .info-panel, -body.voice-capture-focus .welcome-text, -body.voice-capture-focus .quick-stats, -body.voice-capture-focus .radial-menu, -body.voice-capture-focus .constellation-canvas { - opacity: 0.15 !important; - filter: blur(3px) !important; - pointer-events: none !important; - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; -} - -/* ================================ - HIGH-DPI DISPLAY OPTIMIZATION - ================================ */ - -@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - .neural-core { - box-shadow: - 0 0 20px rgba(118, 75, 162, 0.4), - inset 0 1px 2px rgba(255, 255, 255, 0.2); - } - - .progress-track, - .frequency-bar, - .quality-bars .bar { - transform: translateZ(0); - } -} - diff --git a/css/voice-capture.css b/css/voice-capture.css deleted file mode 100644 index 206ee94f..00000000 --- a/css/voice-capture.css +++ /dev/null @@ -1,411 +0,0 @@ -/** - * Voice Capture Experience Styles - * Emma Memory Creation System - */ - -.voice-capture-experience { - padding: 0; - background: none; - max-height: 70vh; - overflow-y: auto; -} - -/* Header Section */ -.voice-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 24px; - padding: 20px 24px 0; -} - -.emma-avatar { - position: relative; -} - -.orb-mini { - width: 48px; - height: 48px; - border-radius: 50%; - background: radial-gradient(circle at 30% 30%, - rgba(138, 94, 250, 0.9) 0%, - rgba(28, 15, 57, 0.95) 100%); - border: 2px solid rgba(138, 94, 250, 0.5); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; - box-shadow: 0 0 20px rgba(138, 94, 250, 0.3); -} - -.orb-mini.pulsing { - animation: orbPulse 2s ease-in-out infinite; - box-shadow: 0 0 30px rgba(138, 94, 250, 0.6); -} - -@keyframes orbPulse { - 0%, 100% { - transform: scale(1); - box-shadow: 0 0 20px rgba(138, 94, 250, 0.3); - } - 50% { - transform: scale(1.1); - box-shadow: 0 0 40px rgba(138, 94, 250, 0.8); - } -} - -.greeting-text h3 { - margin: 0 0 4px 0; - font-size: 20px; - font-weight: 600; - color: var(--emma-purple); - background: linear-gradient(135deg, #8A5EFA, #B794F6); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.greeting-text .hint { - margin: 0; - font-size: 14px; - color: rgba(255, 255, 255, 0.7); - transition: color 0.3s ease; -} - -/* Transcription Display */ -.transcription-container { - margin: 0 24px 24px; - position: relative; -} - -.transcription-display { - background: rgba(28, 15, 57, 0.6); - border: 1px solid rgba(138, 94, 250, 0.3); - border-radius: 12px; - padding: 20px; - min-height: 120px; - max-height: 200px; - overflow-y: auto; - font-size: 16px; - line-height: 1.5; - color: white; - white-space: pre-wrap; - word-wrap: break-word; - transition: all 0.3s ease; -} - -.transcription-display:hover { - border-color: rgba(138, 94, 250, 0.5); -} - -.transcription-placeholder { - color: rgba(255, 255, 255, 0.5); - font-style: italic; -} - -.word-count { - position: absolute; - bottom: -20px; - right: 0; - font-size: 12px; - color: rgba(255, 255, 255, 0.6); -} - -/* Topic Suggestions */ -.topic-suggestions { - margin: 0 24px 24px; -} - -.suggestions-label { - display: block; - font-size: 14px; - font-weight: 500; - color: rgba(255, 255, 255, 0.8); - margin-bottom: 12px; -} - -.suggestion-cards { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.suggestion-card { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: rgba(138, 94, 250, 0.1); - border: 1px solid rgba(138, 94, 250, 0.3); - border-radius: 20px; - cursor: pointer; - transition: all 0.2s ease; - font-size: 13px; - color: rgba(255, 255, 255, 0.9); -} - -.suggestion-card:hover { - background: rgba(138, 94, 250, 0.2); - border-color: rgba(138, 94, 250, 0.5); - transform: translateY(-1px); -} - -.suggestion-card:active { - transform: translateY(0); -} - -.suggestion-icon { - font-size: 14px; -} - -.suggestion-text { - font-weight: 500; -} - -/* Voice Controls */ -.voice-controls { - display: flex; - gap: 12px; - justify-content: center; - margin: 0 24px 20px; - flex-wrap: wrap; -} - -.voice-btn { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - border: none; - border-radius: 8px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - background: rgba(255, 255, 255, 0.1); - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.voice-btn:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); - transform: translateY(-1px); -} - -.voice-btn:active { - transform: translateY(0); -} - -.voice-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; -} - -.voice-btn.primary { - background: rgba(138, 94, 250, 0.8); - border-color: rgba(138, 94, 250, 1); -} - -.voice-btn.primary:hover { - background: rgba(138, 94, 250, 0.9); -} - -.voice-btn.secondary { - background: rgba(255, 193, 7, 0.8); - border-color: rgba(255, 193, 7, 1); - color: #000; -} - -.voice-btn.secondary:hover { - background: rgba(255, 193, 7, 0.9); -} - -.voice-btn.success { - background: rgba(40, 167, 69, 0.8); - border-color: rgba(40, 167, 69, 1); -} - -.voice-btn.success:hover { - background: rgba(40, 167, 69, 0.9); -} - -.voice-icon { - font-size: 16px; -} - -/* Progress Indicator */ -.capture-progress { - margin: 0 24px 20px; -} - -.progress-bar { - width: 100%; - height: 6px; - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; - overflow: hidden; - margin-bottom: 8px; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, #8A5EFA, #B794F6); - border-radius: 3px; - width: 0%; - transition: width 0.3s ease; -} - -.progress-info { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 12px; - color: rgba(255, 255, 255, 0.7); -} - -.separator { - color: rgba(255, 255, 255, 0.5); -} - -.max-duration { - color: rgba(255, 255, 255, 0.5); -} - -/* Audio Visualizer */ -.audio-visualizer { - display: none; - justify-content: center; - align-items: flex-end; - gap: 3px; - height: 40px; - margin: 0 24px 16px; -} - -.wave-bar { - width: 4px; - min-height: 8px; - background: linear-gradient(180deg, #8A5EFA, #B794F6); - border-radius: 2px; - transition: height 0.1s ease; -} - -/* Custom Scrollbar */ -.transcription-display::-webkit-scrollbar { - width: 6px; -} - -.transcription-display::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; -} - -.transcription-display::-webkit-scrollbar-thumb { - background: rgba(138, 94, 250, 0.5); - border-radius: 3px; -} - -.transcription-display::-webkit-scrollbar-thumb:hover { - background: rgba(138, 94, 250, 0.7); -} - -/* Responsive Design */ -@media (max-width: 480px) { - .voice-header { - padding: 16px 20px 0; - } - - .transcription-container, - .topic-suggestions, - .voice-controls, - .capture-progress, - .audio-visualizer { - margin-left: 20px; - margin-right: 20px; - } - - .voice-controls { - flex-direction: column; - align-items: stretch; - } - - .voice-btn { - justify-content: center; - } - - .suggestion-cards { - flex-direction: column; - } - - .suggestion-card { - justify-content: center; - } -} - -/* Focus Mode Animation */ -body.voice-capture-focus .info-panel, -body.voice-capture-focus .welcome-text, -body.voice-capture-focus .quick-stats, -body.voice-capture-focus .radial-menu { - opacity: 0.3 !important; - filter: blur(2px) !important; - pointer-events: none !important; - transition: all 0.3s ease !important; -} - -/* Toast Notifications */ -.voice-toast { - font-family: 'Inter', system-ui, sans-serif; - font-weight: 500; - animation: toastSlideIn 0.3s ease; -} - -@keyframes toastSlideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Special States */ -.voice-capture-experience.error .transcription-display { - border-color: rgba(255, 71, 87, 0.5); -} - -.voice-capture-experience.success .progress-fill { - background: linear-gradient(90deg, #2ed573, #7bed9f); -} - -/* Accessibility */ -.voice-btn:focus, -.suggestion-card:focus { - outline: 2px solid rgba(138, 94, 250, 0.8); - outline-offset: 2px; -} - -@media (prefers-reduced-motion: reduce) { - .orb-mini.pulsing { - animation: none; - } - - .wave-bar { - transition: none; - } - - .voice-toast { - animation: none; - } -} - -/* Dark theme adjustments */ -@media (prefers-color-scheme: dark) { - .voice-btn.secondary { - color: #1a1a1a; - } -} - diff --git a/css/z-index-hierarchy.css b/css/z-index-hierarchy.css index 9da764ab..a3b3474b 100644 --- a/css/z-index-hierarchy.css +++ b/css/z-index-hierarchy.css @@ -269,13 +269,13 @@ body:has(.edit-memory-modal) .memories-container { ============================================================================= */ /* CRITICAL FIX: Add Person button needs white background for visibility */ -button[style*="background: #8658ff"], -button[style*="background:#8658ff"], +button[style*="background: #6f63d9"], +button[style*="background:#6f63d9"], .add-person-btn, button:contains("Add Person") { background: white !important; - color: #8658ff !important; - border: 2px solid #8658ff !important; + color: #6f63d9 !important; + border: 2px solid #6f63d9 !important; font-weight: 600 !important; text-shadow: none !important; } @@ -285,8 +285,8 @@ button:contains("Add Person") { .person-add-button, [style*="border: 2px dashed"] { background: white !important; - color: #8658ff !important; - border: 2px dashed #8658ff !important; + color: #6f63d9 !important; + border: 2px dashed #6f63d9 !important; font-weight: 600 !important; opacity: 1 !important; } @@ -296,8 +296,8 @@ button[title*="Add"], button[aria-label*="Add"], *[onclick*="addPerson"] { background: white !important; - color: #8658ff !important; - border: 2px solid #8658ff !important; + color: #6f63d9 !important; + border: 2px solid #6f63d9 !important; font-weight: 600 !important; } diff --git a/dashboard.html b/dashboard.html index f4a589b3..7d3f9818 100644 --- a/dashboard.html +++ b/dashboard.html @@ -38,1718 +38,20 @@ + + + + - @@ -1818,26 +120,20 @@

emma

-
+
-
- - - +
+
@@ -1852,10 +148,10 @@

emma

-
-
๐ŸŒŸ
-
Upgrade to .emma
-
NEW
+
+
๐ŸŒŸ
+
Upgrade to .emma
+
NEW
0
@@ -2013,18 +309,18 @@

Scan QR Code

- - - @@ -2047,8 +343,10 @@

Scan QR Code

+ + @@ -2062,5788 +360,6 @@

Scan QR Code

- + diff --git a/data/vault.json b/data/vault.json new file mode 100644 index 00000000..bd9e6580 --- /dev/null +++ b/data/vault.json @@ -0,0 +1,4 @@ +{ + "people": [], + "memories": [] +} diff --git a/docs/EMMA-SYSTEM-DOCUMENTATION.md b/docs/EMMA-SYSTEM-DOCUMENTATION.md index 56f3d7c7..0b998729 100644 --- a/docs/EMMA-SYSTEM-DOCUMENTATION.md +++ b/docs/EMMA-SYSTEM-DOCUMENTATION.md @@ -362,11 +362,11 @@ Detailed Steps: **Emma Brand Colors**: ```css ---emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); +--emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); --emma-gradient-2: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #a855f7 100%); --emma-bg-gradient: linear-gradient(135deg, #1a1033 0%, #2d1b69 50%, #0f0c29 100%); --emma-purple: #764ba2; ---emma-pink: #f093fb; +--emma-pink: #deb3e4; --emma-glass: rgba(255, 255, 255, 0.1); ``` @@ -374,7 +374,7 @@ Detailed Steps: - Backdrop blur: `backdrop-filter: blur(20px)` - Transparent backgrounds: `rgba(255, 255, 255, 0.1)` - Subtle borders: `rgba(255, 255, 255, 0.2)` -- Depth with shadows: `0 8px 32px rgba(139, 92, 246, 0.15)` +- Depth with shadows: `0 8px 32px rgba(111, 99, 217, 0.15)` ### **Emma Orb Integration** diff --git a/docs/EMMA-WEB-VAULT-SECURITY-ANALYSIS.md b/docs/EMMA-WEB-VAULT-SECURITY-ANALYSIS.md new file mode 100644 index 00000000..a49b6697 --- /dev/null +++ b/docs/EMMA-WEB-VAULT-SECURITY-ANALYSIS.md @@ -0,0 +1,45 @@ +๏ปฟ# Emma Web Vault Security Analysis (Post-Mitigation) + +_Prepared by Codex โ€” October 19, 2025_ + +This document tracks the security posture of the web vault after the October 2025 hardening work. It records every mitigation that landed in the current branch and highlights remaining risks. + +## Mitigations Implemented +- **Encrypted IndexedDB persistence** โ€“ saveToIndexedDB() now stores AES-256-GCM ciphertext plus metadata. Legacy plaintext backups re-encrypt the next time a vault is unlocked (js/emma-web-vault.js, overrides in the 2700s). +- **No automatic plaintext restoration** โ€“ Vault startup no longer marks the vault as open. The constructor caches only encrypted data; estoreVaultState() and the public unlock flow insist on a passphrase before decryption (js/emma-web-vault.js, constructor / restore helpers, window.emmaAPI.vault.unlock). +- **Media encryption enforcement** โ€“ Data URLs are converted to binary and routed through encryptData(), so every stored attachment carries encrypted: true (js/emma-web-vault.js, media helpers near the 1000s). +- **Passphrase lifecycle hardening** โ€“ The passphrase lives in memory only, clears after ~15 minutes of inactivity, and must be re-entered before any operation that needs the key (js/emma-web-vault.js, helpers around 2600โ€“2700). +- **Safe IndexedDB/file fallback** โ€“ When direct file handles are missing, the vault keeps encrypted backups and prompts the user to re-establish access gracefully. +- **PBKDF2 standardization** โ€“ New saves use PBKDF2-SHA256 with 310k iterations. decryptData() records the iteration count, and exactWorkingDecrypt() now tries 250k and 100k iterations (covering historical extension files and newly saved web vaults) before failing (js/emma-web-vault.js, crypto helpers ~2000โ€“2350). +- **Lazy vault bootstrap** โ€“ index.html exposes ensureEmmaWebVault() so UI entries load the vault script on demand before invoking crypto helpers, eliminating null-reference errors when the script is not yet loaded. +- **Inline script reduction / safer CSP** โ€“ The dashboard inline script moved into js/dashboard-init.js, event handlers were swapped for delegated listeners, and the backend CSP drops 'unsafe-eval' (dashboard.html, js/dashboard-init.js, server.js). +- **Console hygiene** โ€“ Emoji-heavy logs were replaced with ASCII-only messages to keep security-relevant traces readable. + +## Findings and Status +### Critical +1. **Plaintext IndexedDB backups** โ€“ _Resolved._ IndexedDB now stores ciphertext/metadata only; legacy records re-encrypt on unlock. +2. **Passphrase bypass via auto-restore** โ€“ _Resolved._ Vaults stay locked until the user supplies the passphrase; cached data remains encrypted. +3. **Unencrypted media attachments** โ€“ _Resolved._ Every attachment is encrypted prior to persistence, and the vault tracks encrypted vs. legacy entries. +4. **Vault APIs exposed with permissive CSP** โ€“ _Partially mitigated._ The dashboard removed inline script and 'unsafe-eval', but many legacy pages still rely on inline handlers, so 'unsafe-inline' remains. Full CSP hardening depends on migrating those views. + +### Medium +5. **Inconsistent KDF iterations** โ€“ _Resolved._ Encryption records the iteration count used; decryption tries the recorded value and known legacy fallbacks. +6. **Verbose debug logging** โ€“ _Outstanding._ Detailed debug output (e.g., attachment previews) remains and should be trimmed or gated before production. + +## Verification Checklist +- IndexedDB (EmmaVault/current) contains encrypted bytes and metadataโ€”no plaintext aultData. +- Unlock flow always prompts for the passphrase; successful unlocks re-encrypt legacy backups. +- Media saved through the UI is flagged encrypted: true and decrypts only after the passphrase is available in memory. +- exactWorkingDecrypt() opens vault files produced both by the legacy extension and the hardened web workflow (it tolerates optional version headers and multiple PBKDF2 iteration counts). +- dashboard.html loads js/dashboard-init.js; no inline + @@ -258,33 +486,49 @@

emma

-
๐Ÿ”’ No vault loaded
+
+ +
+
No vault loaded
+
Connect your .emma vault to get started.
+
+
-
- ๐Ÿ’พ - Sync Status +
+ ๐Ÿ’พ + Sync Status
+
@@ -304,6 +548,27 @@

emma

* Clean Emma Pure Web App * Perfect architecture for Debbe's demo */ + + async function ensureEmmaWebVault() { + if (window.emmaWebVault) return; + + if (typeof EmmaWebVault === 'undefined') { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'js/emma-web-vault.js'; + script.onload = resolve; + script.onerror = () => reject(new Error('Failed to load Emma Web Vault script.')); + document.head.appendChild(script); + }); + } + + if (typeof EmmaWebVault === 'undefined') { + throw new Error('Emma Web Vault system unavailable.'); + } + + window.emmaWebVault = new EmmaWebVault(); + } + class CleanEmmaApp { constructor() { this.vaultStatus = 'locked'; @@ -323,8 +588,11 @@

emma

this.orb = document.getElementById('emmaOrb'); this.vaultStatusDiv = document.getElementById('vaultStatus'); this.vaultStatusText = document.getElementById('vaultStatusText'); + this.vaultStatusIcon = document.getElementById('vaultStatusIcon'); + this.vaultStatusHelper = document.getElementById('vaultStatusHelper'); this.openVaultBtn = document.getElementById('openVaultBtn'); this.createVaultBtn = document.getElementById('createVaultBtn'); + this.recoverVaultBtn = document.getElementById('recoverVaultBtn'); this.goToDashboardBtn = document.getElementById('goToDashboardBtn'); this.dragDropZone = document.getElementById('dragDropZone'); this.fileInput = document.getElementById('vaultFileInput'); @@ -342,7 +610,7 @@

emma

} else { console.warn('โš ๏ธ Emma orb class not available, using CSS fallback'); - this.orb.style.background = 'radial-gradient(circle at 30% 30%, #8658ff, #4f46e5, #f093fb)'; + this.orb.style.background = 'radial-gradient(circle at 30% 30%, #6f63d9, #4f46e5, #deb3e4)'; this.orb.style.animation = 'orb-pulse 4s ease-in-out infinite'; } } catch (error) { @@ -372,6 +640,11 @@

emma

// Create vault button this.createVaultBtn.addEventListener('click', () => this.createVault()); + // Recover browser backup + if (this.recoverVaultBtn) { + this.recoverVaultBtn.addEventListener('click', () => this.recoverFromBrowserBackup()); + } + // Go to dashboard button this.goToDashboardBtn.addEventListener('click', () => this.goToDashboard()); @@ -447,6 +720,7 @@

emma

if (!passphrase) return; // Create new vault using Emma Web Vault + await ensureEmmaWebVault(); const result = await window.emmaWebVault.createVaultFile(vaultName, passphrase); if (result.success) { @@ -467,6 +741,9 @@

emma

// Read file data first const fileData = new Uint8Array(await file.arrayBuffer()); + if (window.emmaWebVault && typeof window.emmaWebVault.inspectVaultBytes === 'function') { + window.emmaWebVault.inspectVaultBytes(fileData, file.name); + } // Use working Emma modal (copied from extension) const passphrase = await this.showEmmaPassphraseModal(file.name); @@ -474,22 +751,46 @@

emma

if (!passphrase) return; // Use EXACT working crypto from extension + await ensureEmmaWebVault(); const vaultData = await window.emmaWebVault.exactWorkingDecrypt(fileData, passphrase); // Set vault data in Emma Web Vault window.emmaWebVault.vaultData = vaultData; window.emmaWebVault.isOpen = true; window.emmaWebVault.passphrase = passphrase; + if (window.emmaWebVault.cacheSessionPassphrase) { + window.emmaWebVault.cacheSessionPassphrase(passphrase); + } + if (typeof window.emmaWebVault.updateOriginalFileName === 'function') { + window.emmaWebVault.updateOriginalFileName(file.name); + } else { + window.emmaWebVault.originalFileName = file.name; + try { + sessionStorage.setItem('emmaVaultOriginalFileName', file.name); + localStorage.setItem('emmaVaultOriginalFileName', file.name); + } catch (_) {} + } if (fileHandle) { window.emmaWebVault.fileHandle = fileHandle; } + if (!fileHandle && window.emmaWebVault && typeof window.emmaWebVault.ensureDirectFileHandle === 'function') { + try { + await window.emmaWebVault.ensureDirectFileHandle('initial-open', { promptOnFailure: true }); + } catch (handleError) { + console.warn('โš ๏ธ Emma Web Vault: Unable to capture direct file access automatically:', handleError); + } + } + + const normalizedVaultName = window.emmaWebVault.ensureVaultMetadataName( + vaultData.metadata?.name || vaultData.name || file.name + ); // Set session storage sessionStorage.setItem('emmaVaultActive', 'true'); - sessionStorage.setItem('emmaVaultName', vaultData.metadata?.name || file.name); + sessionStorage.setItem('emmaVaultName', normalizedVaultName); // SECURITY: Never store passphrase in web storage - keep in memory only localStorage.setItem('emmaVaultActive', 'true'); - localStorage.setItem('emmaVaultName', vaultData.metadata?.name || file.name); + localStorage.setItem('emmaVaultName', normalizedVaultName); // Save to IndexedDB for persistence (version 2 - clean rebuild) try { @@ -502,7 +803,7 @@

emma

this.vaultStatus = 'unlocked'; this.currentVault = { - name: vaultData.metadata?.name || file.name, + name: normalizedVaultName, stats: { memoryCount: Object.keys(vaultData.content?.memories || {}).length, peopleCount: Object.keys(vaultData.content?.people || {}).length @@ -559,14 +860,28 @@

emma

if (this.vaultStatus === 'unlocked') { this.orbContainer.classList.remove('vault-locked'); this.vaultStatusDiv.classList.add('unlocked'); - this.vaultStatusText.textContent = `๐Ÿ”“ ${this.currentVault.name} (${this.currentVault.stats?.memoryCount || 0} memories)`; + const vaultName = this.currentVault?.name || 'Vault unlocked'; + const memoryCount = this.currentVault?.stats?.memoryCount || 0; + this.vaultStatusText.textContent = `${vaultName} (${memoryCount} memories)`; + if (this.vaultStatusIcon) { + this.vaultStatusIcon.textContent = '๐Ÿ”“'; + } + if (this.vaultStatusHelper) { + this.vaultStatusHelper.textContent = 'Unlocked โ€ข Secure session active in this browser'; + } // Show dashboard button for mobile users this.goToDashboardBtn.style.display = 'block'; } else { this.orbContainer.classList.add('vault-locked'); this.vaultStatusDiv.classList.remove('unlocked'); - this.vaultStatusText.textContent = '๐Ÿ”’ No vault loaded'; + this.vaultStatusText.textContent = 'No vault loaded'; + if (this.vaultStatusIcon) { + this.vaultStatusIcon.textContent = '๐Ÿ”’'; + } + if (this.vaultStatusHelper) { + this.vaultStatusHelper.textContent = 'Connect your .emma vault to continue.'; + } // Hide dashboard button when locked this.goToDashboardBtn.style.display = 'none'; @@ -578,6 +893,36 @@

emma

window.location.href = 'dashboard.html'; } + async recoverFromBrowserBackup() { + try { + await ensureEmmaWebVault(); + + if (!window.emmaWebVault || typeof window.emmaWebVault.hasBrowserBackup !== 'function') { + this.showError('Recovery system unavailable in this browser.'); + return; + } + + const hasBackup = await window.emmaWebVault.hasBrowserBackup(); + if (!hasBackup) { + this.showError('No browser backup found. Please open your .emma file or create a new vault.'); + return; + } + + const passphrase = await this.showEmmaPassphraseModal('Recovered Vault', true); + if (!passphrase) return; + + await window.emmaWebVault.recoverFromBrowserBackup(passphrase); + this.vaultStatus = 'unlocked'; + this.currentVault = { name: window.emmaWebVault.vaultData?.metadata?.name || 'Recovered Vault' }; + this.updateUI(); + this.showSuccess('Recovered vault restored. Downloading fresh .emma file...'); + await window.emmaWebVault.downloadVaultFile(this.currentVault.name || 'Recovered-Vault'); + } catch (error) { + console.error('? Recovery failed:', error); + this.showError('Failed to recover from browser backup: ' + (error?.message || error)); + } + } + showSuccess(message) { this.showToast(message, 'success'); } @@ -638,8 +983,8 @@

emma

padding: 30px; max-width: 400px; width: 90%; - border: 2px solid rgba(134, 88, 255, 0.3); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + border: 2px solid rgba(111, 99, 217, 0.3); + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.45); `; // SECURITY: Create DOM elements safely without innerHTML injection @@ -666,7 +1011,7 @@

emma

emma emma cursor: pointer; ">Cancel
-

${message}

+

${esc(message)}

@@ -244,7 +248,7 @@ const cleanModalStyles = ` border-radius: 20px; max-width: 450px; width: 90%; - box-shadow: 0 0 40px rgba(134, 88, 255, 0.6); + box-shadow: 0 0 40px rgba(111, 99, 217, 0.6); animation: slideIn 0.3s ease; } @@ -260,7 +264,7 @@ const cleanModalStyles = ` font-size: 1.5rem; font-weight: 700; margin: 0; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -318,7 +322,7 @@ const cleanModalStyles = ` outline: none; border-color: #764ba2; background: rgba(255, 255, 255, 0.08); - box-shadow: 0 0 0 3px rgba(134, 88, 255, 0.2); + box-shadow: 0 0 0 3px rgba(111, 99, 217, 0.2); } .clean-input::placeholder { @@ -375,7 +379,7 @@ const cleanModalStyles = ` .clean-btn-confirm:hover { transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(134, 88, 255, 0.3); + box-shadow: 0 8px 25px rgba(111, 99, 217, 0.3); } @keyframes fadeIn { diff --git a/js/clever-capture-bypass.js b/js/clever-capture-bypass.js index 37e9570f..681eda84 100644 --- a/js/clever-capture-bypass.js +++ b/js/clever-capture-bypass.js @@ -409,7 +409,7 @@ class EmmaCleverCaptureBypass { z-index: 999999; font-family: system-ui; backdrop-filter: blur(20px); - border: 1px solid rgba(139, 92, 246, 0.3); + border: 1px solid rgba(111, 99, 217, 0.3); min-width: 300px; `; diff --git a/js/content-universal.js b/js/content-universal.js index 542b48f2..99574640 100644 --- a/js/content-universal.js +++ b/js/content-universal.js @@ -1275,7 +1275,7 @@ if (window.emmaUniversalInjected) { right: 24px !important; width: 56px !important; height: 56px !important; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%) !important; + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%) !important; border: 2px solid white !important; border-radius: 50% !important; cursor: pointer !important; @@ -1924,7 +1924,7 @@ if (window.emmaUniversalInjected) { createShader(type, source) { const shader = this.gl.createShader(type); this.gl.shaderSource(shader, source); this.gl.compileShader(shader); if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { console.error('Shader compile error:', this.gl.getShaderInfoLog(shader)); this.gl.deleteShader(shader); return null; } return shader; } createProgram(vertexSource, fragmentSource) { const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexSource); const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentSource); if (!vertexShader || !fragmentShader) return null; const program = this.gl.createProgram(); this.gl.attachShader(program, vertexShader); this.gl.attachShader(program, fragmentShader); this.gl.linkProgram(program); if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { console.error('Program link error:', this.gl.getProgramInfoLog(program)); this.gl.deleteProgram(program); return null; } return program; } useFallback() { - this.container.innerHTML = '
'; + this.container.innerHTML = '
'; } } @@ -2077,9 +2077,9 @@ if (window.emmaUniversalInjected) { style.textContent = ` /* Emma CSS Variables (same as dashboard) */ :root { - --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --emma-gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); --emma-purple: #764ba2; - --emma-pink: #f093fb; + --emma-pink: #deb3e4; --emma-blue: #667eea; } @@ -2167,7 +2167,7 @@ if (window.emmaUniversalInjected) { /* Header with Orb */ .emma-panel-header { padding: 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); border-radius: 20px 20px 0 0; display: flex; justify-content: space-between; @@ -2283,7 +2283,7 @@ if (window.emmaUniversalInjected) { .emma-action-card:hover { transform: translateY(-2px); background: rgba(255, 255, 255, 0.08); - border-color: rgba(240, 147, 251, 0.3); + border-color: rgba(222, 179, 228, 0.3); box-shadow: 0 8px 24px rgba(118, 75, 162, 0.2); } @@ -2364,9 +2364,9 @@ if (window.emmaUniversalInjected) { .analysis-value { font-size: 12px; - color: rgba(240, 147, 251, 0.9); + color: rgba(222, 179, 228, 0.9); font-weight: 600; - background: rgba(240, 147, 251, 0.1); + background: rgba(222, 179, 228, 0.1); padding: 4px 8px; border-radius: 6px; } @@ -2854,11 +2854,11 @@ if (window.emmaUniversalInjected) { const panelOrb = document.getElementById('emma-panel-orb'); if (floatingOrb) { - floatingOrb.innerHTML = '
'; + floatingOrb.innerHTML = '
'; } if (panelOrb) { - panelOrb.innerHTML = '
'; + panelOrb.innerHTML = '
'; } } @@ -2875,7 +2875,7 @@ if (window.emmaUniversalInjected) { if (!window.EmmaOrb) { console.error('โŒ NO EMMA ORB CLASS FOUND! Using fallback.'); - orbContainer.innerHTML = '
'; + orbContainer.innerHTML = '
'; return; } @@ -2892,7 +2892,7 @@ if (window.emmaUniversalInjected) { console.error('๐Ÿ’ฅ FAILED TO CREATE YOUR ORB:', error); console.error('Error stack:', error.stack); // Beautiful gradient fallback - orbContainer.innerHTML = '
'; + orbContainer.innerHTML = '
'; } } @@ -2918,11 +2918,11 @@ if (window.emmaUniversalInjected) { } catch (error) { console.warn('Failed to initialize panel orb:', error); // Beautiful gradient fallback (NO BRAIN EMOJI!) - orbContainer.innerHTML = '
'; + orbContainer.innerHTML = '
'; } } else if (orbContainer) { // Beautiful gradient fallback (NO BRAIN EMOJI!) - orbContainer.innerHTML = '
'; + orbContainer.innerHTML = '
'; } } @@ -3275,7 +3275,7 @@ if (window.emmaUniversalInjected) { try { const analysis = await emmaEngine.analyze(); analysisDiv.innerHTML = ` -
+
Page Analysis:
Type: ${context.type} (${context.platform})
@@ -3287,7 +3287,7 @@ if (window.emmaUniversalInjected) { } catch (error) { console.warn('Emma: Error updating analysis panel:', error); analysisDiv.innerHTML = ` -
+
Page Analysis:
Type: ${context.type} (${context.platform})
@@ -4015,8 +4015,8 @@ if (window.emmaUniversalInjected) { } .emma-photo-selectable:hover { transform: scale(1.05) !important; - box-shadow: 0 8px 25px rgba(139, 92, 246, 0.3) !important; - border: 3px solid #8b5cf6 !important; + box-shadow: 0 8px 25px rgba(111, 99, 217, 0.3) !important; + border: 3px solid #6f63d9 !important; } .emma-photo-selected { border: 3px solid #10b981 !important; @@ -4045,7 +4045,7 @@ if (window.emmaUniversalInjected) { right: 20px !important; background: rgba(20, 20, 30, 0.95) !important; backdrop-filter: blur(20px) !important; - border: 1px solid rgba(139, 92, 246, 0.3) !important; + border: 1px solid rgba(111, 99, 217, 0.3) !important; border-radius: 16px !important; padding: 20px !important; color: white !important; @@ -4952,7 +4952,7 @@ window.forceEmmaUI = function() { right: 24px !important; width: 56px !important; height: 56px !important; - background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%) !important; + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%) !important; border: 2px solid white !important; border-radius: 50% !important; cursor: pointer !important; diff --git a/js/dashboard-init.js b/js/dashboard-init.js new file mode 100644 index 00000000..567540fa --- /dev/null +++ b/js/dashboard-init.js @@ -0,0 +1,6898 @@ +'use strict'; +(function attachDashboardNavigationHandlers() { + const clickActions = { + 'navigate-settings': () => { window.location.href = 'pages/emma-settings-redesigned.html'; }, + 'open-migration': () => { if (typeof openMigrationPage === 'function') { openMigrationPage(); } }, + 'open-gallery': () => { window.location.href = 'pages/gallery.html'; }, + 'open-settings': () => { window.location.href = 'pages/emma-settings-redesigned.html'; }, + 'open-people': () => { window.location.href = 'pages/people-emma.html'; } + }; + + document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-action]').forEach(element => { + const action = element.getAttribute('data-action'); + if (clickActions[action]) { + element.addEventListener('click', (event) => { + event.preventDefault(); + try { + clickActions[action](); + } catch (error) { + console.error('Dashboard navigation handler failed:', error); + } + }); + } + }); + }); +})(); + 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); + }); + + // Extension mode - Skip vault session validation + + // Vault initialization moved to end of page after ALL scripts load + + // Dashboard Controller + class EmmaDashboard { + constructor() { + // Original Emma Orb setup (RESTORED) + this.orb = document.getElementById('emma-orb'); + this.radialMenu = document.getElementById('radial-menu'); + this.voiceIndicator = document.getElementById('voice-indicator'); + + // CRITICAL: Expose dashboard globally for Emma Chat Experience + window.emmaDashboard = this; + this.loading = document.getElementById('loading'); + + // Initialize WebGL Emma Orb (RESTORED ORIGINAL) + this.initEmmaOrb(); + + console.log('๐Ÿ”ง Constructor - Dashboard initialized with clean original system'); + this.panels = { + dailyBrief: document.getElementById('daily-brief'), + aiInsights: document.getElementById('ai-insights'), + quickActions: document.getElementById('quick-actions') + }; + + // Burger menu removed for clean interface + this.panelsVisible = false; + + // Utility icons functionality + this.utilityIcons = document.querySelector('.utility-icons'); + this.setupUtilityIcons(); + + this.isMenuOpen = false; + this.emmaChatLoaderPromise = null; + this.stats = { memories: 0, people: 0, today: 0 }; + this.particles = []; + this.nodes = []; + this.neuralCanvas = document.getElementById('neural-canvas'); + this.neuralCtx = this.neuralCanvas ? this.neuralCanvas.getContext('2d') : null; + this.lowPowerMode = this.shouldUsePerformanceLite(); + document.body.classList.toggle('performance-lite', this.lowPowerMode); + + // Neural memory network properties + this.isConstellationMode = false; + this.constellationMemories = []; + this.centralNeuron = null; + + // ๐Ÿ” ZOOM FUNCTIONALITY: Dashboard constellation zoom/pan state + this.zoomState = { + scale: 1, + translateX: 0, + translateY: 0, + isDragging: false, + lastTouchDistance: 0, + minScale: 0.3, + maxScale: 3.0 + }; + this.constellationContainer = null; + this.activeNodeDrag = null; + this.pendingNodeDrag = null; + this.nodeDragPointerId = null; + this.nodeDragOffset = { x: 0, y: 0 }; + this.persistLayoutDebounce = null; + this.constellationLayoutKey = 'emmaConstellationLayoutV1'; + this.constellationFiltersKey = 'emmaConstellationFiltersV1'; + this.nodeDragStart = { x: 0, y: 0 }; + this.nodeDragThreshold = 6; + this.nodeDragTransformData = null; + this.touchHoldDelay = 350; + this.nodeDragHoldTimeout = null; + this.nodeDragHoldActive = false; + this.nodeDragLatestEvent = null; + this.nodeDragLastPointerType = null; + this.touchPanDisabled = false; + this.boundNodePointerMove = (event) => this.handleNodePointerMove(event); + this.boundNodePointerUp = (event) => this.handleNodePointerUp(event); + + // Initialize WebGL Emma Orb + this.initEmmaOrb(); + this.handleThemeApplied = this.handleThemeApplied.bind(this); + window.addEventListener('emmaThemeApplied', this.handleThemeApplied); + try { + const activeTheme = window.emmaThemeManager && typeof window.emmaThemeManager.getActiveTheme === 'function' + ? window.emmaThemeManager.getActiveTheme() + : null; + if (activeTheme) { + this.handleThemeApplied({ detail: { theme: activeTheme } }); + } + } catch (themeError) { + console.warn('DASHBOARD: Unable to apply theme on init', themeError); + } + + // Listen for vault status changes + document.addEventListener('vaultStatusChanged', (event) => { + + this.updateVaultNodeStatus(); + this.updateAllVaultIndicators(); + }); + + this.init(); + } + + // Removed Universal Orb Menu - keeping clean original system + + // Removed Universal Orb Menu action handler - using original radial menu system + + async init() { + // Show loading + this.loading.classList.add('active'); + + // Initialize Emma's orb in loading screen + this.initLoadingOrb(); + + // Initialize constellation background + console.log('๐Ÿ” DEBUG: About to initialize constellation, DOM ready state:', document.readyState); + console.log('๐Ÿ” DEBUG: Constellation canvas exists?', !!document.getElementById('constellation')); + 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); + + // ๐ŸŽฏ SAFE AUTO-TRIGGER: Check URL params for constellation or memory creation + this.handleURLParams(); + + // Start background animations + this.startAnimations(); + } + + initConstellation() { + const canvas = document.getElementById('constellation'); + if (!canvas) { + console.warn('โš ๏ธ Constellation canvas not found, skipping initialization'); + return; + } + + const ctx = canvas.getContext('2d'); + if (!ctx) { + console.warn('โš ๏ธ Could not get constellation canvas context, skipping initialization'); + return; + } + + // Set canvas size + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + resize(); + window.addEventListener('resize', resize); + + // Create memory particles + const particleCount = this.shouldReduceConstellationPhysics() ? 24 : 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 = () => { + const reducePhysics = this.shouldReduceConstellationPhysics(); + 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(111, 99, 217, ${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(111, 99, 217, ${(1 - dist / 100) * 0.2})`; + ctx.stroke(); + } + } + }); + }); + + requestAnimationFrame(animate); + }; + animate(); + } + + setupEventListeners() { + // RESTORED: Original Emma orb click handler + this.orb.addEventListener('click', (e) => { + e.stopPropagation(); + console.log('๐ŸŽฏ ORB CLICKED - Debug info:', { + isConstellationMode: this.isConstellationMode, + isMenuOpen: this.isMenuOpen, + orbExists: !!this.orb, + radialMenuExists: !!this.radialMenu, + dashboardInstance: !!window.emmaDashboard + }); + + if (this.isConstellationMode) { + console.log('๐ŸŒŒ Exiting constellation mode...'); + this.exitMemoryConstellation(); + } else { + console.log('๐Ÿ”˜ Toggling radial menu...'); + this.toggleRadialMenu(); + } + }); + + // RESTORED: Radial menu items + this.radialMenu.querySelectorAll('.radial-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + const action = item.dataset.action; + this.handleRadialAction(action); + }); + }); + + console.log('๐ŸŽฏ Event listeners setup - RESTORED original orb & radial menu'); + + // Track mouse/touch position for neural effects + this.mouseX = 0; + this.mouseY = 0; + + // Mouse support + document.addEventListener('mousemove', (e) => { + this.mouseX = e.clientX; + this.mouseY = e.clientY; + this.updateNodeProximityEffects(); + }); + + // Touch support for mobile + document.addEventListener('touchmove', (e) => { + if (e.touches.length > 0) { + this.mouseX = e.touches[0].clientX; + this.mouseY = e.touches[0].clientY; + this.updateNodeProximityEffects(); + } + }, { passive: true }); + + document.addEventListener('touchstart', (e) => { + if (e.touches.length > 0) { + this.mouseX = e.touches[0].clientX; + this.mouseY = e.touches[0].clientY; + } + }, { passive: true }); + + // Handle window resize for responsive repositioning + window.addEventListener('resize', () => { + // Debounce resize events + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(() => { + if (this.isMenuOpen) { + // ๐Ÿ”„ MOBILE FIX: Keep radial menu bound to orb center + this.repositionMenuToOrb(); + + // CRITICAL FIX: Resize canvas and redraw connections after repositioning + if (this.neuralCanvas) { + this.neuralCanvas.width = window.innerWidth; + this.neuralCanvas.height = window.innerHeight; + } + + // Force immediate redraw of connections with new positions + if (this.render && typeof this.render === 'function') { + this.render(); + } else if (window.neuralRenderer && window.neuralRenderer.render) { + window.neuralRenderer.render(); + } + } else if (this.isConstellationMode) { + // Existing constellation resize handling + this.repositionNodesOnResize(); + + if (this.neuralCanvas) { + this.neuralCanvas.width = window.innerWidth; + this.neuralCanvas.height = window.innerHeight; + } + + if (this.render && typeof this.render === 'function') { + this.render(); + } else if (window.neuralRenderer && window.neuralRenderer.render) { + window.neuralRenderer.render(); + } + } + }, 250); + }); + } + + // Extract proximity effects to separate method for reuse + updateNodeProximityEffects() { + // 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 < 120) { + // Subtle attraction to mouse with organic feel + const force = (120 - dist) / 120; + const angle = Math.atan2(this.mouseY - node.y, this.mouseX - node.x); + + // Very gentle attraction with organic damping + const attractionStrength = force * 0.08; // Much more subtle + node.vx += Math.cos(angle) * attractionStrength; + node.vy += Math.sin(angle) * attractionStrength; + + // Add glow effect (stronger for memory nodes) + const glowStrength = this.isConstellationMode ? 36 : 26; + const baseOpacity = this.isConstellationMode ? 0.6 : 0.45; + const glowSize = glowStrength + force * 18; + const glowOpacity = Math.min(baseOpacity + force * 0.25, 0.75); + if (node.element) { + node.element.style.boxShadow = `0 0 ${glowSize}px rgba(111, 99, 217, ${glowOpacity})`; + } + } else { + // Reset glow when mouse is far + if (node.element) { + node.element.style.boxShadow = ''; + } + } + }); + } + } + + // Reposition nodes on window resize for responsive behavior (CONSTELLATION MODE ONLY) + repositionNodesOnResize() { + if (!this.nodes || this.nodes.length === 0) return; + + // ๐Ÿšจ CRITICAL: This function is now ONLY for constellation mode + // Radial menu repositioning is handled by repositionMenuToOrb() + if (!this.isConstellationMode) { + console.warn('๐Ÿšจ repositionNodesOnResize called for non-constellation mode - use repositionMenuToOrb instead'); + return; + } + + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + + // Reposition each constellation node + this.nodes.forEach((node, i) => { + // For constellation mode, maintain relative positions but scale + const scaleX = window.innerWidth / (this.lastWindowWidth || window.innerWidth); + const scaleY = window.innerHeight / (this.lastWindowHeight || window.innerHeight); + + node.x = centerX + (node.x - centerX) * scaleX; + node.y = centerY + (node.y - centerY) * scaleY; + node.baseX = node.x; + node.baseY = node.y; + + // Update DOM position - UNIFIED: Same size for all devices + if (node.element) { + const elementSize = 45; // Consistent centering for all devices (90px items) + node.element.style.left = (node.x - elementSize) + 'px'; + node.element.style.top = (node.y - elementSize) + 'px'; + } + }); + + // Store current window size for next resize + this.lastWindowWidth = window.innerWidth; + this.lastWindowHeight = window.innerHeight; + + } + + // Keyboard shortcuts + setupKeyboardShortcuts() { + 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)) { + + this.toggleRadialMenu(); + } + }); + + // Hover effects for orb + this.orb.addEventListener('mouseenter', () => { + document.querySelectorAll('.quick-stats').forEach(el => { + el.style.opacity = '1'; + }); + }); + + // Initialize keyboard shortcuts + this.setupKeyboardShortcuts(); + + // RESTORED: Click outside to close menu + document.addEventListener('click', (e) => { + if (this.isMenuOpen && !this.orb.contains(e.target) && !this.radialMenu.contains(e.target)) { + this.toggleRadialMenu(); + } + }); + + // RESTORED: Hover effects for orb + this.orb.addEventListener('mouseenter', () => { + document.querySelectorAll('.quick-stats').forEach(el => { + el.style.opacity = '1'; + el.style.transform = 'translateY(-10px) scale(1.02)'; + }); + }); + + this.orb.addEventListener('mouseleave', () => { + document.querySelectorAll('.quick-stats').forEach(el => { + el.style.opacity = '0.7'; + el.style.transform = 'translateY(0) scale(1)'; + }); + }); + + // CRITICAL FIX: Listen for vault status changes from extension + this.setupVaultStatusListener(); + + // Auto-trigger constellation mode if hash is present + if (window.location.hash === '#constellation') { + console.log('๐ŸŒŸ DASHBOARD: Auto-triggering constellation mode from URL hash'); + setTimeout(() => { + this.enterMemoryConstellation(); + }, 1000); // Wait for vault to load + } + } + + // Listen for vault status changes and update UI + setupVaultStatusListener() { + window.addEventListener('vault-status-changed', (event) => { + + // Update global status + window.currentVaultStatus = event.detail; + + // Update vault node status in UI + this.updateVaultNodeStatus(); + + // Update any vault-dependent UI elements + this.refreshVaultDependentUI(); + }); + + // Also listen for extension-vault-ready directly + window.addEventListener('extension-vault-ready', (event) => { + + // Ensure vault status is updated + if (window.webVaultStatus) { + window.webVaultStatus.status.isUnlocked = true; + window.webVaultStatus.status.hasVault = true; + window.webVaultStatus.status.name = event.detail.vaultName; + window.currentVaultStatus = window.webVaultStatus.status; + } + + // Update UI immediately + this.updateVaultNodeStatus(); + }); + + // CRITICAL: Listen for memory additions from Emma Chat + window.addEventListener('emmaMemoryAdded', (event) => { + console.log('๐Ÿ”„ DASHBOARD: Memory added event received, refreshing constellation'); + + if (this.isConstellationMode) { + // Force constellation refresh with new data - longer delay to ensure vault sync + setTimeout(() => { + this.enterMemoryConstellation(); + console.log('๐Ÿ”„ DASHBOARD: Constellation refreshed after memory addition'); + }, 300); // Increased delay for better data consistency + } + }); + window.addEventListener('emmaMemoryDeleted', (event) => { + console.log('๐Ÿ—‹ DASHBOARD: Memory deleted event received, refreshing constellation'); + + if (this.isConstellationMode) { + setTimeout(() => { + this.enterMemoryConstellation(); + console.log('๐Ÿ—‹ DASHBOARD: Constellation refreshed after memory deletion'); + }, 300); + } + }); + } + + // Refresh UI elements that depend on vault status + refreshVaultDependentUI() { + // Update any vault-dependent elements + const vaultElements = document.querySelectorAll('[data-vault-dependent]'); + vaultElements.forEach(element => { + if (window.currentVaultStatus && window.currentVaultStatus.isUnlocked) { + element.classList.remove('vault-locked'); + element.classList.add('vault-unlocked'); + } else { + element.classList.remove('vault-unlocked'); + element.classList.add('vault-locked'); + } + }); + } + + shouldUsePerformanceLite() { + try { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const compactViewport = window.matchMedia('(max-width: 900px)').matches; + const touchDevice = (navigator.maxTouchPoints || 0) > 1; + return prefersReducedMotion || compactViewport || touchDevice; + } catch (error) { + console.warn('Performance mode detection failed (non-critical):', error); + return false; + } + } + + isCompactViewport() { + return window.innerWidth <= 768; + } + + getConstellationNodeSize() { + const width = window.innerWidth; + if (width <= 480) return 56; + if (width <= 900) return 68; + return 80; + } + + shouldReduceConstellationPhysics() { + return this.lowPowerMode || this.isCompactViewport(); + } + + initNeuralNetwork() { + // ๐ŸŽฏ BACK TO BASICS: Orb-relative positioning that actually works + console.log('๐Ÿ”ง initNeuralNetwork: Using orb-relative positioning'); + + const items = this.radialMenu.querySelectorAll('.radial-item'); + + // ๐Ÿ”ฅ GET ACTUAL ORB POSITION: Don't force it, work with it + const orbRect = this.orb.getBoundingClientRect(); + const orbCenterX = orbRect.left + orbRect.width / 2; + const orbCenterY = orbRect.top + orbRect.height / 2; + + console.log('๐ŸŽฏ Actual orb center:', { x: orbCenterX, y: orbCenterY, orbRect }); + + // ๐ŸŒŒ SIMPLIFIED: One radius that works great for all devices - true orbiting feel + const radius = 140; // Proper orbiting distance - feels like satellites around Emma orb + + console.log('๐ŸŒŒ Using unified orbiting radius:', radius, 'px for all devices'); + + // Clear existing nodes + this.nodes = []; + + items.forEach((item, i) => { + const angle = (i / items.length) * Math.PI * 2 - Math.PI / 2; + + // ๐Ÿ”— ORB-RELATIVE: Calculate position relative to actual orb center + const targetX = orbCenterX + Math.cos(angle) * radius; + const targetY = orbCenterY + Math.sin(angle) * radius; + + // ๐ŸŽฌ ANIMATION SETUP: Start at actual orb center for animate-out effect + const startX = orbCenterX; + const startY = orbCenterY; + + // Position at target location (no scaling animation needed) + item.style.left = (targetX - 45) + 'px'; // Center the 90px item + item.style.top = (targetY - 45) + 'px'; + + // CRITICAL FIX: Ensure item is visible after exitMemoryConstellation reset + item.style.opacity = '1'; + item.style.transform = 'scale(1)'; + + console.log(`๐Ÿ”ง Item ${i} restored:`, { + left: item.style.left, + top: item.style.top, + opacity: item.style.opacity, + transform: item.style.transform + }); + + console.log(`๐ŸŽฏ Item ${i}: start(${startX}, ${startY}) โ†’ target(${targetX}, ${targetY})`); + + // Store position data (simplified for gentle fade) + const nodeData = { + element: item, + x: targetX, // Final position + y: targetY, + targetX: targetX, + targetY: targetY, + baseX: targetX, + baseY: targetY, + angle: angle, + connections: [], + orbBound: true + }; + + this.nodes.push(nodeData); + }); + + // Create connections between nodes + this.nodes.forEach((node, i) => { + // Connect to adjacent nodes + const next = this.nodes[(i + 1) % this.nodes.length]; + const prev = this.nodes[(i - 1 + this.nodes.length) % this.nodes.length]; + node.connections.push(next, prev); + + // Connect to opposite node for cross-connections + const opposite = this.nodes[(i + Math.floor(this.nodes.length / 2)) % this.nodes.length]; + if (opposite !== node) { + node.connections.push(opposite); + } + }); + + // Add central orb as a node at actual orb center + this.centralNode = { + x: orbCenterX, + y: orbCenterY, + element: this.orb + }; + + // Connect all nodes to center + this.nodes.forEach(node => { + node.connections.push(this.centralNode); + }); + } + + // Removed initRadialNeuralNetwork - no longer needed since we exit constellation instead of showing menu overlay + + toggleRadialMenu() { + console.log('๐Ÿ”˜ toggleRadialMenu called - Debug:', { + isMenuOpen: this.isMenuOpen, + radialMenuExists: !!this.radialMenu, + dashboardInstance: !!window.emmaDashboard + }); + + this.isMenuOpen = !this.isMenuOpen; + + if (this.isMenuOpen) { + this.radialMenu.classList.add('active'); + document.body.classList.add('menu-active'); // Fade ALL UI elements + document.body.classList.remove('dashboard-minimal'); + + this.updateVaultNodeStatus(); // Update vault node based on status + + // Initialize the menu nodes (without aggressive physics) + this.initNeuralNetwork(); + + } else { + this.radialMenu.classList.remove('active'); + document.body.classList.remove('menu-active'); + if (!this.isConstellationMode) { + document.body.classList.add('dashboard-minimal'); + } + + // Clear any neural animation + if (this.neuralAnimationId) { + cancelAnimationFrame(this.neuralAnimationId); + this.neuralCtx.clearRect(0, 0, this.neuralCanvas.width, this.neuralCanvas.height); + } + + // Reset nodes array + this.nodes = []; + } + } + + // Simple method to position menu items around orb with gentle fade-in + positionMenuItemsAroundOrb() { + const orbRect = this.orb.getBoundingClientRect(); + const orbCenterX = orbRect.left + orbRect.width / 2; + const orbCenterY = orbRect.top + orbRect.height / 2; + const radius = 140; // Consistent distance from orb + + const menuItems = this.radialMenu.querySelectorAll('.radial-item'); + + menuItems.forEach((item, i) => { + const angle = (i / menuItems.length) * Math.PI * 2 - Math.PI / 2; + const targetX = orbCenterX + Math.cos(angle) * radius; + const targetY = orbCenterY + Math.sin(angle) * radius; + + // Position the item (center the 90px item) + item.style.left = (targetX - 45) + 'px'; + item.style.top = (targetY - 45) + 'px'; + }); + } + + // ๐Ÿง  NEURAL EXPANSION: Expand radial nodes using neural network physics + expandRadialNeuralNetwork() { + console.log('๐Ÿง  Expanding radial neural network from orb center'); + + this.nodes.forEach((node, i) => { + // Set target position for neural physics to move toward + node.baseX = node.targetX; + node.baseY = node.targetY; + + // Add initial velocity toward target (like constellation nodes) + const dx = node.targetX - node.x; + const dy = node.targetY - node.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 1) { + // Give it momentum toward target + node.vx = (dx / distance) * 3; // Fast initial velocity + node.vy = (dy / distance) * 3; + } + + // Show and scale the node + setTimeout(() => { + node.element.style.transition = 'opacity 0.1s ease, transform 0.1s ease'; + node.element.style.opacity = '1'; + node.element.style.transform = 'scale(1)'; + + // Pause float animation during menu display + node.element.style.animationPlayState = 'paused'; + + console.log(`๐Ÿง  Node ${i} neural expansion initiated to (${node.targetX}, ${node.targetY})`); + }, i * 15); // Fast stagger + }); + } + + // ๐Ÿง  NEURAL COLLAPSE: Collapse radial nodes back to orb using neural physics + collapseRadialNeuralNetwork() { + console.log('๐Ÿง  Collapsing radial neural network back to orb center'); + + // Get actual orb center position + const orbRect = this.orb.getBoundingClientRect(); + const orbCenterX = orbRect.left + orbRect.width / 2; + const orbCenterY = orbRect.top + orbRect.height / 2; + + this.nodes.forEach((node, i) => { + // Set orb center as the new target for neural physics + node.baseX = orbCenterX; + node.baseY = orbCenterY; + node.targetX = orbCenterX; + node.targetY = orbCenterY; + + // Add velocity toward orb center (like constellation nodes) + const dx = orbCenterX - node.x; + const dy = orbCenterY - node.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance > 1) { + // Give it momentum toward orb center + node.vx = (dx / distance) * 4; // Fast collapse velocity + node.vy = (dy / distance) * 4; + } + + // Hide and scale down the node + setTimeout(() => { + node.element.style.transition = 'opacity 0.15s ease, transform 0.15s ease'; + node.element.style.opacity = '0'; + node.element.style.transform = 'scale(0)'; + + // Re-enable float animation when menu closes + node.element.style.animationPlayState = 'running'; + + console.log(`๐Ÿง  Node ${i} neural collapse initiated to orb (${orbCenterX}, ${orbCenterY})`); + }, (this.nodes.length - 1 - i) * 20); // Fast reverse stagger + }); + } + + // ๐Ÿ”„ NEW: Reposition menu items to stay bound to orb (for resize/orientation changes) + repositionMenuToOrb() { + if (!this.isMenuOpen || this.nodes.length === 0) return; + + console.log('๐Ÿ”„ Repositioning menu items to stay bound to orb'); + + // Get current orb position (don't force it, work with it) + const orbRect = this.orb.getBoundingClientRect(); + const orbCenterX = orbRect.left + orbRect.width / 2; + const orbCenterY = orbRect.top + orbRect.height / 2; + + // ๐ŸŒŒ SIMPLIFIED: Same orbiting radius for all devices + const radius = 140; // Consistent orbiting distance - satellites around Emma orb + + // Update all node positions relative to actual orb center + this.nodes.forEach((node, i) => { + const angle = (i / this.nodes.length) * Math.PI * 2 - Math.PI / 2; + const newTargetX = orbCenterX + Math.cos(angle) * radius; + const newTargetY = orbCenterY + Math.sin(angle) * radius; + + // Update positions with fast smooth transition + const item = node.element; + item.style.transition = 'all 0.15s ease'; + item.style.left = (newTargetX - 45) + 'px'; + item.style.top = (newTargetY - 45) + 'px'; + + // Update node data + node.x = newTargetX; + node.y = newTargetY; + node.targetX = newTargetX; + node.targetY = newTargetY; + node.baseX = newTargetX; + node.baseY = newTargetY; + }); + } + + initLoadingOrb() { + try { + const loadingOrbContainer = document.querySelector('.loading-orb'); + if (!loadingOrbContainer) { + console.warn('๐ŸŒŸ Loading orb container not found'); + return; + } + + if (window.EmmaOrb) { + // Create WebGL Emma Orb for loading screen + this.loadingOrb = new window.EmmaOrb(loadingOrbContainer, { + hue: 270, // Emma's signature purple-pink + hoverIntensity: 0.5, + rotateOnHover: false, + forceHoverState: true // Always show active state during loading + }); + + } else { + console.warn('๐ŸŒŸ EmmaOrb class not available for loading, using fallback'); + // Fallback to gradient orb if WebGL not available + const fallbackDiv = document.createElement('div'); + fallbackDiv.className = 'loading-orb-fallback'; + loadingOrbContainer.appendChild(fallbackDiv); + } + } catch (error) { + console.error('๐Ÿšจ Error initializing Loading Emma Orb:', error); + // Fallback on error + const loadingOrbContainer = document.querySelector('.loading-orb'); + if (loadingOrbContainer) { + const fallbackDiv = document.createElement('div'); + fallbackDiv.className = 'loading-orb-fallback'; + loadingOrbContainer.appendChild(fallbackDiv); + } + } + } + + initEmmaOrb() { + try { + const orbContainer = document.getElementById('webgl-orb-container'); + if (!orbContainer) { + console.warn('๐ŸŒŸ WebGL orb container not found'); + return; + } + + const computed = getComputedStyle(document.documentElement); + const hueValue = parseFloat(computed.getPropertyValue('--emma-orb-hue')) || 270; + const hoverIntensityValue = parseFloat(computed.getPropertyValue('--emma-orb-hover-intensity')) || 0.35; + + if (window.EmmaOrb) { + try { + // Create WebGL Emma Orb with Emma's signature purple-pink hue + this.webglOrb = new window.EmmaOrb(orbContainer, { + hue: hueValue, + hoverIntensity: hoverIntensityValue, + rotateOnHover: true, + forceHoverState: false + }); + + } catch (webglError) { + console.error('โŒ WebGL orb creation failed:', webglError); + // Force fallback + window.EmmaOrb = null; + } + } + + if (!window.EmmaOrb || !this.webglOrb) { + console.warn('๐ŸŒŸ EmmaOrb class not available, using fallback'); + const fallbackGradient = (computed.getPropertyValue('--emma-gradient-primary') || '').trim(); + const fallbackGlow = (computed.getPropertyValue('--emma-glow-strong') || '0 0 40px rgba(111, 99, 217, 0.6)').trim(); + orbContainer.style.background = fallbackGradient || 'radial-gradient(circle at 30% 30%, #6f63d9, #4f46e5, #deb3e4)'; + orbContainer.style.borderRadius = '50%'; + orbContainer.style.width = '100%'; + orbContainer.style.height = '100%'; + orbContainer.style.boxShadow = `${fallbackGlow}, inset 0 0 40px rgba(255, 255, 255, 0.1)`; + orbContainer.style.animation = 'orb-pulse 3s ease-in-out infinite'; + + } + + // Clean Emma orb - no vault unlock needed on dashboard + } catch (error) { + console.error('๐Ÿšจ Error initializing Emma Orb:', error); + // Ensure fallback is applied + const orbContainer = document.getElementById('webgl-orb-container'); + if (orbContainer) { + const computed = getComputedStyle(document.documentElement); + const fallbackGradient = (computed.getPropertyValue('--emma-gradient-primary') || '').trim(); + const fallbackGlow = (computed.getPropertyValue('--emma-glow-strong') || '0 0 40px rgba(111, 99, 217, 0.6)').trim(); + orbContainer.style.background = fallbackGradient || 'radial-gradient(circle at 30% 30%, #6f63d9, #4f46e5)'; + orbContainer.style.borderRadius = '50%'; + orbContainer.style.width = '100%'; + orbContainer.style.height = '100%'; + orbContainer.style.boxShadow = `${fallbackGlow}, inset 0 0 40px rgba(255, 255, 255, 0.1)`; + } + } + } + + handleThemeApplied(event) { + const theme = event && event.detail ? event.detail.theme : null; + if (!theme) return; + this.applyThemeToOrb(theme); + } + + applyThemeToOrb(theme) { + if (!theme) return; + + if (this.webglOrb) { + const hue = parseFloat(theme.cssVars && theme.cssVars['--emma-orb-hue']); + if (!Number.isNaN(hue)) { + this.webglOrb.options.hue = hue; + } + + const hoverIntensity = parseFloat(theme.cssVars && theme.cssVars['--emma-orb-hover-intensity']); + if (!Number.isNaN(hoverIntensity)) { + this.webglOrb.options.hoverIntensity = hoverIntensity; + } + } else { + const orbContainer = document.getElementById('webgl-orb-container'); + if (!orbContainer) return; + + const gradient = theme.cssVars && theme.cssVars['--emma-gradient-primary']; + const glow = theme.cssVars && theme.cssVars['--emma-glow-strong']; + + if (gradient) { + orbContainer.style.background = gradient.trim(); + } + + if (glow) { + orbContainer.style.boxShadow = `${glow.trim()}, inset 0 0 40px rgba(255, 255, 255, 0.1)`; + } + } + } + + updateVaultNodeStatus() { + try { + + const vaultNode = document.getElementById('vault-node'); + if (!vaultNode) { + console.warn('โš ๏ธ Vault node not found, skipping status update'); + // Try alternative selectors + const altVaultNode = document.querySelector('[data-action="vault"], .radial-item[onclick*="vault"]'); + if (altVaultNode) { + + this.updateAlternativeVaultNode(altVaultNode); + } + return; + } + + const vaultIcon = vaultNode.querySelector('.radial-item-icon'); + if (!vaultIcon) { + console.warn('โš ๏ธ Vault icon not found, trying alternative selector'); + const altIcon = vaultNode.querySelector('span, .icon'); + if (altIcon) { + + this.updateVaultIcon(altIcon, vaultNode); + return; + } + return; + } + + this.updateVaultIcon(vaultIcon, vaultNode); + } catch (error) { + console.warn('โš ๏ธ Error updating vault node status:', error); + } + } + + updateVaultIcon(vaultIcon, vaultNode) { + if (window.currentVaultStatus && window.currentVaultStatus.isUnlocked) { + + vaultIcon.textContent = '๐Ÿ”“'; + vaultNode.style.background = 'rgba(74, 222, 128, 0.1)'; // Green tint when unlocked + vaultNode.style.borderColor = 'rgba(74, 222, 128, 0.3)'; + vaultNode.style.boxShadow = '0 0 14px rgba(74, 222, 128, 0.3)'; + } else { + + vaultIcon.textContent = '๐Ÿ”’'; + vaultNode.style.background = 'var(--emma-glass)'; // Default when locked + vaultNode.style.borderColor = 'rgba(111, 99, 217, 0.3)'; + vaultNode.style.boxShadow = '0 0 12px rgba(111, 99, 217, 0.18)'; + } + } + + updateAlternativeVaultNode(node) { + const icon = node.querySelector('span') || node; + if (window.currentVaultStatus && window.currentVaultStatus.isUnlocked) { + icon.textContent = '๐Ÿ”“'; + node.style.background = 'rgba(74, 222, 128, 0.1)'; + } else { + icon.textContent = '๐Ÿ”’'; + node.style.background = 'var(--emma-glass)'; + } + } + + updateAllVaultIndicators() { + + // Update any vault-related elements + const vaultElements = document.querySelectorAll('[data-action="vault"], .vault-indicator, #vault-node'); + vaultElements.forEach(element => { + const icon = element.querySelector('span, .radial-item-icon') || element; + if (window.currentVaultStatus && window.currentVaultStatus.isUnlocked) { + if (icon.textContent) icon.textContent = '๐Ÿ”“'; + element.style.background = 'rgba(74, 222, 128, 0.1)'; + element.style.borderColor = 'rgba(74, 222, 128, 0.3)'; + element.style.boxShadow = '0 0 14px rgba(74, 222, 128, 0.3)'; + } else { + if (icon.textContent) icon.textContent = '๐Ÿ”’'; + element.style.background = 'var(--emma-glass)'; + element.style.borderColor = 'rgba(111, 99, 217, 0.3)'; + element.style.boxShadow = '0 0 12px rgba(111, 99, 217, 0.18)'; + } + }); + + } + + assignNodeUid(element, prefix) { + if (!element) return `${prefix}-unknown`; + this.nodeUidCounter = (this.nodeUidCounter || 0) + 1; + const uid = `${prefix}-${this.nodeUidCounter}`; + element.dataset.nodeUid = uid; + return uid; + } + + linkNodes(nodeA, nodeB) { + if (!nodeA || !nodeB || nodeA === nodeB) { + return; + } + + if (nodeA.isCreateNode || nodeB.isCreateNode) { + return; + } + + if (!Array.isArray(nodeA.connections)) { + nodeA.connections = []; + } + if (!Array.isArray(nodeB.connections)) { + nodeB.connections = []; + } + + if (!nodeA.connections.includes(nodeB)) { + nodeA.connections.push(nodeB); + } + if (!nodeB.connections.includes(nodeA)) { + nodeB.connections.push(nodeA); + } + } + + purgeCreateNodeConnections() { + const createElement = this.createMemoryElement; + if (!createElement) return; + + this.nodes.forEach(node => { + if (!Array.isArray(node.connections)) return; + node.connections = node.connections.filter(conn => { + if (!conn) return false; + if (conn.isCreateNode) { + if (Array.isArray(conn.connections)) { + conn.connections = conn.connections.filter(inner => inner !== node); + } + return false; + } + if (conn.element === createElement) { + return false; + } + return true; + }); + }); + + if (this.centralNode && Array.isArray(this.centralNode.connections)) { + this.centralNode.connections = this.centralNode.connections.filter(conn => conn && !conn.isCreateNode && conn.element !== createElement); + } + + const createNode = this.nodes.find(node => node.isCreateNode); + if (createNode) { + createNode.connections = []; + } + } + + dedupeNodeConnections() { + this.nodes.forEach(node => { + if (!Array.isArray(node.connections)) { + node.connections = []; + return; + } + + const seen = new Set(); + node.connections = node.connections.filter(conn => { + if (!conn) return false; + const key = conn.uid || (conn.element && conn.element.dataset && conn.element.dataset.nodeUid); + if (!key) return false; + if (seen.has(key)) { + if (Array.isArray(conn.connections)) { + conn.connections = conn.connections.filter(inner => inner !== node); + } + return false; + } + seen.add(key); + return true; + }); + }); + + if (this.centralNode && Array.isArray(this.centralNode.connections)) { + const seenCenter = new Set(); + this.centralNode.connections = this.centralNode.connections.filter(conn => { + if (!conn) return false; + const key = conn.uid || (conn.element && conn.element.dataset && conn.element.dataset.nodeUid); + if (!key) return false; + if (seenCenter.has(key)) { + if (Array.isArray(conn.connections)) { + conn.connections = conn.connections.filter(inner => inner !== this.centralNode); + } + return false; + } + seenCenter.add(key); + return true; + }); + } + } + + // Check if a line intersects with Emma orb circle + lineIntersectsCircle(x1, y1, x2, y2, cx, cy, radius) { + // Vector from point1 to point2 + const dx = x2 - x1; + const dy = y2 - y1; + + // Vector from point1 to circle center + const fx = x1 - cx; + const fy = y1 - cy; + + // Quadratic equation coefficients + const a = dx * dx + dy * dy; + const b = 2 * (fx * dx + fy * dy); + const c = (fx * fx + fy * fy) - radius * radius; + + const discriminant = b * b - 4 * a * c; + + // No intersection if discriminant is negative + if (discriminant < 0) return false; + + // Check if intersection points are within the line segment + const sqrt_discriminant = Math.sqrt(discriminant); + const t1 = (-b - sqrt_discriminant) / (2 * a); + const t2 = (-b + sqrt_discriminant) / (2 * a); + + // If either intersection point is within [0,1], line intersects circle + return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1) || (t1 < 0 && t2 > 1); + } + + animateNeuralNetwork() { + if (!this.neuralCanvas || !this.neuralCtx) { + return; + } + + this.neuralCanvas.width = window.innerWidth; + this.neuralCanvas.height = window.innerHeight; + const ctx = this.neuralCtx; + const skipCanvasRendering = this.lowPowerMode || document.body.classList.contains('performance-lite') || !ctx; + + const animate = () => { + const reducePhysics = this.shouldReduceConstellationPhysics(); + if (!this.isMenuOpen && !this.isConstellationMode) { + this.neuralAnimationId = null; + return; + } + + if (!skipCanvasRendering) { + ctx.clearRect(0, 0, this.neuralCanvas.width, this.neuralCanvas.height); + + if (!reducePhysics && (this.isMenuOpen || this.isConstellationMode)) { + ctx.strokeStyle = 'rgba(111, 99, 217, 0.03)'; + ctx.lineWidth = 1; + const gridSize = 50; + + for (let x = 0; x < this.neuralCanvas.width; x += gridSize) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, this.neuralCanvas.height); + ctx.stroke(); + } + + for (let y = 0; y < this.neuralCanvas.height; y += gridSize) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(this.neuralCanvas.width, y); + ctx.stroke(); + } + } + } + + // Update node positions with organic movement (only for visible nodes) + const time = Date.now() * 0.001; + this.nodes.forEach((node, i) => { + // CRITICAL FIX: Only animate visible nodes + const nodeElement = node.element || document.getElementById(node.id); + if (!nodeElement || nodeElement.style.display === 'none') { + return; // Skip animation for hidden nodes + } + + const halfWidth = (nodeElement.offsetWidth || 0) / 2; + const halfHeight = (nodeElement.offsetHeight || 0) / 2; + + if (node.isDragging) { + node.vx = 0; + node.vy = 0; + node.baseX = node.x; + node.baseY = node.y; + nodeElement.style.left = `${node.x - halfWidth}px`; + nodeElement.style.top = `${node.y - halfHeight}px`; + return; + } + + const isStaticConstellationNode = + this.isConstellationMode && + !node.orbBound && + (node.type === 'memory' || node.isCreateNode); + + if (isStaticConstellationNode) { + // Keep memory nodes and create button steady in constellation mode + node.vx = 0; + node.vy = 0; + node.x = node.baseX; + node.y = node.baseY; + nodeElement.style.left = `${node.x - halfWidth}px`; + nodeElement.style.top = `${node.y - halfHeight}px`; + return; + } + + // Different floating movement for radial vs constellation nodes + let floatX, floatY; + if (node.orbBound) { + // ๐ŸŽฏ RADIAL MENU NODES: Much tighter floating to stay within 50px radius + floatX = Math.sin(time * 0.3 + i) * 3; // Reduced from 12px to 3px + floatY = Math.cos(time * 0.2 + i * 0.5) * 2; // Reduced from 8px to 2px + } else { + // ๐ŸŒŒ CONSTELLATION NODES: Normal floating movement + floatX = Math.sin(time * 0.3 + i) * 12 * (reducePhysics ? 0.6 : 1); + floatY = Math.cos(time * 0.2 + i * 0.5) * 8 * (reducePhysics ? 0.6 : 1); + } + + // Gentle elastic force back to base position (petri dish feel) + const dx = (node.baseX + floatX) - node.x; + const dy = (node.baseY + floatY) - node.y; + + node.vx += dx * (reducePhysics ? 0.003 : 0.005); // Adaptive elastic force + node.vy += dy * (reducePhysics ? 0.003 : 0.005); + + // Apply more damping for organic feel + node.vx *= reducePhysics ? 0.985 : 0.98; + node.vy *= reducePhysics ? 0.985 : 0.98; + + // Add repulsion from Emma orb (prevent overlap) + if (this.centralNode && this.isConstellationMode) { + const orbDx = node.x - this.centralNode.x; + const orbDy = node.y - this.centralNode.y; + const orbDist = Math.sqrt(orbDx * orbDx + orbDy * orbDy); + + // Repulsion zone around Emma orb + const repulsionRadius = reducePhysics ? 130 : 150; + if (orbDist < repulsionRadius && orbDist > 0) { + const repulsionForce = (repulsionRadius - orbDist) / repulsionRadius; + const forceStrength = repulsionForce * (reducePhysics ? 0.2 : 0.3); // Gentle but firm repulsion + + const normalizedDx = orbDx / orbDist; + const normalizedDy = orbDy / orbDist; + + node.vx += normalizedDx * forceStrength; + node.vy += normalizedDy * forceStrength; + } + } + + // Add gentle repulsion between memory nodes (prevent overlap) + if (this.isConstellationMode && !reducePhysics) { + this.nodes.forEach((otherNode, j) => { + if (i !== j) { + // CRITICAL FIX: Only calculate repulsion with visible nodes + const otherElement = otherNode.element || document.getElementById(otherNode.id); + if (!otherElement || otherElement.style.display === 'none') { + return; // Skip repulsion with hidden nodes + } + const nodeDx = node.x - otherNode.x; + const nodeDy = node.y - otherNode.y; + const nodeDist = Math.sqrt(nodeDx * nodeDx + nodeDy * nodeDy); + + // Minimum distance between nodes + const minNodeDistance = 110; + if (nodeDist < minNodeDistance && nodeDist > 0) { + const repulsionForce = (minNodeDistance - nodeDist) / minNodeDistance; + const forceStrength = repulsionForce * 0.1; // Very gentle node repulsion + + const normalizedDx = nodeDx / nodeDist; + const normalizedDy = nodeDy / nodeDist; + + node.vx += normalizedDx * forceStrength; + node.vy += normalizedDy * forceStrength; + } + } + }); + } + + // Update position + node.x += node.vx; + node.y += node.vy; + + // ๐ŸŽฏ RADIAL MENU CONSTRAINT: Hard limit radial nodes with proper breathing room from orb + if (node.orbBound && this.orbNeuralNode) { + const orbDx = node.x - this.orbNeuralNode.x; + const orbDy = node.y - this.orbNeuralNode.y; + const orbDistance = Math.sqrt(orbDx * orbDx + orbDy * orbDy); + + // ๐ŸŒŒ SIMPLIFIED: One max distance for all devices - orbiting constraint + const MAX_RADIAL_DISTANCE = reducePhysics ? 120 : 150; // Orbiting distance + small buffer for physics + + if (orbDistance > MAX_RADIAL_DISTANCE) { + // Constrain to max distance from orb center + const constrainedX = this.orbNeuralNode.x + (orbDx / orbDistance) * MAX_RADIAL_DISTANCE; + const constrainedY = this.orbNeuralNode.y + (orbDy / orbDistance) * MAX_RADIAL_DISTANCE; + node.x = constrainedX; + node.y = constrainedY; + + // Zero out velocity to prevent further drift + node.vx *= 0.5; + node.vy *= 0.5; + } + } + + // Update DOM element position + nodeElement.style.left = `${node.x - halfWidth}px`; + nodeElement.style.top = `${node.y - halfHeight}px`; + }); + + if (!skipCanvasRendering) { + const drawnEdges = new Set(); + this.nodes.forEach((node, nodeIndex) => { + if (node.isCreateNode) { + return; + } + + const sourceElement = node.element || document.getElementById(node.id); + if (!sourceElement || sourceElement.style.display === 'none') { + return; + } + + const sourceKey = node.uid || (sourceElement.dataset && sourceElement.dataset.nodeUid) || `node-${nodeIndex}`; + + node.connections.forEach(target => { + if (!target || target.isCreateNode) { + return; + } + + const targetElement = target.element || document.getElementById(target.id); + if (!targetElement || targetElement.style.display === 'none') { + return; + } + + const targetKey = target.uid || (targetElement.dataset && targetElement.dataset.nodeUid) || 'target'; + const edgeA = sourceKey < targetKey ? sourceKey : targetKey; + const edgeB = sourceKey < targetKey ? targetKey : sourceKey; + const edgeId = `${edgeA}::${edgeB}`; + if (drawnEdges.has(edgeId)) { + return; + } + drawnEdges.add(edgeId); + + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + const orbRadius = 110; + + if (this.lineIntersectsCircle(node.x, node.y, target.x, target.y, centerX, centerY, orbRadius)) { + return; + } + + const dist = Math.sqrt( + Math.pow(node.x - target.x, 2) + + Math.pow(node.y - target.y, 2) + ); + + const isMemoryMode = this.isConstellationMode && !this.isMenuOpen; + const gradient = ctx.createLinearGradient( + node.x, node.y, target.x, target.y + ); + + if (isMemoryMode) { + gradient.addColorStop(0, 'rgba(111, 99, 217, 0.2)'); + gradient.addColorStop(0.5, 'rgba(222, 179, 228, 0.15)'); + gradient.addColorStop(1, 'rgba(111, 99, 217, 0.2)'); + ctx.lineWidth = 1.2 + Math.sin(time + dist * 0.01) * 0.2; + } else { + gradient.addColorStop(0, 'rgba(111, 99, 217, 0.03)'); + gradient.addColorStop(0.5, 'rgba(222, 179, 228, 0.02)'); + gradient.addColorStop(1, 'rgba(111, 99, 217, 0.03)'); + ctx.lineWidth = 0.5 + Math.sin(time + dist * 0.01) * 0.1; + } + + ctx.strokeStyle = gradient; + + ctx.beginPath(); + ctx.moveTo(node.x, node.y); + + const curve1 = Math.sin(time * 0.4 + node.x * 0.005) * 15; + const curve2 = Math.cos(time * 0.3 + node.y * 0.005) * 10; + + ctx.bezierCurveTo( + node.x + curve1, + node.y + curve2, + target.x - curve2, + target.y - curve1, + target.x, + target.y + ); + + ctx.stroke(); + }); + }); + } + + this.neuralAnimationId = requestAnimationFrame(animate); + }; + + animate(); + } + + // ๐ŸŽฏ SAFE URL PARAM HANDLER: Auto-trigger constellation or memory creation + handleURLParams() { + try { + const urlParams = new URLSearchParams(window.location.search); + + if (urlParams.get('constellation') === 'true') { + // Auto-enter constellation mode after a brief delay + setTimeout(() => { + console.log('๐ŸŽฏ AUTO-TRIGGERING: Constellation mode from URL param'); + this.enterMemoryConstellation(); + }, 2000); // Wait for dashboard to fully load + } + + if (urlParams.get('create') === 'true') { + // Auto-start voice capture for memory creation + setTimeout(() => { + console.log('๐ŸŽฏ AUTO-TRIGGERING: Memory creation from URL param'); + this.startVoiceCapture(); + }, 2000); // Wait for dashboard to fully load + } + } catch (error) { + console.warn('๐Ÿ”ง URL param handling failed (non-critical):', error); + // Safe fallback - do nothing if URL parsing fails + } + } + + async handleRadialAction(action) { + + switch(action) { + case 'capture': + this.startVoiceCapture(); + break; + case 'memories': + await this.enterMemoryConstellation(); + break; + case 'people': + window.location.href = 'pages/people-emma.html'; + break; + case 'chat': + this.startEmmaChatExperience(); + break; + } + + // Close menu after action + if (this.isMenuOpen) { + this.toggleRadialMenu(); + } + } + + resolveEmmaChatScriptUrl() { + const existingScript = document.querySelector('script[src*="emma-chat-experience.js"]'); + if (existingScript?.src) { + return existingScript.src; + } + + try { + return new URL('js/emma-chat-experience.js', window.location.href).href; + } catch (error) { + console.warn('[Emma] Unable to resolve chat script URL, falling back to relative path', error); + return 'js/emma-chat-experience.js'; + } + } + + async ensureEmmaChatExperienceLoaded() { + if (typeof EmmaChatExperience !== 'undefined') { + return true; + } + + if (!this.emmaChatLoaderPromise) { + this.emmaChatLoaderPromise = new Promise((resolve, reject) => { + const markLoaded = (scriptEl) => { + if (scriptEl) { + scriptEl.dataset.loaded = 'true'; + } + resolve(); + }; + + const handleError = (error) => reject(error || new Error('Failed to load EmmaChatExperience script')); + const existing = document.querySelector('script[src*="emma-chat-experience.js"]'); + + if (existing) { + const alreadyLoaded = existing.dataset.loaded === 'true' || + existing.readyState === 'complete' || + existing.readyState === 'loaded'; + + if (alreadyLoaded) { + markLoaded(existing); + return; + } + + existing.addEventListener('load', () => markLoaded(existing), { once: true }); + existing.addEventListener('error', () => handleError(new Error('Failed to load EmmaChatExperience script')), { once: true }); + return; + } + + const script = document.createElement('script'); + script.src = this.resolveEmmaChatScriptUrl(); + script.async = true; + script.addEventListener('load', () => markLoaded(script), { once: true }); + script.addEventListener('error', () => handleError(new Error('Failed to load EmmaChatExperience script')), { once: true }); + document.head.appendChild(script); + }).finally(() => { + this.emmaChatLoaderPromise = null; + }); + } + + try { + await this.emmaChatLoaderPromise; + return typeof EmmaChatExperience !== 'undefined'; + } catch (error) { + console.error('[Emma] Unable to load EmmaChatExperience:', error); + return false; + } + } + + async startVoiceCapture() { + document.body.classList.remove('dashboard-minimal'); + + // Calculate elegant position relative to Emma orb + const orbRect = this.orb.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Position the panel optimally + let left = orbRect.right + 24; + let top = Math.max(40, orbRect.top - 80); + + // Ensure it fits in viewport + const panelWidth = 480; + const panelHeight = 700; + + if (left + panelWidth > viewportWidth - 40) { + left = orbRect.left - panelWidth - 24; + } + + if (top + panelHeight > viewportHeight - 40) { + top = viewportHeight - panelHeight - 40; + } + + const position = { + left: Math.max(40, left), + top: Math.max(40, top), + width: panelWidth, + height: panelHeight + }; + + // Create and show voice capture experience + this.voiceCaptureExperience = new VoiceCaptureExperience(position, { + voiceEnabled: true, + autoStart: false // Let user initiate + }); + + try { + await this.voiceCaptureExperience.show(); + + } catch (error) { + console.error('๐ŸŽค Failed to open voice capture:', error); + this.showToast('Failed to open Voice Memory Studio. Please try again.', 'error'); + } finally { + document.body.classList.add('dashboard-minimal'); + } + } + + async startEmmaChatExperience() { + + // Center chat in the middle of the page + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Chat panel dimensions + const panelWidth = 600; // Wider for comfortable chat + const panelHeight = 700; + + // Center position in the middle of the page + const position = { + left: (viewportWidth - panelWidth) / 2, + top: (viewportHeight - panelHeight) / 2, + width: panelWidth, + height: panelHeight + }; + + if (typeof EmmaChatExperience === 'undefined') { + console.warn('[Emma] EmmaChatExperience class not available, attempting to load script'); + this.showToast('Loading chat moduleโ€ฆ', 'info'); + const loaded = await this.ensureEmmaChatExperienceLoaded(); + if (!loaded) { + this.showToast('Chat module could not load. Check your connection and try again.', 'error'); + return; + } + } + + // Create and show Emma chat experience + this.emmaChatExperience = new EmmaChatExperience(position, { + contextAware: true, + memoryIntegration: true + }); + + try { + await this.emmaChatExperience.show(); + + } catch (error) { + console.error('๐Ÿ’ฌ Failed to open Emma Chat:', error); + this.showToast('Failed to open Chat with Emma. Please try again.', 'error'); + } + } + + openVaultModal() { + // Check if extension is available first + if (window.emmaWebVault && window.emmaWebVault.extensionAvailable) { + // Extension is managing vault - dashboard should work automatically + + return; // Don't show vault modal, extension handles everything + } + + const modal = document.getElementById('vault-modal'); + const statusIcon = document.getElementById('status-icon'); + const statusTitle = document.getElementById('status-title'); + const statusDescription = document.getElementById('status-description'); + const actionBtn = document.getElementById('vault-action-btn'); + const statusCard = document.getElementById('vault-status-card'); + const unlockSection = document.getElementById('unlock-section'); + const createVaultSection = document.getElementById('create-vault-section'); + + // Check if .emma vault is active (only if extension not available) + const vaultActive = sessionStorage.getItem('emmaVaultActive') === 'true'; + const vaultName = sessionStorage.getItem('emmaVaultName') || 'Unknown Vault'; + const extensionVaultActive = false; // Extension not available in this branch + + if (extensionVaultActive) { + // Extension is managing vault - show connected status + statusIcon.textContent = '๐Ÿ”—'; + statusTitle.textContent = 'Extension Connected'; + statusDescription.textContent = 'Vault managed by Emma Extension - changes auto-save to local file'; + actionBtn.textContent = '๐Ÿ”’ Manage via Extension'; + actionBtn.onclick = () => alert('Click the Emma extension icon ๐Ÿ”’ in your browser toolbar to manage your vault!'); + actionBtn.style.display = 'block'; + unlockSection.style.display = 'none'; + createVaultSection.style.display = 'none'; + statusCard.classList.add('unlocked'); // Show as unlocked/connected + } else if (window.currentVaultStatus && window.currentVaultStatus.isUnlocked) { + statusIcon.textContent = '๐Ÿ”“'; + statusTitle.textContent = `${vaultName} - Unlocked`; + statusDescription.textContent = `Your .emma vault file is open and your memories are accessible`; + actionBtn.textContent = '๐Ÿ”’ Lock Vault'; + actionBtn.onclick = () => this.lockVault(); + actionBtn.style.display = 'block'; + statusCard.classList.add('unlocked'); + unlockSection.style.display = 'none'; + createVaultSection.style.display = 'none'; + } else { + statusIcon.textContent = '๐Ÿ”’'; + statusTitle.textContent = 'Vault Locked'; + statusDescription.textContent = 'Enter your vault code to access your memories'; + actionBtn.textContent = 'Unlock Vault'; + actionBtn.onclick = () => this.showUnlockForm(); + actionBtn.style.display = 'block'; + statusCard.classList.remove('unlocked'); + unlockSection.style.display = 'none'; + createVaultSection.style.display = 'none'; + } + + modal.style.display = 'flex'; + setTimeout(() => { + modal.classList.add('show'); + }, 10); + + // Store reference for global access + window.openVaultModal = () => this.openVaultModal(); + } + + closeVaultModal() { + const modal = document.getElementById('vault-modal'); + modal.classList.remove('show'); + setTimeout(() => { + modal.style.display = 'none'; + }, 300); + } + + showUnlockForm() { + // Use beautiful universal vault modal instead of inline form + universalVaultModal.show({ + title: "Unlock Vault", + message: "Enter your vault passphrase to access your secure memories", + onSuccess: (result) => { + + // Update vault status using unified manager + const vaultName = sessionStorage.getItem('emmaVaultName') || 'Web Vault'; + window.webVaultStatus.unlock(vaultName); + + // Refresh dashboard data and UI + this.loadDashboardData(); + + // IMMEDIATE vault icon update + const vaultIcon = document.querySelector('#vault-node .radial-item-icon'); + if (vaultIcon) { + vaultIcon.textContent = '๐Ÿ”“'; + + } + + // Update vault node with delay to ensure DOM is ready + setTimeout(() => { + this.updateVaultNodeStatus(); + + // Also try to update any other vault indicators + this.updateAllVaultIndicators(); + + // Force another immediate update + const vaultIconAgain = document.querySelector('#vault-node .radial-item-icon'); + if (vaultIconAgain) { + vaultIconAgain.textContent = '๐Ÿ”“'; + + } + }, 100); + + // Don't reopen modal - let it close naturally + // The next time user opens it, it will show correct status + + this.showToast('โœ… Vault unlocked successfully!', 'success'); + + }, + onCancel: () => { + + } + }); + } + + hideUnlockForm() { + const unlockSection = document.getElementById('unlock-section'); + const passwordInput = document.getElementById('vault-password'); + unlockSection.style.display = 'none'; + passwordInput.value = ''; + } + + async unlockVault() { + const passwordInput = document.getElementById('vault-password'); + const unlockBtn = document.getElementById('unlock-btn'); + const password = passwordInput.value.trim(); + + if (!password) { + passwordInput.focus(); + return; + } + + try { + unlockBtn.disabled = true; + unlockBtn.textContent = '๐Ÿ”“ Unlocking...'; + + if (window.emmaAPI && window.emmaAPI.vault) { + const result = await window.emmaAPI.vault.unlock({ passphrase: password }); + + if (result && result.success) { + // Success - update status and close form + window.currentVaultStatus = { isUnlocked: true }; + await this.loadDashboardData(); + this.updateVaultNodeStatus(); // Update the floating vault node + this.hideUnlockForm(); + this.openVaultModal(); // Refresh modal with new status + this.showToast('โœ… Vault unlocked successfully!', 'success'); + } else { + throw new Error(result?.error || 'Unlock failed'); + } + } else { + // No API available + throw new Error('Vault API not available'); + } + } catch (error) { + console.error('๐Ÿ  Dashboard: Unlock error:', error); + this.showToast('โŒ Failed to unlock vault: ' + error.message, 'error'); + } finally { + unlockBtn.disabled = false; + unlockBtn.textContent = '๐Ÿ”“ Unlock'; + } + } + + async lockVault() { + try { + if (window.emmaAPI && window.emmaAPI.vault) { + const result = await window.emmaAPI.vault.lock(); + + if (result && result.success) { + window.currentVaultStatus = { isUnlocked: false }; + await this.loadDashboardData(); + this.updateVaultNodeStatus(); // Update the floating vault node + this.openVaultModal(); // Refresh modal with new status + this.showToast('๐Ÿ”’ Vault locked successfully!', 'success'); + } else { + throw new Error(result?.error || 'Lock failed'); + } + } else { + // Demo mode + console.warn('๐Ÿ  Dashboard: No Emma API - demo lock'); + window.currentVaultStatus = { isUnlocked: false }; + await this.loadDashboardData(); + this.updateVaultNodeStatus(); // Update the floating vault node + this.openVaultModal(); + this.showToast('๐ŸŽญ Demo vault locked!', 'info'); + } + } catch (error) { + console.error('๐Ÿ  Dashboard: Lock error:', error); + this.showToast('โŒ Failed to lock vault: ' + error.message, 'error'); + } + } + + // Clean dashboard - vault unlock handled by index.html + + async showQRShare() { + + // Check if classes are available + if (typeof EmmaShareExperience === 'undefined') { + console.error('๐Ÿ”— EmmaShareExperience class not found'); + this.showToast('Share feature unavailable. Please refresh the page.', 'error'); + return; + } + + if (typeof QRService === 'undefined') { + console.warn('๐Ÿ”— QRService class not found - will use fallback'); + } + + // Calculate elegant position relative to Emma orb + const orbRect = this.orb.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; // Position the share panel optimally + let left = orbRect.right + 24; + let top = Math.max(40, orbRect.top - 120); + + // Share panel dimensions + const panelWidth = 650; // Wider for share content + const panelHeight = 720; + + // Ensure it fits in viewport + if (left + panelWidth > viewportWidth - 40) { + left = orbRect.left - panelWidth - 24; + } + + if (top + panelHeight > viewportHeight - 40) { + top = viewportHeight - panelHeight - 40; + } + + const position = { + left: Math.max(40, left), + top: Math.max(40, top), + width: panelWidth, + height: panelHeight + }; + + try { + // Create and show Emma share experience + this.emmaShareExperience = new EmmaShareExperience(position, { + contextAware: true, + vaultIntegration: true + }); + + // Set global reference for share actions + window.shareExperience = this.emmaShareExperience; + + await this.emmaShareExperience.show(); + + } catch (error) { + console.error('๐Ÿ”— Failed to open Emma Share:', error); + this.showToast('Failed to open Share interface: ' + error.message, 'error'); + } + } + + async loadDashboardData() { + try { + // Use WebVaultStatus instead of old Electron API + + // Don't override vault status - WebVaultStatus manages it + // window.currentVaultStatus is set by WebVaultStatus manager + + // Keep central orb neutral - Emma branding only (text removed) + + // Load stats from web vault + try { + if (window.emmaWebVault && window.webVaultStatus && window.webVaultStatus.isUnlocked()) { + const stats = window.emmaWebVault.getStats(); + this.updateStats({ + memories: stats.memoryCount || 0, + people: stats.peopleCount || 0, + today: 0 // TODO: Calculate today's memories + }); + } else { + this.updateStats({ memories: 0, people: 0, today: 0 }); + } + } catch (e) { + console.warn('โš ๏ธ Web vault stats unavailable:', e.message); + this.updateStats({ memories: 0, people: 0, today: 0 }); + } + } catch (error) { + console.error('๐Ÿšจ Dashboard data load error:', error); + // Fallback to demo data - but DON'T override vault status! + // WebVaultStatus manager handles vault status, not this function + this.updateStats({ memories: 0, people: 0, today: 0 }); + } + } + + 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 reducePhysics = this.shouldReduceConstellationPhysics(); + 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() { + // Panels are now hidden by default and shown via burger menu + // No longer auto-showing panels to reduce visual distraction + + } + + // Burger menu removed for clean interface + + setupUtilityIcons() { + if (!this.utilityIcons) { + return; + } + + // Add click handlers to utility items + this.utilityIcons.querySelectorAll('.utility-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + const action = item.dataset.action; + this.handleUtilityAction(action); + }); + }); + + } + + handleUtilityAction(action) { + + switch(action) { + case 'settings': + window.location.href = 'pages/emma-settings-redesigned.html'; + break; + default: + + } + } + + togglePanels() { + this.panelsVisible = !this.panelsVisible; + + // Toggle body class for CSS animations + if (this.panelsVisible) { + document.body.classList.add('panels-visible'); + this.burgerMenuBtn.classList.add('active'); + + // Also add active class to each panel for visibility + 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'); + } else { + document.body.classList.remove('panels-visible'); + this.burgerMenuBtn.classList.remove('active'); + + // Remove active class from panels + if (this.panels.dailyBrief) this.panels.dailyBrief.classList.remove('active'); + if (this.panels.aiInsights) this.panels.aiInsights.classList.remove('active'); + if (this.panels.quickActions) this.panels.quickActions.classList.remove('active'); + } + + } + + startAnimations() { + // Subtle pulse animation for WebGL orb + setInterval(() => { + if (window.currentVaultStatus && !window.currentVaultStatus.isUnlocked) { + const orbContainer = this.orb.querySelector('.emma-orb-webgl'); + if (orbContainer) { + orbContainer.style.animation = 'orb-pulse 2s ease-in-out'; + } + } + }, 5000); + } // Enter memory constellation mode + async enterMemoryConstellation() { + document.body.classList.remove('dashboard-minimal'); + // CRITICAL: Clear any existing memory nodes first (prevents duplicates) + this.clearAllMemoryNodesFromDOM(); + + this.isConstellationMode = true; + + // PERSISTENCE: Save constellation state for page refresh + localStorage.setItem('emmaConstellationActive', 'true'); + + // Add constellation-active class to hide all UI + document.body.classList.add('constellation-active'); + + // Shrink central orb + this.shrinkCentralOrb(); + + // ๐Ÿ” CREATE ZOOMABLE CONSTELLATION CONTAINER + this.createConstellationContainer(); + + // Load memory data for constellation + await this.loadMemoriesForConstellation(); + + // Fade out current nodes + await this.fadeOutMenuNodes(); + + // Create memory constellation (await for real memories) + await this.createMemoryConstellation(); + + // Show constellation UI with zoom controls + this.showConstellationUI(); + } + + // Shrink central Emma orb when memories are visible + shrinkCentralOrb() { + + + // ๐ŸŽญ FADE OUT orb first + this.orb.style.transition = 'opacity 0.4s ease, transform 0.8s cubic-bezier(0.4, 0, 0.2, 1)'; + this.orb.style.opacity = '0'; + + // After fade out, reposition to bottom center + setTimeout(() => { + // ๐Ÿ“ REPOSITION: Move to bottom center + this.orb.style.position = 'fixed'; + this.orb.style.top = 'auto'; + this.orb.style.bottom = '40px'; + this.orb.style.left = '50%'; + this.orb.style.transform = 'translateX(-50%) scale(0.6)'; + this.orb.style.zIndex = '99999'; // CRITICAL: Above ALL overlays and constellation nodes + this.orb.style.pointerEvents = 'auto'; // CRITICAL: Ensure orb is clickable + + // ๐ŸŽญ FADE IN at new position + setTimeout(() => { + this.orb.style.opacity = '0.9'; + + }, 100); + + }, 400); // Wait for fade out to complete + + // Add hover effects for memory mode + this.addOrbHoverEffects(); + } + + // Add hover effects to orb in memory mode + addOrbHoverEffects() { + if (this.orbHoverAdded) return; + this.orbHoverAdded = true; + + const orbContainer = this.orb.querySelector('.emma-orb-webgl'); + + this.orb.addEventListener('mouseenter', () => { + if (this.isConstellationMode) { + // ๐ŸŽฏ HOVER: Scale up while maintaining bottom center position + this.orb.style.transform = 'translateX(-50%) scale(0.8)'; + this.orb.style.opacity = '1'; + } + }); + + this.orb.addEventListener('mouseleave', () => { + if (this.isConstellationMode) { + // ๐ŸŽฏ HOVER OUT: Return to normal size at bottom center + this.orb.style.transform = 'translateX(-50%) scale(0.6)'; + this.orb.style.opacity = '0.9'; + } + }); + } + + // Restore central Emma orb to original center position + restoreCentralOrb() { + + + // ๐ŸŽญ FADE OUT from bottom position + this.orb.style.transition = 'opacity 0.3s ease'; + this.orb.style.opacity = '0'; + + setTimeout(() => { + // ๐Ÿ“ RESTORE: Move back to center + this.orb.style.position = 'fixed'; + this.orb.style.top = '50%'; + this.orb.style.bottom = 'auto'; + this.orb.style.left = '50%'; + this.orb.style.transform = 'translate(-50%, -50%) scale(1)'; + this.orb.style.zIndex = '10'; + + // Reset transitions for normal behavior + this.orb.style.transition = 'transform 0.8s cubic-bezier(0.4, 0, 0.2, 1)'; + + // ๐ŸŽญ FADE IN at center + setTimeout(() => { + this.orb.style.opacity = '1'; + + }, 100); + + }, 300); // Wait for fade out + } + + // Load memories for constellation + async loadMemoriesForConstellation() { + try { + // Get memories from API + let memories = await this.getMemories(); + + if (memories.length > 0) { + + console.log('๐Ÿ’ CONSTELLATION DEBUG: Memory titles:', memories.map(m => m.title)); + } + + // Add debugging info about available APIs + + this.constellationMemories = this.organizeMemoriesByTheme(memories); + + // Count total memories + const totalMemories = Object.values(this.constellationMemories).reduce((sum, theme) => sum + theme.length, 0); + + // If no memories, suggest user create some + if (totalMemories === 0) { + + } + + } catch (error) { + + this.constellationMemories = {family: [], travel: [], recent: [], special: []}; + } + } + + // Get memories from vault (same API as gallery) + async getMemories() { + try { + let allMemories = []; + + // 1. FIRST: Get vault memories (if available) + if (window.emmaWebVault && window.emmaWebVault.isOpen && window.emmaWebVault.vaultData) { + + const vaultMemories = window.emmaWebVault.vaultData.content?.memories || {}; + const vaultMedia = window.emmaWebVault.vaultData.content?.media || {}; + + // Convert vault memories to array format with LIGHTWEIGHT LOADING for constellation + const memories = Object.values(vaultMemories).map(memory => { + // ๐Ÿš€ PERFORMANCE FIX: Only create lightweight attachment previews for constellation + // Full media will be loaded on-demand when memory is clicked + const attachmentPreviews = (memory.attachments || []).map(attachment => { + const mediaItem = vaultMedia[attachment.id]; + return { + id: attachment.id, + type: attachment.type, + name: attachment.name, + size: attachment.size, + // ๐Ÿ’ก LAZY LOADING: Store media ID for on-demand loading, don't load data URL yet + mediaId: attachment.id, + hasMedia: !!(mediaItem && mediaItem.data), + isLazyLoaded: true, // Flag to indicate this needs full loading on click + // Only create preview thumbnail for first image attachment + previewUrl: null // Will be set below for first image only + }; + }); + + // ๐ŸŽฏ OPTIMIZATION: Only load preview for FIRST image attachment (for constellation thumbnail) + if (attachmentPreviews.length > 0) { + const firstImageAttachment = attachmentPreviews.find(att => + att.hasMedia && (att.type?.startsWith('image/') || !att.type?.startsWith('video/')) + ); + + if (firstImageAttachment) { + const mediaItem = vaultMedia[firstImageAttachment.id]; + if (mediaItem && mediaItem.data) { + // Only load the first image for constellation thumbnail + firstImageAttachment.previewUrl = mediaItem.data.startsWith('data:') + ? mediaItem.data + : `data:${mediaItem.type};base64,${mediaItem.data}`; + } + } + } + + return { + ...memory, + // FIXED: Transform like gallery pages do - extract title from content/metadata + title: memory.metadata?.title || memory.title || memory.subject || memory.summary || memory.content?.substring(0, 50) + '...' || `Memory`, + attachments: attachmentPreviews, // Lightweight previews only + attachmentCount: attachmentPreviews.length, + hasMedia: attachmentPreviews.some(att => att.hasMedia), + isVaultMemory: true + }; + }); + + // Add thumbnail from first attachment for constellation display + const memoriesWithThumbnails = memories.map(memory => { + const attachments = memory.attachments || []; + let thumbnail = memory.thumbnail; + + // ๐ŸŽฏ PERFORMANCE: Use previewUrl from first image attachment for thumbnail + if (!thumbnail && attachments.length > 0) { + const firstImageWithPreview = attachments.find(att => att.previewUrl); + if (firstImageWithPreview) { + thumbnail = firstImageWithPreview.previewUrl; + } + } + + return { + ...memory, + thumbnail + }; + }); + + allMemories = [...memoriesWithThumbnails]; + console.log('๐Ÿ’ CONSTELLATION: Loaded', allMemories.length, 'vault memories'); + console.log('๐Ÿ’ CONSTELLATION DEBUG: First memory structure:', allMemories[0]); + console.log('๐Ÿ’ CONSTELLATION DEBUG: First memory keys:', Object.keys(allMemories[0] || {})); + } + + // Note: All memories should be stored directly in vault - no fallback needed + + return allMemories; + + } catch (error) { + console.error('๐Ÿ’ CONSTELLATION: Error loading memories from vault:', error); + return []; + } + } + + // Helper to detect category from memory content + detectCategoryFromContent(memory) { + const content = ((memory.title || '') + ' ' + (memory.content || '')).toLowerCase(); + if (content.includes('family') || content.includes('mom') || content.includes('dad')) return 'family'; + if (content.includes('travel') || content.includes('trip') || content.includes('vacation')) return 'travel'; + if (memory.timestamp && new Date() - new Date(memory.timestamp) < 30 * 24 * 60 * 60 * 1000) return 'recent'; + return 'special'; + } + + // Generate organic neural network positions (NOT circular) + generateNeuralNetworkPositions(nodeCount, centerX, centerY, width, height, minDistance) { + const positions = []; + const maxAttempts = 100; + + // First position: Create Memory node in top-center area + positions.push({ + x: centerX + (Math.random() - 0.5) * 100, + y: centerY - height * 0.25 + (Math.random() - 0.5) * 80 + }); + + // Generate remaining positions with organic scatter + for (let i = 1; i < nodeCount; i++) { + let placed = false; + let attempts = 0; + + while (!placed && attempts < maxAttempts) { + // Use clustered random distribution (not uniform) + const clusterX = centerX + (Math.random() - 0.5) * width; + const clusterY = centerY + (Math.random() - 0.5) * height; + + // Add some organic bias toward clusters + const biasStrength = Math.random() * 0.3; + const biasAngle = Math.random() * Math.PI * 2; + const x = clusterX + Math.cos(biasAngle) * biasStrength * 150; + const y = clusterY + Math.sin(biasAngle) * biasStrength * 150; + + // Check minimum distance from all existing nodes AND center orb + let validPosition = true; + + // Check distance from center Emma orb (exclusion zone) + const distFromCenter = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); + if (distFromCenter < 150) { // 150px exclusion zone around Emma orb + validPosition = false; + } + + // Check distance from other nodes + if (validPosition) { + for (const pos of positions) { + const dist = Math.sqrt((x - pos.x) ** 2 + (y - pos.y) ** 2); + if (dist < minDistance) { + validPosition = false; + break; + } + } + } + + // Keep nodes within bounds + if (validPosition && + x > centerX - width/2 + 50 && x < centerX + width/2 - 50 && + y > centerY - height/2 + 50 && y < centerY + height/2 - 50) { + positions.push({ x, y }); + placed = true; + } + + attempts++; + } + + // Fallback if can't place after max attempts + if (!placed) { + const angle = (i / nodeCount) * Math.PI * 2; + const fallbackRadius = 200 + Math.random() * 100; + positions.push({ + x: centerX + Math.cos(angle) * fallbackRadius, + y: centerY + Math.sin(angle) * fallbackRadius + }); + } + } + + return positions; + } // Generate memory thumbnail based on theme + generateMemoryThumbnail(theme) { + const themeEmojis = { + family: ['๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', '๐Ÿ ', 'โค๏ธ', '๐Ÿฝ๏ธ', '๐ŸŽ‚'], + travel: ['โœˆ๏ธ', '๐Ÿ—บ๏ธ', '๐Ÿ“ธ', '๐Ÿ–๏ธ', '๐Ÿ”๏ธ'], + recent: ['๐Ÿ“ฑ', 'โ˜•', '๐ŸŒ…', '๐ŸŽต', '๐Ÿ“š'], + special: ['๐ŸŽ‰', '๐Ÿ’', '๐ŸŒŸ', '๐ŸŽˆ', '๐ŸŽŠ'] + }; + const emojis = themeEmojis[theme] || ['๐Ÿ’ซ']; + return emojis[Math.floor(Math.random() * emojis.length)]; + } + + // Organize memories by theme for constellations + organizeMemoriesByTheme(memories) { + const organized = { + family: [], + travel: [], + recent: [], + special: [] + }; + + memories.forEach(memory => { + const theme = memory.theme || this.detectMemoryTheme(memory); + if (organized[theme]) { + organized[theme].push(memory); + } else { + organized.special.push(memory); + } + }); + + return organized; + } + + // Detect memory theme from content + detectMemoryTheme(memory) { + // Use category first, then fallback to content analysis + if (memory.category) { + return memory.category; + } + + const content = (memory.title + ' ' + (memory.content || memory.description || '')).toLowerCase(); + if (content.includes('family') || content.includes('mom') || content.includes('dad')) return 'family'; + if (content.includes('travel') || content.includes('trip') || content.includes('vacation')) return 'travel'; + if (memory.date && new Date() - new Date(memory.date) < 30 * 24 * 60 * 60 * 1000) return 'recent'; + return 'special'; + } + + // Fade out menu nodes + async fadeOutMenuNodes() { + return new Promise(resolve => { + this.radialMenu.querySelectorAll('.radial-item').forEach((item, i) => { + setTimeout(() => { + item.style.transition = 'all 0.5s ease'; + item.style.opacity = '0'; + item.style.transform = 'scale(0)'; + }, i * 50); + }); + + setTimeout(resolve, 500); + }); + } + + // Create memory constellation using EXACT working neural network system + async createMemoryConstellation() { + + // CRITICAL: Remove any existing memory nodes from DOM first + this.clearAllMemoryNodesFromDOM(); + + // Clear existing nodes array + this.nodes = []; + + // Don't reload memories - use already loaded ones from enterMemoryConstellation() + + // Get memories for constellation + const allMemories = []; + Object.keys(this.constellationMemories).forEach(theme => { + this.constellationMemories[theme].forEach(memory => { + allMemories.push({ ...memory, theme }); + }); + }); + + // Load people from vault for constellation + const allPeople = await this.loadPeopleForConstellation(); + this.constellationPeople = allPeople; // Store for filtering + + if (allMemories.length === 0 && allPeople.length === 0) { + console.warn('๐Ÿ’ No memories or people found in vault - constellation will show only the Create Memory node'); + } + + // Create memory and people nodes using EXACT same pattern as main menu + this.initMemoryNeuralNetwork(allMemories, allPeople); + + // Restore any saved layout preferences before animations begin + this.applySavedConstellationLayout(); + this.persistConstellationLayout(); + + // Set up for constellation mode without triggering menu + this.isMenuOpen = false; // CRITICAL: Constellation mode starts with menu closed + this.radialMenu.classList.remove('active'); // Ensure menu is hidden + document.body.classList.remove('menu-active'); + + // Start the neural animation (EXACT same as main menu) + this.animateNeuralNetwork(); + + } + + // Load people from vault for constellation + async loadPeopleForConstellation() { + try { + + if (window.emmaAPI && window.emmaAPI.people && window.emmaAPI.people.list) { + const result = await window.emmaAPI.people.list(); + + if (result && result.success && Array.isArray(result.items)) { + + return result.items; + } + } + + return []; + + } catch (error) { + console.error('๐Ÿ‘ฅ CONSTELLATION: Error loading people:', error); + return []; + } + } + + // Initialize memory neural network using EXACT same pattern as working main menu + initMemoryNeuralNetwork(memories, people = []) { + + const compactMode = this.shouldReduceConstellationPhysics(); + const nodeSize = this.getConstellationNodeSize(); + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + // Neural network layout parameters + const networkWidth = Math.max(nodeSize * 4, Math.min(window.innerWidth * (compactMode ? 0.9 : 0.7), compactMode ? 640 : 800)); + const networkHeight = Math.max(nodeSize * 4, Math.min(window.innerHeight * (compactMode ? 0.9 : 0.7), compactMode ? 540 : 600)); + const minDistance = Math.max(nodeSize + 40, compactMode ? 100 : 120); // Minimum distance between nodes + + // Take fewer nodes on compact/low power view for performance + const selectedMemories = memories.slice(0, compactMode ? 8 : 12); + const selectedPeople = people.slice(0, compactMode ? 6 : 8); + const totalNodes = selectedMemories.length + selectedPeople.length + 1; // +1 for create node + + // Generate organic neural network positions + const positions = this.generateNeuralNetworkPositions(totalNodes, centerX, centerY, networkWidth, networkHeight, minDistance); + + // Always add "Create New Memory" node at the first position (top-center area) + const createPos = positions[0]; + const createMemoryElement = this.createCreateMemoryNodeElement(createPos.x, createPos.y); + this.createMemoryElement = createMemoryElement; + const createNode = { + element: createMemoryElement, + x: createPos.x, + y: createPos.y, + vx: 0, + vy: 0, + baseX: createPos.x, + baseY: createPos.y, + connections: [], + isCreateNode: true, + uid: createMemoryElement.dataset.nodeUid + }; + this.nodes.push(createNode); + this.enableNodeDragging(createNode); + + // Create memory nodes in organic neural pattern + selectedMemories.forEach((memory, i) => { + const pos = positions[i + 1]; // Skip position 0 (create node) + + // Create memory node element + const memoryElement = this.createMemoryNodeElement(memory, pos.x, pos.y); + + const memoryNode = { + element: memoryElement, + x: pos.x, + y: pos.y, + vx: 0, + vy: 0, + baseX: pos.x, + baseY: pos.y, + connections: [], + memory: memory, + type: 'memory', + uid: memoryElement.dataset.nodeUid + }; + this.nodes.push(memoryNode); + this.enableNodeDragging(memoryNode); + }); + + // Create people nodes in remaining positions + selectedPeople.forEach((person, i) => { + const posIndex = selectedMemories.length + i + 1; // Skip create node and memory nodes + const pos = positions[posIndex]; + + if (pos) { // Make sure we have a position + // Create person node element + const personElement = this.createPersonNodeElement(person, pos.x, pos.y); + + const personNode = { + element: personElement, + x: pos.x, + y: pos.y, + vx: 0, + vy: 0, + baseX: pos.x, + baseY: pos.y, + connections: [], + person: person, + type: 'person', + uid: personElement.dataset.nodeUid + }; + this.nodes.push(personNode); + this.enableNodeDragging(personNode); + } + }); + + // Create intelligent connections between memories and people + this.createMemoryPeopleConnections(selectedMemories, selectedPeople); + + // Create organic neural network connections (NOT spoked wheel) + const organicNodes = this.nodes.filter(node => !node.isCreateNode); + organicNodes.forEach(node => { + organicNodes.forEach(otherNode => { + if (node === otherNode) { + return; + } + + const dist = Math.sqrt( + Math.pow(node.x - otherNode.x, 2) + + Math.pow(node.y - otherNode.y, 2) + ); + + if (dist < 250 && node.connections.filter(conn => !conn.isCreateNode).length < 4) { + this.linkNodes(node, otherNode); + } + }); + + const effectiveConnections = node.connections.filter(conn => !conn.isCreateNode); + if (effectiveConnections.length < 2) { + const nearest = organicNodes + .filter(otherNode => otherNode !== node) + .map(otherNode => ({ + node: otherNode, + dist: Math.sqrt( + Math.pow(node.x - otherNode.x, 2) + + Math.pow(node.y - otherNode.y, 2) + ) + })) + .sort((a, b) => a.dist - b.dist) + .slice(0, 2); + + nearest.forEach(({ node: closestNode }) => { + this.linkNodes(node, closestNode); + }); + } + }); + + // Add central orb as a special node (but only some memory nodes connect to it) + this.centralNode = { + x: centerX, + y: centerY, + element: this.orb, + connections: (this.centralNode && this.centralNode.connections) || [], + uid: 'central-orb' + }; + if (this.orb) { + this.orb.dataset.nodeUid = 'central-orb'; + } + + const eligibleCentralNodes = organicNodes.filter(node => node.type === 'memory'); + const centralConnections = Math.min(4, Math.floor(eligibleCentralNodes.length / 2) || 1); + const shuffledNodes = [...eligibleCentralNodes].sort(() => Math.random() - 0.5); + shuffledNodes.slice(0, centralConnections).forEach(node => { + this.linkNodes(node, this.centralNode); + }); + + this.purgeCreateNodeConnections(); + this.dedupeNodeConnections(); + + } + + // Create memory node element that opens memory capsule dialog + createMemoryNodeElement(memory, x, y) { + const memoryElement = document.createElement('div'); + memoryElement.className = 'memory-node'; + + // Add theme data attribute for styling + memoryElement.setAttribute('data-theme', memory.theme); + + const nodeSize = this.getConstellationNodeSize(); + const borderWidth = Math.max(2, Math.round(nodeSize * 0.04)); + const baseShadow = `0 ${Math.round(nodeSize * 0.08)}px ${Math.round(nodeSize * 0.28)}px rgba(111, 99, 217, 0.3)`; + const hoverShadow = `0 0 ${Math.round(nodeSize * 0.4)}px rgba(111, 99, 217, 0.4), 0 0 ${Math.round(nodeSize * 0.7)}px rgba(111, 99, 217, 0.24)`; + + // CLEAN CIRCULAR NODE - No text labels, pure image capsule + memoryElement.style.cssText = ` + position: fixed; + left: ${x}px; + top: ${y}px; + width: ${nodeSize}px; + height: ${nodeSize}px; + border-radius: 50%; + border: ${borderWidth}px solid rgba(111, 99, 217, 0.6); + cursor: pointer; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 0; + transform: scale(0); + box-shadow: ${baseShadow}; + z-index: 1500; /* CRITICAL FIX: Higher z-index to appear above all connections */ + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + pointer-events: auto; /* CRITICAL: Ensure nodes are clickable even with container pointer-events: none */ + `; + + // APPLY BACKGROUND AFTER BASE STYLING: Memory capsule as single node with image background + if (memory.thumbnail) { + // Use image as background of the entire node - NO separate elements + memoryElement.style.backgroundImage = `url('${memory.thumbnail}')`; + memoryElement.style.backgroundSize = 'cover'; + memoryElement.style.backgroundPosition = 'center'; + + } else { + // Fallback to gradient with emoji + memoryElement.style.background = 'linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%)'; + const emojiDiv = document.createElement('div'); + emojiDiv.style.cssText = `font-size: ${Math.round(nodeSize * 0.3)}px; color: white;`; + emojiDiv.textContent = '๐Ÿ’'; + memoryElement.appendChild(emojiDiv); + + } + + memoryElement.dataset.preventClick = 'false'; + + // Add click handler to open memory capsule dialog + memoryElement.addEventListener('click', (e) => { + if (memoryElement.dataset.preventClick === 'true') { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.stopPropagation(); + this.openMemoryCapsuleDialog(memory); + }); + + // Add hover effects + memoryElement.addEventListener('mouseenter', () => { + memoryElement.style.transform = 'scale(1.1)'; + memoryElement.style.boxShadow = hoverShadow; + }); + + memoryElement.addEventListener('mouseleave', () => { + memoryElement.style.transform = 'scale(1)'; + memoryElement.style.boxShadow = baseShadow; + }); + + // ๐Ÿ” ZOOM FIX: Add to constellation container instead of body + const targetContainer = this.constellationContainer || document.body; + targetContainer.appendChild(memoryElement); + + // Animate in with random delay for organic feel + const delay = Math.random() * 500 + 100; + setTimeout(() => { + memoryElement.style.transform = 'scale(1)'; + memoryElement.style.opacity = '1'; + }, delay); + + this.assignNodeUid(memoryElement, `memory-${memory.id || 'node'}`); + return memoryElement; + } + + // Create the "Create New Memory" node element with elegant SVG + + createCreateMemoryNodeElement(x, y) { + const createElement = document.createElement('div'); + createElement.className = 'create-memory-node'; + const nodeSize = this.getConstellationNodeSize(); + const createSize = Math.max(44, Math.round(nodeSize * 0.75)); + const createBorder = Math.max(2, Math.round(createSize * 0.05)); + const baseCreateShadow = ` + 0 0 ${Math.round(createSize * 0.28)}px rgba(16, 185, 129, 0.35), + 0 0 ${Math.round(createSize * 0.5)}px rgba(16, 185, 129, 0.18), + inset 0 0 ${Math.round(createSize * 0.28)}px rgba(16, 185, 129, 0.12) + `; + const hoverCreateShadow = ` + 0 0 ${Math.round(createSize * 0.4)}px rgba(16, 185, 129, 0.45), + 0 0 ${Math.round(createSize * 0.7)}px rgba(16, 185, 129, 0.28), + inset 0 0 ${Math.round(createSize * 0.38)}px rgba(16, 185, 129, 0.18) + `; + + // Create elegant SVG + icon + const svgPlus = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgPlus.setAttribute('width', '24'); + svgPlus.setAttribute('height', '24'); + svgPlus.setAttribute('viewBox', '0 0 24 24'); + svgPlus.style.cssText = ` + filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.8)); + transition: filter 0.3s ease; + `; + + // Create + lines with elegant styling + const horizontalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + horizontalLine.setAttribute('x1', '6'); + horizontalLine.setAttribute('y1', '12'); + horizontalLine.setAttribute('x2', '18'); + horizontalLine.setAttribute('y2', '12'); + horizontalLine.setAttribute('stroke', 'rgba(16, 185, 129, 1)'); + horizontalLine.setAttribute('stroke-width', '2'); + horizontalLine.setAttribute('stroke-linecap', 'round'); + + const verticalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + verticalLine.setAttribute('x1', '12'); + verticalLine.setAttribute('y1', '6'); + verticalLine.setAttribute('x2', '12'); + verticalLine.setAttribute('y2', '18'); + verticalLine.setAttribute('stroke', 'rgba(16, 185, 129, 1)'); + verticalLine.setAttribute('stroke-width', '2'); + verticalLine.setAttribute('stroke-linecap', 'round'); + + svgPlus.appendChild(horizontalLine); + svgPlus.appendChild(verticalLine); + createElement.appendChild(svgPlus); + + // Smaller, more elegant styling with compelling glow + createElement.style.cssText = ` + position: fixed; + left: ${x - createSize / 2}px; + top: ${y - createSize / 2}px; + width: ${createSize}px; + height: ${createSize}px; + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(34, 197, 94, 0.15)); + backdrop-filter: blur(20px); + border: ${createBorder}px solid rgba(16, 185, 129, 0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 0; + transform: scale(0); + box-shadow: ${baseCreateShadow}; + animation: addNodeGlow 3s ease-in-out infinite; + z-index: 1500; /* CRITICAL FIX: Higher z-index to appear above all connections */ + pointer-events: auto; /* CRITICAL: Ensure create node is clickable even with container pointer-events: none */ + `; + + // Add hover effect with enhanced SVG glow + createElement.addEventListener('mouseenter', () => { + createElement.style.transform = 'scale(1.15)'; + createElement.style.boxShadow = hoverCreateShadow; + svgPlus.style.filter = 'drop-shadow(0 0 12px rgba(16, 185, 129, 1))'; + }); + + createElement.addEventListener('mouseleave', () => { + createElement.style.transform = 'scale(1)'; + createElement.style.boxShadow = baseCreateShadow; + svgPlus.style.filter = 'drop-shadow(0 0 8px rgba(16, 185, 129, 0.8))'; + }); + + createElement.dataset.preventClick = 'false'; + + // Add click handler to open Emma chat for memory creation + createElement.addEventListener('click', (e) => { + if (createElement.dataset.preventClick === 'true') { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.stopPropagation(); + + // Open Emma chat experience for intelligent memory creation + this.startEmmaChatExperience(); + }); + + // ๐Ÿ” ZOOM FIX: Add to constellation container instead of body + const targetContainer = this.constellationContainer || document.body; + targetContainer.appendChild(createElement); + + // Animate in + setTimeout(() => { + createElement.style.transform = 'scale(1)'; + createElement.style.opacity = '1'; + }, 100); + + this.assignNodeUid(createElement, 'create-node'); + return createElement; + } + + // Open Emma memory creation wizard (proper capture wizard) + openEmmaMemoryWizard() { + + // CRITICAL FIX: Debug and ensure AssistantExperience is available + + console.log('๐Ÿง  DEBUG: Available window objects:', Object.keys(window).filter(k => k.includes('Assistant') || k.includes('Experience'))); + + try { + // Wait a moment for scripts to load, then check again + setTimeout(() => { + + if (window.AssistantExperience) { + + // Create and show the full assistant experience popup + const assistantInstance = new window.AssistantExperience( + { x: window.innerWidth / 2, y: window.innerHeight / 2 }, + { showCloseButton: true } + ); + // Show the full popup - this will open with the voice wizard tab active + assistantInstance.show(); + // Store reference for global access + window.assistantInstance = assistantInstance; + } else { + console.error('๐Ÿง  AssistantExperience STILL not available after delay!'); + console.log('๐Ÿง  Available classes:', Object.keys(window).filter(k => k.endsWith('Experience'))); + this.showToast('โŒ Emma Assistant not loaded', 'error'); + } + }, 100); + + } catch (error) { + console.error('Failed to open Emma memory wizard:', error); + this.showToast('โŒ Failed to open memory wizard', 'error'); + } + } + + // Fallback voice memory wizard + openVoiceMemoryWizard() { + console.log('๐Ÿง  Opening voice memory wizard (fallback)'); + + // Create a voice memory wizard modal similar to AssistantExperience + const modalHTML = ` +
+
+
+
EMMA ASKS
+

+ What's your favorite memory with Mom? Take your time, I'm listening. +

+
+ + + +
+
+ +
+
+ +
+
Tap to start recording
+
+ +
+
YOUR STORY
+
Your words will appear here as you speak...
+
+ +
+
1 of 5 questions
+
+
+
+
+ +
+ + +
+
+
+ `; + + // Create and show modal - SECURITY FIX: Use safe DOM creation + const modal = document.createElement('div'); + modal.className = 'modal-overlay'; + modal.id = 'vault-migration-modal'; + modal.style.cssText = 'display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(25px) saturate(180%); z-index: 10000; align-items: center; justify-content: center;'; + + const modalContent = document.createElement('div'); + modalContent.className = 'modal-content'; + modalContent.style.cssText = 'background: rgba(26, 16, 51, 0.92); backdrop-filter: blur(25px) saturate(150%); border: 1px solid rgba(255, 255, 255, 0.12); box-shadow: 0 25px 80px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.12); padding: 40px; border-radius: 20px; max-width: 500px; text-align: center;'; + + const title = document.createElement('h2'); + title.textContent = '๐Ÿ”„ Vault Migration Available'; + title.style.cssText = 'color: white; margin-bottom: 20px; font-size: 24px;'; + + const description = document.createElement('p'); + description.textContent = 'A new vault storage system is available with enhanced security and performance. Would you like to migrate your existing vault?'; + description.style.cssText = 'color: rgba(255, 255, 255, 0.8); margin-bottom: 30px; line-height: 1.6;'; + + const buttonContainer = document.createElement('div'); + buttonContainer.style.cssText = 'display: flex; gap: 15px; justify-content: center;'; + + const skipBtn = document.createElement('button'); + skipBtn.className = 'skip-btn'; + skipBtn.textContent = 'Skip for Now'; + skipBtn.style.cssText = 'padding: 12px 24px; background: rgba(255, 255, 255, 0.1); color: white; border: none; border-radius: 8px; cursor: pointer;'; + + const continueBtn = document.createElement('button'); + continueBtn.className = 'continue-btn'; + continueBtn.textContent = 'Migrate Vault'; + continueBtn.style.cssText = 'padding: 12px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #deb3e4 100%); color: white; border: none; border-radius: 8px; cursor: pointer;'; + + buttonContainer.appendChild(skipBtn); + buttonContainer.appendChild(continueBtn); + modalContent.appendChild(title); + modalContent.appendChild(description); + modalContent.appendChild(buttonContainer); + modal.appendChild(modalContent); + document.body.appendChild(modal); + + // Add event listeners + const skipBtnQuery = modal.querySelector('.skip-btn'); + const continueBtnQuery = modal.querySelector('.continue-btn'); + + skipBtnQuery.addEventListener('click', () => modal.remove()); + continueBtnQuery.addEventListener('click', () => { + // For now, just show a message and close + this.showToast('๐ŸŽค Voice wizard coming soon!', 'info'); + modal.remove(); + }); + + // Close on outside click + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); + } + + // Open memory capsule dialog (reuses existing memory dialog system) + openMemoryCapsuleDialog(memory) { + + this.openMemoryDialog(memory); + } + + // Create neural branches from central Emma neuron + createNeuralBranches(memories) { + + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + + // First layer - primary neurons directly connected to Emma + const primaryCount = Math.min(8, memories.length); // Max 8 primary branches + const primaryAngleStep = (Math.PI * 2) / primaryCount; + const primaryRadius = 200; + + // Create primary neurons + const primaryNeurons = []; + for (let i = 0; i < primaryCount; i++) { + const angle = i * primaryAngleStep - Math.PI / 2; + const x = centerX + Math.cos(angle) * primaryRadius; + const y = centerY + Math.sin(angle) * primaryRadius; + + if (memories[i]) { + const neuron = this.createMemoryNode(memories[i], x, y, memories[i].theme); + neuron.isPrimary = true; + neuron.connections.push(this.centralNeuron); + this.centralNeuron.connections.push(neuron); + primaryNeurons.push(neuron); + this.nodes.push(neuron); + } + } + + // Create secondary and tertiary neurons branching from primaries + let memoryIndex = primaryCount; + primaryNeurons.forEach((primaryNeuron, primaryIndex) => { + // Calculate how many secondary neurons this primary should have + const remainingMemories = memories.length - memoryIndex; + const remainingPrimaries = primaryCount - primaryIndex; + const secondaryCount = Math.ceil(remainingMemories / remainingPrimaries); + + // Create secondary neurons + for (let j = 0; j < secondaryCount && memoryIndex < memories.length; j++) { + const memory = memories[memoryIndex++]; + + // Position secondary neurons in an arc around the primary + const secondaryAngle = primaryIndex * primaryAngleStep + + (j - secondaryCount/2) * (Math.PI / 6) / secondaryCount; + const secondaryRadius = 120 + Math.random() * 60; + + const x = primaryNeuron.x + Math.cos(secondaryAngle) * secondaryRadius; + const y = primaryNeuron.y + Math.sin(secondaryAngle) * secondaryRadius; + + // Ensure within bounds + const padding = 100; + const finalX = Math.max(padding, Math.min(window.innerWidth - padding, x)); + const finalY = Math.max(padding, Math.min(window.innerHeight - padding, y)); + + const secondaryNeuron = this.createMemoryNode(memory, finalX, finalY, memory.theme); + secondaryNeuron.isSecondary = true; + secondaryNeuron.connections.push(primaryNeuron); + primaryNeuron.connections.push(secondaryNeuron); + this.nodes.push(secondaryNeuron); + } + }); + + } + + // Create individual memory node + createMemoryNode(memory, x, y, theme) { + console.log(`๐ŸŒŸ Creating memory node element for "${memory.title}" at (${x}, ${y})`); + + const memoryElement = document.createElement('div'); + memoryElement.className = 'memory-node'; + + // SECURITY FIX: Use safe DOM creation instead of innerHTML + const thumbnail = document.createElement('div'); + thumbnail.className = 'memory-thumbnail'; + thumbnail.textContent = memory.thumbnail; // Safe text content + + const label = document.createElement('div'); + label.className = 'memory-label'; + label.textContent = memory.title; // Safe text content + + memoryElement.appendChild(thumbnail); + memoryElement.appendChild(label); + + // Add theme data attribute for styling + memoryElement.setAttribute('data-theme', theme); + + // Style the memory node + memoryElement.style.cssText = ` + position: fixed; + left: ${x}px; + top: ${y}px; + width: 70px; + height: 70px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1), rgba(111, 99, 217, 0.2)); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1); + transform: scale(0); + opacity: 0; + z-index: 1000; + color: white; + `; + + memoryElement.dataset.preventClick = 'false'; + + // Add click handler for elegant memory dialog + memoryElement.addEventListener('click', (e) => { + if (memoryElement.dataset.preventClick === 'true') { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.stopPropagation(); + this.openMemoryDialog(memory); + }); + + // Add hover effects with theme-based glow + memoryElement.addEventListener('mouseenter', () => { + const themeColors = { + family: 'rgba(255, 105, 135, 0.8)', + travel: 'rgba(52, 211, 153, 0.8)', + recent: 'rgba(96, 165, 250, 0.8)', + special: 'rgba(251, 191, 36, 0.8)' + }; + + memoryElement.style.transform = 'scale(1.3)'; + memoryElement.style.boxShadow = `0 0 32px ${themeColors[theme] || 'rgba(111, 99, 217, 0.55)'}, 0 0 52px ${themeColors[theme] || 'rgba(111, 99, 217, 0.28)'}`; + memoryElement.style.zIndex = '1600'; /* CRITICAL FIX: Even higher on hover to stay above everything */ + }); + + memoryElement.addEventListener('mouseleave', () => { + memoryElement.style.transform = 'scale(1)'; + memoryElement.style.boxShadow = ''; + memoryElement.style.zIndex = '1500'; /* CRITICAL FIX: Maintain high z-index even when not hovering */ + }); + + // ๐Ÿ” ZOOM FIX: Add to constellation container for proper zoom behavior + const targetContainer = this.constellationContainer || document.querySelector('.dashboard') || document.body; + targetContainer.appendChild(memoryElement); + + // Animate in with staggered timing + const delay = Math.random() * 500 + 200; // 200-700ms delay + setTimeout(() => { + + memoryElement.style.transform = 'scale(1)'; + memoryElement.style.opacity = '1'; + }, delay); + + const uid = this.assignNodeUid(memoryElement, `memory-${memory.id || 'node'}`); + + const nodeData = { + element: memoryElement, + x: x, + y: y, + baseX: x, + baseY: y, + vx: 0, + vy: 0, + memory: memory, + theme: theme, + connections: [], + uid: uid, + type: 'memory' + }; + + this.enableNodeDragging(nodeData); + + return nodeData; + } + + // Create person node element for constellation with heartbeat effect + createPersonNodeElement(person, x, y) { + const personElement = document.createElement('div'); + personElement.className = 'person-node'; + + // Create person avatar with first letter of name + const avatar = person.name.charAt(0).toUpperCase(); + const relationColor = this.getRelationColor(person.relation); + const nodeSize = this.getConstellationNodeSize(); + const personSize = Math.max(56, Math.round(nodeSize * 1.05)); + const borderWidth = Math.max(2, Math.round(personSize * 0.045)); + const basePersonShadow = ` + 0 ${Math.round(personSize * 0.08)}px ${Math.round(personSize * 0.3)}px ${relationColor.glow}, + 0 0 ${Math.round(personSize * 0.18)}px rgba(239, 68, 68, 0.26), + 0 0 ${Math.round(personSize * 0.32)}px rgba(239, 68, 68, 0.1) + `; + const hoverPersonShadow = ` + 0 0 ${Math.round(personSize * 0.42)}px ${relationColor.glow}, + 0 0 ${Math.round(personSize * 0.75)}px rgba(239, 68, 68, 0.3) + `; + + // Create BIGGER avatar circle with subtle heartbeat glow - NO TEXT LABELS + personElement.style.cssText = ` + position: absolute; + left: ${x}px; + top: ${y}px; + width: ${personSize}px; + height: ${personSize}px; + border-radius: 50%; + background: ${relationColor.gradient}; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; + font-size: ${Math.round(personSize * 0.33)}px; + border: ${borderWidth}px solid ${relationColor.border}; + box-shadow: ${basePersonShadow}; + overflow: hidden; + cursor: pointer; + transform: scale(0); + opacity: 0; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1); + animation: heartbeat 4s ease-in-out infinite; + z-index: 1500; /* CRITICAL FIX: Higher z-index to appear above all connections */ + pointer-events: auto; /* CRITICAL: Ensure person nodes are clickable even with container pointer-events: none */ + `; + + // Start with letter, then try to load avatar image + personElement.textContent = avatar; + + // Try to load actual avatar image if person has one + if (person.avatarId) { + this.loadPersonAvatar(person, personElement, avatar); + } + + personElement.dataset.preventClick = 'false'; + + // Add click handler to open person summary modal + personElement.addEventListener('click', (e) => { + if (personElement.dataset.preventClick === 'true') { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.stopPropagation(); + this.openPersonSummaryModal(person); + }); + + // Add hover effects with relation-based glow + personElement.addEventListener('mouseenter', () => { + personElement.style.transform = 'scale(1.15)'; + personElement.style.boxShadow = hoverPersonShadow; + personElement.style.zIndex = '1600'; /* CRITICAL FIX: Even higher on hover to stay above everything */ + }); + + personElement.addEventListener('mouseleave', () => { + personElement.style.transform = 'scale(1)'; + personElement.style.boxShadow = basePersonShadow; + personElement.style.zIndex = '1500'; /* CRITICAL FIX: Maintain high z-index even when not hovering */ + }); + + // ๐Ÿ”— CRITICAL FIX: Append to constellation container for zoom/pan anchoring + if (this.constellationContainer) { + this.constellationContainer.appendChild(personElement); + } else { + const container = document.querySelector('.dashboard') || document.body; + container.appendChild(personElement); + } + + // Animate in with staggered timing + const delay = Math.random() * 500 + 300; // 300-800ms delay + setTimeout(() => { + + personElement.style.transform = 'scale(1)'; + personElement.style.opacity = '1'; + }, delay); + + this.assignNodeUid(personElement, `person-${person.id || 'node'}`); + return personElement; + } + + // Load actual avatar image for person node + async loadPersonAvatar(person, personElement, fallbackLetter) { + try { + + // FIXED: Use avatarUrl directly from extension (already reconstructed) + if (person.avatarUrl) { + // Create image element + const img = document.createElement('img'); + img.src = person.avatarUrl; + img.draggable = false; + img.style.pointerEvents = 'none'; + img.alt = person.name || 'Person avatar'; + img.addEventListener('dragstart', (event) => event.preventDefault()); + img.alt = `${person.name} avatar`; + img.style.cssText = ` + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; + `; + + img.onload = () => { + // Replace letter with image once loaded + personElement.innerHTML = ''; + personElement.appendChild(img); + + }; + + img.onerror = () => { + console.error('๐Ÿ“ท CONSTELLATION: Failed to display avatar for:', person.name); + // Keep the letter fallback + }; + } else { + console.warn('๐Ÿ“ท CONSTELLATION: No avatar URL for:', person.name); + } + } catch (error) { + console.error('๐Ÿ“ท CONSTELLATION: Error loading avatar for:', person.name, error); + // Keep the letter fallback + } + } + + // Open revolutionary migration page + openMigrationPage() { + + window.location.href = 'pages/vault-migration.html'; + } + + // Open person summary modal with connected memories + async openPersonSummaryModal(person) { + + // ๐ŸŽฏ FIXED: Use same memory loading approach as people-emma.html + let connectedMemories = []; + try { + if (!window.emmaWebVault || !window.emmaWebVault.vaultData?.content) { + console.error('โŒ CONSTELLATION: Vault data not available'); + return; + } + + const vaultData = window.emmaWebVault.vaultData; + const allMemories = vaultData.content.memories || {}; + + // Find memories that include this person - SAME LOGIC AS PEOPLE-EMMA.HTML + for (const [memoryId, memory] of Object.entries(allMemories)) { + const memoryWithId = memory.id ? memory : { ...memory, id: memoryId }; + + if (window.metadataIncludesPerson(memory.metadata?.people, person)) { + connectedMemories.push(memoryWithId); + continue; + } + + // Content-based inference removed for accuracy; rely on explicit metadata links only + } + + console.log(`๐Ÿ”— CONSTELLATION: Found ${connectedMemories.length} memories for person ${person.id}`); + + } catch (error) { + console.error('โŒ CONSTELLATION: Error loading memories:', error); + } + + // Create modal HTML + const modalHTML = ` +
+
+ + +
+
+ ${person.name.charAt(0).toUpperCase()} +
+
+

${person.name}

+

${person.relation || 'other'}

+

${person.contact || 'No contact info'}

+
+
+ +
+

Connected Memories

+
+ ${connectedMemories.length > 0 ? + connectedMemories.map(memory => { + // Get thumbnail from attachments or fallback + const thumbnail = memory.thumbnail || + (memory.attachments && memory.attachments.length > 0 && memory.attachments[0].url ? memory.attachments[0].url : null); + const memoryTitle = memory.metadata?.title || memory.title || memory.subject || 'Untitled Memory'; + const memoryDate = memory.created || memory.date || memory.timestamp; + + return ` +
+
+ ${thumbnail ? `Memory` : '๐Ÿ’'} +
+
+
${memoryTitle}
+
${memoryDate ? new Date(memoryDate).toLocaleDateString() : 'Unknown date'}
+
+
+ `; + }).join('') : + '
No connected memories yet
' + } +
+ +
+ + +
+
+
+
+ `; + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', modalHTML); + + // Make constellation functions globally accessible for modal + window.memoryConstellation = this; + + // Load avatar if available - CRITICAL for showing person's image + if (person.avatarUrl) { + this.loadAvatarForSummary(person); + } else { + + } + + // Show modal with animation + setTimeout(() => { + document.getElementById('person-summary-overlay').classList.add('active'); + }, 10); + } + + async loadAvatarForSummary(person) { + try { + + // FIXED: Use avatarUrl directly (same as constellation nodes and people page) + if (person.avatarUrl) { + const avatarElement = document.getElementById('person-summary-avatar'); + if (avatarElement) { + + avatarElement.innerHTML = `${person.name}`; + + } else { + console.error('๐Ÿ“ท SUMMARY: Avatar element not found in DOM'); + } + } else { + console.warn('๐Ÿ“ท SUMMARY: No avatarUrl available for:', person.name); + } + } catch (error) { + console.error('๐Ÿ“ท SUMMARY: Error loading avatar for:', person.name, error); + } + } + + closePersonSummaryModal() { + const overlay = document.getElementById('person-summary-overlay'); + if (overlay) { + overlay.classList.remove('active'); + setTimeout(() => overlay.remove(), 300); + } + } + + createMemoryWithPersonFromConstellation(personId) { + this.closePersonSummaryModal(); + window.location.href = `dashboard.html?createMemory=true&person=${personId}`; + } + + async openMemoryFromPerson(memoryId) { + + this.closePersonSummaryModal(); + + // Find the memory in our loaded memories + try { + let allMemories = []; + if (window.emmaWebVault && window.emmaWebVault.extensionAvailable) { + allMemories = await window.emmaWebVault.listMemories(1000); + } + + const memory = allMemories.find(m => m.id === memoryId); + if (memory) { + this.openMemoryDialog(memory); + } else { + console.error('๐Ÿ’ Memory not found:', memoryId); + this.showToast('โŒ Memory not found', 'error'); + } + } catch (error) { + console.error('๐Ÿ’ Error opening memory from person:', error); + this.showToast('โŒ Error opening memory', 'error'); + } + } + + // Get relation-specific colors for people nodes + getRelationColor(relation) { + const colors = { + family: { + gradient: 'linear-gradient(135deg, #ef4444, #f87171)', + border: 'rgba(239, 68, 68, 0.6)', + glow: 'rgba(239, 68, 68, 0.8)' + }, + friend: { + gradient: 'linear-gradient(135deg, #3b82f6, #60a5fa)', + border: 'rgba(59, 130, 246, 0.6)', + glow: 'rgba(59, 130, 246, 0.8)' + }, + colleague: { + gradient: 'linear-gradient(135deg, #10b981, #34d399)', + border: 'rgba(16, 185, 129, 0.6)', + glow: 'rgba(16, 185, 129, 0.8)' + }, + romantic: { + gradient: 'linear-gradient(135deg, #d06fa8, #f472b6)', + border: 'rgba(208, 111, 168, 0.6)', + glow: 'rgba(208, 111, 168, 0.8)' + } + }; + + return colors[relation] || colors.friend; // Default to friend colors + } + + // Open person dialog (placeholder) + openPersonDialog(person) { + + this.showToast(`๐Ÿ‘ฅ ${person.name} (${person.relation})`, 'info'); + } + + // Create intelligent connections between memory nodes and people nodes + createMemoryPeopleConnections(memories, people) { + + const memoryNodes = this.nodes.filter(node => node.type === 'memory'); + const peopleNodes = this.nodes.filter(node => node.type === 'person'); + const reducePhysics = this.shouldReduceConstellationPhysics(); + + // For each memory, connect to people who were involved + memoryNodes.forEach(memoryNode => { + const memory = memoryNode.memory; + + // Check if memory has people data (from wizard selections or manual input) + if (memory.selectedPeople && Array.isArray(memory.selectedPeople)) { + // Connect to people selected in wizard + memory.selectedPeople.forEach(personId => { + const personNode = peopleNodes.find(pNode => pNode.person.id === personId); + if (personNode) { + this.linkNodes(memoryNode, personNode); + } + }); + } + + // Also check if people are mentioned in memory content/responses + const memoryText = (memory.content || '').toLowerCase() + ' ' + + (memory.responses || []).join(' ').toLowerCase(); + + peopleNodes.forEach(personNode => { + const personName = personNode.person.name.toLowerCase(); + + // If person's name is mentioned in the memory text, create connection + if (memoryText.includes(personName)) { + this.linkNodes(memoryNode, personNode); + console.log(`๐Ÿ”— Auto-connected memory "${memory.title}" to person "${personNode.person.name}" (name mentioned)`); + } + }); + }); + + if (!reducePhysics) { + // Create some random connections for people who aren't connected to anything + peopleNodes.forEach(personNode => { + if (personNode.connections.length === 0) { + let closestMemory = null; + let closestDistance = Infinity; + + memoryNodes.forEach(memoryNode => { + const dx = personNode.x - memoryNode.x; + const dy = personNode.y - memoryNode.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < closestDistance && memoryNode.connections.length < 3) { + closestDistance = distance; + closestMemory = memoryNode; + } + }); + + if (closestMemory) { + this.linkNodes(personNode, closestMemory); + } + } + }); + } + } // Update neural network node positions with overlap prevention + updateConstellationNodes() { + const time = Date.now() * 0.001; + + // Update all memory nodes + this.nodes.forEach((node, i) => { + // Reset forces + let forceX = 0; + let forceY = 0; + + // Repulsion from other nodes to prevent overlap + this.nodes.forEach((otherNode, j) => { + if (i !== j) { + const dx = node.x - otherNode.x; + const dy = node.y - otherNode.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + const minDistance = node.isPrimary || otherNode.isPrimary ? 150 : 120; + if (distance < minDistance) { + const force = (minDistance - distance) / distance * 0.3; + forceX += dx * force; + forceY += dy * force; + } + } + }); + + // Also repel from central neuron + if (this.centralNeuron) { + const dx = node.x - this.centralNeuron.x; + const dy = node.y - this.centralNeuron.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 150) { + const force = (150 - distance) / distance * 0.4; + forceX += dx * force; + forceY += dy * force; + } + } + + // Gentle attraction to base position + const baseAttraction = node.isPrimary ? 0.03 : 0.02; + forceX += (node.baseX - node.x) * baseAttraction; + forceY += (node.baseY - node.y) * baseAttraction; + + // Subtle floating motion - less for primary nodes + const floatAmplitude = node.isPrimary ? 1.5 : 2.5; + const floatX = Math.sin(time * 0.15 + i * 0.7) * floatAmplitude; + const floatY = Math.cos(time * 0.12 + i * 0.5) * floatAmplitude; + + // Apply forces + node.vx = (node.vx + forceX) * 0.92; // Damping + node.vy = (node.vy + forceY) * 0.92; + + // Update position + node.x += node.vx + floatX * 0.4; + node.y += node.vy + floatY * 0.4; + + // Keep within bounds + const padding = 80; + node.x = Math.max(padding, Math.min(window.innerWidth - padding, node.x)); + node.y = Math.max(padding, Math.min(window.innerHeight - padding, node.y)); + + // Update DOM position + if (node.element) { + node.element.style.left = node.x + 'px'; + node.element.style.top = node.y + 'px'; + } + }); + + // The central neuron (Emma orb) stays fixed at center + if (this.centralNeuron) { + this.centralNeuron.x = window.innerWidth / 2; + this.centralNeuron.y = window.innerHeight / 2; + } + } + + // Show elegant constellation burger menu + showConstellationUI() { + // Initialize constellation filters state + this.constellationFilters = { + memories: true, + people: true, + family: true, + travel: true, + recent: true, + special: true + }; + const savedFilters = this.loadSavedConstellationFilters(); + if (savedFilters) { + this.constellationFilters = { ...this.constellationFilters, ...savedFilters }; + } + + // Create elegant burger menu + const burgerMenu = document.createElement('div'); + burgerMenu.className = 'constellation-burger-menu'; + burgerMenu.innerHTML = ` + + + + +
+
+

๐ŸŒŸ Constellation Filters

+ +
+ +
+

Node Types

+
+ + + +
+
+ +
+

Memory Themes

+
+ + + + + + + +
+
+ +
+ + +
+
+ `; + + // Add elegant styles + const styles = document.createElement('style'); + styles.textContent = ` + .constellation-burger-menu { + position: fixed; + top: 20px; + left: 20px; + z-index: 2000; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + .constellation-burger-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + background: rgba(17, 17, 27, 0.95); + border: 1px solid rgba(111, 99, 217, 0.4); + border-radius: 12px; + padding: 12px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(15px); + box-shadow: 0 8px 32px rgba(111, 99, 217, 0.15); + } + + .constellation-burger-btn:hover { + background: rgba(111, 99, 217, 0.1); + border-color: rgba(111, 99, 217, 0.6); + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(111, 99, 217, 0.25); + } + + .burger-lines { + display: flex; + flex-direction: column; + gap: 3px; + } + + .burger-lines span { + width: 18px; + height: 2px; + background: rgba(111, 99, 217, 0.8); + border-radius: 1px; + transition: all 0.3s ease; + } + + .constellation-burger-btn:hover .burger-lines span { + background: rgba(111, 99, 217, 1); + } + + .burger-label { + font-size: 10px; + color: rgba(255, 255, 255, 0.7); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .constellation-menu-panel { + position: absolute; + top: 0; + left: 0; + width: 320px; + background: rgba(17, 17, 27, 0.98); + border: 1px solid rgba(111, 99, 217, 0.3); + border-radius: 16px; + backdrop-filter: blur(20px); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + transform: translateX(-100%) scale(0.95); + opacity: 0; + visibility: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; + } + + .constellation-menu-panel.active { + transform: translateX(0) scale(1); + opacity: 1; + visibility: visible; + } + + .panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px 16px; + border-bottom: 1px solid rgba(111, 99, 217, 0.2); + } + + .panel-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: white; + } + + .panel-close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + font-size: 18px; + cursor: pointer; + padding: 4px; + border-radius: 6px; + transition: all 0.2s ease; + } + + .panel-close:hover { + background: rgba(255, 255, 255, 0.1); + color: white; + } + + .filter-section { + padding: 20px 24px; + border-bottom: 1px solid rgba(111, 99, 217, 0.1); + } + + .filter-section:last-of-type { + border-bottom: none; + } + + .filter-section h4 { + margin: 0 0 16px 0; + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.8); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .filter-toggles { + display: flex; + flex-direction: column; + gap: 12px; + } + + .filter-toggle { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 8px 0; + transition: all 0.2s ease; + } + + .filter-toggle:hover { + background: rgba(111, 99, 217, 0.05); + border-radius: 8px; + padding: 8px 8px; + margin: 0 -8px; + } + + .filter-toggle input[type="checkbox"] { + display: none; + } + + .toggle-slider { + position: relative; + width: 40px; + height: 20px; + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + transition: all 0.3s ease; + flex-shrink: 0; + } + + .toggle-slider:before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } + + .filter-toggle input:checked + .toggle-slider { + background: rgba(111, 99, 217, 0.8); + } + + .filter-toggle input:checked + .toggle-slider:before { + transform: translateX(20px); + background: white; + } + + /* Theme-specific toggle colors */ + .toggle-slider.family:before { background: rgba(255, 105, 135, 0.1); } + .filter-toggle input:checked + .toggle-slider.family { background: rgba(255, 105, 135, 0.8); } + + .toggle-slider.travel:before { background: rgba(52, 211, 153, 0.1); } + .filter-toggle input:checked + .toggle-slider.travel { background: rgba(52, 211, 153, 0.8); } + + .toggle-slider.recent:before { background: rgba(96, 165, 250, 0.1); } + .filter-toggle input:checked + .toggle-slider.recent { background: rgba(96, 165, 250, 0.8); } + + .toggle-slider.special:before { background: rgba(251, 191, 36, 0.1); } + .filter-toggle input:checked + .toggle-slider.special { background: rgba(251, 191, 36, 0.8); } + + .toggle-label { + flex: 1; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + font-weight: 500; + } + + .toggle-count { + background: rgba(111, 99, 217, 0.2); + color: rgba(111, 99, 217, 1); + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + min-width: 20px; + text-align: center; + } + + .filter-actions { + padding: 20px 24px; + display: flex; + gap: 12px; + } + + .filter-btn { + flex: 1; + padding: 12px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + } + + .filter-btn.secondary { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + } + + .filter-btn.secondary:hover { + background: rgba(255, 255, 255, 0.15); + color: white; + } + + .filter-btn.primary { + background: rgba(111, 99, 217, 0.8); + color: white; + } + + .filter-btn.primary:hover { + background: rgba(111, 99, 217, 1); + transform: translateY(-1px); + } + `; + + document.head.appendChild(styles); + document.body.appendChild(burgerMenu); + + // Set up burger menu interactions + this.setupConstellationMenu(); + + // Update counts + this.updateConstellationCounts(); + + this.constellationUI = burgerMenu; + + // ๐ŸŽฏ NO CONTROLS NEEDED: Natural gestures (wheel zoom, drag pan, pinch) are intuitive + } + + // Set up constellation menu interactions + setupConstellationMenu() { + const burgerBtn = document.getElementById('constellationBurger'); + const panel = document.getElementById('constellationPanel'); + + // Toggle menu on burger click + burgerBtn.addEventListener('click', () => { + panel.classList.toggle('active'); + }); + + // Close menu when clicking outside + document.addEventListener('click', (e) => { + if (!burgerBtn.contains(e.target) && !panel.contains(e.target)) { + panel.classList.remove('active'); + } + }); + + // Set up filter toggle event listeners + const filterInputs = panel.querySelectorAll('input[type="checkbox"]'); + filterInputs.forEach(input => { + const filterType = input.id.replace('filter-', ''); + if (this.constellationFilters && typeof this.constellationFilters[filterType] === 'boolean') { + input.checked = this.constellationFilters[filterType]; + } else { + this.constellationFilters[filterType] = input.checked; + } + input.addEventListener('change', (e) => { + const filterType = e.target.id.replace('filter-', ''); + this.toggleConstellationFilter(filterType, e.target.checked); + }); + }); + + // Set up action button event listeners + document.getElementById('reset-filters-btn').addEventListener('click', () => { + this.resetConstellationFilters(); + }); + + document.getElementById('apply-filters-btn').addEventListener('click', () => { + this.hideConstellationMenu(); + }); + + // Apply saved filter state immediately so constellation reflects preferences + this.applyConstellationFilters(); + this.persistConstellationFilters(); + } + + // Toggle constellation filter and update display + toggleConstellationFilter(filterType, enabled) { + if (!this.constellationFilters) { + this.constellationFilters = {}; + } + this.constellationFilters[filterType] = enabled; + this.persistConstellationFilters(); + + // Apply filters with smooth animation + this.applyConstellationFilters(); + } + + // Apply constellation filters with smooth animations + applyConstellationFilters() { + const nodes = document.querySelectorAll('.memory-node, .person-node'); + + nodes.forEach(node => { + const nodeType = node.classList.contains('memory-node') ? 'memory' : 'person'; + const theme = node.dataset.theme || 'recent'; + + let shouldShow = false; + + // Check if node type is enabled + if (nodeType === 'memory' && this.constellationFilters.memories) { + // Check if memory theme is enabled + shouldShow = this.constellationFilters[theme]; + } else if (nodeType === 'person' && this.constellationFilters.people) { + shouldShow = true; + } + + // Smooth animation for show/hide + if (shouldShow && node.style.display === 'none') { + // Show with fade in + node.style.display = 'block'; + node.style.opacity = '0'; + node.style.transform = 'scale(0.8)'; + + requestAnimationFrame(() => { + node.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; + node.style.opacity = '1'; + node.style.transform = 'scale(1)'; + }); + } else if (!shouldShow && node.style.display !== 'none') { + // Hide with fade out + node.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + node.style.opacity = '0'; + node.style.transform = 'scale(0.8)'; + + setTimeout(() => { + node.style.display = 'none'; + }, 300); + } + }); + + // Neural connections now update automatically in animation loop based on node visibility + } + + // Neural connections now handled automatically in main animation loop + + // Update constellation counts in the menu + updateConstellationCounts() { + // Count memories by theme + const themeCounts = { + family: 0, + travel: 0, + recent: 0, + special: 0 + }; + + Object.keys(this.constellationMemories || {}).forEach(theme => { + themeCounts[theme] = (this.constellationMemories[theme] || []).length; + }); + + // Count people + const peopleCount = this.constellationPeople ? this.constellationPeople.length : 0; + const totalMemories = Object.values(themeCounts).reduce((sum, count) => sum + count, 0); + + // Update count displays + const updateCount = (id, count) => { + const element = document.getElementById(id); + if (element) element.textContent = count; + }; + + updateCount('count-memories', totalMemories); + updateCount('count-people', peopleCount); + updateCount('count-family', themeCounts.family); + updateCount('count-travel', themeCounts.travel); + updateCount('count-recent', themeCounts.recent); + updateCount('count-special', themeCounts.special); + } + + // Reset all constellation filters + resetConstellationFilters() { + // Reset all filters to true + this.constellationFilters = { + memories: true, + people: true, + family: true, + travel: true, + recent: true, + special: true + }; + + // Update checkboxes + const checkboxes = document.querySelectorAll('#constellationPanel input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + }); + + // Apply filters + this.applyConstellationFilters(); + this.persistConstellationFilters(); + + } + + // Hide constellation menu + hideConstellationMenu() { + const panel = document.getElementById('constellationPanel'); + if (panel) { + panel.classList.remove('active'); + } + } + + // ๐Ÿ” CREATE ZOOMABLE CONSTELLATION CONTAINER + createConstellationContainer() { + // Remove any existing constellation container + const existingContainer = document.getElementById('constellation-zoom-container'); + if (existingContainer) { + existingContainer.remove(); + } + + // Create zoomable container for constellation nodes + this.constellationContainer = document.createElement('div'); + this.constellationContainer.id = 'constellation-zoom-container'; + this.constellationContainer.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1400; + transform-origin: 0 0; + will-change: transform; + `; + + // ๐Ÿ”ง CRITICAL FIX: Container doesn't block events, but child nodes can still be clicked + // Child nodes will have pointer-events: auto set individually + + // Reset zoom state + this.zoomState.scale = 1; + this.zoomState.translateX = 0; + this.zoomState.translateY = 0; + + // Add to dashboard + document.body.appendChild(this.constellationContainer); + + if (this.neuralCanvas) { + this.neuralCanvas.style.transformOrigin = '0 0'; + } + + // Enable zoom/pan interaction on the container + this.setupConstellationZoom(); + + console.log('๐Ÿ” ZOOM: Created constellation container with zoom functionality'); + } + + // ๐Ÿ” SETUP ZOOM AND PAN FUNCTIONALITY + setupConstellationZoom() { + // Enable pointer events for zoom interaction + document.addEventListener('wheel', (e) => { + if (this.isConstellationMode) { + // ๐Ÿšซ SMART SCROLL: Don't intercept wheel events over dialogs/modals/chat + const isOverDialog = e.target.closest('.memory-preview-dialog') || + e.target.closest('.person-summary-modal') || + e.target.closest('.voice-wizard-modal') || + e.target.closest('.modal-overlay') || + e.target.closest('.responsive-memory-container') || + + // ๐Ÿ’ฌ CHAT WINDOW EXCLUSIONS + e.target.closest('.experience-popup') || + e.target.closest('.chat-container') || + e.target.closest('.message-container') || + e.target.closest('.messages-container') || + e.target.closest('.chat-messages') || + e.target.closest('.emma-chat') || + e.target.closest('[class*="chat"]') || + e.target.closest('[class*="message"]') || + e.target.closest('[class*="popup"]') || + + // Vault control panel: allow native scroll inside overlay + e.target.closest('.vault-control-overlay') || + e.target.closest('.vault-control-dialog') || + e.target.closest('.vault-control'); + + if (!isOverDialog) { + // Only zoom constellation when NOT scrolling in a dialog + e.preventDefault(); + this.handleConstellationZoom(e); + } + // If over dialog, allow natural scrolling behavior + } + }, { passive: false }); + + // ๐ŸŽฏ IMPROVED MOUSE DRAG PANNING - More responsive for Debbe + let isDragging = false; + let dragStart = { x: 0, y: 0 }; + let dragStartZoom = { ...this.zoomState }; + + document.addEventListener('mousedown', (e) => { + // Allow dragging when clicking empty constellation space + if (!this.isConstellationMode || e.button !== 0) return; + + const target = e.target; + const interactiveSelectors = [ + '.memory-node', + '.person-node', + '.create-memory-node', + '.constellation-controls', + '.constellation-menu-panel', + '.constellation-burger-btn', + '.memory-dialog', + '.memory-dialog-overlay', + '.person-summary-modal', + '.voice-wizard-modal', + '.modal-overlay', + '.experience-popup', + '.chat-container', + '.chat-messages', + '.radial-menu' + ]; + + const isInteractiveTarget = interactiveSelectors.some(selector => target.closest(selector)); + if (isInteractiveTarget) return; + + const isConstellationSurface = + target.id === 'constellation' || + target.classList?.contains('constellation-canvas') || + target.closest('#constellation-zoom-container') || + target === document.body || + target.classList?.contains('dashboard'); + + if (!isConstellationSurface) return; + + isDragging = true; + dragStart = { x: e.clientX, y: e.clientY }; + dragStartZoom = { ...this.zoomState }; + document.body.style.cursor = 'grabbing'; + e.preventDefault(); // Only prevent when starting drag on empty space + }); + + + + document.addEventListener('mousemove', (e) => { + if (isDragging && this.isConstellationMode) { + const deltaX = e.clientX - dragStart.x; + const deltaY = e.clientY - dragStart.y; + + this.zoomState.translateX = dragStartZoom.translateX + deltaX; + this.zoomState.translateY = dragStartZoom.translateY + deltaY; + + this.updateConstellationTransform(); + } + }); + + document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + document.body.style.cursor = ''; + } + }); + + // ๐Ÿ“ฑ TOUCH GESTURES for tablets/phones + let startTouches = []; + let startZoomState = { ...this.zoomState }; + + document.addEventListener('touchstart', (e) => { + if (!this.isConstellationMode) return; + const touchTarget = e.target; + if (this.isInteractiveConstellationTarget(touchTarget)) { + this.touchPanDisabled = true; + startTouches = []; + return; + } + this.touchPanDisabled = false; + startTouches = Array.from(e.touches); + startZoomState = { ...this.zoomState }; + }); + + document.addEventListener('touchmove', (e) => { + if (this.isConstellationMode && startTouches.length > 0 && !this.touchPanDisabled) { + e.preventDefault(); + this.handleConstellationTouchMove(e, startTouches, startZoomState); + } + }, { passive: false }); + + document.addEventListener('touchend', (e) => { + startTouches = []; + if (e.touches.length === 0) { + this.touchPanDisabled = false; + } + }); + + // Keyboard navigation for accessibility + document.addEventListener('keydown', (e) => { + if (this.isConstellationMode) { + this.handleConstellationKeyboard(e); + } + }); + } + + // Enable node-level dragging + isInteractiveConstellationTarget(target) { + if (!target) return false; + const interactiveSelectors = [ + '.memory-node', + '.person-node', + '.create-memory-node', + '.constellation-controls', + '.constellation-menu-panel', + '.constellation-burger-btn', + '.memory-dialog', + '.memory-dialog-overlay', + '.person-summary-modal', + '.voice-wizard-modal', + '.modal-overlay', + '.experience-popup', + '.chat-container', + '.chat-messages', + '.radial-menu' + ]; + return interactiveSelectors.some(selector => target.closest(selector)); + } + + // Enable node-level dragging for pointer events + enableNodeDragging(node) { + if (!node || !node.element) return; + if (node.orbBound) return; // Skip radial menu nodes + const element = node.element; + if (element.dataset.dragHandlerAttached === 'true') return; + + element.dataset.dragHandlerAttached = 'true'; + element.setAttribute('draggable', 'false'); + element.style.touchAction = 'none'; + element.style.userSelect = 'none'; + element.style.webkitUserSelect = 'none'; + element.addEventListener('dragstart', (event) => { + event.preventDefault(); + }); + element.onselectstart = () => false; + element.addEventListener('pointerdown', () => { + if (document.activeElement && document.activeElement.blur) { + try { document.activeElement.blur(); } catch {} + } + }, { passive: true }); + element.addEventListener('pointerdown', (event) => this.handleNodePointerDown(event, node)); + } + + handleNodePointerDown(event, node) { + if (!this.isConstellationMode) return; + if (!node || !node.element) return; + + const pointerType = event.pointerType || 'mouse'; + this.nodeDragLastPointerType = pointerType; + + if (pointerType === 'mouse' && event.button !== 0) return; + if (this.activeNodeDrag || this.pendingNodeDrag) return; + + if (this.nodeDragHoldTimeout) { + clearTimeout(this.nodeDragHoldTimeout); + this.nodeDragHoldTimeout = null; + } + this.nodeDragHoldActive = false; + this.nodeDragLatestEvent = event; + + this.pendingNodeDrag = node; + this.nodeDragPointerId = event.pointerId ?? 'mouse'; + this.nodeDragStart = { x: event.clientX, y: event.clientY }; + node.justDragged = false; + node.isDragging = false; + + if (node.element.setPointerCapture && event.pointerId !== undefined) { + try { + node.element.setPointerCapture(event.pointerId); + } catch (error) { + console.warn('[Constellation] Unable to capture pointer for node drag', error); + } + } + + document.addEventListener('pointermove', this.boundNodePointerMove); + document.addEventListener('pointerup', this.boundNodePointerUp); + document.addEventListener('pointercancel', this.boundNodePointerUp); + + if (pointerType === 'touch') { + this.touchPanDisabled = true; + this.nodeDragHoldTimeout = setTimeout(() => { + if (!this.isConstellationMode) return; + if (this.pendingNodeDrag !== node || this.activeNodeDrag) return; + this.nodeDragHoldActive = true; + const latestEvent = this.nodeDragLatestEvent || event; + this.beginNodeDrag(node, latestEvent); + }, this.touchHoldDelay); + return; + } + } + + handleNodePointerMove(event) { + if (this.nodeDragPointerId !== null && event.pointerId !== undefined && event.pointerId !== this.nodeDragPointerId) { + return; + } + + const pointerType = event.pointerType || this.nodeDragLastPointerType || 'mouse'; + this.nodeDragLatestEvent = event; + + const candidate = this.activeNodeDrag || this.pendingNodeDrag; + if (!candidate || !candidate.element) return; + + if (pointerType === 'touch') { + if (!this.activeNodeDrag) { + return; + } + event.preventDefault(); + this.updateNodeDragPosition(event); + return; + } + + const deltaX = event.clientX - this.nodeDragStart.x; + const deltaY = event.clientY - this.nodeDragStart.y; + + if (!this.activeNodeDrag) { + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + if (distance < this.nodeDragThreshold) { + return; + } + this.beginNodeDrag(candidate, event); + } + + if (!this.activeNodeDrag) return; + + event.preventDefault(); + this.updateNodeDragPosition(event); + } + + handleNodePointerUp(event) { + if (this.nodeDragPointerId !== null && event.pointerId !== undefined && event.pointerId !== this.nodeDragPointerId) { + return; + } + + const pointerType = event.pointerType || this.nodeDragLastPointerType || 'mouse'; + + if (this.nodeDragHoldTimeout) { + clearTimeout(this.nodeDragHoldTimeout); + this.nodeDragHoldTimeout = null; + } + this.nodeDragHoldActive = false; + this.nodeDragLatestEvent = null; + + const node = this.activeNodeDrag || this.pendingNodeDrag; + + if (node && node.element && node.element.releasePointerCapture && this.nodeDragPointerId !== null && this.nodeDragPointerId !== 'mouse') { + try { + node.element.releasePointerCapture(this.nodeDragPointerId); + } catch (error) { + console.warn('[Constellation] Unable to release pointer capture', error); + } + } + + if (this.activeNodeDrag) { + this.endNodeDrag(); + } + + this.pendingNodeDrag = null; + this.nodeDragPointerId = null; + this.nodeDragTransformData = null; + this.nodeDragLastPointerType = null; + + if (pointerType === 'touch') { + this.touchPanDisabled = false; + } + + document.removeEventListener('pointermove', this.boundNodePointerMove); + document.removeEventListener('pointerup', this.boundNodePointerUp); + document.removeEventListener('pointercancel', this.boundNodePointerUp); + } + + beginNodeDrag(node, event) { + if (!node || !node.element) return; + + if (this.nodeDragHoldTimeout) { + clearTimeout(this.nodeDragHoldTimeout); + this.nodeDragHoldTimeout = null; + } + if (this.nodeDragLastPointerType === 'touch') { + this.nodeDragHoldActive = true; + this.touchPanDisabled = true; + } + this.nodeDragLatestEvent = event; + + this.activeNodeDrag = node; + this.pendingNodeDrag = null; + node.isDragging = true; + node.vx = 0; + node.vy = 0; + node.element.dataset.preventClick = 'true'; + node.originalTransition = node.originalTransition ?? node.element.style.transition; + node.element.style.transition = 'none'; + node.element.classList.add('dragging'); + + document.body.style.cursor = 'grabbing'; + node.element.style.cursor = 'grabbing'; + + this.nodeDragTransformData = this.computeNodeDragTransformData(); + const stagePoint = this.clientToStageCoordinates(event.clientX, event.clientY); + this.nodeDragOffset = { + x: stagePoint.x - node.x, + y: stagePoint.y - node.y + }; + + this.updateNodeDragPosition(event); + } + + updateNodeDragPosition(event) { + if (!this.activeNodeDrag) return; + const node = this.activeNodeDrag; + const element = node.element; + if (!element) return; + + const stagePoint = this.clientToStageCoordinates(event.clientX, event.clientY); + node.x = stagePoint.x - this.nodeDragOffset.x; + node.y = stagePoint.y - this.nodeDragOffset.y; + node.baseX = node.x; + node.baseY = node.y; + node.vx = 0; + node.vy = 0; + + const halfWidth = (element.offsetWidth || 0) / 2; + const halfHeight = (element.offsetHeight || 0) / 2; + + element.style.left = `${node.x - halfWidth}px`; + element.style.top = `${node.y - halfHeight}px`; + } + + endNodeDrag() { + if (!this.activeNodeDrag) return; + const node = this.activeNodeDrag; + const element = node.element; + + node.isDragging = false; + node.baseX = node.x; + node.baseY = node.y; + node.vx = 0; + node.vy = 0; + node.justDragged = true; + + if (element) { + if (node.originalTransition !== undefined) { + element.style.transition = node.originalTransition; + } + element.style.cursor = ''; + element.classList.remove('dragging'); + setTimeout(() => { + element.dataset.preventClick = 'false'; + node.justDragged = false; + }, 100); + } + + document.body.style.cursor = ''; + this.activeNodeDrag = null; + + this.scheduleConstellationLayoutPersist(); + } + + computeNodeDragTransformData() { + const container = this.constellationContainer; + if (!container) return null; + + const rect = container.getBoundingClientRect(); + const transform = window.getComputedStyle(container).transform; + const MatrixClass = window.DOMMatrixReadOnly || window.DOMMatrix; + + if (!MatrixClass) { + return { + rect, + inverseMatrix: { + transformPoint: (point) => ({ x: point.x, y: point.y }) + } + }; + } + + if (!transform || transform === 'none') { + return { + rect, + inverseMatrix: new MatrixClass() + }; + } + + try { + const matrix = new MatrixClass(transform); + const inverseMatrix = typeof matrix.inverse === 'function' ? matrix.inverse() : matrix; + return { + rect, + inverseMatrix + }; + } catch (error) { + console.warn('[Constellation] Failed to compute transform matrix; using identity', error); + return { + rect, + inverseMatrix: new MatrixClass() + }; + } + } + + clientToStageCoordinates(clientX, clientY) { + const container = this.constellationContainer; + if (!container) { + return { x: clientX, y: clientY }; + } + + const data = this.nodeDragTransformData || this.computeNodeDragTransformData(); + if (!data) { + return { x: clientX, y: clientY }; + } + + const { rect, inverseMatrix } = data; + const localX = clientX - rect.left; + const localY = clientY - rect.top; + const pointInit = typeof DOMPoint === 'function' + ? new DOMPoint(localX, localY) + : { x: localX, y: localY, z: 0, w: 1 }; + + const transformed = (inverseMatrix && typeof inverseMatrix.transformPoint === 'function') + ? inverseMatrix.transformPoint(pointInit) + : pointInit; + + return { x: transformed.x, y: transformed.y }; + } + + scheduleConstellationLayoutPersist() { + if (!this.isConstellationMode) return; + if (this.persistLayoutDebounce) { + clearTimeout(this.persistLayoutDebounce); + } + this.persistLayoutDebounce = setTimeout(() => { + this.persistLayoutDebounce = null; + this.persistConstellationLayout(); + }, 150); + } + + loadSavedConstellationLayout() { + if (typeof window === 'undefined' || !window.localStorage) { + return null; + } + try { + const raw = window.localStorage.getItem(this.constellationLayoutKey); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object' || typeof parsed.nodes !== 'object') { + return null; + } + return parsed; + } catch (error) { + console.warn('๐ŸŒŒ CONSTELLATION: Failed to parse saved layout', error); + return null; + } + } + + applySavedConstellationLayout() { + const saved = this.loadSavedConstellationLayout(); + if (!saved || !saved.nodes || !Array.isArray(this.nodes) || this.nodes.length === 0) { + return; + } + + const width = Math.max(window.innerWidth || document.documentElement?.clientWidth || 0, 1); + const height = Math.max(window.innerHeight || document.documentElement?.clientHeight || 0, 1); + const nodeMap = new Map(); + + this.nodes.forEach(node => { + if (node && node.uid) { + nodeMap.set(node.uid, node); + } + }); + + Object.entries(saved.nodes).forEach(([uid, position]) => { + const node = nodeMap.get(uid); + if (!node || !node.element) return; + + const leftRatio = typeof position.leftRatio === 'number' ? position.leftRatio : null; + const topRatio = typeof position.topRatio === 'number' ? position.topRatio : null; + if (leftRatio === null || topRatio === null) return; + + const left = leftRatio * width; + const top = topRatio * height; + + node.x = left; + node.y = top; + node.baseX = left; + node.baseY = top; + node.vx = 0; + node.vy = 0; + + node.element.style.left = `${left}px`; + node.element.style.top = `${top}px`; + }); + + const savedViewport = saved.viewport || {}; + const savedWidth = Number.isFinite(savedViewport.width) && savedViewport.width > 0 ? savedViewport.width : width; + const savedHeight = Number.isFinite(savedViewport.height) && savedViewport.height > 0 ? savedViewport.height : height; + const widthRatio = savedWidth ? width / savedWidth : 1; + const heightRatio = savedHeight ? height / savedHeight : 1; + + let appliedZoom = false; + if (saved.zoom) { + const { scale, translateX, translateY } = saved.zoom; + if (Number.isFinite(scale) && Number.isFinite(translateX) && Number.isFinite(translateY)) { + const clampedScale = Math.max(this.zoomState.minScale, Math.min(this.zoomState.maxScale, scale)); + this.zoomState.scale = clampedScale; + this.zoomState.translateX = translateX * widthRatio; + this.zoomState.translateY = translateY * heightRatio; + appliedZoom = true; + } + } + + if (appliedZoom) { + this.updateConstellationTransform(); + } + } + + persistConstellationLayout() { + if (typeof window === 'undefined' || !window.localStorage) { + return; + } + if (!Array.isArray(this.nodes) || this.nodes.length === 0) { + return; + } + + const width = Math.max(window.innerWidth || document.documentElement?.clientWidth || 0, 1); + const height = Math.max(window.innerHeight || document.documentElement?.clientHeight || 0, 1); + const nodesPayload = {}; + + this.nodes.forEach(node => { + if (!node || node.orbBound || !node.uid || !node.element) { + return; + } + + let left = typeof node.x === 'number' ? node.x : NaN; + let top = typeof node.y === 'number' ? node.y : NaN; + + if (!Number.isFinite(left) || !Number.isFinite(top)) { + const styleLeft = parseFloat(node.element.style.left); + const styleTop = parseFloat(node.element.style.top); + if (!Number.isFinite(left) && Number.isFinite(styleLeft)) { + left = styleLeft; + } + if (!Number.isFinite(top) && Number.isFinite(styleTop)) { + top = styleTop; + } + } + + if (!Number.isFinite(left) || !Number.isFinite(top)) { + const rect = node.element.getBoundingClientRect(); + if (!Number.isFinite(left)) { + left = rect.left; + } + if (!Number.isFinite(top)) { + top = rect.top; + } + } + + if (Number.isFinite(left) && Number.isFinite(top)) { + nodesPayload[node.uid] = { + leftRatio: left / width, + topRatio: top / height + }; + } + }); + + try { + const zoomData = { + scale: Number.isFinite(this.zoomState?.scale) ? this.zoomState.scale : 1, + translateX: Number.isFinite(this.zoomState?.translateX) ? this.zoomState.translateX : 0, + translateY: Number.isFinite(this.zoomState?.translateY) ? this.zoomState.translateY : 0 + }; + + window.localStorage.setItem(this.constellationLayoutKey, JSON.stringify({ + nodes: nodesPayload, + viewport: { width, height }, + zoom: zoomData, + savedAt: Date.now() + })); + } catch (error) { + console.warn('๐ŸŒŒ CONSTELLATION: Failed to persist layout', error); + } + } + + loadSavedConstellationFilters() { + if (typeof window === 'undefined' || !window.localStorage) { + return null; + } + try { + const raw = window.localStorage.getItem(this.constellationFiltersKey); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object') return null; + + const allowedKeys = ['memories', 'people', 'family', 'travel', 'recent', 'special']; + const sanitized = {}; + + allowedKeys.forEach(key => { + if (typeof parsed[key] === 'boolean') { + sanitized[key] = parsed[key]; + } + }); + + return Object.keys(sanitized).length > 0 ? sanitized : null; + } catch (error) { + console.warn('๐ŸŒŒ CONSTELLATION: Failed to parse saved filters', error); + return null; + } + } + + persistConstellationFilters() { + if (typeof window === 'undefined' || !window.localStorage) { + return; + } + if (!this.constellationFilters) { + return; + } + try { + window.localStorage.setItem(this.constellationFiltersKey, JSON.stringify(this.constellationFilters)); + } catch (error) { + console.warn('๐ŸŒŒ CONSTELLATION: Failed to persist filters', error); + } + } + + // ๐Ÿ“ฑ HANDLE TOUCH GESTURES (PINCH TO ZOOM + PAN) + handleConstellationTouchMove(e, startTouches, startZoomState) { + const currentTouches = Array.from(e.touches); + + if (currentTouches.length === 2 && startTouches.length === 2) { + // Pinch to zoom + const startDistance = Math.hypot( + startTouches[0].clientX - startTouches[1].clientX, + startTouches[0].clientY - startTouches[1].clientY + ); + const currentDistance = Math.hypot( + currentTouches[0].clientX - currentTouches[1].clientX, + currentTouches[0].clientY - currentTouches[1].clientY + ); + + const scaleChange = currentDistance / startDistance; + const newScale = Math.max(this.zoomState.minScale, + Math.min(this.zoomState.maxScale, + startZoomState.scale * scaleChange)); + + // Zoom towards pinch center + const centerX = (currentTouches[0].clientX + currentTouches[1].clientX) / 2; + const centerY = (currentTouches[0].clientY + currentTouches[1].clientY) / 2; + + const scaleRatio = newScale / this.zoomState.scale; + this.zoomState.translateX = centerX - (centerX - this.zoomState.translateX) * scaleRatio; + this.zoomState.translateY = centerY - (centerY - this.zoomState.translateY) * scaleRatio; + this.zoomState.scale = newScale; + + this.updateConstellationTransform(); + } else if (currentTouches.length === 1 && startTouches.length === 1) { + // Single finger pan + const deltaX = currentTouches[0].clientX - startTouches[0].clientX; + const deltaY = currentTouches[0].clientY - startTouches[0].clientY; + + this.zoomState.translateX = startZoomState.translateX + deltaX; + this.zoomState.translateY = startZoomState.translateY + deltaY; + this.updateConstellationTransform(); + } + } + + // โŒจ๏ธ KEYBOARD NAVIGATION for constellation + handleConstellationKeyboard(e) { + const panStep = 50; // Pixels to pan per key press + const zoomStep = 0.05; // ๐ŸŽฏ GENTLE: 5% per key press instead of 10% + + switch(e.key) { + case 'ArrowUp': + e.preventDefault(); + this.zoomState.translateY += panStep; + this.updateConstellationTransform(); + break; + case 'ArrowDown': + e.preventDefault(); + this.zoomState.translateY -= panStep; + this.updateConstellationTransform(); + break; + case 'ArrowLeft': + e.preventDefault(); + this.zoomState.translateX += panStep; + this.updateConstellationTransform(); + break; + case 'ArrowRight': + e.preventDefault(); + this.zoomState.translateX -= panStep; + this.updateConstellationTransform(); + break; + case '+': + case '=': + e.preventDefault(); + this.zoomState.scale = Math.min(this.zoomState.maxScale, this.zoomState.scale * (1 + zoomStep)); + this.updateConstellationTransform(); + break; + case '-': + e.preventDefault(); + this.zoomState.scale = Math.max(this.zoomState.minScale, this.zoomState.scale * (1 - zoomStep)); + this.updateConstellationTransform(); + break; + case '0': + e.preventDefault(); + this.fitAllMemories(); + break; + } + } + + // ๐Ÿ” HANDLE MOUSE WHEEL ZOOM + handleConstellationZoom(e) { + // ๐ŸŽฏ GENTLE ZOOM: Much less aggressive (3% per scroll instead of 10%) + const zoomFactor = e.deltaY > 0 ? 0.97 : 1.03; + const newScale = Math.max(this.zoomState.minScale, + Math.min(this.zoomState.maxScale, + this.zoomState.scale * zoomFactor)); + + // Zoom towards mouse position + const mouseX = e.clientX; + const mouseY = e.clientY; + + // Calculate offset to zoom towards mouse + const scaleRatio = newScale / this.zoomState.scale; + this.zoomState.translateX = mouseX - (mouseX - this.zoomState.translateX) * scaleRatio; + this.zoomState.translateY = mouseY - (mouseY - this.zoomState.translateY) * scaleRatio; + this.zoomState.scale = newScale; + + this.updateConstellationTransform(); + } + + // ๐Ÿ” UPDATE CONSTELLATION TRANSFORM + updateConstellationTransform() { + const scale = Number.isFinite(this.zoomState.scale) ? this.zoomState.scale : 1; + const translateX = Number.isFinite(this.zoomState.translateX) ? this.zoomState.translateX : 0; + const translateY = Number.isFinite(this.zoomState.translateY) ? this.zoomState.translateY : 0; + const transform = `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`; + + // Apply transform to constellation container (memory nodes) + if (this.constellationContainer) { + this.constellationContainer.style.transformOrigin = '0 0'; + this.constellationContainer.style.transform = transform; + } + + // ๐Ÿ’ฅ CRITICAL FIX: Apply same transform to neural canvas (connections) + // This ensures connections stay anchored to nodes when zooming/panning + if (this.neuralCanvas) { + this.neuralCanvas.style.transform = transform; + this.neuralCanvas.style.transformOrigin = '0 0'; + } + + if (this.isConstellationMode) { + this.scheduleConstellationLayoutPersist(); + } + + console.log('๐Ÿ”— TRANSFORM: Applied zoom/pan to both nodes and connections'); + } + + // ๐Ÿ” ADD ELEGANT EMMA-BRANDED ZOOM CONTROLS + addConstellationZoomControls() { + // Remove any existing zoom controls + const existingControls = document.getElementById('constellation-zoom-controls'); + if (existingControls) { + existingControls.remove(); + } + + // Create Emma-branded zoom controls container + const zoomControls = document.createElement('div'); + zoomControls.id = 'constellation-zoom-controls'; + zoomControls.style.cssText = ` + position: fixed; + bottom: 40px; + right: 40px; + display: flex; + flex-direction: column; + gap: 12px; + z-index: 9999; + opacity: 1; + transition: all 0.3s ease; + `; + + // Emma-branded button base style + const buttonBaseStyle = ` + width: 56px; + height: 56px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.15); + background: linear-gradient(135deg, + rgba(111, 99, 217, 0.9) 0%, + rgba(124, 58, 237, 0.8) 50%, + rgba(109, 40, 217, 0.9) 100% + ); + backdrop-filter: blur(20px) saturate(150%); + color: white; + font-size: 18px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 6px 22px rgba(111, 99, 217, 0.28), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-weight: 500; + position: relative; + overflow: hidden; + `; + + // Zoom in button + const zoomInBtn = document.createElement('button'); + zoomInBtn.innerHTML = '+'; + zoomInBtn.title = 'Zoom In to Explore'; + zoomInBtn.style.cssText = buttonBaseStyle; + + zoomInBtn.addEventListener('click', () => { + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + // ๐ŸŽฏ GENTLE: 15% per button click instead of 30% + const newScale = Math.min(this.zoomState.maxScale, this.zoomState.scale * 1.15); + + // Zoom towards center for smooth experience + const scaleRatio = newScale / this.zoomState.scale; + this.zoomState.translateX = centerX - (centerX - this.zoomState.translateX) * scaleRatio; + this.zoomState.translateY = centerY - (centerY - this.zoomState.translateY) * scaleRatio; + this.zoomState.scale = newScale; + + this.updateConstellationTransform(); + }); + + // Zoom out button + const zoomOutBtn = document.createElement('button'); + zoomOutBtn.innerHTML = 'โˆ’'; + zoomOutBtn.title = 'Zoom Out to See More'; + zoomOutBtn.style.cssText = buttonBaseStyle; + + zoomOutBtn.addEventListener('click', () => { + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + // ๐ŸŽฏ GENTLE: 15% per button click instead of 30% + const newScale = Math.max(this.zoomState.minScale, this.zoomState.scale / 1.15); + + // Zoom from center for smooth experience + const scaleRatio = newScale / this.zoomState.scale; + this.zoomState.translateX = centerX - (centerX - this.zoomState.translateX) * scaleRatio; + this.zoomState.translateY = centerY - (centerY - this.zoomState.translateY) * scaleRatio; + this.zoomState.scale = newScale; + + this.updateConstellationTransform(); + }); + + // Fit all button - special Emma green + const resetBtn = document.createElement('button'); + resetBtn.innerHTML = 'โŒ‚'; + resetBtn.title = 'Show All Memories'; + resetBtn.style.cssText = buttonBaseStyle.replace( + 'rgba(111, 99, 217, 0.9) 0%, rgba(124, 58, 237, 0.8) 50%, rgba(109, 40, 217, 0.9) 100%', + 'rgba(16, 185, 129, 0.9) 0%, rgba(5, 150, 105, 0.8) 50%, rgba(4, 120, 87, 0.9) 100%' + ).replace( + 'rgba(111, 99, 217, 0.28)', + 'rgba(16, 185, 129, 0.28)' + ); + + resetBtn.addEventListener('click', () => { + this.fitAllMemories(); + }); + + // Add elegant hover effects + [zoomInBtn, zoomOutBtn, resetBtn].forEach(btn => { + btn.addEventListener('mouseenter', () => { + btn.style.transform = 'scale(1.05) translateY(-2px)'; + btn.style.boxShadow = ` + 0 10px 28px rgba(111, 99, 217, 0.32), + 0 0 0 1px rgba(255, 255, 255, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.2) + `; + }); + + btn.addEventListener('mouseleave', () => { + btn.style.transform = 'scale(1) translateY(0)'; + btn.style.boxShadow = ` + 0 6px 22px rgba(111, 99, 217, 0.28), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1) + `; + }); + + // Add subtle click effect + btn.addEventListener('mousedown', () => { + btn.style.transform = 'scale(0.95)'; + }); + + btn.addEventListener('mouseup', () => { + btn.style.transform = 'scale(1.05) translateY(-2px)'; + }); + }); + + zoomControls.appendChild(zoomInBtn); + zoomControls.appendChild(zoomOutBtn); + zoomControls.appendChild(resetBtn); + document.body.appendChild(zoomControls); + + // ๐Ÿงญ ADD NAVIGATION ARROWS for easy panning + this.addConstellationPanControls(); + + console.log('๐ŸŽจ ZOOM: Added elegant Emma-branded zoom controls'); + } + + // ๐Ÿงญ ADD DEMENTIA-FRIENDLY PAN CONTROLS + addConstellationPanControls() { + // Remove any existing pan controls + const existingPanControls = document.getElementById('constellation-pan-controls'); + if (existingPanControls) { + existingPanControls.remove(); + } + + // Create navigation controls container + const panControls = document.createElement('div'); + panControls.id = 'constellation-pan-controls'; + panControls.style.cssText = ` + position: fixed; + bottom: 40px; + left: 40px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr; + gap: 8px; + z-index: 9999; + opacity: 1; + transition: all 0.3s ease; + width: 144px; + height: 144px; + `; + + // Emma-branded button style for arrows + const arrowButtonStyle = ` + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.15); + background: linear-gradient(135deg, + rgba(111, 99, 217, 0.85) 0%, + rgba(124, 58, 237, 0.75) 50%, + rgba(109, 40, 217, 0.85) 100% + ); + backdrop-filter: blur(20px) saturate(150%); + color: white; + font-size: 16px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 6px 24px rgba(111, 99, 217, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-weight: 600; + `; + + const panStep = 100; // Distance to pan in pixels + + // Create directional buttons in a 3x3 grid pattern + const directions = [ + { pos: [0, 1], arrow: 'โ†‘', title: 'Pan Up', dx: 0, dy: panStep }, + { pos: [1, 0], arrow: 'โ†', title: 'Pan Left', dx: panStep, dy: 0 }, + { pos: [1, 1], arrow: 'โŠ•', title: 'Center View', dx: 0, dy: 0, special: 'center' }, + { pos: [1, 2], arrow: 'โ†’', title: 'Pan Right', dx: -panStep, dy: 0 }, + { pos: [2, 1], arrow: 'โ†“', title: 'Pan Down', dx: 0, dy: -panStep } + ]; + + directions.forEach(({ pos, arrow, title, dx, dy, special }) => { + const button = document.createElement('button'); + button.innerHTML = arrow; + button.title = title; + button.style.cssText = arrowButtonStyle; + button.style.gridColumn = pos[1] + 1; + button.style.gridRow = pos[0] + 1; + + // Special styling for center button + if (special === 'center') { + button.style.background = `linear-gradient(135deg, + rgba(16, 185, 129, 0.85) 0%, + rgba(5, 150, 105, 0.75) 50%, + rgba(4, 120, 87, 0.85) 100% + )`; + button.style.boxShadow = ` + 0 6px 24px rgba(16, 185, 129, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1) + `; + } + + button.addEventListener('click', () => { + if (special === 'center') { + // Center the view (reset translation) + this.zoomState.translateX = 0; + this.zoomState.translateY = 0; + } else { + // Pan in the specified direction + this.zoomState.translateX += dx; + this.zoomState.translateY += dy; + } + this.updateConstellationTransform(); + }); + + // Add hover effects + button.addEventListener('mouseenter', () => { + button.style.transform = 'scale(1.05)'; + if (special === 'center') { + button.style.boxShadow = ` + 0 8px 32px rgba(16, 185, 129, 0.4), + 0 0 0 1px rgba(255, 255, 255, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.2) + `; + } else { + button.style.boxShadow = ` + 0 8px 32px rgba(111, 99, 217, 0.4), + 0 0 0 1px rgba(255, 255, 255, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.2) + `; + } + }); + + button.addEventListener('mouseleave', () => { + button.style.transform = 'scale(1)'; + if (special === 'center') { + button.style.boxShadow = ` + 0 6px 24px rgba(16, 185, 129, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1) + `; + } else { + button.style.boxShadow = ` + 0 6px 24px rgba(111, 99, 217, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1) + `; + } + }); + + // Add click effect + button.addEventListener('mousedown', () => { + button.style.transform = 'scale(0.95)'; + }); + + button.addEventListener('mouseup', () => { + button.style.transform = 'scale(1.05)'; + }); + + panControls.appendChild(button); + }); + + document.body.appendChild(panControls); + + console.log('๐Ÿงญ PAN: Added dementia-friendly navigation controls'); + } + + // ๐Ÿ” FIT ALL MEMORIES IN VIEW + fitAllMemories() { + if (!this.nodes || this.nodes.length === 0) { + // Reset to center if no nodes + this.zoomState.scale = 1; + this.zoomState.translateX = 0; + this.zoomState.translateY = 0; + this.updateConstellationTransform(); + return; + } + + // Find bounding box of all nodes + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + this.nodes.forEach(node => { + if (node.x !== undefined && node.y !== undefined) { + minX = Math.min(minX, node.x - 50); // Add padding + minY = Math.min(minY, node.y - 50); + maxX = Math.max(maxX, node.x + 50); + maxY = Math.max(maxY, node.y + 50); + } + }); + + if (minX === Infinity) { + // No valid positions, just center + this.zoomState.scale = 1; + this.zoomState.translateX = 0; + this.zoomState.translateY = 0; + this.updateConstellationTransform(); + return; + } + + // Calculate optimal scale and position + const contentWidth = maxX - minX; + const contentHeight = maxY - minY; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + const scaleX = (viewportWidth * 0.8) / contentWidth; + const scaleY = (viewportHeight * 0.8) / contentHeight; + const optimalScale = Math.min(scaleX, scaleY, this.zoomState.maxScale); + const finalScale = Math.max(optimalScale, this.zoomState.minScale); + + // Center the content + const contentCenterX = (minX + maxX) / 2; + const contentCenterY = (minY + maxY) / 2; + + this.zoomState.scale = finalScale; + this.zoomState.translateX = viewportWidth / 2 - contentCenterX * finalScale; + this.zoomState.translateY = viewportHeight / 2 - contentCenterY * finalScale; + + this.updateConstellationTransform(); + + console.log('๐Ÿ  ZOOM: Fitted all memories in view for Debbe'); + } + + // Removed fadeOutMemoryNodes and fadeInMemoryNodes - no longer needed since constellation mode exits to main dashboard + + // Completely clear all memory nodes from DOM (prevents duplicates) + clearAllMemoryNodesFromDOM() { + + // Remove all memory-node, person-node, and create-memory-node elements (including orphaned ones) + const existingMemoryNodes = document.querySelectorAll('.memory-node, .create-memory-node, .person-node'); + existingMemoryNodes.forEach(node => { + + node.remove(); + }); + + // Clear ONLY memory nodes from the nodes array (preserve radial menu items) + if (this.nodes) { + this.nodes.forEach(node => { + if (node.element && node.element.parentNode) { + // Only remove if it's a constellation node, NOT a radial menu item + if (node.element.classList.contains('memory-node') || + node.element.classList.contains('create-memory-node') || + node.element.classList.contains('person-node')) { + + node.element.remove(); + } else { + + } + } + }); + // Filter out only the constellation nodes, keep radial items + this.nodes = this.nodes.filter(node => + !node.element.classList.contains('memory-node') && + !node.element.classList.contains('create-memory-node') && + !node.element.classList.contains('person-node') + ); + } + + // Reset central node + this.centralNode = null; + this.createMemoryElement = null; + + } + + // Exit constellation mode + exitMemoryConstellation() { + document.body.classList.add('dashboard-minimal'); + console.log('๐ŸŒŒ exitMemoryConstellation called - Current state:', { + isConstellationMode: this.isConstellationMode, + isMenuOpen: this.isMenuOpen, + orbExists: !!this.orb, + radialMenuExists: !!this.radialMenu + }); + + this.isConstellationMode = false; + + // PERSISTENCE: Clear constellation state + localStorage.removeItem('emmaConstellationActive'); + + // Stop neural animation (EXACT same as main menu) + this.isMenuOpen = false; + if (this.neuralAnimationId) { + cancelAnimationFrame(this.neuralAnimationId); + this.neuralCtx.clearRect(0, 0, this.neuralCanvas.width, this.neuralCanvas.height); + } + + // ๐Ÿ” RESET ZOOM STATE: Reset neural canvas transform and clean up + if (this.neuralCanvas) { + this.neuralCanvas.style.transform = ''; + this.neuralCanvas.style.transformOrigin = ''; + } + + // Remove constellation container + const constellationContainer = document.getElementById('constellation-zoom-container'); + if (constellationContainer) { + constellationContainer.remove(); + } + + // Reset zoom state + this.zoomState.scale = 1; + this.zoomState.translateX = 0; + this.zoomState.translateY = 0; + this.constellationContainer = null; + + // Remove constellation-active class to restore UI + document.body.classList.remove('constellation-active'); + document.body.classList.remove('menu-active'); + this.radialMenu.classList.remove('active'); + + // Restore central orb size + this.restoreCentralOrb(); + + // Remove constellation UI + if (this.constellationUI) { + this.constellationUI.remove(); + this.constellationUI = null; + } + + // CRITICAL: Use thorough DOM cleanup to prevent duplicates + this.clearAllMemoryNodesFromDOM(); + + // Clear animation + if (this.neuralAnimationId) { + cancelAnimationFrame(this.neuralAnimationId); + this.neuralCtx.clearRect(0, 0, this.neuralCanvas.width, this.neuralCanvas.height); + } + + // Reset nodes + this.nodes = []; + + // Hide radial menu + this.radialMenu.classList.remove('active'); + this.isMenuOpen = false; + + // Reset radial menu items to default state + const radialItems = this.radialMenu.querySelectorAll('.radial-item'); + + radialItems.forEach((item, i) => { + item.style.opacity = '0'; + item.style.transform = 'scale(0)'; + // CRITICAL: Reset positioning to default + item.style.left = ''; + item.style.top = ''; + item.style.animationPlayState = 'running'; + }); + + console.log('๐ŸŒŒ exitMemoryConstellation complete - Final state:', { + isConstellationMode: this.isConstellationMode, + isMenuOpen: this.isMenuOpen, + radialMenuClasses: this.radialMenu?.className, + bodyClasses: document.body.className + }); + } + + // Sanitize title to remove base64 or corrupted data + sanitizeTitle(title) { + if (!title || typeof title !== 'string') return 'Untitled Memory'; + + // Check if it looks like base64 or corrupted data + if (title.length > 50 || /^[A-Za-z0-9+\/=]{20,}$/.test(title)) { + return 'Memory Capsule'; + } + + // Remove any non-printable characters and limit length + const cleaned = title.replace(/[^\x20-\x7E]/g, '').trim(); + return cleaned.length > 0 ? cleaned.substring(0, 50) : 'Untitled Memory'; + } + + // ๐Ÿ“ฑ๐Ÿ’ป๐Ÿ–ฅ๏ธ RESPONSIVE MEMORY DIALOG - Works on ALL screen sizes! + async openMemoryDialog(memory) { + console.log('๐ŸŽฏ DASHBOARD: Opening responsive memory dialog for:', memory.title || memory.id); + + // ๐Ÿš€ CRITICAL FIX: Load full media if lazy-loaded (from performance optimization) + const memoryWithFullMedia = await this.loadFullMediaForMemory(memory); + + // ๐Ÿ“ฑ๐Ÿ’ป๐Ÿ–ฅ๏ธ USE THE NEW RESPONSIVE MEMORY DIALOG! + this.showResponsiveMemoryDialog(memoryWithFullMedia); + } + + /** + * ๐Ÿš€ PERFORMANCE OPTIMIZATION: Load full media data on-demand for lazy-loaded attachments + * (Reused from memories.js constellation) + */ + async loadFullMediaForMemory(memory) { + // Check if this memory has lazy-loaded attachments that need full loading + const hasLazyAttachments = memory.attachments?.some(att => att.isLazyLoaded && att.hasMedia); + + if (!hasLazyAttachments) { + console.log('๐Ÿ’พ DASHBOARD: Memory already has full media, no loading needed'); + return memory; + } + + console.log('๐Ÿš€ DASHBOARD: Loading full media for', memory.attachments?.length || 0, 'attachments'); + + try { + // Get vault media data + const vaultMedia = window.emmaWebVault?.vaultData?.content?.media || {}; + + // Load full media data for lazy-loaded attachments + const fullAttachments = memory.attachments.map(attachment => { + if (attachment.isLazyLoaded && attachment.hasMedia && attachment.mediaId) { + const mediaItem = vaultMedia[attachment.mediaId]; + if (mediaItem && mediaItem.data) { + console.log('๐Ÿ’พ DASHBOARD: Loading full media for:', attachment.name); + return { + ...attachment, + url: mediaItem.data.startsWith('data:') + ? mediaItem.data + : `data:${mediaItem.type};base64,${mediaItem.data}`, + dataUrl: mediaItem.data.startsWith('data:') + ? mediaItem.data + : `data:${mediaItem.type};base64,${mediaItem.data}`, + data: mediaItem.data, + isLazyLoaded: false, // Mark as fully loaded + isPersisted: true + }; + } + } + + // Return attachment as-is if already loaded or no media + return attachment; + }); + + console.log('โœ… DASHBOARD: Successfully loaded full media data'); + + return { + ...memory, + attachments: fullAttachments + }; + + } catch (error) { + console.error('โŒ DASHBOARD: Failed to load full media:', error); + // Return original memory if loading fails + return memory; + } + } + + /** + * ๐Ÿ“ฑ๐Ÿ’ป๐Ÿ–ฅ๏ธ RESPONSIVE MEMORY DIALOG - Works on ALL screen sizes! + */ + showResponsiveMemoryDialog(memory) { + // Prepare memory data + const hasImages = memory.attachments?.some(att => att.type?.startsWith('image/')) || + memory.mediaItems?.some(item => item.type?.startsWith('image/')); + const hasVideo = memory.attachments?.some(att => att.type?.startsWith('video/')) || + memory.mediaItems?.some(item => item.type?.startsWith('video/')); + const peopleList = memory.metadata?.people || memory.people || []; + + // Create fully responsive dialog for ALL screen sizes + const dialog = document.createElement('div'); + dialog.className = 'memory-preview-dialog responsive dashboard'; + dialog.style.cssText = ` + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + z-index: 10000 !important; + display: flex !important; + align-items: flex-start !important; + justify-content: center !important; + opacity: 0; + animation: dialogFadeIn 0.3s ease forwards; + background: rgba(0, 0, 0, 0.6) !important; + backdrop-filter: blur(25px) saturate(180%) !important; + padding: 20px; + overflow-y: auto !important; + box-sizing: border-box; + pointer-events: auto !important; + `; + + dialog.innerHTML = ` + + +
+ +
+
+

${memory.metadata?.title || memory.title || memory.subject || 'Beautiful Memory'}

+
${this.formatMemoryDate(memory.created || memory.date || memory.timestamp)}
+
+ +
+ + + ${hasImages ? ` + + ` : ''} + + + ${peopleList.length > 0 ? ` +
+

๐Ÿ‘ฅ People in this memory

+
+ +
+
+ ` : ''} + + +
+
+

${memory.content || memory.description || memory.details || 'This precious memory is stored in your vault...'}

+
+ + ${memory.metadata?.emotions?.length > 0 ? ` +
+ ๐Ÿ’ญ +
+ ${memory.metadata.emotions.map(emotion => ` + ${emotion} + `).join('')} +
+
+ ` : ''} + + ${memory.metadata?.location || memory.location ? ` +
+ ๐Ÿ“ + ${memory.metadata?.location || memory.location} +
+ ` : ''} +
+ + + ${(memory.attachments?.length > 1 || memory.mediaItems?.length > 1) || hasVideo ? ` +
+

๐Ÿ“ท All Media (${(memory.attachments || memory.mediaItems || []).length})

+
+ ${(memory.attachments || memory.mediaItems || []).map((attachment, index) => ` +
+ ${attachment.type?.startsWith('image/') ? ` + ${attachment.name} + ` : attachment.type?.startsWith('video/') ? ` + + ` : ` +
+
${attachment.type?.startsWith('audio/') ? '๐ŸŽต' : '๐Ÿ“„'}
+
${attachment.name}
+
+ `} +
+ `).join('')} +
+
+ ` : ''} + + +
+ + +
+
+ `; + + dialog.addEventListener('click', (e) => { + if (e.target === dialog) { + dialog.remove(); + } + }); + + document.body.appendChild(dialog); + this.currentMemoryDialog = dialog; + + // Animate in + setTimeout(() => { + dialog.style.opacity = '1'; + // Load people avatars if needed + this.loadDashboardPeopleAvatars(memory); + }, 100); + } + + /** + * ๐Ÿ“… Format memory date for display + */ + formatMemoryDate(timestamp) { + if (!timestamp) return 'Unknown date'; + + try { + const date = new Date(timestamp); + return date.toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } catch (error) { + console.warn('Error formatting date:', error); + return 'Unknown date'; + } + } + + /** + * ๐ŸŽ  Switch carousel image in dashboard dialog + */ + switchDashboardCarouselImage(index) { + const heroImages = document.querySelectorAll('.hero-image'); + const dots = document.querySelectorAll('.dot'); + + heroImages.forEach((img, i) => { + img.classList.toggle('active', i === index); + }); + + dots.forEach((dot, i) => { + dot.classList.toggle('active', i === index); + }); + } + + /** + * ๐Ÿ‘ฅ Load people avatars for dashboard memory dialog + */ + async loadDashboardPeopleAvatars(memory) { + const peopleGrid = document.getElementById(`people-grid-${memory.id}`); + if (!peopleGrid) return; + + const peopleList = memory.metadata?.people || memory.people || []; + if (peopleList.length === 0) return; + + try { + // Get vault data for people info + let vaultData = null; + if (window.emmaWebVault && window.emmaWebVault.vaultData) { + vaultData = window.emmaWebVault.vaultData; + } + + if (!vaultData?.content?.people) { + console.warn('โš ๏ธ No vault people data available'); + return; + } + + const vaultPeople = vaultData.content.people; + const vaultMedia = vaultData.content.media || {}; + + // Create avatar elements for each person + peopleList.forEach(personId => { + const person = vaultPeople[personId]; + if (!person) return; + + // Resolve avatar URL + let avatarUrl = person.avatarUrl; + if (!avatarUrl && person.avatarId && vaultMedia[person.avatarId]) { + const mediaItem = vaultMedia[person.avatarId]; + avatarUrl = mediaItem.data?.startsWith('data:') + ? mediaItem.data + : `data:${mediaItem.type};base64,${mediaItem.data}`; + } + + const firstName = (person.name || 'Unknown').split(' ')[0]; + const initials = firstName.charAt(0).toUpperCase(); + + const avatarElement = document.createElement('div'); + avatarElement.className = 'memory-person-avatar'; + avatarElement.onclick = () => this.openPersonDialog(personId); + + if (avatarUrl) { + const img = document.createElement('img'); + img.src = avatarUrl; + img.alt = person.name; + img.onerror = () => { + // Fallback to initials if image fails + avatarElement.innerHTML = `${initials}`; + }; + avatarElement.appendChild(img); + } else { + avatarElement.innerHTML = `${initials}`; + } + + // Add name label below avatar + const nameLabel = document.createElement('div'); + nameLabel.style.cssText = ` + color: white; + font-size: clamp(12px, 2vw, 14px); + font-weight: 500; + margin-top: 8px; + text-align: center; + `; + nameLabel.textContent = firstName; + + const avatarContainer = document.createElement('div'); + avatarContainer.appendChild(avatarElement); + avatarContainer.appendChild(nameLabel); + + peopleGrid.appendChild(avatarContainer); + }); + + } catch (error) { + console.error('โŒ Error loading dashboard people avatars:', error); + } + } + + /** + * โœ๏ธ Edit memory from dashboard dialog - DIRECT APPROACH + */ + editDashboardMemory(memoryId) { + // Close current dialog + this.closeMemoryDialog(); + + // Simple approach: Create temporary instance JUST for the edit dialog + if (typeof EmmaChatExperience !== 'undefined') { + const tempChatExperience = new EmmaChatExperience(); + tempChatExperience.editMemoryDetails(memoryId); + } else { + console.error('โŒ EmmaChatExperience not available'); + } + } + + /** + * ๐Ÿ”— Share memory from dashboard dialog + */ + shareDashboardMemory(memoryId) { + const shareUrl = `${window.location.origin}/index.html?memory=${memoryId}`; + + if (navigator.share && navigator.canShare && navigator.canShare({ url: shareUrl })) { + navigator.share({ + title: 'Emma Memory', + text: 'Check out this memory from Emma', + url: shareUrl + }).catch(err => { + console.log('Share cancelled:', err); + // Fallback to copy + this.copyToClipboard(shareUrl); + }); + } else { + // Fallback to copy to clipboard + this.copyToClipboard(shareUrl); + } + } + + /** + * ๐Ÿ“‹ Copy text to clipboard utility + */ + copyToClipboard(text) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(() => { + this.showToast('๐Ÿ”— Memory link copied to clipboard!', 'success'); + }).catch(err => { + console.error('Failed to copy:', err); + this.fallbackCopyToClipboard(text); + }); + } else { + this.fallbackCopyToClipboard(text); + } + } + + /** + * ๐Ÿ“‹ Fallback copy method for older browsers + */ + fallbackCopyToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + this.showToast('๐Ÿ”— Memory link copied to clipboard!', 'success'); + } catch (err) { + console.error('Fallback copy failed:', err); + this.showToast('โŒ Failed to copy link', 'error'); + } + + document.body.removeChild(textArea); + } + + /** + * ๐Ÿž Show toast notification + */ + showToast(message, type = 'info') { + // Create toast element + const toast = document.createElement('div'); + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === 'success' ? 'linear-gradient(135deg, #10b981, #059669)' : + type === 'error' ? 'linear-gradient(135deg, #ef4444, #dc2626)' : + 'linear-gradient(135deg, #6f63d9, #7c3aed)'}; + color: white; + padding: 16px 24px; + border-radius: 12px; + font-size: 14px; + font-weight: 500; + z-index: 20000; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + `; + toast.textContent = message; + + document.body.appendChild(toast); + + // Animate in + setTimeout(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }, 100); + + // Remove after 3 seconds + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 300); + }, 3000); + } + + // Close memory dialog + closeMemoryDialog() { + if (this.currentMemoryDialog) { + this.currentMemoryDialog.style.opacity = '0'; + setTimeout(() => { + if (this.currentMemoryDialog) { + this.currentMemoryDialog.remove(); + this.currentMemoryDialog = null; + } + }, 300); + } + } + + // Open person dialog from memory + openPersonDialog(personId) { + + // Navigate to people page with person selected + window.location.href = `pages/people-emma.html?person=${personId}`; + } + + // Memory dialog slideshow controls + prevSlide() { + + // Demo functionality - would implement real slideshow + } + + nextSlide() { + + // Demo functionality - would implement real slideshow + } + + shareMemory(memoryId) { + + this.showToast('๐Ÿ“ค Memory shared!', 'success'); + } + + editMemory(memoryId) { + + this.showToast('โœ๏ธ Opening memory editor...', 'info'); + } + + // Filter constellation by theme + filterByTheme(theme) { + + this.nodes.forEach(node => { + if (node.theme === theme) { + node.element.style.opacity = '1'; + node.element.style.transform = 'scale(1)'; + } else { + node.element.style.opacity = '0.3'; + node.element.style.transform = 'scale(0.8)'; + } + }); + } + + 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 + document.addEventListener('DOMContentLoaded', () => { + document.body.classList.add('dashboard-minimal'); + // Small delay to ensure all elements are definitely available + setTimeout(() => { + console.log('๐Ÿ” DEBUG: Creating EmmaDashboard instance...'); + + // CRITICAL FIX: Only create if not already exists + if (!window.emmaDashboard) { + window.emmaDashboard = new EmmaDashboard(); + console.log('โœ… EmmaDashboard created successfully'); + } else { + console.log('โœ… EmmaDashboard already exists, skipping duplicate creation'); + } + + // Setup vault modal event listeners AFTER dashboard is created + const dashboard = window.emmaDashboard; + + // Close button + document.getElementById('close-vault-modal').addEventListener('click', () => { + dashboard.closeVaultModal(); + }); + + // Click outside to close + document.getElementById('vault-modal').addEventListener('click', (e) => { + if (e.target.id === 'vault-modal') { + dashboard.closeVaultModal(); + } + }); + + // Unlock form handlers + document.getElementById('unlock-btn').addEventListener('click', () => { + dashboard.unlockVault(); + }); + + document.getElementById('cancel-unlock-btn').addEventListener('click', () => { + dashboard.hideUnlockForm(); + }); + + // Create vault button handler + document.getElementById('create-vault-btn').addEventListener('click', async () => { + + try { + if (window.emmaAPI && window.emmaAPI.vault) { + const st = await window.emmaAPI.vault.status(); + if (st && st.initialized) { + + dashboard.showUnlockForm(); + return; + } + } + } catch {} + // Load welcome page for vault setup + window.location.assign('pages/welcome.html'); + }); + + // Password input enter key + document.getElementById('vault-password').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + dashboard.unlockVault(); + } + }); + + // QR Code handlers + document.getElementById('generate-vault-qr-dashboard').addEventListener('click', () => { + dashboard.showToast('๐Ÿ“ฑ Vault QR generation coming soon!', 'info'); + }); + + document.getElementById('open-qr-scanner-dashboard').addEventListener('click', () => { + dashboard.showToast('๐Ÿ“ท QR scanner coming soon!', 'info'); + }); + + // Escape key to close vault modal + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + const modal = document.getElementById('vault-modal'); + if (modal.classList.contains('show')) { + dashboard.closeVaultModal(); + } + } + }); + + // Vault status is managed by WebVaultStatus - don't override here! + localStorage.removeItem('emmaConstellationActive'); + const params = new URLSearchParams(window.location.search); + if (params.get('constellation') === 'true') { + setTimeout(() => { + if (window.emmaDashboard && window.emmaDashboard.enterMemoryConstellation) { + window.emmaDashboard.enterMemoryConstellation(); + } + }, 1000); + } + + // CRITICAL: Initialize vault objects AFTER all scripts load + setTimeout(() => { + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: setTimeout running - starting vault initialization...'); + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: EmmaWebVault class available:', typeof EmmaWebVault); + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: window.emmaWebVault exists:', !!window.emmaWebVault); + + // CRITICAL FIX: Create the global vault instance (was missing!) + if (typeof EmmaWebVault !== 'undefined' && !window.emmaWebVault) { + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: Creating new EmmaWebVault instance...'); + window.emmaWebVault = new EmmaWebVault(); + console.log('โœ… DASHBOARD: EmmaWebVault instance created and ready'); + console.log('โœ… DASHBOARD: window.emmaWebVault now exists:', !!window.emmaWebVault); + + // IMMEDIATE VAULT RESTORATION for extension compatibility + if (sessionStorage.getItem('emmaVaultActive') === 'true') { + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: Immediately restoring vault for extension compatibility...'); + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: Session vault name:', sessionStorage.getItem('emmaVaultName')); + console.log('๐Ÿšจ๐Ÿ”ง DASHBOARD: Document ready state:', document.readyState); + + window.emmaWebVault.restoreVaultState() + .then(result => { + if (result) { + console.log('๐Ÿšจโœ… DASHBOARD: Vault restored successfully for extension!', { + isOpen: window.emmaWebVault.isOpen, + hasVaultData: !!window.emmaWebVault.vaultData, + hasAddMemoryMethod: typeof window.emmaWebVault.addMemory + }); + } else { + console.log('๐Ÿšจโš ๏ธ DASHBOARD: Vault restore returned false - troubleshooting...'); + console.log('๐Ÿšจโš ๏ธ DASHBOARD: Current vault state:', { + isOpen: window.emmaWebVault.isOpen, + hasVaultData: !!window.emmaWebVault.vaultData, + sessionActive: sessionStorage.getItem('emmaVaultActive'), + sessionName: sessionStorage.getItem('emmaVaultName') + }); + } + }) + .catch(error => { + console.error('๐ŸšจโŒ DASHBOARD: Could not restore vault:', error); + console.error('๐ŸšจโŒ DASHBOARD: Restore error details:', { + errorMessage: error.message, + errorStack: error.stack, + vaultExists: !!window.emmaWebVault, + sessionActive: sessionStorage.getItem('emmaVaultActive') + }); + }); + } else { + console.log('๐Ÿšจโ„น๏ธ DASHBOARD: No active vault session found - extension will need manual unlock'); + } + } else if (typeof EmmaWebVault === 'undefined') { + console.error('๐ŸšจโŒ DASHBOARD: EmmaWebVault class not available! Check script loading.'); + } else if (window.emmaWebVault) { + console.log('๐Ÿšจโ„น๏ธ DASHBOARD: window.emmaWebVault already exists, skipping creation'); + } else { + console.error('๐ŸšจโŒ DASHBOARD: Unknown condition preventing vault creation'); + } + + // Check what's available + // Extension mode - Set up extension-aware vault status + if (window.emmaWebVault && window.emmaWebVault.extensionAvailable) { + + // Set dashboard as ready - extension handles all vault operations + const statusIcon = document.getElementById('status-icon'); + const statusTitle = document.getElementById('status-title'); + const statusDescription = document.getElementById('status-description'); + const actionBtn = document.getElementById('vault-action-btn'); + + if (statusIcon) statusIcon.textContent = '๐Ÿ”—'; + if (statusTitle) statusTitle.textContent = 'Extension Connected'; + if (statusDescription) statusDescription.textContent = 'Ready to preserve memories! Extension manages your vault automatically.'; + if (actionBtn) actionBtn.style.display = 'none'; // Hide action button + + // Listen for extension vault ready event + window.addEventListener('extension-vault-ready', (event) => { + + // Force update vault icon to unlocked + const vaultIcon = document.querySelector('#vault-node .radial-item-icon'); + if (vaultIcon) { + vaultIcon.textContent = '๐Ÿ”“'; + + } + + // Update vault node status + setTimeout(() => { + if (window.dashboard && window.dashboard.updateVaultNodeStatus) { + window.dashboard.updateVaultNodeStatus(); + + } + }, 100); + }); + } else if (window.emmaWebVault && window.emmaWebVault.isOpen) { + // Vault is already open and ready + console.log('โœ… DASHBOARD: Vault is already open and ready'); + } + + }, 100); // Small delay for DOM elements to be available + + }, 100); // Small delay to ensure all elements are definitely available + }); + + // ๐Ÿšจ CRITICAL: Add postMessage handlers for content script communication + window.addEventListener('message', (event) => { + // Only handle messages from same origin + if (event.origin !== window.location.origin) return; + + if (event.data?.type === 'EMMA_VAULT_CHECK') { + console.log('๐Ÿšจ๐Ÿ“จ DASHBOARD: Received vault check request from content script'); + + const vaultStatus = { + exists: !!window.emmaWebVault, + isOpen: window.emmaWebVault?.isOpen, + canAddMemory: typeof window.emmaWebVault?.addMemory === 'function', + timestamp: Date.now() + }; + + console.log('๐Ÿšจ๐Ÿ“จ DASHBOARD: Sending vault status:', vaultStatus); + + window.postMessage({ + type: 'EMMA_VAULT_RESPONSE', + messageId: event.data.messageId, + vaultStatus: vaultStatus + }, '*'); + } + + if (event.data?.type === 'EMMA_VAULT_SAVE' && window.emmaWebVault && window.emmaWebVault.isOpen) { + console.log('๐Ÿšจ๐Ÿ’พ DASHBOARD: Received save request from content script'); + console.log('๐Ÿšจ๐Ÿ’พ DASHBOARD: Memory data size:', JSON.stringify(event.data.memoryData).length); + + window.emmaWebVault.addMemory(event.data.memoryData) + .then(result => { + console.log('๐Ÿšจโœ… DASHBOARD: Save successful:', result); + + // ๐ŸŽฏ CRITICAL: Trigger constellation refresh after extension save + console.log('๐Ÿ”„ DASHBOARD: Triggering constellation refresh after extension save...'); + window.dispatchEvent(new CustomEvent('emmaMemoryAdded', { + detail: { + memoryId: result.id, + source: 'extension', + timestamp: Date.now() + } + })); + + window.postMessage({ + type: 'EMMA_VAULT_SAVE_RESPONSE', + messageId: event.data.messageId, + result: { success: true, result } + }, '*'); + }) + .catch(error => { + console.error('๐ŸšจโŒ DASHBOARD: Save failed:', error); + window.postMessage({ + type: 'EMMA_VAULT_SAVE_RESPONSE', + messageId: event.data.messageId, + result: { success: false, error: error.message } + }, '*'); + }); + } + }); + + + + + + + + + + + + + + + + + + + + + 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..ed4868fc 100644 --- a/js/emma-browser-client.js +++ b/js/emma-browser-client.js @@ -23,6 +23,12 @@ class EmmaBrowserClient { // Privacy-first tools (for local execution) this.tools = new EmmaVoiceTools(); + + this.apiKey = typeof this.options.apiKey === 'string' && this.options.apiKey.trim() + ? this.options.apiKey.trim() + : null; + this._lastSentApiKey = undefined; + this.voicePlaybackEnabled = true; console.log('๐ŸŽ™๏ธ Emma Browser Client initialized'); @@ -32,6 +38,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 +57,7 @@ class EmmaBrowserClient { */ async startVoiceSession() { try { + this._disableRecognition = false; this.setState('connecting'); // Connect to Emma backend @@ -74,12 +92,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 +180,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); } @@ -151,13 +215,31 @@ class EmmaBrowserClient { */ async connectToEmmaAgent() { try { - const backendUrl = window.location.hostname === 'localhost' - ? 'ws://localhost:3001' - : 'wss://emma-voice-backend.onrender.com'; - - const wsUrl = `${backendUrl}/voice`; - - console.log('๐Ÿ”— Connecting to Emma agent...'); + const wsUrl = (typeof window.getEmmaBackendWsUrl === 'function') + ? window.getEmmaBackendWsUrl() + : 'wss://emma-lite-optimized.onrender.com/voice'; + + // FORCE debug logging bypassing production suppression + const forceLog = (...args) => Function.prototype.call.call(console.log, console, ...args); + + forceLog('๐Ÿ”— ===== EMMA WEBSOCKET CONNECTION DEBUG ====='); + forceLog(' wsUrl:', wsUrl); + forceLog(' window.location.href:', window.location.href); + forceLog(' window.location.hostname:', window.location.hostname); + forceLog(' window.location.port:', window.location.port); + forceLog(' window.location.protocol:', window.location.protocol); + forceLog(' window.EMMA_ENV:', window.EMMA_ENV); + forceLog(' window.EMMA_DEBUG:', window.EMMA_DEBUG); + forceLog(' window.EMMA_BACKEND_ORIGIN:', window.EMMA_BACKEND_ORIGIN); + forceLog(' typeof getEmmaBackendWsUrl:', typeof window.getEmmaBackendWsUrl); + forceLog(' typeof getEmmaBackendOrigin:', typeof window.getEmmaBackendOrigin); + if (typeof window.getEmmaBackendOrigin === 'function') { + forceLog(' getEmmaBackendOrigin() result:', window.getEmmaBackendOrigin()); + } + if (typeof window.getEmmaBackendWsUrl === 'function') { + forceLog(' getEmmaBackendWsUrl() result:', window.getEmmaBackendWsUrl()); + } + forceLog('=============================================='); this.websocket = new WebSocket(wsUrl); this.setupWebSocketHandlers(); @@ -172,7 +254,9 @@ class EmmaBrowserClient { clearTimeout(timeout); console.log('โœ… Connected to Emma agent'); this.isConnected = true; - + + this.sendRuntimeApiKey({ force: Boolean(this.apiKey) }); + // Notify chat of connection if (this.chatInstance) { this.chatInstance.addMessage('system', '๐Ÿ”— Connected to Emma backend'); @@ -297,7 +381,7 @@ class EmmaBrowserClient { */ speak(text) { try { - if (!this.synth) return; + if (!this.synth || !this.voicePlaybackEnabled) return; // Stop any queued utterances for snappy response this.synth.cancel(); const utter = new SpeechSynthesisUtterance(text); @@ -323,11 +407,14 @@ class EmmaBrowserClient { case 'emma_ready': this.setState('listening'); if (this.chatInstance) { + if (typeof this.chatInstance.markAgentReady === 'function') { + this.chatInstance.markAgentReady(); + } this.chatInstance.addMessage('system', 'โœ… Emma is ready to talk!'); this.chatInstance.addMessage('system', '๐ŸŽค Say something to Emma - transcription will appear here'); } break; - + case 'user_transcription': // Display user's speech in chat if (this.chatInstance && message.transcript) { @@ -338,7 +425,11 @@ class EmmaBrowserClient { case 'emma_transcription': // Display Emma's speech in chat if (this.chatInstance && message.transcript) { - this.chatInstance.addMessage(message.transcript, 'emma', { isVoice: true }); + const isVoice = message.source === 'voice'; + this.chatInstance.addMessage(message.transcript, 'emma', { isVoice }); + if (typeof this.chatInstance.hideTypingIndicator === 'function') { + this.chatInstance.hideTypingIndicator(); + } } // Store text for fallback and wait for server audio this.lastEmmaText = message.transcript; @@ -348,6 +439,10 @@ class EmmaBrowserClient { case 'emma_audio': // High-quality server-synthesized audio (mp3 base64) - PRIORITY PLAYBACK if (message.audio && message.encoding === 'base64/mp3') { + if (!this.voicePlaybackEnabled) { + console.log('๐Ÿ”‡ Voice playback muted - skipping server audio'); + return; + } try { console.log('๐ŸŽค Playing OpenAI TTS audio (Alloy voice)'); @@ -375,6 +470,15 @@ class EmmaBrowserClient { case 'state_change': this.setState(message.state); + if (this.chatInstance) { + if (message.state === 'thinking' && typeof this.chatInstance.showTypingIndicator === 'function') { + this.chatInstance.showTypingIndicator(); + } + if ((message.state === 'speaking' || message.state === 'listening') && + typeof this.chatInstance.hideTypingIndicator === 'function') { + this.chatInstance.hideTypingIndicator(); + } + } break; case 'tool_request': @@ -409,6 +513,9 @@ class EmmaBrowserClient { console.log('๐Ÿ”‡ Emma agent connection closed'); this.isConnected = false; this.setState('idle'); + if (this.chatInstance && typeof this.chatInstance.markAgentDisconnected === 'function') { + this.chatInstance.markAgentDisconnected(); + } }; } @@ -450,20 +557,31 @@ class EmmaBrowserClient { */ displayToolResult(toolName, params, result) { if (!this.chatInstance) return; - + + if (result && result.error) { + this.chatInstance.addMessage(`โš ๏ธ ${result.error}`, 'system'); + return; + } + switch (toolName) { case 'get_people': if (result.people && result.people.length > 0) { this.chatInstance.displayPeopleResults(result.people); } break; - + case 'get_memories': if (result.memories && result.memories.length > 0) { this.chatInstance.displayMemoryResults(result.memories); } break; - + + case 'summarize_memory': + if (result.summary) { + this.chatInstance.addMessage(`๐Ÿ“ Here's a gentle summary: ${result.summary}`, 'emma'); + } + break; + case 'create_memory_from_voice': if (result.success) { this.chatInstance.addMessage('system', `๐Ÿ’ญ New memory created: "${params.content.substring(0, 50)}..."`, { @@ -472,7 +590,17 @@ class EmmaBrowserClient { }); } break; - + + case 'create_memory_capsule': + if (result.success) { + const label = result.title || params.title || params.content?.substring(0, 50) || 'New memory'; + this.chatInstance.addMessage('system', `๐Ÿ’พ Saved memory: "${label}"`, { + type: 'memory-created', + memoryId: result.memoryId + }); + } + break; + case 'update_person': if (result.success) { this.chatInstance.addMessage('system', `๐Ÿ‘ค Updated ${result.personName} with new details`, { @@ -480,9 +608,86 @@ class EmmaBrowserClient { }); } break; + + case 'create_person_profile': + if (result.success) { + this.chatInstance.addMessage('system', `๐Ÿ‘ค Added ${result.personName} to your people`, { + type: 'person-created', + personId: result.personId + }); + } + break; + + case 'update_memory_capsule': + if (result.success) { + const fields = result.updatedFields?.length + ? result.updatedFields.join(', ') + : 'memory details'; + this.chatInstance.addMessage('system', `๐Ÿ“ Updated ${fields} for your memory`, { + type: 'memory-updated', + memoryId: result.memoryId + }); + } + break; + + case 'attach_memory_media': + if (result.success) { + const count = result.attachmentCount ?? (params.media?.length || 0); + this.chatInstance.addMessage('system', `๐Ÿ“Ž Added ${count} new ${count === 1 ? 'attachment' : 'attachments'} to your memory`, { + type: 'memory-media-added', + memoryId: result.memoryId + }); + } + break; + } + } + + /** + * Update runtime API key (shared with server-side agent) + */ + setApiKey(apiKey, { silent = false } = {}) { + const normalized = typeof apiKey === 'string' ? apiKey.trim() : ''; + const nextValue = normalized || null; + const changed = nextValue !== this.apiKey; + + this.apiKey = nextValue; + if (changed) { + this._lastSentApiKey = undefined; + } + + if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { + this.sendRuntimeApiKey({ force: changed }); + } else if (!silent && this.apiKey) { + console.log('๐Ÿ” Stored OpenAI key for next Emma connection'); } } + /** + * Send current API key to Emma server (if connected) + */ + sendRuntimeApiKey({ force = false } = {}) { + if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { + return; + } + + const normalized = typeof this.apiKey === 'string' ? this.apiKey.trim() : ''; + const payload = normalized || null; + + if (!force && payload === this._lastSentApiKey) { + return; + } + + if (!payload && !force) { + return; + } + + this._lastSentApiKey = payload; + this.sendToAgent({ + type: 'set_api_key', + apiKey: payload + }); + } + /** * Send message to Emma agent */ @@ -515,12 +720,12 @@ class EmmaBrowserClient { }; const message = statusMessages[newState]; - if (message && this.chatInstance.typingIndicator) { + if (this.chatInstance.typingIndicator) { const span = this.chatInstance.typingIndicator.querySelector('span'); - if (span) { + if (span && message) { span.textContent = message; - this.chatInstance.typingIndicator.style.display = message ? 'block' : 'none'; } + this.chatInstance.typingIndicator.style.display = message ? 'flex' : 'none'; } } } @@ -545,11 +750,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 +928,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 +987,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'); @@ -656,6 +1006,9 @@ class EmmaBrowserClient { } playAudioFallback(base64Audio) { + if (!this.voicePlaybackEnabled) { + return; + } try { console.log('๐ŸŽค Trying Blob URL fallback for audio playback'); @@ -692,6 +1045,22 @@ class EmmaBrowserClient { this.speak(this.lastEmmaText || 'I\'m having trouble with my voice right now.'); } } + + setVoicePlaybackEnabled(enabled) { + const normalized = Boolean(enabled); + if (this.voicePlaybackEnabled === normalized) { + return; + } + this.voicePlaybackEnabled = normalized; + if (!this.voicePlaybackEnabled && this.synth) { + try { + this.synth.cancel(); + } catch (_) { + // ignore cancellation errors + } + } + console.log(`๐Ÿ”Š Emma voice playback ${this.voicePlaybackEnabled ? 'enabled' : 'muted'}`); + } } // Export for global use diff --git a/js/emma-chat-experience.js b/js/emma-chat-experience.js index f47fd0c7..de094b1e 100644 --- a/js/emma-chat-experience.js +++ b/js/emma-chat-experience.js @@ -1,4 +1,4 @@ -/** +๏ปฟ/** * Emma Chat Experience - Intelligent Memory Companion Chat Interface * CTO-approved implementation following Emma's premium design principles * @@ -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,34 +60,27 @@ class EmmaChatExperience extends ExperiencePopup { this.messageContainer = null; this.inputField = null; this.sendButton = null; + this.voiceIntegrationInitialized = false; + this.voiceIntegrationInitializing = false; + this.initialWelcomeTimeout = null; + this.localWelcomeShown = false; + this.remoteGreetingReceived = false; + this.voicePlaybackEnabled = false; + this.voiceToggleButton = null; // ๐Ÿง  Vectorless AI Engine Integration this.vectorlessEngine = null; this.apiKey = null; this.isVectorlessEnabled = false; - + // ๐Ÿง  CTO REBUILD: UNIFIED EMMA INTELLIGENCE SYSTEM this.unifiedIntelligence = new EmmaUnifiedIntelligence({ dementiaMode: true, validationTherapy: true, vaultAccess: () => window.emmaWebVault?.vaultData?.content, - apiKey: null // Will be set when available + apiKey: this.apiKey // Will be set when available }); - // Legacy context for transition period - this.conversationContext = { - currentTopic: null, - recentPeople: new Set(), - emotionalState: 'positive', - lastMemoryMentioned: null, - conversationFlow: [], - dementiaMode: true, - validationMode: true, - personalContext: { - recentActivities: [], - emotionalTone: 'warm and loving' - } - }; // ๐Ÿ’ Intelligent Memory Capture Integration this.intelligentCapture = null; @@ -70,6 +103,17 @@ class EmmaChatExperience extends ExperiencePopup { capabilities: ["memory insights", "capture suggestions", "conversation", "vectorless AI"] }; + // Agent SDK chat support + this.agentChatEnabled = false; + this.agentConnectPromise = null; + this.agentConnectionNotified = false; + + // WebSocket retry backoff + this.agentRetryCount = 0; + this.agentMaxRetries = 5; + this.agentRetryDelay = 2000; // Start with 2 seconds + this.agentRetryTimeout = null; + // ๐Ÿš€ CRITICAL: Initialize AI systems on startup this.initializeEmmaIntelligence(); @@ -136,13 +180,13 @@ class EmmaChatExperience extends ExperiencePopup { async initialize() { this.initializeEmmaOrb(); this.setupChatInterface(); - + // ๐ŸŽ™๏ธ PHASE 3: Initialize voice integration after chat setup setTimeout(() => this.initializeVoiceIntegration(), 500); this.setupKeyboardShortcuts(); - // Add initial Emma welcome message (single clean bubble) - await this.addInitialWelcomeMessage(); + // Add initial Emma welcome message only if remote agent doesnโ€™t greet + this.scheduleInitialWelcome(); this.loadChatHistory(); // ๐Ÿง  Initialize Vectorless AI Engine @@ -167,6 +211,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...'); @@ -177,7 +227,8 @@ class EmmaChatExperience extends ExperiencePopup { speed: 1.0, tone: 'caring', pacing: 2.5, - validationMode: true + validationMode: true, + apiKey: this.apiKey || null }); } else { console.warn('โš ๏ธ EmmaBrowserClient not available'); @@ -189,15 +240,24 @@ class EmmaChatExperience extends ExperiencePopup { if (this.emmaVoice) { // ๐Ÿ”— CRITICAL: Connect voice system to chat for transcription this.emmaVoice.chatInstance = this; + if (typeof this.emmaVoice.setVoicePlaybackEnabled === 'function') { + this.emmaVoice.setVoicePlaybackEnabled(this.voicePlaybackEnabled); + } + this.syncVoiceApiKey(true); // 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(); } + // Ensure the chat interface connects to the Agent SDK backend + this.connectAgentForChat(); + console.log('โœ… Emma voice client ready!'); } else { console.warn('โš ๏ธ Emma voice client not available'); @@ -205,10 +265,118 @@ class EmmaChatExperience extends ExperiencePopup { this.voiceButton.title = 'Voice system not available'; } } catch (error) { - console.error('โŒ Voice integration failed:', error); + console.error('[VOICE] Integration failed:', error); this.voiceButton.style.opacity = '0.5'; this.voiceButton.title = 'Voice system error: ' + error.message; + } finally { + this.voiceIntegrationInitializing = false; + } + } + + /** + * Sync the runtime API key with the voice client (if connected) + */ + syncVoiceApiKey(silent = true) { + if (this.emmaVoice && typeof this.emmaVoice.setApiKey === 'function') { + this.emmaVoice.setApiKey(this.apiKey, { silent }); + } + } + + /** + * Ensure the chat agent connection is established for tool usage + */ + async connectAgentForChat() { + try { + if (!this.emmaVoice) return; + if (this.agentChatEnabled && this.emmaVoice.isConnected) return; + if (this.agentConnectPromise) return this.agentConnectPromise; + + this.agentConnectPromise = (async () => { + try { + if (!this.emmaVoice.isConnected) { + await this.emmaVoice.connectToEmmaAgent(); + } + } catch (error) { + console.error('โŒ Failed to connect Emma agent for chat:', error); + this.agentChatEnabled = false; + if (!this.agentConnectionNotified) { + this.addMessage('system', 'โš ๏ธ I could not reach Emma\'s advanced memory tools yet. I\'ll keep helping locally.'); + this.agentConnectionNotified = true; + } + } finally { + this.agentConnectPromise = null; + } + })(); + + return this.agentConnectPromise; + } catch (error) { + console.error('โŒ Agent connection setup error:', error); + } + } + + /** + * Mark that the Agent SDK connection is active + */ + markAgentReady() { + this.remoteGreetingReceived = true; + if (this.initialWelcomeTimeout) { + clearTimeout(this.initialWelcomeTimeout); + this.initialWelcomeTimeout = null; + } + + // Reset retry count on successful connection + this.agentRetryCount = 0; + if (this.agentRetryTimeout) { + clearTimeout(this.agentRetryTimeout); + this.agentRetryTimeout = null; + } + + if (this.agentChatEnabled) { + if (typeof this.hideTypingIndicator === 'function') { + this.hideTypingIndicator(); + } + return; + } + + this.agentChatEnabled = true; + this.agentConnectionNotified = true; + this.addMessage('system', '๐Ÿค– Emma is connected to your memory tools and can use them for you.'); + if (typeof this.hideTypingIndicator === 'function') { + this.hideTypingIndicator(); + } + } + + /** + * Handle agent disconnects gracefully + */ + markAgentDisconnected() { + this.agentChatEnabled = false; + this.agentConnectionNotified = false; + if (typeof this.hideTypingIndicator === 'function') { + this.hideTypingIndicator(); } + + // Implement exponential backoff for retries + this.agentRetryCount++; + + if (this.agentRetryCount > this.agentMaxRetries) { + this.addMessage('system', 'โš ๏ธ Could not reach Emma\'s advanced memory tools. Using local intelligence only.'); + this.agentConnectionNotified = true; + return; + } + + const delay = Math.min(this.agentRetryDelay * Math.pow(2, this.agentRetryCount - 1), 30000); + this.addMessage('system', `โš ๏ธ Emma lost connection to the advanced memory tools. Retrying in ${delay/1000}s... (${this.agentRetryCount}/${this.agentMaxRetries})`); + + // Clear existing retry timeout + if (this.agentRetryTimeout) { + clearTimeout(this.agentRetryTimeout); + } + + // Schedule retry with exponential backoff + this.agentRetryTimeout = setTimeout(() => { + this.connectAgentForChat(); + }, delay); } /** @@ -298,13 +466,13 @@ class EmmaChatExperience extends ExperiencePopup { this.displayPeopleResults(result.people); } break; - + case 'get_memories': if (result.memories && result.memories.length > 0) { this.displayMemoryResults(result.memories); } break; - + case 'create_memory_from_voice': if (result.success) { this.addMessage('system', `๐Ÿ’ญ New memory created: "${params.content.substring(0, 50)}..."`, { @@ -313,7 +481,7 @@ class EmmaChatExperience extends ExperiencePopup { }); } break; - + case 'update_person': if (result.success) { this.addMessage('system', `๐Ÿ‘ค Updated ${result.personName} with new details`, { @@ -357,7 +525,7 @@ class EmmaChatExperience extends ExperiencePopup { `).join(''); this.addMessage('emma', `I found ${memories.length} memories:`, { - type: 'memory-results', + type: 'memory-results', html: `
${memoriesHtml}
` }); } @@ -383,6 +551,34 @@ class EmmaChatExperience extends ExperiencePopup { } } + getHeaderActions() { + const toggle = this.createVoiceToggleButton(); + return toggle ? [toggle] : []; + } + + createVoiceToggleButton() { + if (this.voiceToggleButton && this.voiceToggleButton.tagName) { + return this.voiceToggleButton; + } + + const button = document.createElement('button'); + button.id = 'voice-tts-toggle'; + button.className = 'voice-toggle is-on'; + button.setAttribute('aria-pressed', 'true'); + + const icon = document.createElement('span'); + icon.className = 'voice-toggle-icon'; + icon.textContent = '๐Ÿ”Š'; + + const label = document.createElement('span'); + label.className = 'voice-toggle-label'; + label.textContent = 'Voice On'; + + button.append(icon, label); + this.voiceToggleButton = button; + return button; + } + renderContent(contentElement) { // CLEAN REDESIGN: Remove inner container styling - let ExperiencePopup handle the container // Just set up the content layout without duplicating container styles @@ -461,6 +657,7 @@ class EmmaChatExperience extends ExperiencePopup { this.sendButton = document.getElementById('send-btn'); // NO DUPLICATE close button - ExperiencePopup handles this this.voiceButton = document.getElementById('voice-input-btn'); + this.voiceToggleButton = this.voiceToggleButton || document.getElementById('voice-tts-toggle'); // Settings button removed - clean chat interface if (!this.messageContainer || !this.inputField || !this.sendButton || !this.voiceButton) { @@ -476,6 +673,11 @@ class EmmaChatExperience extends ExperiencePopup { this.inputField.addEventListener('keydown', (e) => this.handleInputKeydown(e)); this.sendButton.addEventListener('click', () => this.sendMessage()); this.voiceButton.addEventListener('click', () => this.toggleVoiceInput()); + if (this.voiceToggleButton && !this.voiceToggleButton.dataset.toggleBound) { + this.voiceToggleButton.addEventListener('click', () => this.toggleVoicePlayback()); + this.voiceToggleButton.dataset.toggleBound = 'true'; + } + this.updateVoiceToggleUI(); // Settings removed from chat - access via main settings panel // NO DUPLICATE close button event listener - ExperiencePopup handles this @@ -488,11 +690,39 @@ class EmmaChatExperience extends ExperiencePopup { // Setup settings modal this.setupSettingsModal(); - + // ๐ŸŽฏ Setup dynamic quick start prompts this.setupQuickStartPrompts(); } + toggleVoicePlayback() { + this.voicePlaybackEnabled = !this.voicePlaybackEnabled; + if (this.emmaVoice && typeof this.emmaVoice.setVoicePlaybackEnabled === 'function') { + this.emmaVoice.setVoicePlaybackEnabled(this.voicePlaybackEnabled); + } + this.updateVoiceToggleUI(); + console.log(`๐Ÿ”Š Emma TTS ${this.voicePlaybackEnabled ? 'enabled' : 'muted'}`); + } + + updateVoiceToggleUI() { + if (!this.voiceToggleButton) { + return; + } + + const label = this.voiceToggleButton.querySelector('.voice-toggle-label'); + const icon = this.voiceToggleButton.querySelector('.voice-toggle-icon'); + this.voiceToggleButton.classList.toggle('is-on', this.voicePlaybackEnabled); + this.voiceToggleButton.classList.toggle('is-muted', !this.voicePlaybackEnabled); + this.voiceToggleButton.setAttribute('aria-pressed', this.voicePlaybackEnabled ? 'true' : 'false'); + this.voiceToggleButton.setAttribute('title', this.voicePlaybackEnabled ? 'Emma will speak responses' : 'Emma voice muted'); + if (label) { + label.textContent = this.voicePlaybackEnabled ? 'Voice On' : 'Voice Off'; + } + if (icon) { + icon.textContent = this.voicePlaybackEnabled ? '๐Ÿ”Š' : '๐Ÿ”‡'; + } + } + /** * ๐ŸŽฏ Setup elegant quick start prompts for user engagement * Dynamic, on-brand suggestions to guide conversation - appear in chat, disappear when conversation starts @@ -514,13 +744,13 @@ class EmmaChatExperience extends ExperiencePopup { }, { text: "Let's save a new memory", - icon: "๐Ÿ’", + icon: "๐Ÿ“", action: "memory", description: "Create a new memory capsule" }, { text: "Ask me about any of your memories", - icon: "๐Ÿง ", + icon: "๐Ÿ’ฌ", action: "explore", description: "Explore and reminisce about past memories" } @@ -723,12 +953,15 @@ class EmmaChatExperience extends ExperiencePopup { button.addEventListener('click', (e) => { const action = e.currentTarget.dataset.action; const text = e.currentTarget.dataset.text; - - // Visual feedback - e.currentTarget.style.transform = 'scale(0.95)'; - setTimeout(() => { - e.currentTarget.style.transform = ''; - }, 150); + + // Visual feedback - store reference to avoid null after timeout + const button = e.currentTarget; + if (button) { + button.style.transform = 'scale(0.95)'; + setTimeout(() => { + if (button) button.style.transform = ''; + }, 150); + } // Execute the appropriate action this.handlePromptAction(action, text); @@ -808,10 +1041,10 @@ class EmmaChatExperience extends ExperiencePopup { "Perfect! What special moment would you like to capture in your memory vault?", "I'm here to help! What's the story you'd like to preserve?" ]; - + const response = responses[Math.floor(Math.random() * responses.length)]; this.addMessage(response, 'emma'); - + // Focus on the input for immediate typing if (this.inputField) { this.inputField.focus(); @@ -856,7 +1089,7 @@ class EmmaChatExperience extends ExperiencePopup { for (const pattern of pronounPatterns) { const pronounMatch = message.match(pattern); if (pronounMatch) { - const recentPeople = Array.from(this.conversationContext.recentPeople); + const recentPeople = Array.from(this.unifiedIntelligence.conversationContext.recentPeople); if (recentPeople.length > 0) { const resolvedPerson = recentPeople[recentPeople.length - 1]; // Most recent console.log(`๐ŸŽฏ CTO: Resolved pronoun "${pronounMatch[1]}" to "${resolvedPerson}" from context`); @@ -1225,29 +1458,20 @@ class EmmaChatExperience extends ExperiencePopup { // Find connected memories using same logic as people page let connectedMemories = []; - + 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 }); - } - - // 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 }); - } - } + const normalizedMemory = { ...memory, id: memory.id || memoryId }; + + if (window.metadataIncludesPerson(memory.metadata?.people, personForMatching)) { + connectedMemories.push(normalizedMemory); + continue; } + + // Content-based inference removed; rely on explicit metadata associations only } } @@ -1625,7 +1849,7 @@ class EmmaChatExperience extends ExperiencePopup {
Memory photo @@ -2065,58 +2289,78 @@ class EmmaChatExperience extends ExperiencePopup { const message = this.inputField.value.trim(); if (!message) return; - // Hide quick start prompts when user starts typing their own messages if (this.hasQuickPromptsShowing) { this.hideQuickStartPrompts(); } - // Add user message const messageId = this.addMessage(message, 'user'); this.inputField.value = ''; this.autoResizeTextarea(); this.handleInputChange(); - // ๐ŸŽฏ ENRICHMENT FSM: Check if this is a response to an enrichment question + if (this.emmaVoice) { + await this.connectAgentForChat(); + } + + const canUseAgent = this.agentChatEnabled && this.emmaVoice && this.emmaVoice.isConnected; + const localHandled = await this.processLocalChatMessage(message, messageId, { + allowDefaultResponse: !canUseAgent + }); + + if (localHandled) { + return; + } + + if (canUseAgent) { + if (typeof this.showTypingIndicator === 'function') { + this.showTypingIndicator(); + } + this.emmaVoice.sendToAgent({ type: 'user_text', text: message, source: 'chat' }); + return; + } + + await this.respondAsEmma(message); + } + + + + + async processLocalChatMessage(message, messageId, { allowDefaultResponse = true } = {}) { const activeEnrichment = this.findActiveEnrichmentForResponse(); if (activeEnrichment) { await this.processEnrichmentResponse(activeEnrichment, message); - return; + return true; } - // ๐ŸŽฏ CTO OPTIMIZATION: VAULT OPERATIONS FIRST - Complete intent hierarchy - // This prevents ALL vault queries from being captured as memories const intent = this.classifyUserIntent(message); - - // VAULT OPERATIONS have HIGHEST PRIORITY if (intent.type === 'people_list' || intent.type === 'memory_search' || intent.type === 'person_inquiry') { console.log('๐ŸŽฏ CTO: VAULT OPERATION DETECTED - Bypassing memory capture for:', intent.type, message); this.showTypingIndicator(); setTimeout(() => { this.respondAsEmma(message); }, 1000 + Math.random() * 1500); - return; + return true; } - // ๐Ÿ’ Check for memory detection (new messages only) if (this.intelligentCapture) { const analysisResult = await this.analyzeForMemory(message, messageId); - // If intelligent capture already produced a response (unified prompt), stop here if (analysisResult && analysisResult.handled) { - return; + return true; } - } else { - // ... existing code ... } - // Show typing indicator - this.showTypingIndicator(); + if (!allowDefaultResponse) { + return false; + } - // Simulate Emma thinking and respond + this.showTypingIndicator(); setTimeout(() => { this.respondAsEmma(message); - }, 1000 + Math.random() * 1500); // 1-2.5s realistic delay + }, 1000 + Math.random() * 1500); + return true; } + addMessage(content, sender, options = {}) { // Defensive argument handling: many call sites mistakenly pass (sender, content). // Auto-correct if the first arg looks like a sender label and the second does not. @@ -2134,21 +2378,21 @@ class EmmaChatExperience extends ExperiencePopup { const messageDiv = document.createElement('div'); messageDiv.className = `${sender}-message`; messageDiv.id = messageId; - + // ๐ŸŽ™๏ธ VOICE: Add voice indicators for transcribed messages if (options.isVoice) { messageDiv.setAttribute('data-voice', 'true'); } - + // ๐Ÿ’ CRITICAL: Track people mentioned in Emma's responses too if (sender === 'emma' && content) { const emmaPeople = this.extractPeople(content); - emmaPeople.forEach(person => this.conversationContext.recentPeople.add(person)); - + emmaPeople.forEach(person => this.unifiedIntelligence.conversationContext.recentPeople.add(person)); + console.log('๐Ÿ’ EMMA RESPONSE TRACKING:', { content: content.substring(0, 50) + '...', extractedPeople: emmaPeople, - recentPeople: Array.from(this.conversationContext.recentPeople) + recentPeople: Array.from(this.unifiedIntelligence.conversationContext.recentPeople) }); } @@ -2157,7 +2401,7 @@ class EmmaChatExperience extends ExperiencePopup { if (sender === 'emma') { // SAFETY: Ensure options exists const safeOptions = options || {}; - + // Handle HTML content vs regular text const messageContent = safeOptions.isHtml ? content : `

${this.formatMessageContent(content || '')}

`; @@ -2224,11 +2468,11 @@ class EmmaChatExperience extends ExperiencePopup { console.warn('โš ๏ธ Emma orb fallback for message avatar'); // High-quality fallback gradient orbContainer.style.cssText = ` - background: radial-gradient(circle at 30% 30%, #8A5EFA, #764ba2, #f093fb); + background: radial-gradient(circle at 30% 30%, #8A5EFA, #764ba2, #deb3e4); border-radius: 50%; width: 100%; height: 100%; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); + box-shadow: 0 4px 12px rgba(111, 99, 217, 0.3); `; } } @@ -2257,8 +2501,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 @@ -2315,426 +2563,78 @@ class EmmaChatExperience extends ExperiencePopup { async respondAsEmma(userMessage) { this.hideTypingIndicator(); - - // ๐Ÿšจ CTO EMERGENCY ROLLBACK: Unified system broken, using working legacy system - console.log('๐Ÿšจ CTO: Using proven legacy system (unified intelligence disabled for emergency)'); - - // ๐Ÿค” CHECK FOR ACTIVE PERSON ENRICHMENT FLOW - if (this.currentPersonEnrichment && - this.currentPersonEnrichment.stage !== 'complete' && - Date.now() - this.currentPersonEnrichment.startedAt < 300000) { // 5 minute timeout - - console.log('๐Ÿค” Active person enrichment detected, handling response'); - const handled = await this.handlePersonEnrichmentResponse(userMessage, this.currentPersonEnrichment.personName); - - if (handled) { - // If this completed the enrichment, clear it - if (this.currentPersonEnrichment.stage === 'complete') { - this.currentPersonEnrichment = null; - } - return; - } + const response = await this.unifiedIntelligence.analyzeAndRespond(userMessage, this); + this.addMessage(response.text, 'emma'); + if (response.actions && response.actions.length > 0) { + this.executeIntelligentActions(response.actions, response.analysis); } + } - // ๐ŸŽฏ INTELLIGENT INTENT CLASSIFICATION - const intent = this.classifyUserIntent(userMessage); - console.log('๐Ÿง  CHAT: Intent classified as:', intent); - - // ๐Ÿ‘ฅ PEOPLE LIST REQUEST: Show all people in vault - if (intent.type === 'people_list') { - console.log('๐Ÿ‘ฅ CHAT: People list request detected'); - await this.handlePeopleListRequest(userMessage); - return; - } - // ๐Ÿ‘ค PERSON REQUEST DETECTION: Only for specific person inquiries - if (intent.type === 'person_inquiry') { - const personRequest = this.detectPersonRequest(userMessage); - if (personRequest) { - console.log('๐Ÿ‘ค CHAT: Person request detected:', personRequest); - - // ๐ŸŽฏ CTO CRITICAL: Track the person being queried for context continuity - this.conversationContext.lastQueriedPerson = personRequest.personName; - console.log(`๐ŸŽฏ CTO: Set lastQueriedPerson to: ${personRequest.personName}`); - - await this.handlePersonRequest(personRequest); + /** + * ๐Ÿ” HANDLE MEMORY LIST REQUEST + * Shows user their existing memories when they ask "what are my memories" + */ + async handleMemoryListRequest(userMessage) { + console.log('๐Ÿ” MEMORY LIST: Processing request:', userMessage); + + try { + // Get memories from vault + if (!window.emmaWebVault || !window.emmaWebVault.isOpen) { + this.addMessage("I'd love to show you your memories! Please make sure your vault is unlocked first.", 'emma'); return; } - } - - // ๐Ÿ’ Memory capture detection for ongoing conversations - const hasDetectedMemory = Array.from(this.detectedMemories.values()).some(analysis => - analysis.memory && analysis.memory.originalContent && - analysis.memory.originalContent.includes(userMessage.substring(0, 50)) - ); - - if (hasDetectedMemory) { - const captureResponse = await this.generateMemoryCaptureResponse(userMessage); - this.addMessage(captureResponse, 'emma'); - return; - } - - // ๐Ÿ›๏ธ CORE VAULT OPERATIONS: Emma's primary job (always works!) - if (intent.type === 'vault_operation') { - console.log('๐Ÿ›๏ธ CORE VAULT OPERATION: Handling directly'); - await this.handleVaultOperation(userMessage, intent); - return; - } - - // ๐Ÿ” MEMORY SEARCH: Handle memory queries intelligently - if (intent.type === 'memory_search') { - // Check if this is a simple "what are my memories" type query - if (/\b(what are|show me|list|tell me)\b.*\b(my|the)\b.*\b(memory|memories)\b/i.test(userMessage)) { - console.log('๐Ÿ” MEMORY SEARCH: Handling simple memory list request'); - await this.handleMemoryListRequest(userMessage); + + const memories = await window.emmaWebVault.listMemories(10); // Get first 10 memories + + if (!memories || memories.length === 0) { + this.addMessage("You don't have any memories in your vault yet. Would you like to create your first memory together?", 'emma'); return; } - // For complex memory search queries, use vectorless if available - if (this.isVectorlessEnabled && this.vectorlessEngine) { - try { - const result = await this.vectorlessEngine.processQuestion(userMessage); - if (result.success) { - this.addVectorlessMessage(result.response, result.memories, result.citations, result.suggestions); - return; - } else { - console.warn('๐Ÿ’ฌ Vectorless processing failed, using fallback:', result.error); - } - } catch (error) { - console.error('๐Ÿ’ฌ Vectorless AI error:', error); - } + // Generate a warm, personal response + const memoryCount = memories.length; + const totalMemories = Object.keys(window.emmaWebVault.vaultData?.content?.memories || {}).length; + + let response = `You have ${totalMemories} memory${totalMemories !== 1 ? 'ies' : ''} in your vault! `; + + if (totalMemories > 10) { + response += `Here are your ${memories.length} most recent ones:\n\n`; + } else { + response += `Here they are:\n\n`; } - // Fallback for memory search without vectorless - await this.handleMemoryListRequest(userMessage); - return; - } - - // ๐Ÿš€ ADVANCED AI MODE: Use OpenAI for conversation and questions - if (this.apiKey && intent.type === 'conversation') { - console.log('๐Ÿš€ ADVANCED AI MODE: Using OpenAI for intelligent conversation'); - console.log('๐ŸŽฏ Intent:', intent); - try { - const response = await this.generateIntelligentEmmaResponse(userMessage, intent); - this.addMessage(response, 'emma'); - this.addAIModeIndicator(); // Show that AI was used - return; - } catch (error) { - console.warn('๐Ÿค– AI response failed, using fallback:', error); - // Don't show error for conversation - just fall through to dynamic response + // List recent memories with dates + memories.forEach((memory, index) => { + const title = memory.title || 'Untitled Memory'; + const date = new Date(memory.created || memory.timestamp || Date.now()).toLocaleDateString(); + response += `${index + 1}. **${title}** (${date})\n`; + + // Add a brief preview of the content if available + if (memory.content && memory.content.length > 0) { + const preview = memory.content.substring(0, 100); + response += ` ${preview}${memory.content.length > 100 ? '...' : ''}\n\n`; + } else { + response += '\n'; + } + }); + + if (totalMemories > 10) { + response += `\nYou can view all your memories in the constellation view or ask me to search for specific ones!`; } + + this.addMessage(response, 'emma'); + console.log('โœ… MEMORY LIST: Successfully displayed memories'); + + } catch (error) { + console.error('โŒ MEMORY LIST ERROR:', error); + this.addMessage("I had trouble accessing your memories right now. Please try again in a moment.", 'emma'); } - - // ๐Ÿ’œ FALLBACK: Dynamic responses for basic interactions - const response = await this.generateDynamicEmmaResponse(userMessage); - this.addMessage(response, 'emma'); } /** - * ๐ŸŽฏ INTELLIGENT INTENT CLASSIFICATION - * Determines what the user is trying to do - */ - classifyUserIntent(message) { - const lower = message.toLowerCase().trim(); - - // ๐Ÿ‘ฅ PEOPLE LISTING QUERIES (asking about ALL people) - if (/\b(who are|what are|show me|list|see|view)\b.*\b(my|the|all)\b.*\b(people|person|contacts|family|friends)\b/i.test(message) || - /\b(my|all)\b.*\b(people|contacts|family|friends)\b/i.test(message) || - /\bpeople\b.*\b(list|vault|have|know)\b/i.test(message)) { - return { type: 'people_list', confidence: 0.95 }; - } - - // ๐Ÿ‘ค Person inquiries (asking ABOUT someone specific) - if (/^(who is|tell me about|show me|what about|how about)\s+[A-Z]/i.test(message) || - /^[A-Z][a-z]+\s*\?+\s*$/.test(message)) { - return { type: 'person_inquiry', confidence: 0.9 }; - } - - // ๐Ÿ” Memory search queries - ENHANCED to catch edge cases - if (/\b(find|search|show|what|who|when|where|list|see|view|tell me)\b.*\b(memory|memories|remember)\b/i.test(message) || - /\b(what are|show me|tell me|list)\b.*\b(my|the)\b.*\b(memory|memories)\b/i.test(message) || - /\bmemory\b.*\b(list|search|find|show)\b/i.test(message) || - /\bmemories\b.*\b(list|search|find|show)\b/i.test(message)) { - return { type: 'memory_search', confidence: 0.9 }; - } - - // ๐Ÿ›๏ธ CORE VAULT OPERATIONS (Emma's primary job - always works!) - // ULTRA-ROBUST for dementia users - catch every possible way they might ask - if ( - // Direct vault operations - /\b(add|create|new|save)\b.*\b(person|people|memory|memories|vault)\b/i.test(message) || - /\b(add|save)\b.*\b(to|in)\b.*\b(vault|emma)\b/i.test(message) || - /\blet'?s?\s+(add|create|save)\b/i.test(message) || - - // Natural language patterns for dementia users - /\b(?:want|need|would\s+like)\s+(?:to\s+)?(?:add|save|create|put)\b/i.test(message) || - /\b(?:please|can\s+you)\s+(?:add|save|create|put)\b/i.test(message) || - /\b(?:put|place)\s+[a-zA-Z]+\b/i.test(message) || - - // Simple "add [name]" pattern - /\badd\s+[a-zA-Z]+/i.test(message) || - - // "I have a person/friend/family member" patterns - /\b(?:have|met|know)\s+(?:a\s+)?(?:person|friend|family|someone)\b/i.test(message) - ) { - return { type: 'vault_operation', confidence: 0.95 }; - } - - // ๐Ÿ’ฌ General conversation & questions (use AI if available) - if (/^(what|who|when|where|why|how|can|could|would|should|do|does|did|is|are|was|were)\b/i.test(message) || - message.includes('?') || - /\b(hello|hi|hey|thank|thanks|help|tell me|explain)\b/i.test(message)) { - return { type: 'conversation', confidence: 0.6 }; - } - - // ๐ŸŽฏ Other commands/actions - if (/\b(add|create|new|save|let|want|need|help)\b/i.test(message)) { - return { type: 'general_command', confidence: 0.5 }; - } - - // ๐Ÿ’ฌ Default: General conversation - return { type: 'conversation', confidence: 0.4 }; - } - - /** - * ๐Ÿš€ GENERATE INTELLIGENT EMMA RESPONSE using OpenAI - * This is where the real AI magic happens! - */ - async generateIntelligentEmmaResponse(userMessage, intent) { - const env = (typeof window !== 'undefined' && window.EMMA_ENV) ? window.EMMA_ENV : 'production'; - const useLLM = env !== 'production' && !!this.apiKey; - - try { - // Get vault context for personalization - const vaultContext = await this.getVaultContextForAI(); - - if (!useLLM) { - // Gentle, local-first fallback aligned with Emma ethos - const topics = (vaultContext.recentTopics || []).slice(0, 3); - const topicStr = topics.length ? ` I remember recent topics like ${topics.join(', ')}.` : ''; - const suggestions = [ - 'Would you like me to save this as a memory?', - 'Should we look for related photos together?', - 'Want to add who was with you in this memory?' - ]; - const suggestion = suggestions[Math.floor(Math.random() * suggestions.length)]; - return ( - `Iโ€™m here with you. Letโ€™s explore this together.${topicStr} ` + - `${suggestion}` - ); - } - - // Build the prompt based on intent and context - const prompt = this.buildEmmaPrompt(userMessage, intent, vaultContext); - - const response = await fetch('https://api.openai.com/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}` - }, - body: JSON.stringify({ - model: 'gpt-4', - messages: [ - { role: 'system', content: prompt.system }, - { role: 'user', content: prompt.user } - ], - max_tokens: 300, - temperature: 0.7 - }) - }); - - if (!response.ok) throw new Error(`OpenAI API error: ${response.status}`); - const data = await response.json(); - const aiResponse = data.choices[0]?.message?.content; - if (aiResponse) return aiResponse.trim(); - throw new Error('No response content from OpenAI'); - - } catch (error) { - // Final safe fallback to ensure no broken UX - const calm = 'Iโ€™m here with you. Letโ€™s take this step by step.'; - return `${calm} Would you like me to save this as a memory?`; - } - } - - /** - * ๐Ÿง  BUILD EMMA-SPECIFIC PROMPT for OpenAI - */ - buildEmmaPrompt(userMessage, intent, vaultContext) { - // ๐Ÿ’ CRITICAL: Include conversation context for continuity - const conversationHistory = this.conversationContext.conversationFlow - .slice(-3) - .map(flow => `User: ${flow.message}`) - .join('\n'); - - const systemPrompt = `You are Emma, an intelligent memory companion. You help users organize, explore, and interact with their personal memories in a warm, helpful way. - -PERSONALITY: -- Warm, caring, and memory-focused -- Always helpful but never overwhelming -- Gentle and patient, especially for users with memory challenges -- Focus on preserving and celebrating memories - -CRITICAL - DEMENTIA CARE MODE (ALWAYS ACTIVE FOR THIS USER): -- NEVER correct or contradict any memories, even if they seem inconsistent -- Use validation therapy - affirm their feelings and experiences always -- Focus on emotional truth rather than factual accuracy -- Maintain warmth and patience in every single response -- If memories seem confused, gently redirect to positive aspects -- ALWAYS validate what they're sharing as real and meaningful -- Never use phrases like "Do you remember" or "Are you sure" -- When asked about people ("show me Mark"), provide warm recall assistance -- Help them remember relationships, shared experiences, and emotional connections - -CONVERSATION CONTEXT: -Recent conversation flow: -${conversationHistory} - -People mentioned recently: ${Array.from(this.conversationContext.recentPeople).join(', ') || 'None'} -Current emotional tone: ${this.conversationContext.emotionalState} -Family/People context: Based on vault data and conversation - -VAULT CONTEXT: -- User has ${vaultContext.memoryCount} memories in their vault -- Recent memories include: ${vaultContext.recentTopics.join(', ')} -- People in vault: ${vaultContext.people.join(', ')} - -CAPABILITIES: -- Provide warm conversation about memories -- Answer questions about their vault and memories -- Give gentle encouragement about memory preservation -- Help them reflect on their experiences - -NOTE: Core vault operations (adding people, creating memories) are handled by Emma's built-in systems. Focus on conversation, encouragement, and emotional support. - -INTENT: ${intent.type} -${intent.type === 'conversation' ? 'User wants to have a conversation. Be warm, engaging, and memory-focused. Provide emotional support and encouragement.' : ''} - -RULES: -- Keep responses concise (1-3 sentences) -- Always be encouraging about memory preservation -- NEVER contradict or correct memories - validate everything -- Never mention technical details or AI limitations -- If unsure, offer to help them explore their memories`; - - return { - system: systemPrompt, - user: userMessage - }; - } - - /** - * ๐Ÿ“Š GET VAULT CONTEXT for AI responses - */ - async getVaultContextForAI() { - try { - const vault = window.emmaWebVault?.vaultData?.content; - - if (!vault) { - return { - memoryCount: 0, - recentTopics: ['No memories yet'], - people: ['No people yet'] - }; - } - - const memories = vault.memories || {}; - const people = vault.people || {}; - - // Get recent memory topics - const memoryList = Object.values(memories); - const recentTopics = memoryList - .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)) - .slice(0, 3) - .map(m => m.title || 'Untitled memory') - .filter(Boolean); - - // Get people names - const peopleNames = Object.values(people) - .map(p => p.name) - .filter(Boolean) - .slice(0, 5); - - return { - memoryCount: memoryList.length, - recentTopics: recentTopics.length ? recentTopics : ['No recent memories'], - people: peopleNames.length ? peopleNames : ['No people yet'] - }; - - } catch (error) { - console.error('๐Ÿ“Š Error getting vault context:', error); - return { - memoryCount: 0, - recentTopics: ['Context unavailable'], - people: ['Context unavailable'] - }; - } - } - - /** - * ๐Ÿ” HANDLE MEMORY LIST REQUEST - * Shows user their existing memories when they ask "what are my memories" - */ - async handleMemoryListRequest(userMessage) { - console.log('๐Ÿ” MEMORY LIST: Processing request:', userMessage); - - try { - // Get memories from vault - if (!window.emmaWebVault || !window.emmaWebVault.isOpen) { - this.addMessage("I'd love to show you your memories! Please make sure your vault is unlocked first.", 'emma'); - return; - } - - const memories = await window.emmaWebVault.listMemories(10); // Get first 10 memories - - if (!memories || memories.length === 0) { - this.addMessage("You don't have any memories in your vault yet. Would you like to create your first memory together?", 'emma'); - return; - } - - // Generate a warm, personal response - const memoryCount = memories.length; - const totalMemories = Object.keys(window.emmaWebVault.vaultData?.content?.memories || {}).length; - - let response = `You have ${totalMemories} memory${totalMemories !== 1 ? 'ies' : ''} in your vault! `; - - if (totalMemories > 10) { - response += `Here are your ${memories.length} most recent ones:\n\n`; - } else { - response += `Here they are:\n\n`; - } - - // List recent memories with dates - memories.forEach((memory, index) => { - const title = memory.title || 'Untitled Memory'; - const date = new Date(memory.created || memory.timestamp || Date.now()).toLocaleDateString(); - response += `${index + 1}. **${title}** (${date})\n`; - - // Add a brief preview of the content if available - if (memory.content && memory.content.length > 0) { - const preview = memory.content.substring(0, 100); - response += ` ${preview}${memory.content.length > 100 ? '...' : ''}\n\n`; - } else { - response += '\n'; - } - }); - - if (totalMemories > 10) { - response += `\nYou can view all your memories in the constellation view or ask me to search for specific ones!`; - } - - this.addMessage(response, 'emma'); - console.log('โœ… MEMORY LIST: Successfully displayed memories'); - - } catch (error) { - console.error('โŒ MEMORY LIST ERROR:', error); - this.addMessage("I had trouble accessing your memories right now. Please try again in a moment.", 'emma'); - } - } - - /** - * ๐Ÿ›๏ธ HANDLE CORE VAULT OPERATIONS - * Emma's primary job - managing memories and people + * ๐Ÿ›๏ธ HANDLE CORE VAULT OPERATIONS + * Emma's primary job - managing memories and people */ async handleVaultOperation(userMessage, intent) { console.log('๐Ÿ›๏ธ Processing vault operation:', userMessage); @@ -2932,37 +2832,38 @@ RULES: if (!window.emmaWebVault.vaultData.content.people) { window.emmaWebVault.vaultData.content.people = {}; } - + window.emmaWebVault.vaultData.content.people[newPerson.id] = newPerson; console.log('๐Ÿ‘ค Added to vault data structure'); - + // Trigger save await window.emmaWebVault.scheduleElegantSave(); console.log('๐Ÿ‘ค Vault save scheduled'); - + // ๐Ÿ’œ DEMENTIA-FRIENDLY SUCCESS MESSAGE this.addMessage(`โœ… Perfect! I've added ${personName} to your vault.`, 'emma'); this.addVaultOperationIndicator(); // Show that this was a core vault operation console.log('โœ… PERSON ADDED SUCCESSFULLY:', personName); - + // ๐ŸŒŸ CRITICAL: Refresh constellation after adding new person setTimeout(() => { this.refreshConstellationAfterPersonAdd(); }, 500); - + // ๐Ÿค” GENTLE FOLLOW-UP CONVERSATION setTimeout(() => { this.startPersonEnrichmentFlow(personName); }, 1500); - + + saved = true; } else { - console.error('โŒ Vault not available:', { - hasWebVault: !!window.emmaWebVault, - hasVaultData: !!window.emmaWebVault?.vaultData + console.error('โŒ Vault not available:', { + hasWebVault: !!window.emmaWebVault, + hasVaultData: !!window.emmaWebVault?.vaultData }); this.addMessage("I'm having trouble accessing your vault right now. Please make sure it's unlocked and try again.", 'emma'); } - + } catch (error) { console.error('โŒ PERSON ADD ERROR:', error); this.addMessage(`I had trouble adding ${personName} to your vault. Let me try again in a moment.`, 'emma'); @@ -3005,7 +2906,7 @@ RULES: */ async startPersonEnrichmentFlow(personName) { console.log('๐Ÿค” Starting person enrichment flow for:', personName); - + // ๐Ÿ’œ GENTLE, NON-OVERWHELMING QUESTIONS const followUpQuestions = [ `Tell me a little about ${personName}. How do you know them?`, @@ -3013,10 +2914,10 @@ RULES: `Would you like to tell me how ${personName} is important to you?`, `Is there anything particular about ${personName} that makes you smile?` ]; - + // Pick a random gentle question const question = followUpQuestions[Math.floor(Math.random() * followUpQuestions.length)]; - + // ๐Ÿ•ฐ๏ธ Give them time to process the success first setTimeout(() => { this.addMessage(question, 'emma'); @@ -3027,7 +2928,7 @@ RULES: stage: 'relationship', // relationship, details, memories, complete startedAt: Date.now() }; - + }, 2000); // Wait 2 seconds after success message } @@ -3037,23 +2938,23 @@ RULES: */ async handlePersonEnrichmentResponse(userMessage, personName) { console.log('๐Ÿค” Handling enrichment response for:', personName, userMessage); - + // ๐Ÿšซ CHECK FOR SKIP/DECLINE SIGNALS const skipPatterns = [ /\b(no|nah|skip|pass|not now|maybe later|that'?s enough|i'?m good|all set)\b/i, /\b(move on|next|done|finished|enough|nothing else)\b/i, /\b(don'?t want|not interested|not really)\b/i ]; - + const isSkipping = skipPatterns.some(pattern => pattern.test(userMessage.toLowerCase())); - + if (isSkipping) { console.log('๐Ÿค” User is skipping enrichment'); this.addMessage(`That's perfectly fine! ${personName} is safely in your vault. We can always add more details later.`, 'emma'); this.currentPersonEnrichment = null; // Clear the enrichment flow return true; } - + try { // Find the person in vault const vault = window.emmaWebVault?.vaultData?.content; @@ -3061,7 +2962,7 @@ RULES: console.error('โŒ No people section in vault'); return false; } - + // Find person by name let targetPerson = null; for (const [id, person] of Object.entries(vault.people)) { @@ -3075,7 +2976,7 @@ RULES: console.error('โŒ Person not found in vault:', personName); return false; } - + // ๐Ÿ’ ADD ENRICHMENT INFO if (!targetPerson.enrichment) { targetPerson.enrichment = { @@ -3085,13 +2986,13 @@ RULES: addedAt: new Date().toISOString() }; } - + const stage = this.currentPersonEnrichment?.stage || 'relationship'; - + if (stage === 'relationship') { targetPerson.enrichment.relationship = userMessage; targetPerson.notes = `${targetPerson.notes}\nRelationship: ${userMessage}`; - + // ๐Ÿ’ฌ ACKNOWLEDGING RESPONSE const acknowledgments = [ `That's wonderful! ${userMessage.includes('friend') ? 'Friends are so precious.' : 'Thank you for sharing that.'}`, @@ -3099,17 +3000,17 @@ RULES: `That's beautiful. ${personName} sounds like someone special.`, `Thank you for telling me about your connection with ${personName}.` ]; - + const ack = acknowledgments[Math.floor(Math.random() * acknowledgments.length)]; this.addMessage(ack, 'emma'); - + // ๐ŸŽฏ OPTIONAL FOLLOW-UP setTimeout(() => { this.addMessage(`Is there anything else you'd like me to remember about ${personName}? Or we can move on to something else - whatever feels right for you.`, 'emma'); - + this.currentPersonEnrichment.stage = 'complete'; }, 2500); - + } else { // Additional details if (!targetPerson.enrichment.details) { @@ -3117,19 +3018,19 @@ RULES: } else { targetPerson.enrichment.details += `\n${userMessage}`; } - + targetPerson.notes += `\nAdditional: ${userMessage}`; - + this.addMessage(`Thank you for sharing that about ${personName}. I'll keep that in your vault.`, 'emma'); this.currentPersonEnrichment.stage = 'complete'; } - + // ๐Ÿ’พ SAVE ENRICHMENT await window.emmaWebVault.scheduleElegantSave(); console.log('โœ… Person enrichment saved:', targetPerson.enrichment); - + return true; - + } catch (error) { console.error('โŒ Error handling person enrichment:', error); return false; @@ -3540,6 +3441,12 @@ RULES: } } break; + + case 'displayMemoryResults': + if (analysis.memories) { + this.displayMemoryResults(analysis.memories); + } + break; } } } @@ -3547,7 +3454,7 @@ RULES: // ๐Ÿ’ CRITICAL: Update conversation context for continuity updateConversationContext(userMessage) { // Track conversation flow - this.conversationContext.conversationFlow.push({ + this.unifiedIntelligence.conversationContext.conversationFlow.push({ timestamp: Date.now(), message: userMessage, detected: { @@ -3558,37 +3465,37 @@ RULES: }); // Keep only last 5 exchanges for context - if (this.conversationContext.conversationFlow.length > 5) { - this.conversationContext.conversationFlow.shift(); + if (this.unifiedIntelligence.conversationContext.conversationFlow.length > 5) { + this.unifiedIntelligence.conversationContext.conversationFlow.shift(); } // Update people mentioned const people = this.extractPeople(userMessage); - people.forEach(person => this.conversationContext.recentPeople.add(person)); + people.forEach(person => this.unifiedIntelligence.conversationContext.recentPeople.add(person)); // ๐Ÿ’œ CTO ENHANCEMENT: Enhanced context tracking for memory companion - this.conversationContext.currentTopic = this.extractTopic(userMessage); - this.conversationContext.lastUserIntent = people.length > 0 ? 'discussing_person' : 'general'; + this.unifiedIntelligence.conversationContext.currentTopic = this.extractTopic(userMessage); + this.unifiedIntelligence.conversationContext.lastUserIntent = people.length > 0 ? 'discussing_person' : 'general'; // ๐ŸŽฏ CTO CRITICAL: Track the MAIN SUBJECT of user queries (not just mentioned people) if (people.length > 0) { - this.conversationContext.lastQueriedPerson = people[0]; // The person user specifically asked about + this.unifiedIntelligence.conversationContext.lastQueriedPerson = people[0]; // The person user specifically asked about } console.log('๐Ÿ’ CTO CONTEXT UPDATE:', { userMessage, extractedPeople: people, - recentPeople: Array.from(this.conversationContext.recentPeople), - currentTopic: this.conversationContext.currentTopic, - lastIntent: this.conversationContext.lastUserIntent, - conversationFlow: this.conversationContext.conversationFlow.length + recentPeople: Array.from(this.unifiedIntelligence.conversationContext.recentPeople), + currentTopic: this.unifiedIntelligence.conversationContext.currentTopic, + lastIntent: this.unifiedIntelligence.conversationContext.lastUserIntent, + conversationFlow: this.unifiedIntelligence.conversationContext.conversationFlow.length }); } // ๐Ÿง  DEMENTIA-AWARE RESPONSES: Always validate, never contradict generateDementiaAwareResponse(userMessage) { const lower = userMessage.toLowerCase(); - const context = this.conversationContext; + const context = this.unifiedIntelligence.conversationContext; // Dynamically detect people from vault and respond appropriately const mentionedPeople = this.extractPeople(userMessage); @@ -3920,9 +3827,9 @@ RULES: // Create a photo preview message const photoPreview = ` -
+
-
+
๐Ÿ“ท Photo with ${personName} - Ready to save!
@@ -4741,6 +4648,10 @@ RULES: * DYNAMIC & PERSONAL: Different greetings based on time, vault content, recent activity */ async addInitialWelcomeMessage() { + if (this.remoteGreetingReceived || this.localWelcomeShown) { + return; + } + this.localWelcomeShown = true; // Get current session context const hour = new Date().getHours(); @@ -4791,6 +4702,18 @@ RULES: console.log('๐Ÿ’ฌ UNIQUE WELCOME GENERATED:', welcomeMessage.substring(0, 50) + '...'); } + scheduleInitialWelcome() { + if (this.initialWelcomeTimeout || this.localWelcomeShown) { + return; + } + this.initialWelcomeTimeout = setTimeout(() => { + this.initialWelcomeTimeout = null; + if (!this.remoteGreetingReceived) { + this.addInitialWelcomeMessage(); + } + }, 1200); + } + // Chat settings modal removed - access via main settings panel /** @@ -4922,6 +4845,8 @@ RULES: this.debugMode = debugMode; this.dementiaMode = dementiaMode; + this.syncVoiceApiKey(false); + // Reinitialize vectorless engine if API key changed if (apiKey) { this.initializeVectorlessEngine(); @@ -5027,6 +4952,8 @@ RULES: this.dementiaMode = newDementiaMode; this.debugMode = newDebugMode; + this.syncVoiceApiKey(false); + // Save to localStorage const settings = { apiKey: this.apiKey, @@ -5084,37 +5011,87 @@ RULES: */ loadVectorlessSettings() { try { - // ๐Ÿš€ DEMO SECURITY: API keys disabled for family demo - - // 1. SECURITY LOCKDOWN: No API key access for demo safety - this.apiKey = null; // DEMO MODE: API keys disabled for security - - // 2. Check old vectorless settings format (backup) - if (!this.apiKey) { - const stored = localStorage.getItem('emma-vectorless-settings'); - if (stored) { - const settings = JSON.parse(stored); - this.apiKey = settings.apiKey || null; + const normalizeApiKey = (value) => { + if (!value || typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed.length ? trimmed : null; + }; + + this.apiKey = null; + let legacySettings = null; + + // 1. Primary source: redesigned settings page + const generalSettingsRaw = localStorage.getItem('emmaSettings'); + if (generalSettingsRaw) { + try { + const generalSettings = JSON.parse(generalSettingsRaw); + const modernKey = normalizeApiKey(generalSettings?.apiKey); + if (modernKey) { + this.apiKey = modernKey; + } + } catch (settingsError) { + if (window.EMMA_DEBUG) { + console.warn('emmaSettings parse failed:', settingsError); + } } } - - // 3. Check global API key (backup) + + // 2. Legacy storage fallback (vectorless modal) + const legacyRaw = localStorage.getItem('emma-vectorless-settings'); + if (legacyRaw) { + try { + legacySettings = JSON.parse(legacyRaw); + if (!this.apiKey) { + const legacyKey = normalizeApiKey(legacySettings.apiKey); + if (legacyKey) { + this.apiKey = legacyKey; + } + } + } catch (legacyError) { + if (window.EMMA_DEBUG) { + console.warn('Legacy vectorless settings parse failed:', legacyError); + } + legacySettings = null; + } + } + + // 3. Global fallback for embedded demos or manual overrides if (!this.apiKey && window.API_KEY) { - this.apiKey = window.API_KEY; - console.log('๐Ÿ’ฌ Using global API key for Emma responses'); + const globalKey = normalizeApiKey(window.API_KEY); + if (globalKey) { + this.apiKey = globalKey; + console.log('๐Ÿ’ฌ Using global API key for Emma responses'); + } } - - // ๐Ÿ”ง Load other settings - this.dementiaMode = localStorage.getItem('emma-dementia-mode') === 'true'; - this.debugMode = localStorage.getItem('emma-debug-mode') === 'true'; - + + // 4. Feature toggles (persisted separately, with legacy fallback) + const dementiaFlag = localStorage.getItem('emma-dementia-mode'); + if (dementiaFlag !== null) { + this.dementiaMode = dementiaFlag === 'true'; + } else if (legacySettings && typeof legacySettings.dementiaMode === 'boolean') { + this.dementiaMode = legacySettings.dementiaMode; + } else { + this.dementiaMode = false; + } + + const debugFlag = localStorage.getItem('emma-debug-mode'); + if (debugFlag !== null) { + this.debugMode = debugFlag === 'true'; + } else if (legacySettings && typeof legacySettings.debugMode === 'boolean') { + this.debugMode = legacySettings.debugMode; + } else { + this.debugMode = false; + } + + this.syncVoiceApiKey(true); + // ๐ŸŽฏ LOG RESULTS if (this.apiKey) { console.log('โœ… OpenAI API key found and loaded successfully!'); } else { console.warn('๐Ÿ’ฌ No API key found - Emma will use intelligent fallbacks'); } - + } catch (error) { console.warn('๐Ÿ’ฌ Could not load vectorless settings:', error); } @@ -5241,7 +5218,7 @@ RULES: try { // Generate dynamic response for media requests const response = await this.generateDynamicEmmaResponse(`The user wants to save photos/media: "${message}"`); - await this.addMessage(response || "I'd love to help you save those photos!", 'emma', null, 'response'); + await this.addMessage(response || "I'd love to help you save those photos!", 'emma', {}); // Create a basic memory from the request const memory = { @@ -5269,8 +5246,36 @@ RULES: } catch (error) { console.error('โŒ Error handling media request:', error); - await this.addMessage("I'd love to help you save those photos! Let me set that up for you.", 'emma', null, 'response'); + await this.addMessage("I'd love to help you save those photos! Let me set that up for you.", 'emma', {}); + } + } + + /** + * Classify user intent for message routing + */ + classifyUserIntent(message) { + const lowerMsg = message.toLowerCase().trim(); + + // People list queries + if (/\b(show|list|display|get|who are)\s+(all\s+)?(my\s+)?(people|persons|contacts|friends|family)\b/i.test(lowerMsg) || + /\b(all\s+)?(my\s+)?people\s+(list|I\s+know)\b/i.test(lowerMsg)) { + return { type: 'people_list', confidence: 0.9 }; + } + + // Memory search queries + if (/\b(show|find|search|display|get|list)\s+(me\s+)?(all\s+)?(my\s+)?memor(y|ies)\b/i.test(lowerMsg) || + /\bwhat\s+memor(y|ies)\b/i.test(lowerMsg) || + /\bdo\s+I\s+have.*memor(y|ies)\b/i.test(lowerMsg)) { + return { type: 'memory_search', confidence: 0.9 }; + } + + // Person inquiry + if (/\b(who\s+is|tell\s+me\s+about|what\s+about|show\s+me)\s+\w+/i.test(lowerMsg)) { + return { type: 'person_inquiry', confidence: 0.7 }; } + + // General conversation + return { type: 'general', confidence: 0.5 }; } /** @@ -5458,7 +5463,11 @@ RULES: return; } - // Show memory preview dialog + const autoSaved = await this.tryAutoSaveMemory(analysis.memory, analysis.memory?.id, 'chat-detected'); + if (autoSaved) { + return; + } + this.showMemoryPreviewDialog(analysis.memory); } @@ -5495,6 +5504,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 +5518,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 +5536,7 @@ RULES: .filter(att => att.type?.startsWith('image/')) .slice(0, 5) .map((image, index) => ` -
+
`).join('')} @@ -5547,8 +5557,8 @@ RULES: ${peopleList.length > 0 ? `
-

๐Ÿ‘ฅ People in this memory

-
+

People in this memory

+
@@ -5557,15 +5567,15 @@ RULES:
-

${memory.content}

+

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

${memory.metadata?.emotions?.length > 0 ? `
- ๐Ÿ’ญ + Emotions
${memory.metadata.emotions.map(emotion => ` - ${emotion} + ${esc(emotion)} `).join('')}
@@ -5573,8 +5583,8 @@ RULES: ${memory.metadata?.location ? `
- ๐Ÿ“ - ${memory.metadata.location} + Location + ${esc(memory.metadata.location)}
` : ''}
@@ -5582,20 +5592,20 @@ RULES: ${memory.attachments?.length > 1 || hasVideo ? `
-

๐Ÿ“ท All Media (${memory.attachments.length})

+

All Media (${memory.attachments.length})

${memory.attachments.map((attachment, index) => `
${attachment.type?.startsWith('image/') ? ` - ${attachment.name} + ${esc(attachment.name || '')} ` : attachment.type?.startsWith('video/') ? ` -
@@ -5633,7 +5643,7 @@ RULES: } .responsive-memory-container { - background: linear-gradient(135deg, rgba(139, 92, 246, 0.95) 0%, rgba(124, 58, 237, 0.95) 50%, rgba(109, 40, 217, 0.95) 100%); + background: linear-gradient(135deg, rgba(111, 99, 217, 0.95) 0%, rgba(124, 58, 237, 0.95) 50%, rgba(109, 40, 217, 0.95) 100%); border-radius: clamp(16px, 3vw, 24px); max-width: 95vw; max-height: 95vh; @@ -5814,7 +5824,7 @@ RULES: border: 3px solid rgba(255, 255, 255, 0.9); overflow: hidden; position: relative; - background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); + background: linear-gradient(135deg, #6f63d9 0%, #d06fa8 100%); display: flex; flex-direction: column; align-items: center; @@ -5825,13 +5835,13 @@ RULES: transition: all 0.3s ease; cursor: pointer; text-align: center; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); + box-shadow: 0 4px 12px rgba(111, 99, 217, 0.3); } .memory-person-avatar:hover { transform: scale(1.05); border-color: white; - box-shadow: 0 6px 20px rgba(139, 92, 246, 0.5); + box-shadow: 0 6px 20px rgba(111, 99, 217, 0.5); } .memory-person-avatar img { @@ -5951,7 +5961,7 @@ RULES: .action-btn.primary { background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); - color: #8b5cf6; + color: #6f63d9; border: 2px solid rgba(255, 255, 255, 0.3); } @@ -5995,7 +6005,7 @@ RULES: .filter(att => att.type?.startsWith('image/')) .slice(0, 5) .map((image, index) => ` -
+
`).join('')} @@ -6016,8 +6026,8 @@ RULES: ${peopleList.length > 0 ? `
-

๐Ÿ‘ฅ People in this memory

-
+

People in this memory

+
@@ -6026,15 +6036,15 @@ RULES:
-

${memory.content}

+

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

${memory.metadata?.emotions?.length > 0 ? `
- ๐Ÿ’ญ + Emotions
${memory.metadata.emotions.map(emotion => ` - ${emotion} + ${esc(emotion)} `).join('')}
@@ -6042,8 +6052,8 @@ RULES: ${memory.metadata?.location ? `
- ๐Ÿ“ - ${memory.metadata.location} + Location + ${esc(memory.metadata.location)}
` : ''}
@@ -6051,7 +6061,7 @@ RULES: ${memory.attachments?.length > 1 || hasVideo ? `
-

๐Ÿ“ท All Media (${memory.attachments.length})

+

All Media (${memory.attachments.length})

${memory.attachments.map((attachment, index) => `
@@ -6059,11 +6069,11 @@ RULES: ${attachment.name} ` : attachment.type?.startsWith('video/') ? ` ` : `
-
${attachment.type?.startsWith('audio/') ? '๐ŸŽต' : '๐Ÿ“„'}
+
${attachment.type?.startsWith('audio/') ? 'Audio' : 'File'}
${attachment.name}
`} @@ -6076,10 +6086,10 @@ RULES:
@@ -6261,7 +6271,7 @@ RULES: - - - -
-
-
- - - - - -
-

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..038376df 100644 --- a/pages/emma-settings-redesigned.html +++ b/pages/emma-settings-redesigned.html @@ -4,30 +4,31 @@ Emma Settings - Your Intelligent Memory Companion + + - - - + + + + + + - - - - - +
- - - - - - Back to Emma - - -
-

emma

-

Configure your intelligent memory companion

-
- - -
-

- ๐Ÿง  Artificial Intelligence -

-

- Emma's AI works beautifully offline with intelligent heuristics. For even more advanced understanding and nuanced conversations, connect your OpenAI API key to unlock Emma's full potential. -

- -
-
- OpenAI API Key - Enable Emma's most advanced AI responses with your OpenAI API key. Emma works great offline with smart heuristics, but an API key unlocks deeper understanding and more nuanced conversations. -
- -
- -
-
- Memory Detection - Emma automatically recognizes when you're sharing meaningful memories and gently suggests saving them to your vault. -
- -
- -
-
- People Recognition - Emma identifies names mentioned in conversations and connects them to people in your vault or suggests adding new ones. +
+
- -
-
- Vectorless AI Status - Real-time status of Emma's intelligent memory processing system. -
-
-
- Initializing... -
-
-
- -
-

- ๐Ÿ’œ Memory & Care -

-

- Specialized features for memory preservation and dementia care support. -

- -
-
- Dementia Care Mode - Enables validation therapy principles and specialized responses for users with memory impairment. Designed with love for Debbe. -
- -
- -
-
- Memory Auto-Capture - Automatically detect and suggest capturing meaningful moments during conversations. +
+ Experience Tip +

Backgrounds you choose now ripple through the vault and modal surfaces so every interaction stays on-theme.

- -
-
- -
-

- ๐Ÿ”’ Privacy & Security -

-

- Emma prioritizes your privacy with local-only processing and secure vault encryption. -

- -
-
- Local Processing - All memory analysis happens on your device. No data is sent to external servers. + + +
+

Emma v1.0 Beta

+

Need guidance? support@emma.ai

-
-
- Always Enabled -
-
- -
-
- Vault Encryption - Your .emma files are protected with AES-256 encryption and PBKDF2 key derivation. -
-
-
- Always Enabled -
-
-
+ + +
+ +
+
+

Visual Themes

+

+ Preview different Emma looks and moods. Themes update instantly so you can find the palette that feels just right, and you can choose a signature background to match. +

+
+
+ +
+ + + + + + + + +
+
+
+ + +
+ +
+ +
+

Artificial Intelligence

+

+ Emma's AI works beautifully offline with intelligent heuristics. For even more advanced understanding and nuanced conversations, connect your OpenAI API key to unlock Emma's full potential. +

+
+
- -
-

- โค๏ธ Dementia Care & Accessibility -

-

- Specialized features designed for users with memory impairment and their caregivers. Emma adapts to provide gentle, supportive interactions. -

- -
-
- Font Size - Adjust text size throughout Emma for better readability. Larger fonts reduce eye strain and improve accessibility. -
-
- - 100% - - -
-
- -
-
- Reduced Motion - Minimize animations and transitions to reduce disorientation. Helpful for users sensitive to visual motion. -
- -
- -
-
- High Contrast Mode - Increase color contrast for better visibility. Makes text and interface elements easier to distinguish. -
- -
- -
-
- Gentle Reminders - Emma uses encouraging language and avoids correcting or contradicting. Based on validation therapy principles. -
-
-
- Always Enabled -
-
- -
-
- Repetition Tolerance - Emma patiently handles repeated questions without highlighting repetition. Maintains dignity and reduces anxiety. -
-
-
- Always Enabled -
-
- -
-
- Response Pacing - Emma waits 2-3 seconds before responding to reduce pressure and allow processing time. -
-
-
- Always Enabled -
-
-
+
+
+
+ OpenAI API Key + Enable Emma's most advanced AI responses with your OpenAI API key. Emma works great offline with smart heuristics, but an API key unlocks deeper understanding and more nuanced conversations. +
+ +
+ +
+
+ Memory Detection + Emma automatically recognizes when you're sharing meaningful memories and gently suggests saving them to your vault. +
+ +
+ +
+
+ People Recognition + Emma identifies names mentioned in conversations and connects them to people in your vault or suggests adding new ones. +
+ +
+ +
+
+ Vectorless AI Status + Real-time status of Emma's intelligent memory processing system. +
+
+
+ Initializing... +
+
+
+
+ + +
+
+

Memory & Care

+

+ Specialized features for memory preservation and dementia care support. +

+
+
+
+
+ Dementia Care Mode + Enables validation therapy principles and specialized responses for users with memory impairment. Designed with love for Debbe. +
+ +
+ +
+
+ Memory Auto-Capture + Automatically detect and suggest capturing meaningful moments during conversations. +
+ +
+
+
+ + +
+
+

Privacy & Security

+

+ Emma prioritizes your privacy with local-only processing and secure vault encryption. +

+
+
+
+
+ Local Processing + All memory analysis happens on your device. No data is sent to external servers. +
+
+
+ Always Enabled +
+
+ +
+
+ Vault Encryption + Your .emma files are protected with AES-256 encryption and PBKDF2 key derivation. +
+
+
+ Always Enabled +
+
+ +
+
+ Vault Auto-Save + Automatically write updates to your .emma vault whenever memories or people change. +
+ +
+
+
+ + +
+
+

Dementia Care & Accessibility

+

+ Specialized features designed for users with memory impairment and their caregivers. Emma adapts to provide gentle, supportive interactions. +

+
+
+
+
+ Font Size + Adjust text size throughout Emma for better readability. Larger fonts reduce eye strain and improve accessibility. +
+
+ + 100% + + +
+
+ +
+
+ Reduced Motion + Minimize animations and transitions to reduce disorientation. Helpful for users sensitive to visual motion. +
+ +
+ +
+
+ High Contrast Mode + Increase color contrast for better visibility. Makes text and interface elements easier to distinguish. +
+ +
+ +
+
+ Gentle Reminders + Emma uses encouraging language and avoids correcting or contradicting. Based on validation therapy principles. +
+
+
+ Always Enabled +
+
+ +
+
+ Repetition Tolerance + Emma patiently handles repeated questions without highlighting repetition. Maintains dignity and reduces anxiety. +
+
+
+ Always Enabled +
+
+ +
+
+ Response Pacing + Emma waits 2-3 seconds before responding to reduce pressure and allow processing time. +
+
+
+ Always Enabled +
+
+
+
+ + +
+
+

System Information

+

+ Basic system information and compatibility status. +

+
+
+
+
+ Emma Version + Current version of Emma and compatibility information. +
+
+
+ v1.0 Beta +
+
+
+
- -
-

- โ„น๏ธ System Information -

-

- Basic system information and compatibility status. -

- -
-
- Emma Version - Current version of Emma and compatibility information. -
-
-
- v1.0 Beta +
+
-
+
- - -
- @@ -1219,7 +2204,7 @@

-

๐Ÿ”‘ OpenAI API Key Setup

+

OpenAI API Key Setup

@@ -1238,7 +2223,7 @@

๐Ÿ”‘ OpenAI API Key Setup

rows="3" >
- ๐Ÿ’ก Your API key should start with "sk-proj-" or "sk-" + Note: Your API key should start with "sk-proj-" or "sk-"
@@ -1251,5 +2236,17 @@

๐Ÿ”‘ OpenAI API Key Setup

+ + + + + + + + + + diff --git a/pages/gallery.html b/pages/gallery.html index dcaf805d..016ffda1 100644 --- a/pages/gallery.html +++ b/pages/gallery.html @@ -15,11 +15,11 @@ @@ -1509,6 +1660,184 @@

MTAP Protocol Active

+ +
+

Visual Themes

+

Choose a theme and background to match your style. Themes update instantly so you can preview before you commit.

+ +
+ + + + + + + + +
+

โœจ Choose Your Emma

@@ -1896,7 +2225,7 @@

Migration Results

+
๐ŸŽจ Visual Style
-
+
-
+
+
- +
@@ -2292,27 +2621,30 @@

๐Ÿ”’ Privacy & Data

transition: all 0.3s ease; z-index: 1000; " onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'"> - ๐Ÿ  + ๐Ÿ€ - - - - + + + + - - - - - - - - + + + + + + + + + + + - - + - \ No newline at end of file + + + diff --git a/pages/people-emma.html b/pages/people-emma.html index b7332ecd..46105777 100644 --- a/pages/people-emma.html +++ b/pages/people-emma.html @@ -1,9 +1,10 @@ - +๏ปฟ Emma โ€ข People + + -
- -
-
-
-

People

-

Manage your family, friends, and connections

-
- -
- - -
-
-
- - - - - -
- -
-
-
- - -
-
-
๐Ÿ‘ฅ
-
-

Total People

-

0

-
-
-
-
โค๏ธ
-
-

Family & Close

-

0

-
-
-
-
๐Ÿ’ผ
-
-

Colleagues

-

0

-
-
-
- - -
- -
- - -
-
-
- - - -
- - -
- -
- - - -
- - - - -
-
-
๐Ÿ“…
-
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...

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