Spaces:
Sleeping
Sleeping
| // Global Variables | |
| let debounceTimer; | |
| // --- INITIALIZATION --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const accessKey = sessionStorage.getItem('accessKey'); | |
| if (!accessKey) { | |
| window.location.href = '/'; | |
| return; | |
| } | |
| // Key is deliberately persisted across page reloads and back/forward navigation | |
| // Expandable Cards Logic | |
| document.addEventListener('click', (e) => { | |
| const card = e.target.closest('.expandable-card'); | |
| if (card) { | |
| // Close others (optional accordion style) or just toggle this one: | |
| card.classList.toggle('expanded'); | |
| } | |
| }); | |
| // Mouse Glow Tracking | |
| document.addEventListener("mousemove", (e) => { | |
| document.querySelectorAll(".mouse-glow, .glass-panel, .expandable-card").forEach((el) => { | |
| const rect = el.getBoundingClientRect(); | |
| el.style.setProperty("--mouse-x", `${e.clientX - rect.left}px`); | |
| el.style.setProperty("--mouse-y", `${e.clientY - rect.top}px`); | |
| el.classList.add("mouse-glow"); // dynamically attach glow class if not present | |
| }); | |
| }); | |
| initGSAPAnimations(); | |
| initMarketTicker(); | |
| initHeroRadar(); | |
| init3DUniverse(); | |
| const riskSlider = document.getElementById('risk'); | |
| const riskVal = document.getElementById('riskVal'); | |
| if (riskSlider && riskVal) { | |
| riskSlider.addEventListener('input', (e) => { | |
| riskVal.textContent = e.target.value; | |
| // GSAP tactical feedback animation | |
| gsap.fromTo(riskVal, | |
| { scale: 1.5, color: '#3b82f6', textShadow: '0 0 20px #3b82f6' }, | |
| { scale: 1, color: '#f8fafc', textShadow: 'none', duration: 0.4, ease: "back.out(1.7)" } | |
| ); | |
| }); | |
| } | |
| // Attach preview listeners ONLY to explicit change events, not typing/sliding | |
| const inputs = document.querySelectorAll('#portfolioForm input, #portfolioForm select'); | |
| inputs.forEach(input => { | |
| // Removed the 'input' event listener to disable live auto-updating | |
| }); | |
| // Removed explicit Preview Button since live panel is deleted | |
| // Suite Tabs logic | |
| document.querySelectorAll('.suite-tab').forEach(tab => { | |
| tab.addEventListener('click', (e) => { | |
| document.querySelectorAll('.suite-tab').forEach(t => t.classList.remove('active')); | |
| e.target.classList.add('active'); | |
| // Currently all tabs just show the "View Comprehensive Report" button | |
| }); | |
| }); | |
| // Main Form Submit (Full Report) | |
| const form = document.getElementById('portfolioForm'); | |
| if (form) { | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| await generateFullReport(); | |
| }); | |
| } | |
| // Router History Listener | |
| window.addEventListener('popstate', (e) => { | |
| if (e.state && e.state.viewId) { | |
| switchView(e.state.viewId, false); | |
| } else { | |
| // Handle hash fallback or default home | |
| const hash = window.location.hash.replace('#', ''); | |
| if (hash) { | |
| switchView(hash, false); | |
| } else { | |
| switchView('hero', false); | |
| } | |
| } | |
| }); | |
| // Check initial hash | |
| const initialHash = window.location.hash.replace('#', ''); | |
| if (initialHash) { | |
| switchView(initialHash, false); | |
| } | |
| }); | |
| // --- NAVIGATION ROUTER --- | |
| window.switchView = function(viewId, pushHistory = true) { | |
| document.querySelectorAll('.view-section').forEach(el => { | |
| el.classList.remove('active'); | |
| el.style.opacity = 0; | |
| }); | |
| document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active')); | |
| const targetView = document.getElementById('view-' + viewId); | |
| if(targetView) { | |
| targetView.classList.add('active'); | |
| // Elegant GSAP fade in | |
| if (window.gsap) { | |
| gsap.fromTo(targetView, | |
| { opacity: 0, y: 30 }, | |
| { opacity: 1, y: 0, duration: 0.6, ease: "power2.out" } | |
| ); | |
| } else { | |
| targetView.style.opacity = 1; | |
| } | |
| } | |
| const link = document.querySelector(`.nav-link[data-target="${viewId}"]`); | |
| if(link) link.classList.add('active'); | |
| if (pushHistory) { | |
| window.history.pushState({ viewId: viewId }, '', '#' + viewId); | |
| } | |
| }; | |
| // --- GSAP ANIMATIONS --- | |
| function initGSAPAnimations() { | |
| if (typeof gsap === 'undefined') return; | |
| if (typeof ScrollTrigger !== 'undefined') { | |
| gsap.registerPlugin(ScrollTrigger); | |
| } | |
| // Staggered entry for Model Zoo Cards | |
| document.querySelectorAll('.zoo-grid').forEach(grid => { | |
| const cards = grid.querySelectorAll('.expandable-card'); | |
| if (cards.length === 0) return; | |
| gsap.fromTo(cards, | |
| { opacity: 0, y: 50 }, | |
| { | |
| opacity: 1, | |
| y: 0, | |
| duration: 0.8, | |
| stagger: 0.15, | |
| ease: "power3.out", | |
| scrollTrigger: { | |
| trigger: grid, | |
| start: "top 85%" | |
| } | |
| } | |
| ); | |
| }); | |
| } | |
| // --- MARKET TICKER --- | |
| async function initMarketTicker() { | |
| try { | |
| const res = await fetch('/api/market_ticker'); | |
| const data = await res.json(); | |
| const container = document.getElementById('liveTickerContent'); | |
| if(data && data.length > 0) { | |
| let html = ''; | |
| // Duplicate array for seamless infinite scrolling | |
| const displayData = [...data, ...data, ...data]; | |
| displayData.forEach(item => { | |
| const colorClass = item.change >= 0 ? 'ticker-positive' : 'ticker-negative'; | |
| const sign = item.change > 0 ? '+' : ''; | |
| html += `<div class="ticker-item"> | |
| <strong>${item.name}</strong> | |
| <span>${item.price.toLocaleString()}</span> | |
| <span class="${colorClass}">${sign}${item.change}%</span> | |
| </div>`; | |
| }); | |
| container.innerHTML = html; | |
| } else { | |
| container.innerHTML = "<span>Market data unavailable</span>"; | |
| } | |
| } catch(e) { | |
| console.error("Ticker fetch failed:", e); | |
| } | |
| } | |
| // --- PAYLOAD GENERATOR --- | |
| function getPayload() { | |
| let custom_constraints = []; | |
| const advInput = document.getElementById('custom_constraints_input'); | |
| if (advInput && advInput.value.trim() !== '') { | |
| const lines = advInput.value.split('\n'); | |
| lines.forEach(line => { | |
| const parts = line.split(',').map(p => p.trim()); | |
| if (parts.length === 3) { | |
| let asset = parts[0]; | |
| let direction = parts[1].toLowerCase(); | |
| let limit = parseFloat(parts[2]); | |
| if (!isNaN(limit)) { | |
| custom_constraints.push({ | |
| asset: asset, | |
| direction: direction, | |
| limit: limit / 100.0 | |
| }); | |
| } | |
| } | |
| }); | |
| } | |
| return { | |
| tickers: document.getElementById('tickers').value.split(',').map(t => t.trim()).filter(t => t), | |
| capital: parseFloat(document.getElementById('capital').value) || 100000, | |
| risk_input: parseInt(document.getElementById('risk').value), | |
| model: parseInt(document.getElementById('model').value), | |
| allocation_engine: parseInt(document.getElementById('allocation_engine').value), | |
| allow_shorting: document.getElementById('allow_shorting').checked, | |
| tax_enabled: document.getElementById('tax_enabled').checked, | |
| garch_enabled: document.getElementById('garch_enabled').checked, | |
| custom_constraints: custom_constraints | |
| }; | |
| } | |
| // --- HERO RADAR CHART --- | |
| function initHeroRadar() { | |
| const ctx = document.getElementById('heroRadarChart'); | |
| if (!ctx) return; | |
| const data = { | |
| labels: ['Value', 'Momentum', 'Quality', 'Low Volatility', 'Yield'], | |
| datasets: [{ | |
| label: 'Current Regime Exposure', | |
| data: [65, 85, 40, 70, 50], | |
| backgroundColor: 'rgba(96, 165, 250, 0.2)', | |
| borderColor: 'rgba(96, 165, 250, 1)', | |
| pointBackgroundColor: 'rgba(96, 165, 250, 1)', | |
| pointBorderColor: '#fff', | |
| pointHoverBackgroundColor: '#fff', | |
| pointHoverBorderColor: 'rgba(96, 165, 250, 1)' | |
| }] | |
| }; | |
| const config = { | |
| type: 'radar', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| r: { | |
| angleLines: { color: 'rgba(255, 255, 255, 0.1)' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.1)' }, | |
| pointLabels: { color: '#94a3b8', font: { size: 11, family: 'Inter' } }, | |
| ticks: { display: false, max: 100, min: 0 } | |
| } | |
| }, | |
| plugins: { | |
| legend: { display: false } | |
| } | |
| } | |
| }; | |
| const chart = new Chart(ctx, config); | |
| // Simulate dynamic factor shifting | |
| setInterval(() => { | |
| chart.data.datasets[0].data = chart.data.datasets[0].data.map(val => { | |
| let shift = (Math.random() - 0.5) * 15; | |
| return Math.max(10, Math.min(100, val + shift)); | |
| }); | |
| chart.update('active'); | |
| }, 3000); | |
| } | |
| // --- FULL REPORT GENERATION --- | |
| async function generateFullReport() { | |
| const payload = getPayload(); | |
| const accessKey = sessionStorage.getItem('accessKey') || ""; | |
| // Trigger Cinematic Matrix Loader | |
| const matrixLoader = document.getElementById('matrix-loader'); | |
| const matrixLogs = document.getElementById('matrix-logs'); | |
| const matrixProgress = document.getElementById('matrix-progress'); | |
| matrixLoader.style.display = 'flex'; | |
| matrixLogs.innerHTML = ''; | |
| matrixProgress.style.width = '0%'; | |
| document.body.classList.add('engine-running'); | |
| const steps = [ | |
| "Initializing quantum-inspired core engine...", | |
| "Fetching multi-asset historical data vectors...", | |
| "Computing eigen-decomposition of covariance matrix Σ...", | |
| "Simulating 1,500 Monte Carlo forward paths...", | |
| "Applying dynamic L1-Norm tax constraints...", | |
| "Identifying Hidden Markov Model market regimes...", | |
| "Converging non-linear convex solver...", | |
| "Compiling institutional HTML report matrix..." | |
| ]; | |
| const infiniteSteps = [ | |
| "Calculating gradients...", | |
| "Rebalancing tensors...", | |
| "Analyzing tail risks...", | |
| "Synchronizing with Hugging Face ML Cluster...", | |
| "Backpropagating loss function...", | |
| "Filtering noise from alpha signals...", | |
| "Executing stochastic bounds check...", | |
| "Optimizing L2 regularization weights..." | |
| ]; | |
| let logIdx = 0; | |
| let infiniteIdx = 0; | |
| const interval = setInterval(() => { | |
| const el = document.createElement('div'); | |
| if(logIdx < steps.length) { | |
| el.innerHTML = `<span style="color: #fff">></span> ${steps[logIdx]}`; | |
| matrixProgress.style.width = `${((logIdx + 1) / steps.length) * 100}%`; | |
| logIdx++; | |
| } else { | |
| // Infinite loading loop | |
| el.innerHTML = `<span style="color: #3b82f6">></span> <span style="opacity: 0.8;">[ML CLUSTER]</span> ${infiniteSteps[infiniteIdx % infiniteSteps.length]} <span class="spinner-anim">◓</span>`; | |
| infiniteIdx++; | |
| // Keep progress bar pulsing | |
| matrixProgress.style.opacity = (Math.sin(infiniteIdx) * 0.5 + 0.5).toString(); | |
| } | |
| matrixLogs.appendChild(el); | |
| // Auto scroll | |
| const scrollAmt = Math.max(0, matrixLogs.children.length - 6); | |
| if (scrollAmt > 0) { | |
| matrixLogs.style.transform = `translateY(-${scrollAmt * 20}px)`; | |
| } | |
| }, 800); // Cinematic log streaming | |
| try { | |
| const res = await fetch('/api/generate', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Access-Key': accessKey | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!res.ok) { | |
| clearInterval(interval); | |
| let errTxt = "Generation Failed"; | |
| try { | |
| const errData = await res.json(); | |
| errTxt = errData.detail || errTxt; | |
| } catch (e) {} | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> ERROR: ${errTxt}</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| }, 4000); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (data.status !== "queued") { | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> Unexpected server response.</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| }, 3000); | |
| return; | |
| } | |
| const taskId = data.task_id; | |
| matrixLogs.innerHTML += `<div style="color: #60a5fa; margin-top: 1rem;">> Job ${taskId.substring(0,8)} queued. Polling matrix cluster...</div>`; | |
| const pollInterval = setInterval(async () => { | |
| try { | |
| const statusRes = await fetch(`/api/status/${taskId}`, { | |
| headers: { 'X-Access-Key': accessKey } | |
| }); | |
| if (!statusRes.ok) { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444;">> Polling failed.</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| }, 3000); | |
| return; | |
| } | |
| const statusData = await statusRes.json(); | |
| if (statusData.status === "completed") { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixProgress.style.width = '100%'; | |
| matrixLogs.innerHTML += `<div style="color: #10b981; margin-top: 1rem; font-weight: bold;">> MATRIX OPTIMIZATION COMPLETE. REDIRECTING...</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| window.location.href = "/report"; | |
| }, 1500); | |
| } else if (statusData.status === "error") { | |
| clearInterval(pollInterval); | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: ${statusData.message}</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| }, 4000); | |
| } | |
| } catch (err) { | |
| // Ignore network blips during polling | |
| } | |
| }, 2000); | |
| } catch(err) { | |
| clearInterval(interval); | |
| matrixLogs.innerHTML += `<div style="color: #ef4444; margin-top: 1rem;">> CRITICAL ERROR: Network failure.</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| document.body.classList.remove('engine-running'); | |
| }, 4000); | |
| } | |
| } | |
| // --- WIZARD LOGIC --- | |
| window.nextWizardStep = function(step) { | |
| document.querySelectorAll('.wizard-step').forEach(el => el.style.display = 'none'); | |
| const target = document.getElementById('wizardStep' + step); | |
| if(target) { | |
| target.style.display = 'block'; | |
| } | |
| }; | |
| window.runWizard = async function() { | |
| const macro = document.getElementById('wizardMacro').value; | |
| const reaction = document.getElementById('wizardReaction').value; | |
| const basket = document.getElementById('wizardBasket').value; | |
| // Auto-fill the Sandbox form under the hood | |
| document.getElementById('tickers').value = basket; | |
| let risk = 5; | |
| if (reaction === 'buy') risk = 2; | |
| if (reaction === 'hold') risk = 5; | |
| if (reaction === 'sell') risk = 8; | |
| document.getElementById('risk').value = risk; | |
| document.getElementById('riskVal').textContent = risk; | |
| let model = 5; // XGBoost default | |
| if (macro === 'growth') model = 4; // Fama-French | |
| if (macro === 'recession') model = 7; // HMM | |
| if (macro === 'inflation') model = 3; // Bayesian Shrinkage | |
| document.getElementById('model').value = model.toString(); | |
| // Switch to Sandbox view | |
| const modal = document.getElementById('wizardOverlay'); | |
| if(modal) modal.style.display = 'none'; | |
| switchView('sandbox'); | |
| // Trigger the full report generation automatically | |
| await generateFullReport(); | |
| }; | |
| // 3D Universe logic removed per user request (Restored Vanta.js NET) | |
| // --- REPORT FRAME LOGIC --- | |
| window.openReportFrame = async function() { | |
| const reportContainer = document.getElementById('reportContainer'); | |
| const reportView = document.getElementById('report-view'); | |
| // Check if report actually exists before opening iframe | |
| try { | |
| const checkRes = await fetch('/report'); | |
| if (!checkRes.ok) { | |
| alert("Report generation failed or returned a blank response. Check server logs."); | |
| return; | |
| } | |
| } catch(e) { | |
| alert("Error fetching report."); | |
| return; | |
| } | |
| document.querySelector('.main-content').style.display = 'none'; | |
| document.querySelector('nav').style.display = 'none'; | |
| document.querySelector('.market-ticker-bar').style.display = 'none'; | |
| reportContainer.style.display = 'block'; | |
| reportView.src = '/report?t=' + new Date().getTime(); | |
| }; | |
| window.closeReport = function() { | |
| document.getElementById('reportContainer').style.display = 'none'; | |
| document.querySelector('.main-content').style.display = 'block'; | |
| document.querySelector('nav').style.display = 'flex'; | |
| document.querySelector('.market-ticker-bar').style.display = 'flex'; | |
| }; | |
| // Ambient Background relies entirely on Vanta JS now. | |
| // --- AUTHENTICATION FLOW --- | |
| window.logout = function() { | |
| sessionStorage.removeItem('accessKey'); | |
| window.location.href = '/'; | |
| }; | |