Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover"> | |
| <title>K1RL QUASAR — Quantitative Intelligence Observatory</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <!-- ✅ MIGRATED: Ably removed — now using Redis-backed HTTP polling via HF Spaces API --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> | |
| <style> | |
| /* ============ RESET & VARIABLES ============ */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| :root { | |
| /* Base font - DESKTOP FIRST */ | |
| font-size: 16px; | |
| /* ===== TYPOGRAPHY SCALE ===== */ | |
| --text-xs: 0.75rem; | |
| --text-sm: 0.875rem; | |
| --text-base: 1rem; | |
| --text-md: 1.25rem; | |
| --text-lg: 1.5rem; | |
| --text-xl: 2rem; | |
| --text-2xl: 2.5rem; | |
| --text-3xl: 3.5rem; | |
| --text-4xl: 5rem; | |
| --text-hero: 6rem; | |
| /* Font weights */ | |
| --weight-light: 200; | |
| --weight-regular: 400; | |
| --weight-medium: 500; | |
| --weight-semibold: 600; | |
| --weight-bold: 700; | |
| /* Letter spacing */ | |
| --tracking-tight: -0.02em; | |
| --tracking-normal: 0; | |
| --tracking-wide: 0.05em; | |
| --tracking-wider: 0.1em; | |
| --tracking-widest: 0.2em; | |
| --tracking-ultra: 0.4em; | |
| /* Line heights */ | |
| --leading-none: 1; | |
| --leading-tight: 1.1; | |
| --leading-snug: 1.2; | |
| --leading-normal: 1.4; | |
| --leading-relaxed: 1.6; | |
| /* Colors */ | |
| --bg-deep: #000810; | |
| --bg-space: #001020; | |
| --accent-cyan: #00d4ff; | |
| --accent-cyan-dim: rgba(0, 212, 255, 0.3); | |
| --accent-cyan-glow: rgba(0, 212, 255, 0.15); | |
| --success: #00ff88; | |
| --danger: #ff4466; | |
| --warning: #ffaa00; | |
| --text-primary: #ffffff; | |
| --text-secondary: rgba(255, 255, 255, 0.7); | |
| --text-muted: rgba(255, 255, 255, 0.5); | |
| --text-dim: rgba(255, 255, 255, 0.35); | |
| --glass-bg: rgba(0, 20, 40, 0.4); | |
| --glass-border: rgba(0, 212, 255, 0.2); | |
| } | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background: var(--bg-deep); | |
| color: var(--text-primary); | |
| line-height: var(--leading-normal); | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| width: 100%; | |
| max-width: 100vw; | |
| overflow-x: hidden; | |
| } | |
| /* ============ TYPOGRAPHY UTILITIES ============ */ | |
| .font-mono { font-family: 'Space Mono', monospace; } | |
| .tracking-tight { letter-spacing: var(--tracking-tight); } | |
| .tracking-normal { letter-spacing: var(--tracking-normal); } | |
| .tracking-wide { letter-spacing: var(--tracking-wide); } | |
| .tracking-wider { letter-spacing: var(--tracking-wider); } | |
| .tracking-widest { letter-spacing: var(--tracking-widest); } | |
| .tracking-ultra { letter-spacing: var(--tracking-ultra); } | |
| .text-xs { font-size: var(--text-xs); } | |
| .text-sm { font-size: var(--text-sm); } | |
| .text-base { font-size: var(--text-base); } | |
| .text-md { font-size: var(--text-md); } | |
| .text-lg { font-size: var(--text-lg); } | |
| .text-xl { font-size: var(--text-xl); } | |
| .text-2xl { font-size: var(--text-2xl); } | |
| .text-3xl { font-size: var(--text-3xl); } | |
| .text-4xl { font-size: var(--text-4xl); } | |
| .text-hero { font-size: var(--text-hero); } | |
| .weight-light { font-weight: var(--weight-light); } | |
| .weight-regular { font-weight: var(--weight-regular); } | |
| .weight-medium { font-weight: var(--weight-medium); } | |
| .weight-semibold { font-weight: var(--weight-semibold); } | |
| .weight-bold { font-weight: var(--weight-bold); } | |
| /* ============ THREE.JS BACKGROUND ============ */ | |
| #canvas-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 0; | |
| pointer-events: none; | |
| } | |
| .gradient-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(ellipse at 50% 100%, transparent 0%, rgba(0,8,16,0.7) 50%, rgba(0,8,16,0.95) 100%); | |
| z-index: 1; | |
| pointer-events: none; | |
| } | |
| /* ============ LAYOUT - DESKTOP FIRST ============ */ | |
| .main-content { | |
| position: relative; | |
| z-index: 10; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 100px 40px 40px; | |
| width: 100%; | |
| } | |
| /* ============ NAVIGATION ============ */ | |
| .nav-bar { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 20px 40px; | |
| background: rgba(0,8,16,0.85); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid rgba(0,212,255,0.1); | |
| } | |
| .nav-logo { | |
| font-size: var(--text-lg); | |
| font-weight: var(--weight-light); | |
| letter-spacing: var(--tracking-widest); | |
| color: var(--text-primary); | |
| } | |
| .nav-logo span { | |
| color: var(--accent-cyan); | |
| font-weight: var(--weight-semibold); | |
| } | |
| .nav-links { | |
| display: flex; | |
| gap: 40px; | |
| list-style: none; | |
| } | |
| .nav-links a { | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| font-size: var(--text-sm); | |
| font-weight: var(--weight-medium); | |
| letter-spacing: var(--tracking-wider); | |
| transition: color 0.2s; | |
| } | |
| .nav-links a:hover { | |
| color: var(--accent-cyan); | |
| } | |
| /* ============ LIVE INDICATOR ============ */ | |
| .live-indicator { | |
| position: fixed; | |
| top: 20px; | |
| right: 40px; | |
| z-index: 101; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 20px; | |
| background: rgba(0, 255, 136, 0.08); | |
| border: 1px solid rgba(0, 255, 136, 0.2); | |
| border-radius: 100px; | |
| backdrop-filter: blur(8px); | |
| } | |
| .live-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--success); | |
| border-radius: 50%; | |
| animation: pulse 1.5s infinite; | |
| } | |
| .live-text { | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| letter-spacing: var(--tracking-wider); | |
| color: var(--success); | |
| font-family: 'Space Mono', monospace; | |
| } | |
| /* ============ HEADER ============ */ | |
| .header { | |
| text-align: center; | |
| margin-bottom: 32px; | |
| } | |
| .logo-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 20px; | |
| margin-bottom: 16px; | |
| } | |
| .logo-icon { | |
| width: 56px; | |
| height: 56px; | |
| position: relative; | |
| } | |
| .logo-icon::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| border: 2px solid var(--accent-cyan); | |
| border-radius: 50%; | |
| animation: pulse-ring 2s infinite; | |
| } | |
| .logo-icon::after { | |
| content: ''; | |
| position: absolute; | |
| inset: 14px; | |
| background: var(--accent-cyan); | |
| border-radius: 50%; | |
| box-shadow: 0 0 30px var(--accent-cyan); | |
| } | |
| .logo-text { | |
| font-size: var(--text-hero); | |
| font-weight: var(--weight-light); | |
| letter-spacing: var(--tracking-ultra); | |
| color: var(--text-primary); | |
| text-shadow: 0 0 40px var(--accent-cyan-dim); | |
| line-height: var(--leading-none); | |
| } | |
| .logo-text span { | |
| font-weight: var(--weight-bold); | |
| color: var(--accent-cyan); | |
| } | |
| .subtitle { | |
| font-size: var(--text-sm); | |
| font-weight: var(--weight-light); | |
| letter-spacing: var(--tracking-widest); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 8px; | |
| } | |
| .timestamp { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xs); | |
| color: var(--text-dim); | |
| letter-spacing: var(--tracking-wide); | |
| } | |
| .inst-bar { | |
| text-align: center; | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xs); | |
| letter-spacing: var(--tracking-wider); | |
| color: rgba(255,255,255,0.3); | |
| margin: 20px 0 32px; | |
| } | |
| /* ============ GLASS CARDS ============ */ | |
| .glass-card { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(16px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 24px; | |
| padding: 28px; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .glass-card:hover { | |
| border-color: var(--accent-cyan-dim); | |
| box-shadow: 0 20px 40px rgba(0, 212, 255, 0.1); | |
| } | |
| .card-title { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title::before { | |
| content: ''; | |
| width: 4px; | |
| height: 4px; | |
| background: var(--accent-cyan); | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px var(--accent-cyan); | |
| } | |
| .card-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* ============ SIGNAL PANEL ============ */ | |
| .signal-panel { | |
| text-align: center; | |
| padding: 40px; | |
| margin-bottom: 32px; | |
| } | |
| .signal-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-sm); | |
| font-weight: var(--weight-medium); | |
| letter-spacing: var(--tracking-widest); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 16px; | |
| } | |
| .signal-value { | |
| font-size: var(--text-4xl); | |
| font-weight: var(--weight-bold); | |
| letter-spacing: var(--tracking-wider); | |
| margin-bottom: 20px; | |
| line-height: var(--leading-tight); | |
| } | |
| .signal-buy { | |
| color: var(--success); | |
| text-shadow: 0 0 40px var(--success), 0 0 80px rgba(0, 255, 136, 0.2); | |
| } | |
| .signal-sell { | |
| color: var(--danger); | |
| text-shadow: 0 0 40px var(--danger), 0 0 80px rgba(255, 68, 102, 0.2); | |
| } | |
| .signal-neutral { | |
| color: var(--warning); | |
| text-shadow: 0 0 40px var(--warning), 0 0 80px rgba(255, 170, 0, 0.2); | |
| } | |
| .signal-stats { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-sm); | |
| color: var(--text-secondary); | |
| letter-spacing: var(--tracking-wide); | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 16px; | |
| margin-bottom: 20px; | |
| } | |
| /* ============ FORCE BALANCE - COMPLETE REDESIGN ============ */ | |
| .force-balance { | |
| margin-top: 32px; | |
| width: 100%; | |
| max-width: 600px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| padding: 0 20px; | |
| } | |
| /* Pressure Labels Row */ | |
| .pressure-labels { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 40px; | |
| margin-bottom: 16px; | |
| } | |
| .pressure-item { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .pressure-item.buy { | |
| align-items: flex-start; | |
| } | |
| .pressure-item.sell { | |
| align-items: flex-end; | |
| text-align: right; | |
| } | |
| .pressure-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 6px; | |
| } | |
| .pressure-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xl); | |
| font-weight: var(--weight-bold); | |
| letter-spacing: var(--tracking-wide); | |
| } | |
| .pressure-value.buy-value { | |
| color: #4fc3f7; | |
| text-shadow: 0 0 20px rgba(79, 195, 247, 0.3); | |
| } | |
| .pressure-value.sell-value { | |
| color: #ef5350; | |
| text-shadow: 0 0 20px rgba(239, 83, 80, 0.3); | |
| } | |
| /* Pressure Gauge Track */ | |
| .pressure-gauge { | |
| position: relative; | |
| margin: 24px 0; | |
| } | |
| .gauge-track { | |
| position: relative; | |
| height: 32px; | |
| background: linear-gradient(90deg, | |
| rgba(21, 101, 192, 0.15) 0%, | |
| rgba(0, 0, 0, 0.4) 50%, | |
| rgba(198, 40, 40, 0.15) 100%); | |
| border-radius: 8px; | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| overflow: visible; | |
| } | |
| .gauge-fill-buy { | |
| position: absolute; | |
| top: 2px; | |
| bottom: 2px; | |
| left: 2px; | |
| right: 50%; | |
| background: linear-gradient(90deg, #1565C0, #42A5F5); | |
| border-radius: 6px 0 0 6px; | |
| transform-origin: right center; | |
| transform: scaleX(0); | |
| transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .gauge-fill-sell { | |
| position: absolute; | |
| top: 2px; | |
| bottom: 2px; | |
| left: 50%; | |
| right: 2px; | |
| background: linear-gradient(90deg, #ef5350, #c62828); | |
| border-radius: 0 6px 6px 0; | |
| transform-origin: left center; | |
| transform: scaleX(0); | |
| transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Center Line */ | |
| .gauge-center { | |
| position: absolute; | |
| left: 50%; | |
| top: -6px; | |
| bottom: -6px; | |
| width: 2px; | |
| background: rgba(255, 255, 255, 0.3); | |
| transform: translateX(-50%); | |
| z-index: 5; | |
| } | |
| .gauge-center::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 8px; | |
| height: 8px; | |
| background: rgba(255, 255, 255, 0.4); | |
| border-radius: 50%; | |
| } | |
| .gauge-center::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 8px; | |
| height: 8px; | |
| background: rgba(255, 255, 255, 0.4); | |
| border-radius: 50%; | |
| } | |
| /* Position Marker */ | |
| .gauge-marker { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 20px; | |
| height: 20px; | |
| background: white; | |
| border: 3px solid rgba(0, 0, 0, 0.8); | |
| border-radius: 50%; | |
| z-index: 10; | |
| box-shadow: | |
| 0 0 20px rgba(255, 255, 255, 0.5), | |
| 0 2px 8px rgba(0, 0, 0, 0.4); | |
| transition: left 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .gauge-marker.buy-dominant { | |
| background: #4fc3f7; | |
| border-color: #1565C0; | |
| } | |
| .gauge-marker.sell-dominant { | |
| background: #ef5350; | |
| border-color: #c62828; | |
| } | |
| /* Scale Labels */ | |
| .gauge-scale { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 10px; | |
| padding: 0 4px; | |
| } | |
| .gauge-scale span { | |
| font-family: 'Space Mono', monospace; | |
| font-size: 0.65rem; | |
| color: rgba(255, 255, 255, 0.25); | |
| letter-spacing: 0.05em; | |
| } | |
| .gauge-scale span:nth-child(3) { | |
| color: rgba(255, 255, 255, 0.4); | |
| font-weight: 700; | |
| } | |
| /* Net Position */ | |
| .net-position { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| margin-top: 24px; | |
| padding-top: 20px; | |
| border-top: 1px solid rgba(255, 255, 255, 0.06); | |
| } | |
| .net-position-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| } | |
| .net-position-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xl); | |
| font-weight: var(--weight-bold); | |
| letter-spacing: var(--tracking-wide); | |
| min-width: 80px; | |
| text-align: center; | |
| } | |
| .net-position-value.positive { | |
| color: #4fc3f7; | |
| text-shadow: 0 0 15px rgba(79, 195, 247, 0.3); | |
| } | |
| .net-position-value.negative { | |
| color: #ef5350; | |
| text-shadow: 0 0 15px rgba(239, 83, 80, 0.3); | |
| } | |
| .net-position-value.neutral { | |
| color: var(--text-secondary); | |
| } | |
| /* ============ GRID LAYOUTS - IMPROVED ============ */ | |
| .grid-3 { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 28px; | |
| margin-bottom: 28px; | |
| } | |
| /* ============ METRIC ROWS - FILL SPACE ============ */ | |
| .metric-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| padding: 16px 0; | |
| border-bottom: 1px solid rgba(255,255,255,0.03); | |
| } | |
| .metric-row:last-child { | |
| border-bottom: none; | |
| padding-bottom: 0; | |
| } | |
| .metric-row:first-child { | |
| padding-top: 0; | |
| } | |
| .metric-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-sm); | |
| font-weight: var(--weight-regular); | |
| color: var(--text-secondary); | |
| letter-spacing: var(--tracking-wide); | |
| } | |
| .metric-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-base); | |
| font-weight: var(--weight-semibold); | |
| color: var(--text-primary); | |
| letter-spacing: var(--tracking-normal); | |
| } | |
| .metric-value.positive { color: var(--success); } | |
| .metric-value.warning { color: var(--warning); } | |
| .metric-value.negative { color: var(--danger); } | |
| /* ============ BIG NUMBERS ============ */ | |
| .big-number { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-2xl); | |
| font-weight: var(--weight-bold); | |
| text-align: center; | |
| color: var(--accent-cyan); | |
| text-shadow: 0 0 30px var(--accent-cyan-dim); | |
| margin: 20px 0; | |
| line-height: var(--leading-tight); | |
| letter-spacing: var(--tracking-wide); | |
| } | |
| /* ============ PROCESS STATUS - FILL SPACE ============ */ | |
| .process-status { | |
| text-align: center; | |
| padding: 24px; | |
| margin-top: auto; | |
| border-radius: 16px; | |
| } | |
| .process-ok { | |
| background: rgba(0,255,136,0.08); | |
| border: 1px solid rgba(0,255,136,0.15); | |
| } | |
| .process-warning { | |
| background: rgba(255,68,102,0.08); | |
| border: 1px solid rgba(255,68,102,0.15); | |
| } | |
| .process-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-medium); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 10px; | |
| } | |
| .process-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-2xl); | |
| font-weight: var(--weight-bold); | |
| line-height: var(--leading-tight); | |
| } | |
| .process-ok .process-value { color: var(--success); } | |
| .process-warning .process-value { color: var(--danger); } | |
| /* ============ STATUS BADGES ============ */ | |
| .status-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| border-radius: 100px; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| text-transform: uppercase; | |
| letter-spacing: var(--tracking-wider); | |
| } | |
| .status-active { | |
| background: rgba(0,255,136,0.1); | |
| color: var(--success); | |
| border: 1px solid rgba(0,255,136,0.2); | |
| } | |
| .status-unknown { | |
| background: rgba(255,170,0,0.1); | |
| color: #ffaa00; | |
| border: 1px solid rgba(255,170,0,0.3); | |
| } | |
| .status-inactive { | |
| background: rgba(255,68,102,0.1); | |
| color: var(--danger); | |
| border: 1px solid rgba(255,68,102,0.2); | |
| } | |
| .status-badge::before { | |
| content: ''; | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| } | |
| /* ============ PROGRESS BAR ============ */ | |
| .progress-container { | |
| margin-top: 16px; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 8px; | |
| background: rgba(255,255,255,0.06); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent-cyan), var(--success)); | |
| border-radius: 4px; | |
| transition: width 0.4s ease; | |
| } | |
| /* ============ LOG CONTAINER ============ */ | |
| .log-container { | |
| background: rgba(0,0,0,0.25); | |
| border-radius: 16px; | |
| padding: 20px; | |
| height: 240px; | |
| overflow-y: auto; | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xs); | |
| line-height: var(--leading-relaxed); | |
| border: 1px solid rgba(255,255,255,0.03); | |
| } | |
| .log-line { | |
| padding: 5px 0; | |
| word-break: break-all; | |
| color: var(--text-secondary); | |
| } | |
| .log-error { color: var(--danger); } | |
| .log-warning { color: var(--warning); } | |
| .log-info { color: var(--success); } | |
| /* ============ ACCURACY CHART ============ */ | |
| .accuracy-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| margin-bottom: 24px; | |
| } | |
| .accuracy-metrics { | |
| display: flex; | |
| gap: 32px; | |
| flex-wrap: wrap; | |
| } | |
| .accuracy-stat { | |
| text-align: center; | |
| } | |
| .accuracy-stat-label { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-medium); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 6px; | |
| } | |
| .accuracy-stat-value { | |
| font-family: 'Space Mono', monospace; | |
| font-size: var(--text-xl); | |
| font-weight: var(--weight-bold); | |
| } | |
| .accuracy-legend { | |
| display: flex; | |
| gap: 20px; | |
| align-items: center; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| color: var(--text-secondary); | |
| } | |
| .legend-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| } | |
| .legend-dot.current { background: var(--success); } | |
| .legend-dot.average { background: var(--accent-cyan); } | |
| .legend-dot.trend { background: var(--warning); } | |
| .accuracy-trend-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 16px; | |
| border-radius: 100px; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: var(--text-xs); | |
| font-weight: var(--weight-semibold); | |
| letter-spacing: var(--tracking-wider); | |
| text-transform: uppercase; | |
| } | |
| .accuracy-trend-badge.rising { | |
| background: rgba(0, 255, 136, 0.1); | |
| border: 1px solid rgba(0, 255, 136, 0.2); | |
| color: var(--success); | |
| } | |
| .accuracy-trend-badge.stable { | |
| background: rgba(0, 212, 255, 0.1); | |
| border: 1px solid rgba(0, 212, 255, 0.2); | |
| color: var(--accent-cyan); | |
| } | |
| .accuracy-trend-badge.declining { | |
| background: rgba(255, 68, 102, 0.1); | |
| border: 1px solid rgba(255, 68, 102, 0.2); | |
| color: var(--danger); | |
| } | |
| .trend-arrow { | |
| font-weight: var(--weight-bold); | |
| font-size: var(--text-sm); | |
| } | |
| .chart-container { | |
| height: 200px; | |
| width: 100%; | |
| margin-top: 16px; | |
| } | |
| /* ============ VISITOR BADGE ============ */ | |
| .visitor-badge { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 24px; | |
| background: rgba(0,20,30,0.9); | |
| border: 1px solid rgba(0,212,170,0.2); | |
| border-radius: 12px; | |
| padding: 12px 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| z-index: 200; | |
| backdrop-filter: blur(12px); | |
| } | |
| .visitor-dot { | |
| width: 10px; | |
| height: 10px; | |
| background: #00d4aa; | |
| border-radius: 50%; | |
| animation: pulse 2s infinite; | |
| } | |
| .visitor-count { | |
| color: #00d4aa; | |
| font-size: var(--text-base); | |
| font-weight: var(--weight-bold); | |
| font-family: 'Space Mono', monospace; | |
| } | |
| .visitor-label { | |
| font-family: 'Outfit', sans-serif; | |
| color: rgba(255,255,255,0.5); | |
| font-size: var(--text-xs); | |
| text-transform: uppercase; | |
| letter-spacing: var(--tracking-wider); | |
| } | |
| .visitor-total { | |
| color: rgba(255,255,255,0.3); | |
| font-size: 0.7rem; | |
| display: block; | |
| font-family: 'Space Mono', monospace; | |
| } | |
| /* ============ CONNECTION STATUS ============ */ | |
| .connection-status { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 24px; | |
| padding: 10px 20px; | |
| border-radius: 100px; | |
| font-size: var(--text-xs); | |
| font-family: 'Space Mono', monospace; | |
| backdrop-filter: blur(12px); | |
| z-index: 200; | |
| letter-spacing: var(--tracking-wide); | |
| } | |
| .connection-ok { | |
| background: rgba(0,255,136,0.1); | |
| border: 1px solid rgba(0,255,136,0.2); | |
| color: var(--success); | |
| } | |
| .connection-error { | |
| background: rgba(255,68,102,0.1); | |
| border: 1px solid rgba(255,68,102,0.2); | |
| color: var(--danger); | |
| } | |
| /* ============ LOADING OVERLAY ============ */ | |
| .loading-overlay { | |
| position: fixed; | |
| bottom: 90px; | |
| left: 24px; | |
| background: rgba(0,20,30,0.95); | |
| border: 1px solid rgba(0,212,255,0.2); | |
| border-radius: 12px; | |
| padding: 14px 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| z-index: 300; | |
| backdrop-filter: blur(12px); | |
| font-size: var(--text-sm); | |
| } | |
| .loading-spinner { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid rgba(0,212,255,0.2); | |
| border-top-color: var(--accent-cyan); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| .hidden { | |
| display: none ; | |
| } | |
| /* ============ FOOTER ============ */ | |
| .footer { | |
| text-align: center; | |
| padding: 50px 24px 24px; | |
| color: var(--text-dim); | |
| font-size: var(--text-xs); | |
| letter-spacing: var(--tracking-wider); | |
| } | |
| .footer p { | |
| margin-bottom: 6px; | |
| } | |
| /* ============ ANIMATIONS ============ */ | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.5; transform: scale(0.8); } | |
| } | |
| @keyframes pulse-ring { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 100% { transform: scale(1.5); opacity: 0; } | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .glass-card { | |
| animation: fadeInUp 0.5s ease forwards; | |
| } | |
| .grid-3 .glass-card:nth-child(1) { animation-delay: 0.1s; } | |
| .grid-3 .glass-card:nth-child(2) { animation-delay: 0.15s; } | |
| .grid-3 .glass-card:nth-child(3) { animation-delay: 0.2s; } | |
| /* ============ TABLET STYLES (1024px and below) ============ */ | |
| @media (max-width: 1024px) { | |
| .grid-3 { | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 24px; | |
| } | |
| } | |
| /* ============ MOBILE STYLES (768px and below) ============ */ | |
| @media (max-width: 768px) { | |
| :root { | |
| font-size: 14px; | |
| } | |
| .main-content { | |
| padding: 80px 16px 20px; | |
| } | |
| .nav-bar { | |
| padding: 14px 16px; | |
| } | |
| .nav-links { | |
| display: none; | |
| } | |
| .nav-logo { | |
| font-size: var(--text-base); | |
| letter-spacing: var(--tracking-wider); | |
| } | |
| .live-indicator { | |
| top: 14px; | |
| right: 16px; | |
| padding: 8px 14px; | |
| } | |
| .logo-icon { | |
| width: 40px; | |
| height: 40px; | |
| } | |
| .logo-icon::after { | |
| inset: 10px; | |
| } | |
| .logo-text { | |
| font-size: 3rem; | |
| letter-spacing: 0.25em; | |
| } | |
| .subtitle { | |
| font-size: 0.7rem; | |
| letter-spacing: 0.2em; | |
| } | |
| .grid-3 { | |
| grid-template-columns: 1fr; | |
| gap: 16px; | |
| } | |
| .glass-card { | |
| padding: 20px; | |
| border-radius: 20px; | |
| } | |
| .signal-panel { | |
| padding: 24px 16px; | |
| } | |
| .signal-value { | |
| font-size: var(--text-2xl); | |
| } | |
| .signal-stats { | |
| font-size: var(--text-xs); | |
| gap: 12px; | |
| } | |
| /* Force Balance Mobile */ | |
| .force-balance { | |
| padding: 0 10px; | |
| } | |
| .pressure-labels { | |
| gap: 20px; | |
| } | |
| .pressure-value { | |
| font-size: var(--text-lg); | |
| } | |
| .gauge-track { | |
| height: 28px; | |
| } | |
| .gauge-marker { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .net-position-value { | |
| font-size: var(--text-lg); | |
| } | |
| .accuracy-header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .accuracy-metrics { | |
| gap: 24px; | |
| } | |
| .accuracy-stat-value { | |
| font-size: var(--text-lg); | |
| } | |
| .big-number { | |
| font-size: var(--text-xl); | |
| } | |
| .visitor-badge { | |
| left: 16px; | |
| bottom: 16px; | |
| padding: 10px 16px; | |
| } | |
| .connection-status { | |
| right: 16px; | |
| bottom: 16px; | |
| padding: 8px 16px; | |
| } | |
| } | |
| /* ============ SMALL MOBILE (480px and below) ============ */ | |
| @media (max-width: 480px) { | |
| :root { | |
| font-size: 12px; | |
| } | |
| .logo-icon { | |
| width: 32px; | |
| height: 32px; | |
| } | |
| .logo-icon::after { | |
| inset: 8px; | |
| } | |
| .logo-text { | |
| font-size: 2.2rem; | |
| letter-spacing: 0.15em; | |
| } | |
| .subtitle { | |
| font-size: 0.6rem; | |
| letter-spacing: 0.15em; | |
| } | |
| .force-balance { | |
| margin-top: 20px; | |
| padding: 0 5px; | |
| } | |
| .pressure-labels { | |
| gap: 10px; | |
| } | |
| .pressure-label { | |
| font-size: 0.65rem; | |
| } | |
| .pressure-value { | |
| font-size: var(--text-md); | |
| } | |
| .gauge-track { | |
| height: 24px; | |
| } | |
| .gauge-scale span { | |
| font-size: 0.55rem; | |
| } | |
| .net-position { | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .accuracy-metrics { | |
| gap: 16px; | |
| } | |
| .accuracy-legend { | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| } | |
| /* ============ VERY SMALL MOBILE (360px and below) ============ */ | |
| @media (max-width: 360px) { | |
| :root { | |
| font-size: 11px; | |
| } | |
| } | |
| /* ============ HOVER STATES - DESKTOP ONLY ============ */ | |
| @media (hover: hover) and (pointer: fine) { | |
| .glass-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 24px 48px rgba(0, 212, 255, 0.12); | |
| } | |
| .nav-links a:hover { | |
| color: var(--accent-cyan); | |
| } | |
| } | |
| /* Disable hover effects on touch devices */ | |
| @media (hover: none) { | |
| .glass-card:hover { | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Three.js Canvas --> | |
| <div id="canvas-container"> | |
| <canvas id="three-canvas"></canvas> | |
| </div> | |
| <div class="gradient-overlay"></div> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-spinner"></div> | |
| <span id="loadingText">Initializing Observatory...</span> | |
| </div> | |
| <!-- Connection Status --> | |
| <div class="connection-status connection-ok" id="connectionStatus">● Connected</div> | |
| <!-- Visitor Counter --> | |
| <div class="visitor-badge"> | |
| <div class="visitor-dot"></div> | |
| <div> | |
| <span class="visitor-count" id="activeVisitors">—</span> | |
| <span class="visitor-label">Online</span> | |
| <span class="visitor-total" id="totalVisitors">— total</span> | |
| </div> | |
| </div> | |
| <!-- Navigation --> | |
| <nav class="nav-bar"> | |
| <div class="nav-logo">K1RL <span>QUASAR</span></div> | |
| <ul class="nav-links"> | |
| <li><a href="#dashboard">Dashboard</a></li> | |
| <li><a href="#metrics">Metrics</a></li> | |
| <li><a href="#system">System</a></li> | |
| </ul> | |
| </nav> | |
| <!-- Live Indicator --> | |
| <div class="live-indicator"> | |
| <div class="live-dot"></div> | |
| <span class="live-text">LIVE</span> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Header --> | |
| <header class="header" id="dashboard"> | |
| <div class="logo-container"> | |
| <div class="logo-icon"></div> | |
| <h1 class="logo-text">QU<span>A</span>SAR</h1> | |
| </div> | |
| <p class="subtitle">Quantitative Intelligence Observatory</p> | |
| <p class="timestamp" id="lastUpdate">Last Observation: Initializing...</p> | |
| <div class="inst-bar" id="instBar">VOLATILITY 75 INDEX │ DERIV</div> | |
| </header> | |
| <!-- SIGNAL PANEL --> | |
| <div class="glass-card signal-panel"> | |
| <div class="card-title">Market Directive — 10M Agent — Last 5 Minutes</div> | |
| <div class="signal-label">Dominant Signal</div> | |
| <div class="signal-value signal-neutral" id="dominantSignal">NEUTRAL</div> | |
| <div class="signal-stats" id="signalStats"> | |
| <span>BUY: 0 (0%)</span> • <span>SELL: 0 (0%)</span> • <span>TOTAL: 0</span> | |
| </div> | |
| <!-- FORCE BALANCE --> | |
| <div class="force-balance"> | |
| <!-- Pressure Labels --> | |
| <div class="pressure-labels"> | |
| <div class="pressure-item buy"> | |
| <span class="pressure-label">Buy Pressure</span> | |
| <span class="pressure-value buy-value" id="forceBuyValue">0.0%</span> | |
| </div> | |
| <div class="pressure-item sell"> | |
| <span class="pressure-label">Sell Pressure</span> | |
| <span class="pressure-value sell-value" id="forceSellValue">0.0%</span> | |
| </div> | |
| </div> | |
| <!-- Pressure Gauge --> | |
| <div class="pressure-gauge"> | |
| <div class="gauge-track"> | |
| <div class="gauge-fill-buy" id="gaugeFillBuy"></div> | |
| <div class="gauge-fill-sell" id="gaugeFillSell"></div> | |
| <div class="gauge-center"></div> | |
| <div class="gauge-marker" id="gaugeMarker"></div> | |
| </div> | |
| <div class="gauge-scale"> | |
| <span>100</span> | |
| <span>50</span> | |
| <span>0</span> | |
| <span>50</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <!-- Net Position --> | |
| <div class="net-position"> | |
| <span class="net-position-label">Net Position</span> | |
| <span class="net-position-value neutral" id="netPositionValue">0.0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TRAINING & REWARDS --> | |
| <div class="grid-3" id="metrics"> | |
| <!-- Training Progression --> | |
| <div class="glass-card"> | |
| <div class="card-title">Training Progression</div> | |
| <div class="card-content"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Iterations Completed</span> | |
| <span class="metric-value" id="trainingSteps" style="color: #00ff88;">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Actor Loss</span> | |
| <span class="metric-value" id="actorLoss" style="color: #00ff88;">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Critic Loss</span> | |
| <span class="metric-value" id="criticLoss">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">AVN Loss</span> | |
| <span class="metric-value" id="avnLoss">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">AVN Accuracy</span> | |
| <span class="metric-value" id="avnAccuracy" style="color: #00d4aa;">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Buffer Capacity</span> | |
| <span class="metric-value" id="bufferSize">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Reward Mechanisms --> | |
| <div class="glass-card"> | |
| <div class="card-title">Reward Mechanisms</div> | |
| <div class="card-content"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Matched Signals</span> | |
| <span class="metric-value positive" id="matchedRewards">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Unmatched Signals</span> | |
| <span class="metric-value warning" id="unmatchedRewards">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Duplicate Entries</span> | |
| <span class="metric-value" id="duplicates">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Match Efficacy</span> | |
| <span class="metric-value" id="matchRate">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Service Registry --> | |
| <div class="glass-card" id="system"> | |
| <div class="card-title">Service Registry</div> | |
| <div class="card-content"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Quasar Engine</span> | |
| <span class="status-badge status-active" id="statusQuasar">Active</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Feature Pipeline</span> | |
| <span class="status-badge status-active" id="statusFeatures">Active</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Reward System</span> | |
| <span class="status-badge status-active" id="statusRewards">Active</span> | |
| </div> | |
| <div class="process-status process-ok" id="processCount"> | |
| <div class="process-label">Quasar Processes</div> | |
| <div class="process-value" id="processCountValue">1</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SYSTEM RESOURCES --> | |
| <div class="grid-3"> | |
| <!-- Computational Load --> | |
| <div class="glass-card"> | |
| <div class="card-title">Computational Load</div> | |
| <div class="card-content"> | |
| <div class="big-number" id="cpuPercent">—%</div> | |
| <div class="progress-container"> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="cpuBar" style="width:0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Memory Allocation --> | |
| <div class="glass-card"> | |
| <div class="card-title">Memory Allocation</div> | |
| <div class="card-content"> | |
| <div class="big-number" id="memoryPercent">—%</div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Used</span> | |
| <span class="metric-value" id="memoryUsed">—/— GB</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Quasar</span> | |
| <span class="metric-value positive" id="quasarMemory">— GB</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Latest Checkpoint --> | |
| <div class="glass-card"> | |
| <div class="card-title">Latest Checkpoint</div> | |
| <div class="card-content"> | |
| <div class="metric-row"> | |
| <span class="metric-label">Filename</span> | |
| <span class="metric-value" id="checkpointFile" style="font-size:0.7rem;">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Training Step</span> | |
| <span class="metric-value" id="checkpointStep">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Size</span> | |
| <span class="metric-value" id="checkpointSize">—</span> | |
| </div> | |
| <div class="metric-row"> | |
| <span class="metric-label">Modified</span> | |
| <span class="metric-value" id="checkpointModified" style="font-size:0.75rem;">—</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ACCURACY TRENDS --> | |
| <div class="glass-card"> | |
| <div class="card-title">Signal Precision Analytics</div> | |
| <div class="accuracy-header"> | |
| <div class="accuracy-metrics"> | |
| <div class="accuracy-stat"> | |
| <div class="accuracy-stat-label">Current</div> | |
| <div class="accuracy-stat-value" id="accuracy-current" style="color: var(--success);">—</div> | |
| </div> | |
| <div class="accuracy-stat"> | |
| <div class="accuracy-stat-label">Average</div> | |
| <div class="accuracy-stat-value" id="accuracy-avg" style="color: var(--accent-cyan);">—</div> | |
| </div> | |
| <div class="accuracy-stat"> | |
| <div class="accuracy-stat-label">Last 10</div> | |
| <div class="accuracy-stat-value" id="accuracy-last10" style="color: var(--warning);">—</div> | |
| </div> | |
| </div> | |
| <div class="accuracy-trend-badge stable" id="accuracy-trend"> | |
| <span class="trend-arrow">―</span> | |
| <span>STABLE</span> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="accuracyChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- SYSTEM LOGS --> | |
| <div class="glass-card"> | |
| <div class="card-title">Quasar Intelligence Stream</div> | |
| <div class="log-container" id="logContainer"> | |
| <div class="log-line">Awaiting signal intelligence...</div> | |
| </div> | |
| </div> | |
| <!-- HuggingFace Spaces Platform Badge --> | |
| <div style="position:fixed;bottom:20px;right:20px;z-index:101;padding:8px 16px;background:rgba(255,136,0,0.1);border:1px solid rgba(255,136,0,0.3);border-radius:20px;backdrop-filter:blur(8px);font-size:0.75rem;font-weight:500;color:#ffaa00;letter-spacing:0.05em;">🤗 HuggingFace Spaces</div> | |
| <!-- Footer --> | |
| <footer class="footer"> | |
| <p>K1RL QUASAR Observatory v10.0 — HF Spaces Edition</p> | |
| <p>Reinforcement Learning • Quantitative Intelligence • VOLATILITY 75 Index • Redis-Backed</p> | |
| </footer> | |
| </div> | |
| <script> | |
| // ==================== THREE.JS WITH REALISTIC EARTH ==================== | |
| (function() { | |
| const canvas = document.getElementById('three-canvas'); | |
| const isMobile = window.innerWidth <= 768; | |
| const renderer = new THREE.WebGLRenderer({ | |
| canvas, | |
| antialias: !isMobile, | |
| alpha: true, | |
| powerPreference: 'high-performance' | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.5 : 2)); | |
| renderer.setClearColor(0x000810, 1); | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x000810, 0.0006); | |
| const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 3000); | |
| camera.position.set(0, 50, 500); | |
| camera.lookAt(0, -50, 0); | |
| const textureLoader = new THREE.TextureLoader(); | |
| // ===== STARFIELD ===== | |
| const starCount = isMobile ? 800 : 2000; | |
| const starGeo = new THREE.BufferGeometry(); | |
| const starPos = new Float32Array(starCount * 3); | |
| for (let i = 0; i < starCount * 3; i += 3) { | |
| starPos[i] = (Math.random() - 0.5) * 2000; | |
| starPos[i+1] = (Math.random() - 0.5) * 2000; | |
| starPos[i+2] = (Math.random() - 0.5) * 2000; | |
| } | |
| starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3)); | |
| const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({ | |
| color: 0xffffff, | |
| size: 1.2, | |
| transparent: true, | |
| opacity: 0.8, | |
| sizeAttenuation: true | |
| })); | |
| scene.add(stars); | |
| // ==================== NEURAL NETWORK WITH PHYSICS ==================== | |
| const nodeCount = isMobile ? 60 : 100; | |
| const nodes = []; | |
| // Physics constants | |
| const PHYSICS = { | |
| // Core forces | |
| centralGravity: 0.00008, // Pull towards center | |
| repulsion: 800, // Node-to-node repulsion strength | |
| repulsionDistance: 120, // Max distance for repulsion | |
| springStrength: 0.0003, // Connection spring force | |
| springRestLength: 100, // Ideal connection length | |
| // Boundaries | |
| minRadius: 200, | |
| maxRadius: 550, | |
| // Damping & limits | |
| damping: 0.985, // Velocity decay | |
| maxVelocity: 2.5, | |
| // Mouse interaction | |
| mouseRadius: 200, | |
| mouseForce: 0.15, | |
| mouseAttract: false, // false = repel, true = attract | |
| // Connection | |
| connectionDistance: 140, | |
| maxConnections: 6 | |
| }; | |
| // Mouse tracking | |
| const mouse = { x: 0, y: 0, z: 0, active: false }; | |
| const mouseVector = new THREE.Vector3(); | |
| const raycaster = new THREE.Raycaster(); | |
| const mouse2D = new THREE.Vector2(); | |
| // Create nodes with physics properties | |
| const nodeGeometry = new THREE.BufferGeometry(); | |
| const nodePositions = new Float32Array(nodeCount * 3); | |
| const nodeSizes = new Float32Array(nodeCount); | |
| const nodeColors = new Float32Array(nodeCount * 3); | |
| for (let i = 0; i < nodeCount; i++) { | |
| const i3 = i * 3; | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = Math.acos(2 * Math.random() - 1); | |
| const radius = PHYSICS.minRadius + Math.random() * (PHYSICS.maxRadius - PHYSICS.minRadius); | |
| const x = radius * Math.sin(phi) * Math.cos(theta); | |
| const y = radius * Math.sin(phi) * Math.sin(theta); | |
| const z = radius * Math.cos(phi); | |
| nodePositions[i3] = x; | |
| nodePositions[i3 + 1] = y; | |
| nodePositions[i3 + 2] = z; | |
| // Random mass affects node size and physics response | |
| const mass = 0.5 + Math.random() * 1.5; | |
| nodeSizes[i] = isMobile ? mass * 3 : mass * 4; | |
| // Color gradient: cyan to blue based on distance from center | |
| const colorFactor = Math.random(); | |
| nodeColors[i3] = 0.0 + colorFactor * 0.2; // R | |
| nodeColors[i3 + 1] = 0.7 + colorFactor * 0.3; // G | |
| nodeColors[i3 + 2] = 1.0; // B | |
| nodes.push({ | |
| x, y, z, | |
| vx: (Math.random() - 0.5) * 0.5, | |
| vy: (Math.random() - 0.5) * 0.5, | |
| vz: (Math.random() - 0.5) * 0.5, | |
| mass: mass, | |
| connections: [], | |
| energy: 0.5 + Math.random() * 0.5 // For pulse effects | |
| }); | |
| } | |
| nodeGeometry.setAttribute('position', new THREE.BufferAttribute(nodePositions, 3)); | |
| nodeGeometry.setAttribute('size', new THREE.BufferAttribute(nodeSizes, 1)); | |
| nodeGeometry.setAttribute('color', new THREE.BufferAttribute(nodeColors, 3)); | |
| // Custom shader for variable-size glowing nodes | |
| const nodeShaderMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| time: { value: 0 }, | |
| pixelRatio: { value: renderer.getPixelRatio() } | |
| }, | |
| vertexShader: ` | |
| attribute float size; | |
| attribute vec3 color; | |
| varying vec3 vColor; | |
| varying float vSize; | |
| uniform float time; | |
| uniform float pixelRatio; | |
| void main() { | |
| vColor = color; | |
| vSize = size; | |
| vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); | |
| // Subtle pulse based on position | |
| float pulse = 1.0 + 0.15 * sin(time * 2.0 + position.x * 0.01 + position.y * 0.01); | |
| gl_PointSize = size * pulse * pixelRatio * (300.0 / -mvPosition.z); | |
| gl_Position = projectionMatrix * mvPosition; | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying vec3 vColor; | |
| varying float vSize; | |
| void main() { | |
| vec2 center = gl_PointCoord - vec2(0.5); | |
| float dist = length(center); | |
| if (dist > 0.5) discard; | |
| // Soft glow falloff | |
| float alpha = 1.0 - smoothstep(0.0, 0.5, dist); | |
| float glow = exp(-dist * 3.0); | |
| vec3 finalColor = vColor + vec3(0.3, 0.5, 0.8) * glow * 0.5; | |
| gl_FragColor = vec4(finalColor, alpha * 0.9); | |
| } | |
| `, | |
| transparent: true, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const nodePoints = new THREE.Points(nodeGeometry, nodeShaderMaterial); | |
| scene.add(nodePoints); | |
| // Connection lines with varying opacity | |
| const maxLines = nodeCount * PHYSICS.maxConnections; | |
| const linePositions = new Float32Array(maxLines * 6); | |
| const lineColors = new Float32Array(maxLines * 6); | |
| const linesGeometry = new THREE.BufferGeometry(); | |
| linesGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)); | |
| linesGeometry.setAttribute('color', new THREE.BufferAttribute(lineColors, 3)); | |
| const linesMaterial = new THREE.LineBasicMaterial({ | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.6, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const lines = new THREE.LineSegments(linesGeometry, linesMaterial); | |
| scene.add(lines); | |
| // Build spatial hash for efficient neighbor lookup | |
| function buildSpatialHash() { | |
| const cellSize = PHYSICS.repulsionDistance; | |
| const hash = {}; | |
| for (let i = 0; i < nodeCount; i++) { | |
| const node = nodes[i]; | |
| const cellX = Math.floor(node.x / cellSize); | |
| const cellY = Math.floor(node.y / cellSize); | |
| const cellZ = Math.floor(node.z / cellSize); | |
| const key = `${cellX},${cellY},${cellZ}`; | |
| if (!hash[key]) hash[key] = []; | |
| hash[key].push(i); | |
| } | |
| return hash; | |
| } | |
| // Get neighbors from spatial hash | |
| function getNeighborCells(x, y, z, cellSize) { | |
| const cellX = Math.floor(x / cellSize); | |
| const cellY = Math.floor(y / cellSize); | |
| const cellZ = Math.floor(z / cellSize); | |
| const neighbors = []; | |
| for (let dx = -1; dx <= 1; dx++) { | |
| for (let dy = -1; dy <= 1; dy++) { | |
| for (let dz = -1; dz <= 1; dz++) { | |
| neighbors.push(`${cellX + dx},${cellY + dy},${cellZ + dz}`); | |
| } | |
| } | |
| } | |
| return neighbors; | |
| } | |
| // Physics simulation step | |
| function simulatePhysics(deltaTime) { | |
| const dt = Math.min(deltaTime, 0.033); // Cap at ~30fps worth | |
| const spatialHash = buildSpatialHash(); | |
| // Reset connections | |
| for (let i = 0; i < nodeCount; i++) { | |
| nodes[i].connections = []; | |
| } | |
| // Calculate forces for each node | |
| for (let i = 0; i < nodeCount; i++) { | |
| const node = nodes[i]; | |
| let fx = 0, fy = 0, fz = 0; | |
| // 1. Central gravity - pull towards origin | |
| const distFromCenter = Math.sqrt(node.x * node.x + node.y * node.y + node.z * node.z); | |
| if (distFromCenter > 0.01) { | |
| const gravityStrength = PHYSICS.centralGravity * node.mass; | |
| fx -= (node.x / distFromCenter) * gravityStrength * distFromCenter; | |
| fy -= (node.y / distFromCenter) * gravityStrength * distFromCenter; | |
| fz -= (node.z / distFromCenter) * gravityStrength * distFromCenter; | |
| } | |
| // 2. Boundary force - soft repulsion at edges | |
| if (distFromCenter > PHYSICS.maxRadius) { | |
| const overflow = distFromCenter - PHYSICS.maxRadius; | |
| const boundaryForce = overflow * 0.01; | |
| fx -= (node.x / distFromCenter) * boundaryForce; | |
| fy -= (node.y / distFromCenter) * boundaryForce; | |
| fz -= (node.z / distFromCenter) * boundaryForce; | |
| } else if (distFromCenter < PHYSICS.minRadius) { | |
| const underflow = PHYSICS.minRadius - distFromCenter; | |
| const boundaryForce = underflow * 0.01; | |
| fx += (node.x / distFromCenter) * boundaryForce; | |
| fy += (node.y / distFromCenter) * boundaryForce; | |
| fz += (node.z / distFromCenter) * boundaryForce; | |
| } | |
| // 3. Node-to-node repulsion (using spatial hash for efficiency) | |
| const neighborCells = getNeighborCells(node.x, node.y, node.z, PHYSICS.repulsionDistance); | |
| for (const cellKey of neighborCells) { | |
| const cellNodes = spatialHash[cellKey]; | |
| if (!cellNodes) continue; | |
| for (const j of cellNodes) { | |
| if (i === j) continue; | |
| const other = nodes[j]; | |
| const dx = node.x - other.x; | |
| const dy = node.y - other.y; | |
| const dz = node.z - other.z; | |
| const distSq = dx * dx + dy * dy + dz * dz; | |
| const dist = Math.sqrt(distSq); | |
| if (dist < PHYSICS.repulsionDistance && dist > 0.1) { | |
| // Inverse square repulsion | |
| const force = PHYSICS.repulsion / (distSq + 100); | |
| fx += (dx / dist) * force / node.mass; | |
| fy += (dy / dist) * force / node.mass; | |
| fz += (dz / dist) * force / node.mass; | |
| // Track connections for springs and rendering | |
| if (dist < PHYSICS.connectionDistance && node.connections.length < PHYSICS.maxConnections) { | |
| if (!node.connections.includes(j)) { | |
| node.connections.push(j); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // 4. Spring forces along connections | |
| for (const j of node.connections) { | |
| const other = nodes[j]; | |
| const dx = other.x - node.x; | |
| const dy = other.y - node.y; | |
| const dz = other.z - node.z; | |
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); | |
| if (dist > 0.1) { | |
| const displacement = dist - PHYSICS.springRestLength; | |
| const springForce = displacement * PHYSICS.springStrength; | |
| fx += (dx / dist) * springForce; | |
| fy += (dy / dist) * springForce; | |
| fz += (dz / dist) * springForce; | |
| } | |
| } | |
| // 5. Mouse interaction | |
| if (mouse.active) { | |
| const mdx = node.x - mouse.x; | |
| const mdy = node.y - mouse.y; | |
| const mdz = node.z - mouse.z; | |
| const mouseDist = Math.sqrt(mdx * mdx + mdy * mdy + mdz * mdz); | |
| if (mouseDist < PHYSICS.mouseRadius && mouseDist > 1) { | |
| const mouseStrength = (1 - mouseDist / PHYSICS.mouseRadius) * PHYSICS.mouseForce; | |
| const direction = PHYSICS.mouseAttract ? -1 : 1; | |
| fx += direction * (mdx / mouseDist) * mouseStrength; | |
| fy += direction * (mdy / mouseDist) * mouseStrength; | |
| fz += direction * (mdz / mouseDist) * mouseStrength; | |
| } | |
| } | |
| // Apply forces to velocity | |
| node.vx += fx * dt; | |
| node.vy += fy * dt; | |
| node.vz += fz * dt; | |
| // Damping | |
| node.vx *= PHYSICS.damping; | |
| node.vy *= PHYSICS.damping; | |
| node.vz *= PHYSICS.damping; | |
| // Velocity limit | |
| const speed = Math.sqrt(node.vx * node.vx + node.vy * node.vy + node.vz * node.vz); | |
| if (speed > PHYSICS.maxVelocity) { | |
| const scale = PHYSICS.maxVelocity / speed; | |
| node.vx *= scale; | |
| node.vy *= scale; | |
| node.vz *= scale; | |
| } | |
| } | |
| // Update positions | |
| const positions = nodeGeometry.attributes.position.array; | |
| const sizes = nodeGeometry.attributes.size.array; | |
| for (let i = 0; i < nodeCount; i++) { | |
| const node = nodes[i]; | |
| const i3 = i * 3; | |
| node.x += node.vx; | |
| node.y += node.vy; | |
| node.z += node.vz; | |
| positions[i3] = node.x; | |
| positions[i3 + 1] = node.y; | |
| positions[i3 + 2] = node.z; | |
| // Size varies with connections (more connected = larger) | |
| const connectionBonus = node.connections.length * 0.3; | |
| sizes[i] = (isMobile ? 3 : 4) * node.mass + connectionBonus; | |
| } | |
| nodeGeometry.attributes.position.needsUpdate = true; | |
| nodeGeometry.attributes.size.needsUpdate = true; | |
| } | |
| // Update connection lines | |
| function updateConnections() { | |
| const linePos = linesGeometry.attributes.position.array; | |
| const lineCol = linesGeometry.attributes.color.array; | |
| let lineIndex = 0; | |
| for (let i = 0; i < nodeCount; i++) { | |
| const node = nodes[i]; | |
| for (const j of node.connections) { | |
| if (j > i) continue; // Avoid duplicates | |
| const other = nodes[j]; | |
| const dx = other.x - node.x; | |
| const dy = other.y - node.y; | |
| const dz = other.z - node.z; | |
| const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); | |
| // Line positions | |
| const idx = lineIndex * 6; | |
| linePos[idx] = node.x; | |
| linePos[idx + 1] = node.y; | |
| linePos[idx + 2] = node.z; | |
| linePos[idx + 3] = other.x; | |
| linePos[idx + 4] = other.y; | |
| linePos[idx + 5] = other.z; | |
| // Color based on distance (closer = brighter) | |
| const intensity = 1 - (dist / PHYSICS.connectionDistance); | |
| const r = 0.0 * intensity; | |
| const g = 0.83 * intensity; | |
| const b = 1.0 * intensity; | |
| lineCol[idx] = r; | |
| lineCol[idx + 1] = g; | |
| lineCol[idx + 2] = b; | |
| lineCol[idx + 3] = r; | |
| lineCol[idx + 4] = g; | |
| lineCol[idx + 5] = b; | |
| lineIndex++; | |
| } | |
| } | |
| // Clear unused line segments | |
| for (let i = lineIndex * 6; i < linePos.length; i++) { | |
| linePos[i] = 0; | |
| lineCol[i] = 0; | |
| } | |
| linesGeometry.attributes.position.needsUpdate = true; | |
| linesGeometry.attributes.color.needsUpdate = true; | |
| linesGeometry.setDrawRange(0, lineIndex * 2); | |
| } | |
| // Mouse event handlers | |
| function onMouseMove(event) { | |
| mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| raycaster.setFromCamera(mouse2D, camera); | |
| // Project mouse into 3D space at network depth | |
| const planeZ = -100; | |
| const planeNormal = new THREE.Vector3(0, 0, 1); | |
| const planePoint = new THREE.Vector3(0, 0, planeZ); | |
| const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(planeNormal, planePoint); | |
| const intersectPoint = new THREE.Vector3(); | |
| raycaster.ray.intersectPlane(plane, intersectPoint); | |
| if (intersectPoint) { | |
| mouse.x = intersectPoint.x; | |
| mouse.y = intersectPoint.y; | |
| mouse.z = intersectPoint.z; | |
| mouse.active = true; | |
| } | |
| } | |
| function onMouseLeave() { | |
| mouse.active = false; | |
| } | |
| // Touch support | |
| function onTouchMove(event) { | |
| if (event.touches.length > 0) { | |
| const touch = event.touches[0]; | |
| onMouseMove({ clientX: touch.clientX, clientY: touch.clientY }); | |
| } | |
| } | |
| function onTouchEnd() { | |
| mouse.active = false; | |
| } | |
| // Add event listeners | |
| document.addEventListener('mousemove', onMouseMove); | |
| document.addEventListener('mouseleave', onMouseLeave); | |
| document.addEventListener('touchmove', onTouchMove, { passive: true }); | |
| document.addEventListener('touchend', onTouchEnd); | |
| // ==================== REALISTIC EARTH ==================== | |
| const earthRadius = isMobile ? 160 : 180; | |
| const earthGroup = new THREE.Group(); | |
| earthGroup.position.set(0, isMobile ? -350 : -380, -100); | |
| scene.add(earthGroup); | |
| // Earth texture URLs (from three-globe) | |
| const textureURLs = { | |
| dayMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-day.jpg', | |
| nightMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-night.jpg', | |
| bumpMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-topology.png', | |
| cloudsMap: 'https://unpkg.com/three-globe@2.24.13/example/img/earth-clouds.png' | |
| }; | |
| // Load textures | |
| const earthDayTexture = textureLoader.load(textureURLs.dayMap); | |
| const earthNightTexture = textureLoader.load(textureURLs.nightMap); | |
| const earthBumpTexture = textureLoader.load(textureURLs.bumpMap); | |
| const earthCloudsTexture = textureLoader.load(textureURLs.cloudsMap); | |
| // Earth geometry | |
| const earthGeometry = new THREE.SphereGeometry(earthRadius, isMobile ? 48 : 64, isMobile ? 48 : 64); | |
| // Custom shader for day/night cycle | |
| const earthMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| dayTexture: { value: earthDayTexture }, | |
| nightTexture: { value: earthNightTexture }, | |
| sunDirection: { value: new THREE.Vector3(1, 0.5, 1).normalize() } | |
| }, | |
| vertexShader: ` | |
| varying vec2 vUv; | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| void main() { | |
| vUv = uv; | |
| vNormal = normalize(normalMatrix * normal); | |
| vPosition = (modelMatrix * vec4(position, 1.0)).xyz; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform sampler2D dayTexture; | |
| uniform sampler2D nightTexture; | |
| uniform vec3 sunDirection; | |
| varying vec2 vUv; | |
| varying vec3 vNormal; | |
| varying vec3 vPosition; | |
| void main() { | |
| vec3 dayColor = texture2D(dayTexture, vUv).rgb; | |
| vec3 nightColor = texture2D(nightTexture, vUv).rgb; | |
| float sunIntensity = dot(vNormal, sunDirection); | |
| float dayFactor = smoothstep(-0.2, 0.4, sunIntensity); | |
| nightColor *= 1.5; | |
| vec3 color = mix(nightColor, dayColor, dayFactor); | |
| color += vec3(0.02, 0.03, 0.05); | |
| float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0))), 3.0); | |
| vec3 atmosphereColor = vec3(0.3, 0.6, 1.0); | |
| color = mix(color, atmosphereColor, fresnel * 0.3); | |
| gl_FragColor = vec4(color, 1.0); | |
| } | |
| ` | |
| }); | |
| const earth = new THREE.Mesh(earthGeometry, earthMaterial); | |
| earthGroup.add(earth); | |
| // Cloud layer | |
| const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.02, isMobile ? 48 : 64, isMobile ? 48 : 64); | |
| const cloudMaterial = new THREE.MeshPhongMaterial({ | |
| map: earthCloudsTexture, | |
| transparent: true, | |
| opacity: 0.4, | |
| depthWrite: false, | |
| side: THREE.DoubleSide | |
| }); | |
| const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial); | |
| earthGroup.add(clouds); | |
| // Atmosphere glow | |
| const atmosphereGeometry = new THREE.SphereGeometry(earthRadius * 1.15, isMobile ? 48 : 64, isMobile ? 48 : 64); | |
| const atmosphereMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| glowColor: { value: new THREE.Color(0x3388ff) }, | |
| viewVector: { value: camera.position } | |
| }, | |
| vertexShader: ` | |
| uniform vec3 viewVector; | |
| varying float intensity; | |
| void main() { | |
| vec3 vNormal = normalize(normalMatrix * normal); | |
| vec3 vNormel = normalize(normalMatrix * viewVector); | |
| intensity = pow(0.7 - dot(vNormal, vNormel), 2.0); | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform vec3 glowColor; | |
| varying float intensity; | |
| void main() { | |
| vec3 glow = glowColor * intensity; | |
| gl_FragColor = vec4(glow, intensity * 0.6); | |
| } | |
| `, | |
| side: THREE.BackSide, | |
| blending: THREE.AdditiveBlending, | |
| transparent: true | |
| }); | |
| const atmosphereOuter = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial); | |
| earthGroup.add(atmosphereOuter); | |
| // ===== LIGHTING ===== | |
| const ambientLight = new THREE.AmbientLight(isMobile ? 0x555555 : 0x333333); | |
| scene.add(ambientLight); | |
| const sunLight = new THREE.DirectionalLight(0xffffff, 1.5); | |
| sunLight.position.set(5, 3, 5); | |
| scene.add(sunLight); | |
| // ==================== AMBIENT ASTRONAUT — DETAILED EVA SUIT ==================== | |
| let updateAstronaut = null; | |
| const astronautEnabled = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| if (astronautEnabled) { | |
| const astronautGroup = new THREE.Group(); // Keep name for compatibility | |
| // ═══════════════════════════════════════════════════════════════ | |
| // ██ UFO SPACESHIP CONSTRUCTION ██ | |
| // ═══════════════════════════════════════════════════════════════ | |
| // ──────────── MATERIALS LIBRARY ──────────── | |
| const mobileEmissiveBoost = isMobile ? 1.8 : 1.0; // Brighter on mobile | |
| const mat = { | |
| // Main hull - lighter metallic with self-illumination | |
| hull: new THREE.MeshStandardMaterial({ | |
| color: 0x5a7a9a, roughness: 0.3, metalness: 0.7, | |
| emissive: 0x2a4a6a, emissiveIntensity: 0.4 * mobileEmissiveBoost | |
| }), | |
| // Hull accent - lighter | |
| hullAccent: new THREE.MeshStandardMaterial({ | |
| color: 0x6a8aaa, roughness: 0.4, metalness: 0.6, | |
| emissive: 0x3a5a7a, emissiveIntensity: 0.35 * mobileEmissiveBoost | |
| }), | |
| // Chrome trim - brighter | |
| chrome: new THREE.MeshStandardMaterial({ | |
| color: 0xddddee, roughness: 0.1, metalness: 1.0, | |
| emissive: 0x556677, emissiveIntensity: 0.4 * mobileEmissiveBoost | |
| }), | |
| // Dome glass - brighter tint | |
| domeGlass: new THREE.MeshStandardMaterial({ | |
| color: 0x55bbdd, roughness: 0.05, metalness: 0.3, | |
| transparent: true, opacity: 0.6, | |
| emissive: 0x338899, emissiveIntensity: 0.7 * mobileEmissiveBoost | |
| }), | |
| // Inner dome glow - brighter | |
| domeInner: new THREE.MeshStandardMaterial({ | |
| color: 0x00ffcc, emissive: 0x00ffaa, emissiveIntensity: 1.5 * mobileEmissiveBoost, | |
| transparent: true, opacity: 0.7 | |
| }), | |
| // Cyan lights (primary) - brighter | |
| lightCyan: new THREE.MeshStandardMaterial({ | |
| color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 4.0 * mobileEmissiveBoost, | |
| transparent: true, opacity: 1.0 | |
| }), | |
| // Green lights (accent) - brighter | |
| lightGreen: new THREE.MeshStandardMaterial({ | |
| color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.5 * mobileEmissiveBoost | |
| }), | |
| // Blue thrust core - brighter | |
| thrustCore: new THREE.MeshStandardMaterial({ | |
| color: 0x00ccff, emissive: 0x00aaff, emissiveIntensity: 5.0 * mobileEmissiveBoost, | |
| transparent: true, opacity: 0.95 | |
| }), | |
| // Thrust mid layer | |
| thrustMid: new THREE.MeshStandardMaterial({ | |
| color: 0x00aaff, emissive: 0x0088ee, emissiveIntensity: 2.5 * mobileEmissiveBoost, | |
| transparent: true, opacity: 0.55, side: THREE.DoubleSide | |
| }), | |
| // Thrust outer glow | |
| thrustOuter: new THREE.MeshStandardMaterial({ | |
| color: 0x0088dd, emissive: 0x0066bb, emissiveIntensity: 1.2 * mobileEmissiveBoost, | |
| transparent: true, opacity: 0.3, side: THREE.DoubleSide | |
| }), | |
| // Dark panels - lighter | |
| panel: new THREE.MeshStandardMaterial({ | |
| color: 0x3a5a7a, roughness: 0.7, metalness: 0.4, | |
| emissive: 0x2a3a4a, emissiveIntensity: 0.3 * mobileEmissiveBoost | |
| }) | |
| }; | |
| // ──────────── MAIN SAUCER BODY ──────────── | |
| // Upper dome housing (flattened sphere) | |
| const upperDome = new THREE.Mesh( | |
| new THREE.SphereGeometry(8, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5), | |
| mat.hull | |
| ); | |
| upperDome.scale.set(1, 0.5, 1); | |
| upperDome.position.y = 2; | |
| astronautGroup.add(upperDome); | |
| // Glass cockpit dome | |
| const glassDome = new THREE.Mesh( | |
| new THREE.SphereGeometry(5, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.55), | |
| mat.domeGlass | |
| ); | |
| glassDome.scale.set(1, 0.7, 1); | |
| glassDome.position.y = 3.5; | |
| astronautGroup.add(glassDome); | |
| // Inner dome glow | |
| const innerGlow = new THREE.Mesh( | |
| new THREE.SphereGeometry(4.5, 24, 12, 0, Math.PI * 2, 0, Math.PI * 0.5), | |
| mat.domeInner | |
| ); | |
| innerGlow.scale.set(1, 0.6, 1); | |
| innerGlow.position.y = 3.5; | |
| astronautGroup.add(innerGlow); | |
| // Main disc body (torus-like shape using lathe) | |
| const discProfile = []; | |
| for (let i = 0; i <= 20; i++) { | |
| const t = i / 20; | |
| const angle = t * Math.PI; | |
| const r = 8 + Math.sin(angle) * 7; | |
| const y = Math.cos(angle) * 2.5; | |
| discProfile.push(new THREE.Vector2(r, y)); | |
| } | |
| const discGeo = new THREE.LatheGeometry(discProfile, 48); | |
| const mainDisc = new THREE.Mesh(discGeo, mat.hull); | |
| mainDisc.position.y = 0; | |
| astronautGroup.add(mainDisc); | |
| // Chrome ring (equator trim) | |
| const chromeRing = new THREE.Mesh( | |
| new THREE.TorusGeometry(15, 0.4, 16, 64), | |
| mat.chrome | |
| ); | |
| chromeRing.rotation.x = Math.PI / 2; | |
| chromeRing.position.y = 0; | |
| astronautGroup.add(chromeRing); | |
| // Upper chrome ring | |
| const upperRing = new THREE.Mesh( | |
| new THREE.TorusGeometry(8, 0.25, 12, 48), | |
| mat.chrome | |
| ); | |
| upperRing.rotation.x = Math.PI / 2; | |
| upperRing.position.y = 2; | |
| astronautGroup.add(upperRing); | |
| // ──────────── BOTTOM STRUCTURE ──────────── | |
| // Bottom dome (inverted, flatter) | |
| const bottomDome = new THREE.Mesh( | |
| new THREE.SphereGeometry(10, 32, 16, 0, Math.PI * 2, Math.PI * 0.5, Math.PI * 0.5), | |
| mat.hullAccent | |
| ); | |
| bottomDome.scale.set(1, 0.35, 1); | |
| bottomDome.position.y = -2.5; | |
| astronautGroup.add(bottomDome); | |
| // Central thrust housing | |
| const thrustHousing = new THREE.Mesh( | |
| new THREE.CylinderGeometry(4, 5, 2, 24), | |
| mat.hull | |
| ); | |
| thrustHousing.position.y = -4; | |
| astronautGroup.add(thrustHousing); | |
| // Thrust emitter ring | |
| const thrustEmitterRing = new THREE.Mesh( | |
| new THREE.TorusGeometry(4.5, 0.3, 12, 32), | |
| mat.chrome | |
| ); | |
| thrustEmitterRing.rotation.x = Math.PI / 2; | |
| thrustEmitterRing.position.y = -5; | |
| astronautGroup.add(thrustEmitterRing); | |
| // ──────────── RIM LIGHTS (8 around edge) ──────────── | |
| const rimLightCount = 8; | |
| const rimLights = []; | |
| const rimLightMats = []; | |
| for (let i = 0; i < rimLightCount; i++) { | |
| const angle = (i / rimLightCount) * Math.PI * 2; | |
| const lightGroup = new THREE.Group(); | |
| // Light housing | |
| const housing = new THREE.Mesh( | |
| new THREE.SphereGeometry(1.2, 16, 12), | |
| mat.hullAccent | |
| ); | |
| housing.scale.set(1, 0.6, 1); | |
| lightGroup.add(housing); | |
| // Light dome (glowing) - unique material per light for animation | |
| const lightMat = new THREE.MeshStandardMaterial({ | |
| color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 3.5 * mobileEmissiveBoost, | |
| transparent: true, opacity: 1.0 | |
| }); | |
| const lightDome = new THREE.Mesh( | |
| new THREE.SphereGeometry(1, 16, 12, 0, Math.PI * 2, 0, Math.PI * 0.6), | |
| lightMat | |
| ); | |
| lightDome.position.y = 0.3; | |
| lightGroup.add(lightDome); | |
| lightGroup.position.set( | |
| Math.cos(angle) * 14, | |
| 0.5, | |
| Math.sin(angle) * 14 | |
| ); | |
| astronautGroup.add(lightGroup); | |
| rimLights.push(lightDome); | |
| rimLightMats.push(lightMat); | |
| } | |
| // ──────────── TOP DOME LIGHTS (4 around dome) ──────────── | |
| const domeLightCount = 4; | |
| const domeLights = []; | |
| const domeLightMats = []; | |
| for (let i = 0; i < domeLightCount; i++) { | |
| const angle = (i / domeLightCount) * Math.PI * 2 + Math.PI / 4; | |
| const lightMat = new THREE.MeshStandardMaterial({ | |
| color: 0x00ff88, emissive: 0x00ff88, emissiveIntensity: 3.0 * mobileEmissiveBoost | |
| }); | |
| const domeLight = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.6, 12, 8), | |
| lightMat | |
| ); | |
| domeLight.position.set( | |
| Math.cos(angle) * 6, | |
| 4, | |
| Math.sin(angle) * 6 | |
| ); | |
| astronautGroup.add(domeLight); | |
| domeLights.push(domeLight); | |
| domeLightMats.push(lightMat); | |
| } | |
| // ──────────── PANEL DETAILS ──────────── | |
| for (let i = 0; i < 12; i++) { | |
| const angle = (i / 12) * Math.PI * 2; | |
| const panelLine = new THREE.Mesh( | |
| new THREE.BoxGeometry(0.1, 0.3, 5), | |
| mat.panel | |
| ); | |
| panelLine.position.set( | |
| Math.cos(angle) * 11, | |
| 1, | |
| Math.sin(angle) * 11 | |
| ); | |
| panelLine.rotation.y = -angle; | |
| astronautGroup.add(panelLine); | |
| } | |
| // ══════════════════════════════════════════════════════════════ | |
| // ██ BLUE/RED THRUST BEAM SYSTEM (Signal Reactive) ██ | |
| // ══════════════════════════════════════════════════════════════ | |
| const mobileLightBoost = isMobile ? 2.0 : 1.0; // Extra brightness on mobile | |
| const thrustGroup = new THREE.Group(); | |
| thrustGroup.position.y = -5; | |
| astronautGroup.add(thrustGroup); | |
| // Core beam (bright center) - LONGER & WIDER | |
| const beamCoreGeo = new THREE.CylinderGeometry(1, 5, 30, 24, 1, true); | |
| const beamCore = new THREE.Mesh(beamCoreGeo, mat.thrustCore); | |
| beamCore.position.y = -15; | |
| thrustGroup.add(beamCore); | |
| // Mid beam layer - LONGER & WIDER | |
| const beamMidGeo = new THREE.CylinderGeometry(2.5, 8, 35, 24, 1, true); | |
| const beamMid = new THREE.Mesh(beamMidGeo, mat.thrustMid); | |
| beamMid.position.y = -17; | |
| thrustGroup.add(beamMid); | |
| // Outer glow cone - LONGER & WIDER | |
| const beamOuterGeo = new THREE.CylinderGeometry(5, 14, 40, 24, 1, true); | |
| const beamOuter = new THREE.Mesh(beamOuterGeo, mat.thrustOuter); | |
| beamOuter.position.y = -20; | |
| thrustGroup.add(beamOuter); | |
| // Thrust particles - wider spread, longer travel (reduced on mobile) | |
| const thrustParticleCount = isMobile ? 80 : 200; | |
| const thrustParticleGeo = new THREE.BufferGeometry(); | |
| const thrustParticlePos = new Float32Array(thrustParticleCount * 3); | |
| const thrustParticleSpeeds = []; | |
| for (let i = 0; i < thrustParticleCount; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = Math.random() * 5; | |
| thrustParticlePos[i * 3] = Math.cos(angle) * radius; | |
| thrustParticlePos[i * 3 + 1] = -Math.random() * 35; | |
| thrustParticlePos[i * 3 + 2] = Math.sin(angle) * radius; | |
| thrustParticleSpeeds.push(0.6 + Math.random() * 1.8); | |
| } | |
| thrustParticleGeo.setAttribute('position', new THREE.BufferAttribute(thrustParticlePos, 3)); | |
| const thrustParticleMat = new THREE.PointsMaterial({ | |
| color: 0x00eeff, size: isMobile ? 1.0 : 0.7, transparent: true, opacity: 1.0, | |
| blending: THREE.AdditiveBlending | |
| }); | |
| const thrustParticles = new THREE.Points(thrustParticleGeo, thrustParticleMat); | |
| thrustGroup.add(thrustParticles); | |
| // Point light for thrust illumination - stronger | |
| const thrustLight = new THREE.PointLight(0x00aaff, 5 * mobileLightBoost, 100); | |
| thrustLight.position.y = -15; | |
| thrustGroup.add(thrustLight); | |
| // ──────────── POINT LIGHTS (brighter for visibility) ──────────── | |
| const ufoMainLight = new THREE.PointLight(0x00d4ff, 1.5 * mobileLightBoost, 150); | |
| ufoMainLight.position.set(0, 8, 0); | |
| astronautGroup.add(ufoMainLight); | |
| const ufoRimLight = new THREE.PointLight(0x00ffaa, 1.0 * mobileLightBoost, 120); | |
| ufoRimLight.position.set(0, -5, 0); | |
| astronautGroup.add(ufoRimLight); | |
| // Additional lights for hull illumination | |
| const ufoTopLight = new THREE.PointLight(0x88aacc, 0.8 * mobileLightBoost, 80); | |
| ufoTopLight.position.set(0, 15, 0); | |
| astronautGroup.add(ufoTopLight); | |
| const ufoFrontLight = new THREE.PointLight(0x00ccff, 0.6 * mobileLightBoost, 60); | |
| ufoFrontLight.position.set(0, 0, 20); | |
| astronautGroup.add(ufoFrontLight); | |
| // Extra bottom light for mobile visibility | |
| if (isMobile) { | |
| const ufoBottomLight = new THREE.PointLight(0x00aaff, 1.5, 100); | |
| ufoBottomLight.position.set(0, -15, 0); | |
| astronautGroup.add(ufoBottomLight); | |
| } | |
| // ──────────── FINAL SETUP ──────────── | |
| const ufoScale = isMobile ? 0.7 : 0.8; // Slightly smaller on mobile | |
| astronautGroup.scale.setScalar(ufoScale); | |
| scene.add(astronautGroup); | |
| // ════════════════════════════════════════════════════════════════════ | |
| // ██ SUPERMAN FLIGHT SYSTEM — Spline-Driven Heroic Motion ██ | |
| // ════════════════════════════════════════════════════════════════════ | |
| // ──────────── MATH UTILITIES ──────────── | |
| function smoothstep(edge0, edge1, x) { | |
| const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0))); | |
| return t * t * (3 - 2 * t); | |
| } | |
| function smootherstep(edge0, edge1, x) { | |
| const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0))); | |
| return t * t * t * (t * (t * 6 - 15) + 10); | |
| } | |
| function catmullRom(p0, p1, p2, p3, t) { | |
| const t2 = t * t; | |
| const t3 = t2 * t; | |
| return new THREE.Vector3( | |
| 0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3), | |
| 0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3), | |
| 0.5 * ((2 * p1.z) + (-p0.z + p2.z) * t + (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t2 + (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t3) | |
| ); | |
| } | |
| function catmullRomTangent(p0, p1, p2, p3, t) { | |
| const t2 = t * t; | |
| return new THREE.Vector3( | |
| 0.5 * ((-p0.x + p2.x) + 2 * (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t + 3 * (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t2), | |
| 0.5 * ((-p0.y + p2.y) + 2 * (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t + 3 * (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t2), | |
| 0.5 * ((-p0.z + p2.z) + 2 * (2 * p0.z - 5 * p1.z + 4 * p2.z - p3.z) * t + 3 * (-p0.z + 3 * p1.z - 3 * p2.z + p3.z) * t2) | |
| ).normalize(); | |
| } | |
| function quaternionFromDirection(direction, up) { | |
| up = up || new THREE.Vector3(0, 1, 0); | |
| const matrix = new THREE.Matrix4(); | |
| matrix.lookAt(new THREE.Vector3(0, 0, 0), direction, up); | |
| const quat = new THREE.Quaternion(); | |
| quat.setFromRotationMatrix(matrix); | |
| return quat; | |
| } | |
| // ──────────── FLIGHT BOUNDS ──────────── | |
| const flightBounds = { | |
| xMin: -180, xMax: 180, | |
| yMin: 60, yMax: 220, | |
| zMin: -120, zMax: 80 | |
| }; | |
| // ──────────── WAYPOINT GENERATOR ──────────── | |
| function generateWaypoint(currentPos, currentDirection) { | |
| const minDist = 100; | |
| const maxDist = 200; | |
| const dist = minDist + Math.random() * (maxDist - minDist); | |
| const forwardBias = 0.6; | |
| let newDir = new THREE.Vector3( | |
| currentDirection.x * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2, | |
| currentDirection.y * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 1.5, | |
| currentDirection.z * forwardBias + (Math.random() - 0.5) * (1 - forwardBias) * 2 | |
| ).normalize(); | |
| let target = new THREE.Vector3( | |
| currentPos.x + newDir.x * dist, | |
| currentPos.y + newDir.y * dist, | |
| currentPos.z + newDir.z * dist | |
| ); | |
| const margin = 30; | |
| if (target.x < flightBounds.xMin + margin) target.x = flightBounds.xMin + margin + Math.random() * 40; | |
| if (target.x > flightBounds.xMax - margin) target.x = flightBounds.xMax - margin - Math.random() * 40; | |
| if (target.y < flightBounds.yMin + margin) target.y = flightBounds.yMin + margin + Math.random() * 30; | |
| if (target.y > flightBounds.yMax - margin) target.y = flightBounds.yMax - margin - Math.random() * 30; | |
| if (target.z < flightBounds.zMin + margin) target.z = flightBounds.zMin + margin + Math.random() * 40; | |
| if (target.z > flightBounds.zMax - 50) target.z = flightBounds.zMax - 50 - Math.random() * 30; | |
| return target; | |
| } | |
| // ──────────── BEHAVIORAL STATE ──────────── | |
| const ec = earthGroup.position; | |
| const startPos = new THREE.Vector3(ec.x + 50, ec.y + 130, ec.z - 30); | |
| const st = { | |
| waypoints: [ | |
| startPos.clone().add(new THREE.Vector3(-100, 20, -50)), | |
| startPos.clone(), | |
| startPos.clone().add(new THREE.Vector3(80, -10, 30)), | |
| startPos.clone().add(new THREE.Vector3(160, 30, -20)) | |
| ], | |
| t: 0, | |
| segmentDuration: 4, | |
| cruiseSpeed: 1.2, | |
| currentSpeed: 0, | |
| currentQuat: new THREE.Quaternion(), | |
| targetQuat: new THREE.Quaternion(), | |
| bankAngle: 0, | |
| targetBankAngle: 0, | |
| verticalWavePhase: Math.random() * Math.PI * 2, | |
| verticalWaveAmp: 1.5, | |
| verticalWaveFreq: 0.08, | |
| prevVelocity: new THREE.Vector3(1, 0, 0), | |
| totalTime: 0, | |
| thrusterIntensity: 1.0, | |
| opacity: 1.0, | |
| targetOp: 1.0, | |
| // Teleport state | |
| teleportCooldown: 15 + Math.random() * 20, // Time until next teleport | |
| teleportTimer: 0, | |
| isTeleporting: false, | |
| teleportPhase: 0, | |
| teleportDuration: 3.5, // How long to stay at Market Directive | |
| teleportFlashIntensity: 0, | |
| preTeleportPos: null, | |
| preTeleportQuat: null | |
| }; | |
| const initialDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize(); | |
| st.currentQuat = quaternionFromDirection(initialDir); | |
| st.targetQuat = st.currentQuat.clone(); | |
| astronautGroup.quaternion.copy(st.currentQuat); | |
| astronautGroup.position.copy(st.waypoints[1]); | |
| function advanceWaypoint() { | |
| st.waypoints[0] = st.waypoints[1].clone(); | |
| st.waypoints[1] = st.waypoints[2].clone(); | |
| st.waypoints[2] = st.waypoints[3].clone(); | |
| const currentDir = new THREE.Vector3().subVectors(st.waypoints[2], st.waypoints[1]).normalize(); | |
| st.waypoints[3] = generateWaypoint(st.waypoints[2], currentDir); | |
| st.t = 0; | |
| st.segmentDuration = 3 + Math.random() * 3; | |
| st.cruiseSpeed = 1.0 + Math.random() * 0.5; | |
| } | |
| // ──────────── UPDATE FUNCTION (UFO Flight) ──────────── | |
| updateAstronaut = function(time, deltaTime) { | |
| const dt = Math.min(deltaTime || 0.016, 0.05); | |
| st.totalTime += dt; | |
| // ════════════════════════════════════════════════════════ | |
| // █ SUPERMAN TRAJECTORY — Spline + Velocity Profile █ | |
| // ════════════════════════════════════════════════════════ | |
| const accelZone = 0.15; | |
| const decelZone = 0.85; | |
| let speedMultiplier; | |
| if (st.t < accelZone) { | |
| speedMultiplier = smootherstep(0, accelZone, st.t); | |
| } else if (st.t > decelZone) { | |
| speedMultiplier = smootherstep(1, decelZone, st.t); | |
| } else { | |
| speedMultiplier = 1.0; | |
| } | |
| st.currentSpeed = speedMultiplier * st.cruiseSpeed; | |
| const baseProgress = dt / st.segmentDuration; | |
| st.t += baseProgress * (0.5 + st.currentSpeed * 1.2); | |
| if (st.t >= 1.0) { | |
| st.t = 0; | |
| advanceWaypoint(); | |
| } | |
| const splinePos = catmullRom( | |
| st.waypoints[0], st.waypoints[1], | |
| st.waypoints[2], st.waypoints[3], | |
| st.t | |
| ); | |
| st.verticalWavePhase += dt * st.verticalWaveFreq * Math.PI * 2; | |
| const verticalOffset = Math.sin(st.verticalWavePhase) * st.verticalWaveAmp; | |
| splinePos.y += verticalOffset; | |
| // ════════════════════════════════════════════════════════ | |
| // █ TELEPORT TO MARKET DIRECTIVE █ | |
| // ════════════════════════════════════════════════════════ | |
| st.teleportTimer += dt; | |
| // Check if it's time to teleport | |
| if (!st.isTeleporting && st.teleportTimer >= st.teleportCooldown) { | |
| // Start teleport sequence | |
| st.isTeleporting = true; | |
| st.teleportPhase = 0; | |
| st.teleportFlashIntensity = 1.0; | |
| st.preTeleportPos = astronautGroup.position.clone(); | |
| st.preTeleportQuat = astronautGroup.quaternion.clone(); | |
| // Get Market Directive element position | |
| const directiveEl = document.querySelector('.signal-panel'); | |
| if (directiveEl) { | |
| const rect = directiveEl.getBoundingClientRect(); | |
| // Convert screen position to normalized device coordinates | |
| const screenX = (rect.left + rect.width / 2) / window.innerWidth * 2 - 1; | |
| const screenY = -((rect.top + rect.height / 2) / window.innerHeight * 2 - 1); | |
| // Project to 3D space (in front of camera) | |
| const teleportZ = 150; // Close to camera | |
| const fov = camera.fov * Math.PI / 180; | |
| const height = 2 * Math.tan(fov / 2) * teleportZ; | |
| const width = height * camera.aspect; | |
| st.teleportTarget = new THREE.Vector3( | |
| screenX * width / 2, | |
| screenY * height / 2 + 20, // Slightly above center | |
| camera.position.z - teleportZ | |
| ); | |
| } | |
| } | |
| // Handle teleport animation | |
| if (st.isTeleporting) { | |
| st.teleportPhase += dt; | |
| // Flash effect fades | |
| st.teleportFlashIntensity = Math.max(0, st.teleportFlashIntensity - dt * 2); | |
| if (st.teleportPhase < st.teleportDuration) { | |
| // At Market Directive - hover with gentle movement | |
| if (st.teleportTarget) { | |
| const hoverOffset = new THREE.Vector3( | |
| Math.sin(st.teleportPhase * 2) * 8, | |
| Math.sin(st.teleportPhase * 1.5) * 5, | |
| Math.sin(st.teleportPhase * 1.8) * 4 | |
| ); | |
| astronautGroup.position.copy(st.teleportTarget).add(hoverOffset); | |
| // Face the camera (slightly tilted) | |
| const lookDir = new THREE.Vector3(0, 0, 1); | |
| const tiltAngle = Math.sin(st.teleportPhase * 2) * 0.15; | |
| st.targetQuat = quaternionFromDirection(lookDir); | |
| const tiltQuat = new THREE.Quaternion().setFromAxisAngle( | |
| new THREE.Vector3(1, 0, 0), tiltAngle | |
| ); | |
| st.targetQuat.multiply(tiltQuat); | |
| st.currentQuat.slerp(st.targetQuat, 0.1); | |
| astronautGroup.quaternion.copy(st.currentQuat); | |
| } | |
| } else { | |
| // Teleport back - flash and return | |
| if (st.teleportPhase < st.teleportDuration + 0.5) { | |
| st.teleportFlashIntensity = 1.0 - (st.teleportPhase - st.teleportDuration) * 2; | |
| } else { | |
| // End teleport | |
| st.isTeleporting = false; | |
| st.teleportTimer = 0; | |
| st.teleportCooldown = 20 + Math.random() * 25; // Next teleport in 20-45 seconds | |
| st.teleportFlashIntensity = 0; | |
| // Reset waypoints from current spline position | |
| st.waypoints[1] = splinePos.clone(); | |
| const dir = st.prevVelocity.clone().normalize(); | |
| st.waypoints[0] = splinePos.clone().sub(dir.clone().multiplyScalar(50)); | |
| st.waypoints[2] = generateWaypoint(splinePos, dir); | |
| st.waypoints[3] = generateWaypoint(st.waypoints[2], dir); | |
| st.t = 0; | |
| } | |
| } | |
| } else { | |
| // Normal flight - apply spline position | |
| astronautGroup.position.copy(splinePos); | |
| } | |
| // ════════════════════════════════════════════════════════ | |
| // █ ORIENTATION — Face Velocity + Banking █ | |
| // ════════════════════════════════════════════════════════ | |
| // Only do normal orientation when not teleporting | |
| if (!st.isTeleporting) { | |
| const velocity = catmullRomTangent( | |
| st.waypoints[0], st.waypoints[1], | |
| st.waypoints[2], st.waypoints[3], | |
| st.t | |
| ); | |
| const turnAxis = new THREE.Vector3().crossVectors(st.prevVelocity, velocity); | |
| const turnAmount = turnAxis.y; | |
| st.targetBankAngle = -turnAmount * 35 * (Math.PI / 180); | |
| st.bankAngle += (st.targetBankAngle - st.bankAngle) * 0.08; | |
| st.prevVelocity.copy(velocity); | |
| st.targetQuat = quaternionFromDirection(velocity); | |
| const bankQuat = new THREE.Quaternion(); | |
| bankQuat.setFromAxisAngle(velocity, st.bankAngle); | |
| st.targetQuat.multiply(bankQuat); | |
| st.currentQuat.slerp(st.targetQuat, 0.12); | |
| astronautGroup.quaternion.copy(st.currentQuat); | |
| } | |
| // ════════════════════════════════════════════════════════ | |
| // █ UFO LIGHT ANIMATIONS █ | |
| // ════════════════════════════════════════════════════════ | |
| // Rim lights pulse in sequence (brighter) | |
| for (let i = 0; i < rimLightCount; i++) { | |
| const phase = time * 2 + (i / rimLightCount) * Math.PI * 2; | |
| rimLightMats[i].emissiveIntensity = 2.5 + Math.sin(phase) * 1.0; | |
| rimLightMats[i].opacity = 0.85 + Math.sin(phase) * 0.15; | |
| } | |
| // Dome lights alternate (brighter) | |
| for (let i = 0; i < domeLightCount; i++) { | |
| const phase = time * 3 + (i / domeLightCount) * Math.PI * 2; | |
| domeLightMats[i].emissiveIntensity = 2.0 + Math.sin(phase) * 1.0; | |
| } | |
| // Inner dome pulse (brighter) | |
| mat.domeInner.emissiveIntensity = 0.8 + Math.sin(time * 1.5) * 0.4; | |
| mat.domeInner.opacity = 0.5 + Math.sin(time * 1.5) * 0.15; | |
| // ════════════════════════════════════════════════════════ | |
| // █ THRUST BEAM ANIMATIONS (Signal-Reactive Color) █ | |
| // ════════════════════════════════════════════════════════ | |
| // Check dominant signal for beam color | |
| const signalEl = document.getElementById('dominantSignal'); | |
| const dominantSignal = signalEl ? signalEl.textContent.trim().toUpperCase() : 'NEUTRAL'; | |
| const isSellSignal = dominantSignal === 'SELL'; | |
| const isBuySignal = dominantSignal === 'BUY'; | |
| const isNeutral = !isSellSignal && !isBuySignal; | |
| // Color targets based on signal | |
| // SELL = Red, BUY = Blue, NEUTRAL = Golden | |
| let targetCoreColor, targetCoreEmissive, targetMidColor, targetMidEmissive; | |
| let targetOuterColor, targetOuterEmissive, targetParticleColor, targetLightColor; | |
| if (isSellSignal) { | |
| // RED for SELL | |
| targetCoreColor = new THREE.Color(0xff4444); | |
| targetCoreEmissive = new THREE.Color(0xff2222); | |
| targetMidColor = new THREE.Color(0xff6644); | |
| targetMidEmissive = new THREE.Color(0xee4422); | |
| targetOuterColor = new THREE.Color(0xcc4422); | |
| targetOuterEmissive = new THREE.Color(0xaa3311); | |
| targetParticleColor = new THREE.Color(0xff6655); | |
| targetLightColor = 0xff4433; | |
| } else if (isBuySignal) { | |
| // BLUE for BUY | |
| targetCoreColor = new THREE.Color(0x00ccff); | |
| targetCoreEmissive = new THREE.Color(0x00aaff); | |
| targetMidColor = new THREE.Color(0x00aaff); | |
| targetMidEmissive = new THREE.Color(0x0088ee); | |
| targetOuterColor = new THREE.Color(0x0088dd); | |
| targetOuterEmissive = new THREE.Color(0x0066bb); | |
| targetParticleColor = new THREE.Color(0x00eeff); | |
| targetLightColor = 0x00aaff; | |
| } else { | |
| // GOLDEN for NEUTRAL | |
| targetCoreColor = new THREE.Color(0xffcc44); | |
| targetCoreEmissive = new THREE.Color(0xffaa22); | |
| targetMidColor = new THREE.Color(0xddaa33); | |
| targetMidEmissive = new THREE.Color(0xcc9922); | |
| targetOuterColor = new THREE.Color(0xbb8822); | |
| targetOuterEmissive = new THREE.Color(0x996611); | |
| targetParticleColor = new THREE.Color(0xffdd66); | |
| targetLightColor = 0xffaa33; | |
| } | |
| // Smoothly lerp colors | |
| mat.thrustCore.color.lerp(targetCoreColor, 0.08); | |
| mat.thrustCore.emissive.lerp(targetCoreEmissive, 0.08); | |
| mat.thrustMid.color.lerp(targetMidColor, 0.08); | |
| mat.thrustMid.emissive.lerp(targetMidEmissive, 0.08); | |
| mat.thrustOuter.color.lerp(targetOuterColor, 0.08); | |
| mat.thrustOuter.emissive.lerp(targetOuterEmissive, 0.08); | |
| thrustParticleMat.color.lerp(targetParticleColor, 0.08); | |
| // Lerp light color | |
| const currentLightColor = new THREE.Color(thrustLight.color); | |
| currentLightColor.lerp(new THREE.Color(targetLightColor), 0.08); | |
| thrustLight.color.copy(currentLightColor); | |
| // Thrust intensity based on speed (boost during teleport) | |
| const isAccelerating = st.t < accelZone && st.currentSpeed > 0.3; | |
| const isTurning = Math.abs(st.targetBankAngle) > 0.05; | |
| const thrustActive = isAccelerating || isTurning || st.currentSpeed > 0.5 || st.isTeleporting; | |
| if (thrustActive) { | |
| st.thrusterIntensity = Math.min(st.thrusterIntensity + dt * 3, st.isTeleporting ? 1.2 : 1); | |
| } else { | |
| st.thrusterIntensity = Math.max(st.thrusterIntensity - dt * 2, 0.3); | |
| } | |
| // Core beam intensity fluctuation (brighter) | |
| mat.thrustCore.emissiveIntensity = (3.5 + Math.sin(time * 8) * 0.8) * st.thrusterIntensity; | |
| mat.thrustCore.opacity = (0.9 + Math.sin(time * 6) * 0.1) * st.thrusterIntensity; | |
| // Beam layers subtle rotation | |
| beamCore.rotation.y = time * 0.5; | |
| beamMid.rotation.y = -time * 0.3; | |
| beamOuter.rotation.y = time * 0.2; | |
| // Beam scale based on thrust intensity | |
| const thrustScale = 0.5 + st.thrusterIntensity * 0.5; | |
| thrustGroup.scale.set(thrustScale, thrustScale, thrustScale); | |
| // Thrust light flicker (brighter) | |
| thrustLight.intensity = (4.0 + Math.sin(time * 10) * 1.0 + Math.sin(time * 17) * 0.5) * st.thrusterIntensity; | |
| // Animate thrust particles - longer travel distance | |
| const positions = thrustParticleGeo.attributes.position.array; | |
| for (let i = 0; i < thrustParticleCount; i++) { | |
| positions[i * 3 + 1] -= thrustParticleSpeeds[i] * 0.6 * st.thrusterIntensity; | |
| if (positions[i * 3 + 1] < -38) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = Math.random() * 5; | |
| positions[i * 3] = Math.cos(angle) * radius; | |
| positions[i * 3 + 1] = 0; | |
| positions[i * 3 + 2] = Math.sin(angle) * radius; | |
| } | |
| positions[i * 3] *= 1.003; | |
| positions[i * 3 + 2] *= 1.003; | |
| } | |
| thrustParticleGeo.attributes.position.needsUpdate = true; | |
| thrustParticleMat.opacity = 0.85 * st.thrusterIntensity; | |
| // Main UFO light pulse (brighter) | |
| ufoMainLight.intensity = 1.2 + Math.sin(time * 1.5) * 0.4 + st.teleportFlashIntensity * 3; | |
| ufoRimLight.intensity = 0.8 + st.thrusterIntensity * 0.5 + st.teleportFlashIntensity * 2; | |
| // Teleport flash on all rim lights | |
| if (st.teleportFlashIntensity > 0) { | |
| for (let i = 0; i < rimLightCount; i++) { | |
| rimLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 5; | |
| } | |
| for (let i = 0; i < domeLightCount; i++) { | |
| domeLightMats[i].emissiveIntensity += st.teleportFlashIntensity * 4; | |
| } | |
| mat.domeInner.emissiveIntensity += st.teleportFlashIntensity * 3; | |
| } | |
| // ════════════════════════════════════════════════════════ | |
| // █ DEPTH OPACITY █ | |
| // ════════════════════════════════════════════════════════ | |
| // Make UFO fully visible during teleport or on mobile | |
| if (st.isTeleporting || isMobile) { | |
| st.targetOp = 1.0; | |
| st.opacity = 1.0; | |
| } else { | |
| const distCam = astronautGroup.position.distanceTo(camera.position); | |
| st.targetOp = distCam < 250 ? 0.4 : distCam < 450 ? 0.7 : 1.0; | |
| st.opacity += (st.targetOp - st.opacity) * 0.015; | |
| } | |
| }; | |
| } | |
| // ===== ANIMATION ===== | |
| let time = 0; | |
| let lastTime = performance.now(); | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const currentTime = performance.now(); | |
| const deltaTime = (currentTime - lastTime) / 1000; | |
| lastTime = currentTime; | |
| time += 0.016; // Consistent time for visual effects | |
| // Stars rotation | |
| stars.rotation.y += 0.0001; | |
| stars.rotation.x += 0.00005; | |
| // ===== PHYSICS SIMULATION ===== | |
| simulatePhysics(deltaTime); | |
| updateConnections(); | |
| // Update node shader time uniform | |
| nodeShaderMaterial.uniforms.time.value = time; | |
| // Gentle rotation of entire network | |
| nodePoints.rotation.y += 0.0002; | |
| lines.rotation.y += 0.0002; | |
| // Earth rotation | |
| earth.rotation.y += 0.0005; | |
| clouds.rotation.y += 0.0006; | |
| clouds.rotation.x += 0.0001; | |
| // Sun position for day/night cycle | |
| const sunAngle = time * 0.05; | |
| const sunDir = new THREE.Vector3( | |
| Math.cos(sunAngle), | |
| 0.3, | |
| Math.sin(sunAngle) | |
| ).normalize(); | |
| earthMaterial.uniforms.sunDirection.value = sunDir; | |
| sunLight.position.set(sunDir.x * 5, sunDir.y * 5, sunDir.z * 5); | |
| // Update atmosphere view vector | |
| atmosphereMaterial.uniforms.viewVector.value = new THREE.Vector3().subVectors( | |
| camera.position, | |
| earthGroup.position | |
| ); | |
| // ===== ASTRONAUT UPDATE ===== | |
| if (updateAstronaut) updateAstronaut(time, deltaTime); | |
| renderer.render(scene, camera); | |
| } | |
| // ===== RESIZE HANDLER ===== | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| // Initialize connections and start animation | |
| updateConnections(); | |
| animate(); | |
| // Hide loading after init | |
| setTimeout(() => { | |
| document.getElementById('loadingOverlay')?.classList.add('hidden'); | |
| }, 1500); | |
| })(); | |
| // ==================== DASHBOARD LOGIC ==================== | |
| const signalHistory = []; | |
| const SIGNAL_WINDOW = 5 * 60 * 1000; | |
| const TARGET_AGENT = '10m'; | |
| let consecutiveErrors = 0; | |
| // ==================== REDIS-BACKED SIGNAL TRACKING ==================== | |
| // ✅ MIGRATED: Signals now extracted from log polling instead of Ably WebSocket | |
| // This is compatible with HuggingFace Spaces + Redis architecture | |
| function extractAgentName(signalKey) { | |
| if (!signalKey) return 'unknown'; | |
| signalKey = signalKey.toString().trim().toLowerCase(); | |
| if (signalKey.includes('_')) { | |
| return signalKey.split('_')[0]; | |
| } | |
| const match = signalKey.match(/\d+[mhdw]/); | |
| return match ? match[0] : 'unknown'; | |
| } | |
| // Force Balance - Updated for new gauge design | |
| function updateForceBalance(buyPercent, sellPercent, dominant) { | |
| const buyFill = document.getElementById('gaugeFillBuy'); | |
| const sellFill = document.getElementById('gaugeFillSell'); | |
| const marker = document.getElementById('gaugeMarker'); | |
| const buyVal = document.getElementById('forceBuyValue'); | |
| const sellVal = document.getElementById('forceSellValue'); | |
| const netVal = document.getElementById('netPositionValue'); | |
| // Clamp values | |
| buyPercent = Math.min(100, Math.max(0, buyPercent || 0)); | |
| sellPercent = Math.min(100, Math.max(0, sellPercent || 0)); | |
| // Update fill bars using scaleX transform (0 to 1 range) | |
| if (buyFill) { | |
| buyFill.style.transform = `scaleX(${buyPercent / 100})`; | |
| } | |
| if (sellFill) { | |
| sellFill.style.transform = `scaleX(${sellPercent / 100})`; | |
| } | |
| // Update percentage values | |
| if (buyVal) buyVal.textContent = buyPercent.toFixed(1) + '%'; | |
| if (sellVal) sellVal.textContent = sellPercent.toFixed(1) + '%'; | |
| // Calculate and display net position | |
| const net = buyPercent - sellPercent; | |
| if (netVal) { | |
| netVal.textContent = (net >= 0 ? '+' : '') + net.toFixed(1); | |
| netVal.classList.remove('positive', 'negative', 'neutral'); | |
| if (net > 0) { | |
| netVal.classList.add('positive'); | |
| } else if (net < 0) { | |
| netVal.classList.add('negative'); | |
| } else { | |
| netVal.classList.add('neutral'); | |
| } | |
| } | |
| // Position marker: 50% = center | |
| // Buy pushes left (towards 0%), Sell pushes right (towards 100%) | |
| const markerPos = 50 + ((sellPercent - buyPercent) / 2); | |
| if (marker) { | |
| marker.style.left = markerPos + '%'; | |
| marker.classList.remove('buy-dominant', 'sell-dominant'); | |
| if (dominant === 'BUY') { | |
| marker.classList.add('buy-dominant'); | |
| } else if (dominant === 'SELL') { | |
| marker.classList.add('sell-dominant'); | |
| } | |
| } | |
| } | |
| function updateDominantSignal() { | |
| const now = Date.now(); | |
| const recentSignals = signalHistory.filter(s => now - s.timestamp < SIGNAL_WINDOW); | |
| const buyCount = recentSignals.filter(s => s.action === 'BUY').length; | |
| const sellCount = recentSignals.filter(s => s.action === 'SELL').length; | |
| const total = buyCount + sellCount; | |
| const el = document.getElementById('dominantSignal'); | |
| const statsEl = document.getElementById('signalStats'); | |
| let dominant = 'NEUTRAL'; | |
| if (total > 0) { | |
| if (buyCount > sellCount) dominant = 'BUY'; | |
| else if (sellCount > buyCount) dominant = 'SELL'; | |
| } | |
| if (el) { | |
| el.textContent = dominant; | |
| el.className = 'signal-value signal-' + dominant.toLowerCase(); | |
| } | |
| if (statsEl && total > 0) { | |
| const buyPct = ((buyCount / total) * 100).toFixed(0); | |
| const sellPct = ((sellCount / total) * 100).toFixed(0); | |
| statsEl.innerHTML = `<span>BUY: ${buyCount} (${buyPct}%)</span> • <span>SELL: ${sellCount} (${sellPct}%)</span> • <span>TOTAL: ${total}</span>`; | |
| updateForceBalance(parseFloat(buyPct), parseFloat(sellPct), dominant); | |
| } | |
| } | |
| // ==================== HELPER FUNCTIONS ==================== | |
| // ✅ Safe text setter — guards against null, undefined, NaN, and "undefined" strings | |
| function setText(id, text) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| if (text === null || text === undefined || String(text).includes('undefined') || String(text).includes('NaN')) { | |
| el.textContent = '—'; | |
| } else { | |
| el.textContent = text; | |
| } | |
| } | |
| } | |
| // ✅ Number formatter — compact display for large numbers | |
| function formatNumber(num) { | |
| if (num === undefined || num === null || isNaN(Number(num))) return '—'; | |
| num = Number(num); | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; | |
| return num.toLocaleString(); | |
| } | |
| // ✅ Safe number formatter with decimal control | |
| function safeNum(val, decimals = 1) { | |
| if (val === undefined || val === null || val === '' || isNaN(Number(val))) return null; | |
| return Number(val).toFixed(decimals); | |
| } | |
| // ✅ Safe format with decimal control (returns string or dash) | |
| function formatNum(val, decimals) { | |
| if (val === undefined || val === null || val === '' || isNaN(Number(val))) return '—'; | |
| return Number(val).toFixed(decimals); | |
| } | |
| function updateConnectionStatus(connected) { | |
| const el = document.getElementById('connectionStatus'); | |
| if (el) { | |
| el.textContent = connected ? '● Connected' : '● Disconnected'; | |
| el.className = 'connection-status ' + (connected ? 'connection-ok' : 'connection-error'); | |
| } | |
| } | |
| // ✅ MIGRATED: HF Spaces compatible metrics fetch with fallback patterns | |
| function updateMetrics() { | |
| fetch('/api/metrics') | |
| .then(r => r.json()) | |
| .then(data => { | |
| consecutiveErrors = 0; | |
| updateConnectionStatus(true); | |
| const now = new Date(); | |
| setText('lastUpdate', 'Last Observation: ' + now.toLocaleTimeString('en-US', { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit', | |
| hour12: false | |
| })); | |
| // V75: Update instrument bar from API asset field | |
| if (data.asset) { | |
| const instBar = document.getElementById('instBar'); | |
| if (instBar) instBar.textContent = data.asset; | |
| } | |
| // Training metrics - use data.metrics path | |
| if (data.metrics) { | |
| setText('trainingSteps', formatNumber(data.metrics.training_steps)); | |
| setText('actorLoss', formatNum(data.metrics.actor_loss, 2)); | |
| setText('criticLoss', formatNum(data.metrics.critic_loss, 4)); | |
| setText('avnLoss', formatNum(data.metrics.avn_loss, 4)); | |
| // AVN Accuracy with color coding | |
| const accEl = document.getElementById('avnAccuracy'); | |
| if (accEl) { | |
| const accRaw = data.metrics.avn_accuracy; | |
| const accValid = accRaw !== undefined && accRaw !== null && !isNaN(Number(accRaw)); | |
| if (accValid) { | |
| const accVal = Number(accRaw).toFixed(1); | |
| accEl.textContent = accVal + '%'; | |
| accEl.style.color = parseFloat(accVal) >= 55 ? '#00d4aa' : '#ffa500'; | |
| } else { | |
| accEl.textContent = '—'; | |
| } | |
| } | |
| setText('bufferSize', formatNumber(data.metrics.buffer_size)); | |
| // Reward metrics with match efficiency calculation | |
| const matched = data.metrics.matched_rewards || 0; | |
| const unmatched = data.metrics.unmatched_rewards || 0; | |
| const total = matched + unmatched; | |
| const matchRate = total > 0 ? (matched / total * 100).toFixed(1) + '%' : '—'; | |
| setText('matchedRewards', matched > 0 ? formatNumber(matched) : '—'); | |
| setText('unmatchedRewards', unmatched > 0 ? formatNumber(unmatched) : '—'); | |
| setText('duplicates', formatNumber(data.metrics.duplicates)); | |
| setText('matchRate', matchRate); | |
| } | |
| // ✅ Service status — handles different name formats from HF Spaces | |
| if (data.services) { | |
| updateServiceStatus('statusQuasar', data.services['quasar_engine'] || data.services['quasar'] || 'unknown'); | |
| updateServiceStatus('statusFeatures', data.services['features'] || data.services['feature_pipeline'] || 'active'); | |
| updateServiceStatus('statusRewards', data.services['rewards'] || data.services['reward_system'] || 'active'); | |
| } | |
| // Process count — handles HF Spaces format | |
| const processCount = data.process_count || data.resources?.process_count || 1; | |
| const procEl = document.getElementById('processCount'); | |
| const procVal = document.getElementById('processCountValue'); | |
| if (procVal) procVal.textContent = processCount; | |
| if (procEl) { | |
| procEl.classList.remove('process-ok', 'process-warning'); | |
| procEl.classList.add(processCount >= 1 ? 'process-ok' : 'process-warning'); | |
| } | |
| // ✅ System resources — handles HF Spaces data shapes | |
| if (data.resources) { | |
| const cpuVal = safeNum(data.resources.cpu_percent); | |
| setText('cpuPercent', cpuVal !== null ? cpuVal + '%' : '—'); | |
| const cpuBar = document.getElementById('cpuBar'); | |
| if (cpuBar) { | |
| cpuBar.style.width = (cpuVal !== null ? Math.min(parseFloat(cpuVal), 100) : 0) + '%'; | |
| cpuBar.className = 'progress-fill' + (parseFloat(cpuVal) > 80 ? ' warning' : ''); | |
| } | |
| const memPct = safeNum(data.resources.memory_percent); | |
| setText('memoryPercent', memPct !== null ? memPct + '%' : '—'); | |
| const memUsed = data.resources.memory_used_gb; | |
| const memTotal = data.resources.memory_total_gb; | |
| const memUsedValid = memUsed !== undefined && memUsed !== null && memUsed !== ''; | |
| const memTotalValid = memTotal !== undefined && memTotal !== null && memTotal !== ''; | |
| setText('memoryUsed', (memUsedValid && memTotalValid) | |
| ? memUsed + ' / ' + memTotal + ' GB' | |
| : '—'); | |
| const quasarMem = data.resources.quasar_memory_gb; | |
| const quasarValid = quasarMem !== undefined && quasarMem !== null && quasarMem !== ''; | |
| setText('quasarMemory', quasarValid ? quasarMem + ' GB' : '—'); | |
| } else { | |
| setText('cpuPercent', '—'); | |
| setText('memoryPercent', '—'); | |
| setText('memoryUsed', '—'); | |
| setText('quasarMemory', '—'); | |
| } | |
| // Checkpoint data | |
| if (data.checkpoint) { | |
| setText('checkpointFile', data.checkpoint.filename ?? '—'); | |
| setText('checkpointStep', data.checkpoint.step?.toLocaleString() ?? '—'); | |
| setText('checkpointSize', data.checkpoint.size_mb !== undefined ? data.checkpoint.size_mb + ' MB' : '—'); | |
| setText('checkpointModified', data.checkpoint.modified ?? '—'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching metrics:', error); | |
| consecutiveErrors++; | |
| if (consecutiveErrors >= 3) updateConnectionStatus(false); | |
| }); | |
| } | |
| function updateServiceStatus(elementId, status) { | |
| const elem = document.getElementById(elementId); | |
| if (!elem) return; | |
| elem.textContent = (status || 'unknown').toUpperCase(); | |
| if (status === 'active') { | |
| elem.className = 'status-badge status-active'; | |
| } else if (status === 'unknown' || status === undefined || status === null) { | |
| // Unknown = booting/pending, not necessarily failed — show amber | |
| elem.className = 'status-badge status-unknown'; | |
| elem.textContent = 'PENDING'; | |
| } else { | |
| elem.className = 'status-badge status-inactive'; | |
| } | |
| } | |
| // ✅ MIGRATED: HF Spaces compatible logs fetch | |
| // Strategy: Try /logs/quasar_engine (text) → fallback /api/logs/quasar (JSON) | |
| function updateLogs() { | |
| fetch('/logs/quasar_engine') | |
| .then(response => { | |
| if (!response.ok) { | |
| // Fallback to alternative endpoint | |
| return fetch('/api/logs/quasar'); | |
| } | |
| return response; | |
| }) | |
| .then(response => { | |
| const contentType = response.headers.get('content-type') || ''; | |
| if (contentType.includes('application/json')) { | |
| return response.json().then(data => (data.lines || []).join('\n')); | |
| } | |
| return response.text(); | |
| }) | |
| .then(logText => { | |
| const cont = document.getElementById('logContainer'); | |
| if (!cont) return; | |
| cont.innerHTML = ''; | |
| const lines = logText.split('\n').filter(line => line.trim()); | |
| const recentLines = lines.slice(-50); | |
| recentLines.forEach(line => { | |
| if (!line.trim()) return; | |
| const div = document.createElement('div'); | |
| div.className = 'log-line'; | |
| // Style log lines based on content | |
| if (line.includes('ERROR') || line.includes('error') || line.includes('Error') || line.includes('❌')) { | |
| div.classList.add('log-error'); | |
| } else if (line.includes('WARNING') || line.includes('warning') || line.includes('Warning') || line.includes('⚠️')) { | |
| div.classList.add('log-warning'); | |
| } else if (line.includes('INFO') || line.includes('✅') || line.includes('SUCCESS')) { | |
| div.classList.add('log-info'); | |
| } | |
| // Extract signals for dominant signal tracker - filter by target agent | |
| let agent = null; | |
| const agentMatch1 = line.match(/(\d+[mhdw])_\d+/i); | |
| if (agentMatch1) agent = agentMatch1[1].toLowerCase(); | |
| if (!agent) { | |
| const agentMatch2 = line.match(/[\[\(](\d+[mhdw])[\]\)]/i); | |
| if (agentMatch2) agent = agentMatch2[1].toLowerCase(); | |
| } | |
| if (!agent) { | |
| const agentMatch3 = line.match(/AVN[\s\-_]?(\d+[mhdw])[:\s]/i); | |
| if (agentMatch3) agent = agentMatch3[1].toLowerCase(); | |
| } | |
| // Skip signals from other agents | |
| if (agent && agent !== TARGET_AGENT) { | |
| return; | |
| } | |
| if (line.includes('AVN') && (line.includes('BUY') || line.includes('SELL'))) { | |
| const buyMatch = line.match(/BUY.*conf[idence]*[=:]\s*([\d.]+)/i); | |
| const sellMatch = line.match(/SELL.*conf[idence]*[=:]\s*([\d.]+)/i); | |
| if (buyMatch || sellMatch) { | |
| const signal = { | |
| action: buyMatch ? 'BUY' : 'SELL', | |
| confidence: buyMatch ? parseFloat(buyMatch[1]) : parseFloat(sellMatch[1]), | |
| timestamp: Date.now() | |
| }; | |
| signalHistory.push(signal); | |
| if (signalHistory.length > 100) { | |
| signalHistory.shift(); | |
| } | |
| } | |
| } | |
| div.textContent = line; | |
| cont.appendChild(div); | |
| }); | |
| cont.scrollTop = cont.scrollHeight; | |
| updateDominantSignal(); | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching logs:', error); | |
| }); | |
| } | |
| // Visitors | |
| function updateVisitors() { | |
| fetch('/api/visitors') | |
| .then(r => r.json()) | |
| .then(v => { | |
| setText('activeVisitors', v.active_now || 0); | |
| const tel = document.getElementById('totalVisitors'); | |
| if (tel) tel.textContent = (v.all_time_unique || 0) + ' total'; | |
| }) | |
| .catch(() => {}); | |
| } | |
| // Accuracy Chart | |
| let accuracyChart; | |
| function initChart() { | |
| const ctx = document.getElementById('accuracyChart')?.getContext('2d'); | |
| if (!ctx) return; | |
| // Chart.js gradient with earth tones | |
| const gradient = ctx.createLinearGradient(0, 0, 0, 200); | |
| gradient.addColorStop(0, 'rgba(0, 255, 136, 0.25)'); | |
| gradient.addColorStop(0.5, 'rgba(64, 192, 160, 0.1)'); | |
| gradient.addColorStop(1, 'rgba(0, 255, 136, 0)'); | |
| accuracyChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'AVN Accuracy', | |
| borderColor: '#00ff88', | |
| backgroundColor: gradient, | |
| borderWidth: 2.5, | |
| fill: true, | |
| tension: 0.4, | |
| pointRadius: 0, | |
| pointHoverRadius: 6, | |
| pointHoverBackgroundColor: '#00ff88', | |
| pointHoverBorderColor: '#fff', | |
| pointHoverBorderWidth: 2, | |
| data: [] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { | |
| intersect: false, | |
| mode: 'index' | |
| }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: 'rgba(0, 20, 40, 0.95)', | |
| titleColor: '#fff', | |
| titleFont: { size: 12, weight: '600', family: 'Outfit' }, | |
| bodyColor: '#00ff88', | |
| bodyFont: { size: 14, weight: '700', family: 'Space Mono' }, | |
| borderColor: 'rgba(0, 255, 136, 0.3)', | |
| borderWidth: 1, | |
| padding: 12, | |
| cornerRadius: 8, | |
| displayColors: false, | |
| callbacks: { | |
| title: (items) => `Iteration ${items[0].label}`, | |
| label: (item) => `Accuracy: ${item.raw.toFixed(1)}%` | |
| } | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| min: 0, | |
| max: 100, | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.04)', | |
| drawBorder: false | |
| }, | |
| border: { display: false }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.4)', | |
| callback: v => v + '%', | |
| font: { size: 11, family: 'Space Mono' }, | |
| padding: 10, | |
| stepSize: 20 | |
| } | |
| }, | |
| x: { | |
| grid: { display: false }, | |
| border: { display: false }, | |
| ticks: { | |
| color: 'rgba(255, 255, 255, 0.4)', | |
| maxTicksLimit: window.innerWidth <= 768 ? 5 : 8, | |
| font: { size: 10, family: 'Space Mono' }, | |
| padding: 8 | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ✅ MIGRATED: Resilient accuracy fetch with fallbacks for HF Spaces | |
| async function updateAccuracy() { | |
| try { | |
| // Try dedicated accuracy endpoints first | |
| let stats = null; | |
| let history = []; | |
| try { | |
| stats = await fetch('/api/accuracy/stats').then(r => r.json()); | |
| } catch (e) { | |
| // Fallback: extract from main metrics | |
| try { | |
| const metricsData = await fetch('/api/metrics').then(r => r.json()); | |
| if (metricsData.metrics && metricsData.metrics.avn_accuracy !== undefined) { | |
| stats = { | |
| current: metricsData.metrics.avn_accuracy, | |
| average: metricsData.metrics.avn_accuracy, | |
| last_10_avg: metricsData.metrics.avn_accuracy, | |
| trend: 'stable' | |
| }; | |
| } | |
| } catch (e2) { | |
| console.warn('Accuracy fallback also failed:', e2); | |
| } | |
| } | |
| if (stats) { | |
| setText('accuracy-current', (stats.current?.toFixed(1) || '—') + (stats.current !== undefined ? '%' : '')); | |
| setText('accuracy-avg', (stats.average?.toFixed(1) || '—') + (stats.average !== undefined ? '%' : '')); | |
| setText('accuracy-last10', (stats.last_10_avg?.toFixed(1) || '—') + (stats.last_10_avg !== undefined ? '%' : '')); | |
| const trend = document.getElementById('accuracy-trend'); | |
| if (trend && stats.trend) { | |
| trend.classList.remove('rising', 'stable', 'declining'); | |
| if (stats.trend === 'up') { | |
| trend.classList.add('rising'); | |
| trend.innerHTML = '<span class="trend-arrow">▲</span><span>RISING</span>'; | |
| } else if (stats.trend === 'down') { | |
| trend.classList.add('declining'); | |
| trend.innerHTML = '<span class="trend-arrow">▼</span><span>DECLINING</span>'; | |
| } else { | |
| trend.classList.add('stable'); | |
| trend.innerHTML = '<span class="trend-arrow">―</span><span>STABLE</span>'; | |
| } | |
| } | |
| } | |
| // Try to get accuracy history — real data only | |
| try { | |
| history = await fetch('/api/accuracy/history').then(r => r.json()); | |
| } catch (e) { | |
| console.warn('Accuracy history endpoint unavailable'); | |
| } | |
| // Update chart | |
| if (history.length > 0 && accuracyChart) { | |
| const recent = history.slice(-(window.innerWidth <= 768 ? 30 : 50)); | |
| accuracyChart.data.labels = recent.map((_, i) => i + 1); | |
| accuracyChart.data.datasets[0].data = recent.map(d => d.accuracy); | |
| accuracyChart.update('none'); | |
| } | |
| } catch(e) { | |
| console.warn('Accuracy fetch failed:', e); | |
| } | |
| } | |
| // ✅ INITIALIZATION — Tuned for HuggingFace Spaces + Redis | |
| updateMetrics(); | |
| updateLogs(); | |
| updateVisitors(); | |
| initChart(); | |
| setTimeout(updateAccuracy, 500); | |
| // Intervals — gentler on HF Spaces (slightly slower than local dev) | |
| setInterval(updateMetrics, 5000); // Every 5 seconds | |
| setInterval(updateLogs, 8000); // Every 8 seconds | |
| setInterval(updateVisitors, 30000); // Every 30 seconds | |
| setInterval(updateAccuracy, 15000); // Every 15 seconds | |
| // ✅ Debug info for HF Spaces | |
| console.log('🌌 K1RL QUASAR Full-Featured Dashboard initialized'); | |
| console.log('Platform: HuggingFace Spaces (Redis-backed)'); | |
| console.log('Signal Tracking: Log-based extraction (replaced Ably WebSocket)'); | |
| console.log('Target Agent:', TARGET_AGENT); | |
| console.log('API Endpoints: /api/metrics, /logs/quasar_engine, /api/visitors, /api/accuracy/*'); | |
| </script> | |
| </body> | |
| </html> |