// 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);
}
});
}
});