engineportf's picture
Initial Deployment from Local Engine
208fbf8 verified
Raw
History Blame Contribute Delete
23.5 kB
// 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);
}
});
}
});