document.addEventListener('DOMContentLoaded', async () => { // 1. Get Job ID const pathParts = window.location.pathname.split('/'); const jobId = pathParts[pathParts.length - 1]; if (!jobId) { alert('Invalid Job ID'); return; } // 2. Fetch Data try { const response = await fetch(`/api/status/${jobId}`); const jobData = await response.json(); if (jobData.status !== 'completed' || !jobData.result) { console.error('Job status check failed:', jobData); alert('Analysis not ready or failed.'); return; } console.log("--------------------------------------------------"); console.log("✅ DATA RECEIVED FROM BACKEND:"); console.log(jobData.result); console.log("--------------------------------------------------"); renderResults(jobData.result); } catch (error) { console.error('Error fetching results:', error); alert('Failed to load results.'); } // 3. Render Logic function renderResults(data) { const { company_name, analysis_date, news, fundamentals, peers, signal } = data; // --- Header --- try { document.getElementById('companyTicker').textContent = company_name.toUpperCase(); // Use sector + name fallback const sector = fundamentals.sector || "Unknown Sector"; let displayName = company_name; if (company_name.includes('(')) { displayName = company_name.split('(')[0]; } document.getElementById('companyName').textContent = `${sector} | ${displayName}`; document.getElementById('analysisDate').textContent = new Date(analysis_date).toLocaleDateString() + " " + new Date(analysis_date).toLocaleTimeString(); // Contrarian Score document.getElementById('contrarianScore').textContent = signal.signal_strength * 10; // Convert 1-10 to 1-100 const sigType = (signal.signal_type || 'Hold'); const sigElem = document.getElementById('signalType'); sigElem.textContent = sigType.toUpperCase().replace('_', ' '); // Color code signal if (sigType.toLowerCase().includes('buy')) sigElem.style.color = '#33f20d'; else if (sigType.toLowerCase().includes('avoid')) sigElem.style.color = '#ef4444'; else sigElem.style.color = '#fbbf24'; } catch (e) { console.error("Error rendering Header:", e); } // --- Market Psyche (Gauge) --- try { const score = news.score; // -10 to +10 // Map -10 -> -90deg, +10 -> +90deg const rotation = (score * 9); const needle = document.getElementById('marketGaugeNeedle'); if (needle) { needle.style.transform = `translateX(-50%) rotate(${rotation}deg)`; } const textElem = document.getElementById('marketPsycheText'); if (score <= -3) { textElem.textContent = "Fear"; textElem.classList.add('danger-text'); } else if (score >= 3) { textElem.textContent = "Greed"; textElem.classList.add('neon-text'); } else { textElem.textContent = "Neutral"; textElem.style.color = "gray"; } document.getElementById('marketPsycheSignal').textContent = `Sentiment Score: ${score}/10`; } catch (e) { console.error("Error rendering Gauge:", e); } // --- News Sentiment Analysis --- try { const total = news.positive_count + news.negative_count + news.neutral_count || 1; const posPct = Math.round((news.positive_count / total) * 100); const negPct = Math.round((news.negative_count / total) * 100); const neuPct = Math.round((news.neutral_count / total) * 100); document.getElementById('newsPosBar').style.width = `${posPct}%`; document.getElementById('newsPosText').textContent = `${posPct}%`; // FIX: Make positive text white for visibility on green document.getElementById('newsPosText').classList.add('text-white'); document.getElementById('newsPosText').classList.remove('text-black'); // ensure document.getElementById('newsNegBar').style.width = `${negPct}%`; document.getElementById('newsNegText').textContent = `${negPct}%`; document.getElementById('newsNeuBar').style.width = `${neuPct}%`; document.getElementById('newsNeuText').textContent = `${neuPct}%`; // Headlines Capsules (Top 5) const headlinesGrid = document.getElementById('newsHeadlinesGrid'); headlinesGrid.innerHTML = ''; const headlines = Array.isArray(news.headlines) ? news.headlines : []; // Helper for class and text const getHeadlineData = (item) => { let sentiment = 'neutral'; let text = ''; if (typeof item === 'string') { text = item; // Fallback heuristic for legacy data const lower = text.toLowerCase(); if (lower.includes('gain') || lower.includes('up') || lower.includes('rally') || lower.includes('high') || lower.includes('growth')) sentiment = 'positive'; else if (lower.includes('loss') || lower.includes('down') || lower.includes('crash') || lower.includes('risk') || lower.includes('miss')) sentiment = 'negative'; } else { text = item.title; sentiment = (item.sentiment || 'neutral').toLowerCase(); } let colorClass = 'text-gray-400 border-white/5 bg-[#1e293b]'; if (sentiment === 'positive') colorClass = 'text-black bg-primary/80 border-primary/20 font-bold'; else if (sentiment === 'negative') colorClass = 'text-white bg-danger/80 border-danger/20 font-bold'; return { text, colorClass }; }; // Show top 5 in capsules headlines.slice(0, 5).forEach(h => { const { text, colorClass } = getHeadlineData(h); const span = document.createElement('span'); span.className = `px-3 py-1.5 rounded-full border text-xs font-mono hover:scale-105 cursor-default transition-all ${colorClass}`; span.textContent = text.length > 50 ? text.substring(0, 50) + '...' : text; headlinesGrid.appendChild(span); }); // "View All" Logic const viewAllBtn = document.getElementById('viewAllHeadlinesBtn'); const modal = document.getElementById('headlinesModal'); const modalList = document.getElementById('allHeadlinesList'); const closeBtn = document.getElementById('closeHeadlinesModal'); const modalBg = document.getElementById('headlinesModalBg'); if (headlines.length > 5) { viewAllBtn.classList.remove('hidden'); viewAllBtn.onclick = () => { modalList.innerHTML = ''; // Clear headlines.forEach(h => { const { text, colorClass } = getHeadlineData(h); const div = document.createElement('div'); div.className = "mb-2"; // Render full width capsule in modal div.innerHTML = `
${text}
`; modalList.appendChild(div); }); modal.classList.remove('hidden'); }; const closeModal = () => modal.classList.add('hidden'); closeBtn.onclick = closeModal; modalBg.onclick = closeModal; } else { viewAllBtn.classList.add('hidden'); } } catch (e) { console.error("Error rendering News:", e); } // --- Peer Comparison (Radar SVG - 6 Axes) --- try { // Axes: Growth(0), Profitability(60), Efficiency(120), Valuation(180), Dividend(240), Momentum(300) const scores = fundamentals.normalized_scores || { "Growth": 50, "Profitability": 50, "Efficiency": 50, "Valuation": 50, "Dividend Yield": 0, "Momentum": 50 }; // Normalize values 0-100 for the chart radius (max 80px) const p1 = Math.min(Math.max(scores["Growth"] || 0, 10), 100); const p2 = Math.min(Math.max(scores["Profitability"] || 0, 10), 100); const p3 = Math.min(Math.max(scores["Efficiency"] || 0, 10), 100); const p4 = Math.min(Math.max(scores["Valuation"] || 0, 10), 100); const p5 = Math.min(Math.max(scores["Dividend Yield"] || 0, 10), 100); const p6 = Math.min(Math.max(scores["Momentum"] || 0, 10), 100); // Angles: 0, 60, 120, 180, 240, 300 // Center (100, 100), Max Radius 80. function getPoint(val, angleDeg) { const angleRad = (angleDeg - 90) * Math.PI / 180; const r = (val / 100) * 80; const x = 100 + r * Math.cos(angleRad); const y = 100 + r * Math.sin(angleRad); return `${x},${y}`; } const points = [ getPoint(p1, 0), getPoint(p2, 60), getPoint(p3, 120), getPoint(p4, 180), getPoint(p5, 240), getPoint(p6, 300) ].join(' '); const poly = document.getElementById('peerRadarPolygon'); if (poly) { poly.setAttribute('points', points); } } catch (e) { console.error("Error rendering Peer Radar:", e); } // --- NEW: Detailed Metrics Grid --- try { const grid = document.getElementById('metricsGrid'); if (grid) { grid.innerHTML = ''; // Clear loading state const createCard = (title, value, sub = '') => { const card = document.createElement('div'); card.className = "metric-card"; let valColor = "text-white"; if (title.includes('Return') || title.includes('Growth')) { const num = parseFloat(value); if (num > 0) valColor = "text-primary"; else if (num < 0) valColor = "text-danger"; } card.innerHTML = ` ${title}
${value !== undefined && value !== null ? value : '--'} ${sub}
`; return card; }; const f = fundamentals; // Row 1: Valuation grid.appendChild(createCard("Market Cap", f.market_cap ? f.market_cap.toLocaleString() : '--', " Cr")); grid.appendChild(createCard("P/E Ratio", f.pe_ratio)); grid.appendChild(createCard("Ind. P/E", f.industry_pe)); grid.appendChild(createCard("P/B Ratio", f.pb_ratio)); // Row 2: Profitability grid.appendChild(createCard("Div. Yield", f.dividend_yield, "%")); grid.appendChild(createCard("EPS", f.eps)); grid.appendChild(createCard("ROE", f.roe, "%")); grid.appendChild(createCard("ROCE", f.roce, "%")); // Row 3: Returns grid.appendChild(createCard("1Y Return", f.returns_1y, "%")); grid.appendChild(createCard("3Y Return", f.returns_3y, "%")); grid.appendChild(createCard("5Y Return", f.returns_5y, "%")); // Debt/Eq removed per user request } } catch (e) { console.error("Error rendering Detailed Metrics:", e); } // --- NEW: Peer Comparison Table --- try { const tbody = document.getElementById('peerTableBody'); tbody.innerHTML = ''; const peerMap = peers.peer_metrics || {}; const rows = []; // Target Row rows.push({ name: company_name, mc: fundamentals.market_cap, pe: fundamentals.pe_ratio, roe: fundamentals.roe, roce: fundamentals.roce, ret: fundamentals.returns_1y, dy: fundamentals.dividend_yield, isTarget: true }); // Peers for (const [pName, pM] of Object.entries(peerMap)) { rows.push({ name: pName, mc: pM.market_cap, pe: pM.pe_ratio, roe: pM.roe, roce: pM.roce, ret: pM.returns_1y, dy: pM.dividend_yield, isTarget: false }); } rows.forEach(r => { const tr = document.createElement('tr'); tr.className = r.isTarget ? "bg-primary/5 border-b border-primary/20" : "border-b border-white/5 hover:bg-white/5 transition-colors"; tr.innerHTML = ` ${r.name} ${r.isTarget ? 'You' : ''} ${r.mc ? r.mc.toLocaleString() : '--'} ${r.pe || '--'} ${r.roe || '--'}% ${r.roce || '--'}% ${r.ret || '--'}% `; tbody.appendChild(tr); }); } catch (e) { console.error("Error rendering Peer Table:", e); } // --- Investment Thesis --- try { // Opportunity const oppList = document.getElementById('thesisOpportunity'); oppList.innerHTML = ''; (signal.opportunity_reasons || []).forEach(item => { const li = document.createElement('li'); li.textContent = item; oppList.appendChild(li); }); // Management Outlook document.getElementById('thesisOutlook').textContent = signal.management_outlook || "N/A"; // Future document.getElementById('thesisFuture').textContent = signal.future_development || "N/A"; // Risks const risksList = document.getElementById('thesisRisks'); risksList.innerHTML = ''; (signal.risk_factors || []).forEach(item => { const li = document.createElement('li'); li.textContent = item; risksList.appendChild(li); }); // Competitive Moats const moatsList = document.getElementById('moatsList'); if (moatsList) { moatsList.innerHTML = ''; const moats = signal.competitive_moats || ["Analysis pending..."]; moats.forEach(item => { const li = document.createElement('li'); li.className = "flex gap-3 text-sm text-gray-300"; li.innerHTML = ` check_circle ${item} `; moatsList.appendChild(li); }); } } catch (e) { console.error("Error rendering Thesis/Moats:", e); } } // 4. Chat Interactions // Use the existing element IDs const chatInput = document.getElementById('questionInput'); const askBtn = document.getElementById('askBtn'); const chatHistory = document.getElementById('chatHistory'); const downloadBtn = document.getElementById('downloadReportBtn'); if (downloadBtn) { downloadBtn.addEventListener('click', () => window.print()); } async function askQuestion(q) { if (!q) return; appendMessage('user', q); chatInput.value = ''; try { // Show typing indicator? // appendMessage('bot', 'Typing...'); const res = await fetch(`/api/ask/${jobId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: q }) }); const data = await res.json(); // Remove typing indicator logic if added appendMessage('bot', data.answer); } catch (err) { console.error(err); appendMessage('bot', 'Connection error.'); } } function appendMessage(role, text) { // Create matching bubbles based on Bento template style // Template uses: // Bot: w-8 h-8 rounded-full bg-[#1e293b] ... // User: flex-row-reverse ... const wrapper = document.createElement('div'); wrapper.className = `flex gap-4 ${role === 'user' ? 'flex-row-reverse' : ''}`; const iconDiv = document.createElement('div'); iconDiv.className = `w-8 h-8 rounded-full ${role === 'user' ? 'bg-primary' : 'bg-[#1e293b]'} flex items-center justify-center shrink-0 border border-white/10`; const iconSpan = document.createElement('span'); iconSpan.className = `material-symbols-outlined text-[16px] ${role === 'user' ? 'text-black' : 'text-primary'}`; iconSpan.textContent = role === 'user' ? 'person' : 'smart_toy'; iconDiv.appendChild(iconSpan); const contentDiv = document.createElement('div'); contentDiv.className = `flex flex-col gap-1 max-w-[80%] ${role === 'user' ? 'items-end' : ''}`; const nameSpan = document.createElement('span'); nameSpan.className = "text-xs text-gray-500 font-mono"; nameSpan.textContent = role === 'user' ? "You" : "Contra.AI"; const msgBubble = document.createElement('div'); if (role === 'user') { msgBubble.className = "bg-primary/10 border border-primary/20 p-4 rounded-2xl rounded-tr-none text-sm text-white leading-relaxed shadow-glow-sm"; } else { msgBubble.className = "bg-[#1e293b]/50 border border-white/5 p-4 rounded-2xl rounded-tl-none text-sm text-gray-200 leading-relaxed shadow-sm"; } const p = document.createElement('p'); p.textContent = text; msgBubble.appendChild(p); contentDiv.appendChild(nameSpan); contentDiv.appendChild(msgBubble); wrapper.appendChild(iconDiv); wrapper.appendChild(contentDiv); chatHistory.appendChild(wrapper); chatHistory.scrollTop = chatHistory.scrollHeight; } if (askBtn) askBtn.addEventListener('click', () => askQuestion(chatInput.value)); if (chatInput) { chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') askQuestion(chatInput.value); }); } });