// Global Variables let debounceTimer; // --- INITIALIZATION --- document.addEventListener('DOMContentLoaded', () => { const accessKey = sessionStorage.getItem('accessKey'); if (!accessKey) { window.location.href = '/'; return; } // Clear key on refresh or tab close window.addEventListener('beforeunload', () => { sessionStorage.removeItem('accessKey'); }); // 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(); // Initialize Vanta Background initVantaBackground(); 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; 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 += `
${item.name} ${item.price.toLocaleString()} ${sign}${item.change}%
`; }); container.innerHTML = html; } else { container.innerHTML = "Market data unavailable"; } } 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'); const matrixProgressText = document.getElementById('matrix-progress-text'); matrixLoader.style.display = 'flex'; matrixLogs.innerHTML = ''; matrixProgress.style.width = '0%'; const steps = [ "Initializing quantitative core engine...", "Fetching multi-asset historical data from market sources...", "Computing eigen-decomposition of the covariance matrix...", "Executing probabilistic stress tests and factor attribution...", "Applying dynamic constraint sets (allocation, sector, limits)...", "Identifying market volatility regimes...", "Converging convex optimization solver...", "Compiling institutional HTML portfolio report..." ]; let logIdx = 0; const interval = setInterval(() => { if(logIdx < steps.length) { const el = document.createElement('div'); el.style.margin = "2px 0"; el.innerHTML = `> ${steps[logIdx]}`; matrixLogs.appendChild(el); // Auto scroll to bottom matrixLogs.scrollTop = matrixLogs.scrollHeight; // Progress bar (capping at 99% until fully complete) let pct = Math.floor(Math.min(((logIdx + 1) / steps.length) * 99, 99)); matrixProgress.style.width = `${pct}%`; if(matrixProgressText) matrixProgressText.innerText = `${pct}%`; logIdx++; } }, 600); // Fast 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 += `
> ERROR: ${errTxt}
`; setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); return; } const data = await res.json(); if (data.status !== "queued") { clearInterval(interval); matrixLogs.innerHTML += `
> Unexpected server response.
`; setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); return; } const taskId = data.task_id; matrixLogs.innerHTML += `
> Job ${taskId.substring(0,8)} queued. Polling compute engine...
`; 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 += `
> Polling failed.
`; setTimeout(() => { matrixLoader.style.display = 'none'; }, 3000); return; } const statusData = await statusRes.json(); if (statusData.status === "completed") { clearInterval(pollInterval); clearInterval(interval); matrixProgress.style.width = '100%'; if(matrixProgressText) matrixProgressText.innerText = '100%'; if (statusData.target_weights) { sessionStorage.setItem("portfolio_context", JSON.stringify(statusData.target_weights)); } matrixLogs.innerHTML += `
> OPTIMIZATION COMPLETE. REDIRECTING...
`; setTimeout(() => { matrixLoader.style.display = 'none'; window.openReportFrame(); }, 1500); } else if (statusData.status === "error") { clearInterval(pollInterval); clearInterval(interval); matrixLogs.innerHTML += `
> CRITICAL ERROR: ${statusData.message}
`; setTimeout(() => { matrixLoader.style.display = 'none'; }, 4000); } } catch (err) { // Ignore network blips during polling } }, 2000); } catch(err) { clearInterval(interval); matrixLogs.innerHTML += `
> CRITICAL ERROR: Network failure.
`; setTimeout(() => { matrixLoader.style.display = 'none'; }, 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(); }; // --- VANTA JS BACKGROUND --- function initVantaBackground() { const container = document.getElementById('vanta-bg'); if (!container) return; try { window.vantaEffect = VANTA.NET({ el: "#vanta-bg", mouseControls: true, touchControls: true, gyroControls: false, minHeight: 200.00, minWidth: 200.00, scale: 1.00, scaleMobile: 1.00, color: 0x3b82f6, backgroundColor: 0x050814, points: 12.00, maxDistance: 22.00, spacing: 16.00 }); // Ensure Vanta resizes correctly on window resize window.addEventListener('resize', () => { if (window.vantaEffect) { window.vantaEffect.resize(); } }); } catch (e) { console.warn("Vanta JS failed to initialize:", e); } } // --- 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 = '/'; }; // --- AI CHAT WIDGET LOGIC --- document.addEventListener('DOMContentLoaded', () => { // Clear stale optimization context on fresh page load so the AI doesn't hallucinate previous sessions sessionStorage.removeItem("portfolio_context"); // Pre-populate some assets to make the UI look aliveviously orphaned if (typeof initHeroRadar === 'function') { initHeroRadar(); } const chatToggleBtn = document.getElementById('chat-toggle-btn'); const chatWindow = document.getElementById('chat-window'); const chatCloseBtn = document.getElementById('chat-close-btn'); const chatForm = document.getElementById('chat-form'); const chatInput = document.getElementById('chat-input'); const chatMessages = document.getElementById('chat-messages'); // Maintain conversation history locally let chatHistory = []; if (chatToggleBtn && chatWindow) { chatToggleBtn.addEventListener('click', () => { chatWindow.style.display = 'flex'; chatToggleBtn.style.transform = 'scale(0)'; }); chatCloseBtn.addEventListener('click', () => { chatWindow.style.display = 'none'; chatToggleBtn.style.transform = 'scale(1)'; }); chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const msg = chatInput.value.trim(); if (!msg) return; // Display user message const userMsg = document.createElement('div'); userMsg.style.cssText = "background: rgba(255,255,255,0.1); padding: 10px 14px; border-radius: 12px; border-top-right-radius: 4px; align-self: flex-end; max-width: 85%; color: white;"; userMsg.innerText = msg; chatMessages.appendChild(userMsg); chatInput.value = ''; chatMessages.scrollTop = chatMessages.scrollHeight; // Save user message to history chatHistory.push({ role: "user", content: msg }); // Display loading const loadingMsg = document.createElement('div'); loadingMsg.style.cssText = "color: #94a3b8; font-size: 0.9rem; margin-top: 4px; font-style: italic;"; loadingMsg.innerHTML = `Thinking...`; chatMessages.appendChild(loadingMsg); chatMessages.scrollTop = chatMessages.scrollHeight; // Get context let ctx = {}; try { ctx = JSON.parse(sessionStorage.getItem("portfolio_context") || "{}"); } catch(e) {} try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg, history: chatHistory.slice(-10), portfolio_context: ctx }) }); const data = await res.json(); chatMessages.removeChild(loadingMsg); const responseText = data.response || data.reply || (data.detail ? "Error: " + data.detail : "No response from AI."); chatHistory.push({ role: "assistant", content: responseText }); const aiMsg = document.createElement('div'); aiMsg.style.cssText = "background: rgba(59, 130, 246, 0.1); padding: 12px 16px; border-radius: 12px; border-top-left-radius: 4px; align-self: flex-start; max-width: 85%; color: #e2e8f0; line-height: 1.5;"; aiMsg.innerText = responseText; chatMessages.appendChild(aiMsg); chatMessages.scrollTop = chatMessages.scrollHeight; } catch (err) { chatMessages.removeChild(loadingMsg); const errMsg = document.createElement('div'); errMsg.style.cssText = "color: #ef4444; font-size: 0.9rem;"; errMsg.innerText = "Connection failed. Please try again."; chatMessages.appendChild(errMsg); } }); } });