Spaces:
Sleeping
Sleeping
| // 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 += `<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'); | |
| 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 = `<span style="color: #3b82f6">></span> ${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 += `<div style="color: #ef4444; margin-top: 1rem;">> ERROR: ${errTxt}</div>`; | |
| setTimeout(() => { matrixLoader.style.display = 'none'; }, 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'; }, 3000); | |
| return; | |
| } | |
| const taskId = data.task_id; | |
| matrixLogs.innerHTML += `<div style="color: #60a5fa; margin-top: 1rem;">> Job ${taskId.substring(0,8)} queued. Polling compute engine...</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'; }, 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 += `<div style="color: #10b981; margin-top: 1rem; font-weight: bold;">> OPTIMIZATION COMPLETE. REDIRECTING...</div>`; | |
| setTimeout(() => { | |
| matrixLoader.style.display = 'none'; | |
| window.openReportFrame(); | |
| }, 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'; }, 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'; }, 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); | |
| } | |
| }); | |
| } | |
| }); | |