import apiClient from './apiClient.js'; import { escapeHtml } from '../shared/js/utils/sanitizer.js'; class NewsView { constructor(section) { this.section = section; this.tableBody = section.querySelector('[data-news-body]'); this.filterInput = section.querySelector('[data-news-search]'); this.rangeSelect = section.querySelector('[data-news-range]'); this.symbolFilter = section.querySelector('[data-news-symbol]'); this.modalBackdrop = section.querySelector('[data-news-modal]'); this.modalContent = section.querySelector('[data-news-modal-content]'); this.closeModalBtn = section.querySelector('[data-close-news-modal]'); this.dataset = []; this.datasetMap = new Map(); } async init() { this.tableBody.innerHTML = 'Loading news...'; await this.loadNews(); this.bindEvents(); } bindEvents() { if (this.filterInput) { this.filterInput.addEventListener('input', () => this.renderRows()); } if (this.rangeSelect) { this.rangeSelect.addEventListener('change', () => this.renderRows()); } if (this.symbolFilter) { this.symbolFilter.addEventListener('input', () => this.renderRows()); } if (this.closeModalBtn) { this.closeModalBtn.addEventListener('click', () => this.hideModal()); } if (this.modalBackdrop) { this.modalBackdrop.addEventListener('click', (event) => { if (event.target === this.modalBackdrop) { this.hideModal(); } }); } } async loadNews() { const result = await apiClient.getLatestNews(40); if (!result.ok) { const errorMsg = escapeHtml(result.error || 'Failed to load news'); this.tableBody.innerHTML = `
${errorMsg}
`; return; } this.dataset = result.data || []; this.datasetMap.clear(); this.dataset.forEach((item, index) => { const rowId = item.id || `${item.title}-${index}`; this.datasetMap.set(rowId, item); }); this.renderRows(); } renderRows() { const searchTerm = (this.filterInput?.value || '').toLowerCase(); const symbolFilter = (this.symbolFilter?.value || '').toLowerCase(); const range = this.rangeSelect?.value || '24h'; const rangeMap = { '24h': 86_400_000, '7d': 604_800_000, '30d': 2_592_000_000 }; const limit = rangeMap[range] || rangeMap['24h']; const filtered = this.dataset.filter((item) => { const matchesText = `${item.title} ${item.summary}`.toLowerCase().includes(searchTerm); const matchesSymbol = symbolFilter ? (item.symbols || []).some((symbol) => symbol.toLowerCase().includes(symbolFilter)) : true; const published = new Date(item.published_at || item.date || Date.now()).getTime(); const withinRange = Date.now() - published <= limit; return matchesText && matchesSymbol && withinRange; }); if (!filtered.length) { this.tableBody.innerHTML = 'No news for selected filters.'; return; } this.tableBody.innerHTML = filtered .map((news, index) => { const rowId = news.id || `${escapeHtml(news.title || '')}-${index}`; this.datasetMap.set(rowId, news); // Sanitize all dynamic content const source = escapeHtml(news.source || 'N/A'); const title = escapeHtml(news.title || ''); const symbols = (news.symbols || []).map(s => escapeHtml(s)); const sentiment = escapeHtml(news.sentiment || 'Unknown'); return ` ${new Date(news.published_at || news.date).toLocaleString()} ${source} ${title} ${symbols.map((s) => `${s}`).join(' ')} ${sentiment} `; }) .join(''); this.section.querySelectorAll('tr[data-news-id]').forEach((row) => { row.addEventListener('click', () => { const id = row.dataset.newsId; const item = this.datasetMap.get(id); if (item) { this.showModal(item); } }); }); this.section.querySelectorAll('[data-news-summarize]').forEach((button) => { button.addEventListener('click', (event) => { event.stopPropagation(); const { newsSummarize } = button.dataset; this.summarizeArticle(newsSummarize, button); }); }); } getSentimentClass(sentiment) { switch ((sentiment || '').toLowerCase()) { case 'bullish': return 'badge-success'; case 'bearish': return 'badge-danger'; default: return 'badge-neutral'; } } async summarizeArticle(rowId, button) { const item = this.datasetMap.get(rowId); if (!item || !button) return; button.disabled = true; const original = button.textContent; button.textContent = 'Summarizing…'; const payload = { title: item.title, body: item.body || item.summary || item.description || '', source: item.source || '', }; const result = await apiClient.summarizeNews(payload); button.disabled = false; button.textContent = original; if (!result.ok) { this.showModal(item, null, result.error); return; } this.showModal(item, result.data?.analysis || result.data); } async showModal(item, analysis = null, errorMessage = null) { if (!this.modalContent) return; this.modalBackdrop.classList.add('active'); // Sanitize all user data before inserting into HTML const title = escapeHtml(item.title || ''); const source = escapeHtml(item.source || ''); const summary = escapeHtml(item.summary || item.description || ''); const symbols = (item.symbols || []).map(s => escapeHtml(s)); this.modalContent.innerHTML = `

${title}

${new Date(item.published_at || item.date).toLocaleString()} • ${source}

${summary}

${symbols.map((s) => `${s}`).join('')}
${analysis ? '' : errorMessage ? '' : 'Click Summarize to run AI insights.'}
`; const aiBlock = this.modalContent.querySelector('.ai-block'); if (!aiBlock) return; if (errorMessage) { aiBlock.innerHTML = `
${escapeHtml(errorMessage)}
`; return; } if (!analysis) { aiBlock.innerHTML = '
Use the Summarize button to request AI analysis.
'; return; } const sentiment = analysis.sentiment || analysis.analysis?.sentiment; const analysisSummary = escapeHtml(analysis.summary || analysis.analysis?.summary || 'Model returned no summary.'); const sentimentLabel = escapeHtml(sentiment?.label || sentiment || 'Unknown'); const sentimentScore = sentiment?.score !== undefined ? escapeHtml(String(sentiment.score)) : ''; aiBlock.innerHTML = `

AI Summary

${analysisSummary}

Sentiment: ${sentimentLabel}${sentimentScore ? ` (${sentimentScore})` : ''}

`; } hideModal() { if (this.modalBackdrop) { this.modalBackdrop.classList.remove('active'); } } } export default NewsView;