diff --git a/bigdata_briefs/__main__.py b/bigdata_briefs/__main__.py index 9bf8b4b..12fb245 100644 --- a/bigdata_briefs/__main__.py +++ b/bigdata_briefs/__main__.py @@ -4,4 +4,4 @@ if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000, log_level=LOG_LEVEL.lower()) + uvicorn.run(app, host="0.0.0.0", port=8001, log_level=LOG_LEVEL.lower()) diff --git a/bigdata_briefs/api/app.py b/bigdata_briefs/api/app.py index 7376735..d464108 100644 --- a/bigdata_briefs/api/app.py +++ b/bigdata_briefs/api/app.py @@ -129,6 +129,7 @@ async def sample_frontend(_: str = Security(query_scheme)) -> HTMLResponse: sources=example_values["sources"], example_watchlists=list(dict(ExampleWatchlists).values()), example_request_id=str(EXAMPLE_UUID), + version=__version__, ), media_type="text/html", ) diff --git a/bigdata_briefs/static/fonts/HankenGrotesk-Bold.ttf b/bigdata_briefs/static/fonts/HankenGrotesk-Bold.ttf new file mode 100644 index 0000000..654c465 Binary files /dev/null and b/bigdata_briefs/static/fonts/HankenGrotesk-Bold.ttf differ diff --git a/bigdata_briefs/static/fonts/HankenGrotesk-Italic.ttf b/bigdata_briefs/static/fonts/HankenGrotesk-Italic.ttf new file mode 100644 index 0000000..fb0b658 Binary files /dev/null and b/bigdata_briefs/static/fonts/HankenGrotesk-Italic.ttf differ diff --git a/bigdata_briefs/static/fonts/HankenGrotesk-Regular.ttf b/bigdata_briefs/static/fonts/HankenGrotesk-Regular.ttf new file mode 100644 index 0000000..6dc3655 Binary files /dev/null and b/bigdata_briefs/static/fonts/HankenGrotesk-Regular.ttf differ diff --git a/bigdata_briefs/static/scripts/company_browser.js b/bigdata_briefs/static/scripts/company_browser.js new file mode 100644 index 0000000..d2b373d --- /dev/null +++ b/bigdata_briefs/static/scripts/company_browser.js @@ -0,0 +1,213 @@ +// Company Browser - Handles browsing and searching through company reports + +class CompanyBrowser { + constructor() { + this.companies = []; + this.filteredCompanies = []; + this.searchTerm = ''; + this.expandedCompany = null; + } + + init(companyReports) { + this.companies = companyReports || []; + this.filteredCompanies = [...this.companies]; + this.render(); + } + + filter(searchTerm) { + this.searchTerm = searchTerm.toLowerCase().trim(); + + if (!this.searchTerm) { + this.filteredCompanies = [...this.companies]; + } else { + this.filteredCompanies = this.companies.filter(company => { + const name = (company.entity_info?.name || '').toLowerCase(); + const ticker = (company.entity_info?.ticker || '').toLowerCase(); + const sector = (company.entity_info?.sector || '').toLowerCase(); + const entityId = (company.entity_id || '').toLowerCase(); + + return name.includes(this.searchTerm) || + ticker.includes(this.searchTerm) || + sector.includes(this.searchTerm) || + entityId.includes(this.searchTerm); + }); + } + + this.render(); + } + + toggleCompany(entityId) { + if (this.expandedCompany === entityId) { + this.expandedCompany = null; + } else { + this.expandedCompany = entityId; + } + this.render(); + } + + render() { + const container = document.querySelector('[data-tab-content="companies"] .tab-actual-content'); + if (!container) return; + + // Create search bar + let searchHtml = ` +
+
+ + + + +
+
+ Showing ${this.filteredCompanies.length} of ${this.companies.length} companies +
+
+ `; + + // Create company list + if (this.filteredCompanies.length === 0) { + searchHtml += ` +
+

No companies found matching "${this.searchTerm}"

+
+ `; + } else { + searchHtml += '
'; + + this.filteredCompanies.forEach(company => { + const entityId = company.entity_id; + const entityInfo = company.entity_info || {}; + const name = entityInfo.name || 'Unknown Company'; + const ticker = entityInfo.ticker || 'N/A'; + const sector = entityInfo.sector || 'N/A'; + const bulletCount = (company.content || []).length; + const isExpanded = this.expandedCompany === entityId; + + searchHtml += ` +
+ + + ${isExpanded ? this.renderCompanyDetails(company) : ''} +
+ `; + }); + + searchHtml += '
'; + } + + container.innerHTML = searchHtml; + } + + renderCompanyDetails(company) { + const entityInfo = company.entity_info || {}; + const bulletPoints = company.content || []; + + let html = ` +
+
+

Company Information

+
+ `; + + // Display all available entity info fields + for (const [key, value] of Object.entries(entityInfo)) { + if (value !== null && value !== undefined && value !== '') { + html += ` +
+ ${this.formatKey(key)}: + ${this.escapeHtml(String(value))} +
+ `; + } + } + + html += ` +
+
+ +
+

Report Bullet Points

+
+ `; + + if (bulletPoints.length === 0) { + html += '

No bullet points available for this company.

'; + } else { + bulletPoints.forEach((bullet, index) => { + const bulletText = bullet.bullet_point || ''; + const sources = bullet.sources || []; + + html += ` +
+
+ ${index + 1}. +
+

${this.escapeHtml(bulletText)}

+ ${sources.length > 0 ? ` +
+

Sources:

+
+ ${sources.map(source => ` + + ${this.escapeHtml(source)} + + `).join('')} +
+
+ ` : ''} +
+
+
+ `; + }); + } + + html += ` +
+
+
+ `; + + return html; + } + + formatKey(key) { + // Convert snake_case or camelCase to Title Case + return key + .replace(/_/g, ' ') + .replace(/([A-Z])/g, ' $1') + .replace(/^./, str => str.toUpperCase()) + .trim(); + } + + escapeHtml(text) { + if (text === null || text === undefined) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +// Initialize company browser +window.companyBrowser = new CompanyBrowser(); + diff --git a/bigdata_briefs/static/scripts/config_panel.js b/bigdata_briefs/static/scripts/config_panel.js new file mode 100644 index 0000000..64c7c19 --- /dev/null +++ b/bigdata_briefs/static/scripts/config_panel.js @@ -0,0 +1,50 @@ +// Configuration Panel - Sliding Side Panel + +function toggleConfigPanel() { + const panel = document.getElementById('configPanel'); + const backdrop = document.getElementById('configBackdrop'); + + if (panel && backdrop) { + const isOpen = !panel.classList.contains('translate-x-full'); + + if (isOpen) { + // Close + panel.classList.add('translate-x-full'); + backdrop.classList.add('hidden'); + backdrop.classList.remove('opacity-100'); + } else { + // Open + panel.classList.remove('translate-x-full'); + backdrop.classList.remove('hidden'); + setTimeout(() => backdrop.classList.add('opacity-100'), 10); + } + } +} + +function closeConfigPanel() { + const panel = document.getElementById('configPanel'); + const backdrop = document.getElementById('configBackdrop'); + + if (panel && backdrop) { + panel.classList.add('translate-x-full'); + backdrop.classList.add('hidden'); + backdrop.classList.remove('opacity-100'); + } +} + +// Make functions globally available +window.toggleConfigPanel = toggleConfigPanel; +window.closeConfigPanel = closeConfigPanel; + +// Close panel when clicking backdrop +document.addEventListener('DOMContentLoaded', function() { + const backdrop = document.getElementById('configBackdrop'); + if (backdrop) { + backdrop.addEventListener('click', function(e) { + if (e.target === backdrop) { + closeConfigPanel(); + } + }); + } +}); + diff --git a/bigdata_briefs/static/scripts/csv_upload.js b/bigdata_briefs/static/scripts/csv_upload.js new file mode 100644 index 0000000..95ac2cf --- /dev/null +++ b/bigdata_briefs/static/scripts/csv_upload.js @@ -0,0 +1,31 @@ +// CSV Upload Handler - Additional utility functions if needed + +// Validate CSV entities format +function validateCsvEntities(entities) { + // Basic validation: check if entities are non-empty strings + return entities.filter(entity => entity && entity.trim().length > 0); +} + +// Export entities for use in form submission +function getCsvEntities() { + return window.csvEntities || []; +} + +// Clear CSV upload +function clearCsvUpload() { + window.csvEntities = []; + const fileInput = document.getElementById('csvFile'); + if (fileInput) { + fileInput.value = ''; + } + const preview = document.getElementById('csvPreview'); + if (preview) { + preview.classList.add('hidden'); + } +} + +// Make functions globally available +window.validateCsvEntities = validateCsvEntities; +window.getCsvEntities = getCsvEntities; +window.clearCsvUpload = clearCsvUpload; + diff --git a/bigdata_briefs/static/scripts/form.js b/bigdata_briefs/static/scripts/form.js index 0ef1a9f..97b8ad5 100644 --- a/bigdata_briefs/static/scripts/form.js +++ b/bigdata_briefs/static/scripts/form.js @@ -1,168 +1,320 @@ -document.getElementById('briefForm').onsubmit = async function (e) { - e.preventDefault(); - const output = document.getElementById('output'); - const spinner = document.getElementById('spinner'); - const showJsonBtn = document.getElementById('showJsonBtn'); - const submitBtn = document.querySelector('button[type="submit"]'); - output.innerHTML = ''; - output.classList.remove('error'); - showJsonBtn.style.display = 'none'; - lastReport = null; - - // Disable the submit button - submitBtn.disabled = true; - submitBtn.textContent = 'Waiting for response...'; - - // Get companies and check if its available in the watchlists - let companies = document.getElementById('companies_text').value.trim(); - const foundWatchlist = watchlists.find(w => w.name === companies); - if (foundWatchlist) { - companies = foundWatchlist.id; - } - else if (!companies) { - output.innerHTML = `❌ Error: Company Universe is required.`; - output.classList.add('error'); - submitBtn.disabled = false; - submitBtn.textContent = 'Generate Brief'; - return; - } - const start_date = document.getElementById('start_date').value; - const end_date = document.getElementById('end_date').value; +// Form submission handler +document.addEventListener('DOMContentLoaded', function() { + const briefForm = document.getElementById('briefForm'); + if (!briefForm) return; + + briefForm.onsubmit = async function (e) { + e.preventDefault(); + + const spinner = document.getElementById('spinner'); + const submitBtn = document.querySelector('button[type="submit"]'); + const logViewer = document.getElementById('logViewer'); - const llm_model = document.getElementById('llm_model').value.trim(); + // Close config panel + if (window.closeConfigPanel) { + closeConfigPanel(); + } - // Build request payload - let payload = { - }; - - // Use topic_sentences array if it exists and has content, otherwise fall back to topics input field - let topicsArray = []; - if (typeof topic_sentences !== 'undefined' && topic_sentences.length > 0) { - topicsArray = topic_sentences; - } - - if (topicsArray.length > 0) { - // Validate that ALL topics contain the {company} placeholder - const topicsWithoutPlaceholder = topicsArray.filter(topic => !topic.includes('{company}')); - if (topicsWithoutPlaceholder.length > 0) { - const failingTopicsList = topicsWithoutPlaceholder.map(topic => `• ${escapeHtml(topic)}`).join('
'); - output.innerHTML = `❌ Error: The following topics are missing the {company} placeholder:
${failingTopicsList}
`; - output.classList.add('error'); - submitBtn.disabled = false; - submitBtn.textContent = 'Generate Brief'; + // Hide empty state and show dashboard + const emptyState = document.getElementById('emptyState'); + const dashboardSection = document.getElementById('dashboardSection'); + if (emptyState) emptyState.style.display = 'none'; + if (dashboardSection) dashboardSection.classList.remove('hidden'); + + // Clear previous results + window.lastReport = null; + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + const companiesContent = document.querySelector('[data-tab-content="companies"] .tab-actual-content'); + if (overviewContent) overviewContent.innerHTML = ''; + if (companiesContent) companiesContent.innerHTML = ''; + + // Show loading indicators + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.remove('hidden'); + }); + + // Disable submit button + submitBtn.disabled = true; + submitBtn.innerHTML = ` +
+ Generating... + `; + + // Show spinner + if (spinner) spinner.classList.remove('hidden'); + + // Open logs + const logViewerContainer = document.getElementById('logViewerContainer'); + if (logViewerContainer && logViewerContainer.classList.contains('hidden')) { + toggleProcessLogs(); + } + + if (logViewer) logViewer.innerHTML = '
Starting brief generation...
'; + + // Get companies based on selected input method + let companies = null; + if (currentCompanyInputMethod === 'watchlist') { + const companiesText = document.getElementById('companies_text')?.value.trim(); + const foundWatchlist = watchlists.find(w => w.name === companiesText); + if (foundWatchlist) { + companies = foundWatchlist.id; + } else if (companiesText) { + // Could be a watchlist ID + companies = companiesText; + } + } else if (currentCompanyInputMethod === 'csv') { + const csvEntities = window.csvEntities || []; + if (csvEntities.length > 0) { + companies = csvEntities; + } + } else if (currentCompanyInputMethod === 'manual') { + const manualInput = document.getElementById('companies_manual')?.value.trim(); + if (manualInput) { + companies = manualInput.split(',').map(s => s.trim()).filter(Boolean); + } + } + + if (!companies || (Array.isArray(companies) && companies.length === 0)) { + showError('Company Universe is required. Please select a watchlist, upload a CSV, or enter entity IDs manually.'); + resetFormState(); return; } - payload.topics = topicsArray; - } - const novelty = document.getElementById('novelty').value === 'true'; - let sources = document.getElementById('sources').value.trim(); - if (sources) { - if (sources.includes(',')) { - payload.sources = sources.split(',').map(s => s.trim()).filter(Boolean); - // A single RP Entity ID + const start_date = document.getElementById('start_date')?.value; + const end_date = document.getElementById('end_date')?.value; + + if (!start_date || !end_date) { + showError('Start date and end date are required.'); + resetFormState(); + return; } - else { - payload.sources = [sources]; + + // Build request payload + let payload = { + companies: companies, + report_start_date: start_date, + report_end_date: end_date + }; + + // Topics + if (typeof topic_sentences !== 'undefined' && topic_sentences && topic_sentences.length > 0) { + // Validate that ALL topics contain the {company} placeholder + const topicsWithoutPlaceholder = topic_sentences.filter(topic => !topic.includes('{company}')); + if (topicsWithoutPlaceholder.length > 0) { + const failingTopicsList = topicsWithoutPlaceholder.map(topic => `• ${escapeHtml(topic)}`).join('
'); + showError(`The following topics are missing the {company} placeholder:
${failingTopicsList}`); + resetFormState(); + return; + } + payload.topics = topic_sentences; + } + + // Novelty + const novelty = document.getElementById('novelty')?.checked || false; + payload.novelty = novelty; + + // Sources + const sourcesInput = document.getElementById('sources')?.value.trim(); + if (sourcesInput) { + payload.sources = sourcesInput.split(',').map(s => s.trim()).filter(Boolean); + } + + // Source Rank Boost + const sourceRankBoost = document.getElementById('source_rank_boost')?.value; + if (sourceRankBoost && sourceRankBoost !== '') { + const value = parseInt(sourceRankBoost); + if (!isNaN(value) && value >= 0 && value <= 10) { + payload.source_rank_boost = value; + } + } + + // Freshness Boost + const freshnessBoost = document.getElementById('freshness_boost')?.value; + if (freshnessBoost && freshnessBoost !== '') { + const value = parseInt(freshnessBoost); + if (!isNaN(value) && value >= 0 && value <= 10) { + payload.freshness_boost = value; + } } - } - // A list of companies - if (companies.includes(',')) { - payload.companies = companies.split(',').map(s => s.trim()).filter(Boolean); - // A single RP Entity ID - } else if (companies.length === 6) { - payload.companies = [companies]; - // A watchlist ID - } else if (companies.length > 6) { - payload.companies = companies; - } + // Include Title and Summary (for future backend support) + const includeTitleSummary = document.getElementById('includeTitleSummary')?.checked; + // Store for future use when backend supports it + // payload.include_title_summary = includeTitleSummary; + + // Update header info + updateHeaderInfo(start_date, end_date, companies); + + // Add token from URL param if present + const params = new URLSearchParams(); + const token = getUrlParam('token'); + if (token) { + params.append("token", token); + } - if (start_date) payload.report_start_date = start_date; - if (end_date) payload.report_end_date = end_date; - if (novelty) payload.novelty = novelty; + try { + const response = await fetch(`/briefs/create?${params}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `HTTP error ${response.status}`); + } + + const data = await response.json(); + + // Start polling status endpoint + if (data && data.request_id) { + const requestId = data.request_id; + pollStatus(requestId, params, submitBtn, spinner); + } else { + throw new Error('No request_id received from server'); + } + } catch (err) { + showError(`Error: ${err.message}`); + resetFormState(); + } + }; +}); - // Add token from URL param if present - const params = new URLSearchParams(); - const token = getUrlParam('token'); - if (token) { - params.append("token", token); +function showError(message) { + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + if (overviewContent) { + overviewContent.innerHTML = ` +
+

${message}

+
+ `; } + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.add('hidden'); + }); +} - try { - const response = await fetch(`/briefs/create?${params}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - if (!response.ok) { - throw new Error(`HTTP error ${response.status}`); +function resetFormState() { + const submitBtn = document.querySelector('button[type="submit"]'); + const spinner = document.getElementById('spinner'); + + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = ` + + + + + Generate Brief + `; + } + if (spinner) spinner.classList.add('hidden'); +} + +function updateHeaderInfo(startDate, endDate, companies) { + const dateRangeEl = document.getElementById('currentDateRange'); + const companiesEl = document.getElementById('currentCompanies'); + + if (dateRangeEl) { + const start = new Date(startDate).toLocaleDateString(); + const end = new Date(endDate).toLocaleDateString(); + dateRangeEl.textContent = `${start} - ${end}`; + } + + if (companiesEl) { + if (Array.isArray(companies)) { + companiesEl.textContent = `${companies.length} companies`; + } else { + const watchlist = watchlists.find(w => w.id === companies); + companiesEl.textContent = watchlist ? watchlist.name : 'Custom'; } - const data = await response.json(); - // Start polling status endpoint every 5 seconds using request_id - if (data && data.request_id) { - const requestId = data.request_id; - let polling = true; - const logViewer = document.getElementById('logViewer'); - async function pollStatus() { - - try { - const statusResp = await fetch(`/briefs/status/${requestId}?${params}`); - if (!statusResp.ok) { - throw new Error(`Status HTTP error ${statusResp.status}`); - } - const statusData = await statusResp.json(); - spinner.style.display = 'block'; - // Render logs if available - if (statusData.logs && Array.isArray(statusData.logs)) { - logViewer.innerHTML = statusData.logs.map(line => { - let base = 'mb-1'; - let color = ''; - if (line.toLowerCase().includes('error')) color = 'text-red-400'; - else if (line.toLowerCase().includes('success')) color = 'text-green-400'; - else if (line.toLowerCase().includes('info')) color = 'text-sky-400'; - return `
${line}
`; - }).join(''); - logViewer.scrollTop = logViewer.scrollHeight; - } else if (statusData.log) { - logViewer.textContent = statusData.log; - } else { - logViewer.textContent = 'No logs yet.'; - } - // Stop polling if status is 'completed' or 'failed' - if (statusData.status === 'completed' || statusData.status === 'failed') { - polling = false; - if (statusData.status === 'completed') { - output.innerHTML = renderBriefReport(statusData.report) - showJsonBtn.style.display = 'inline-block'; - lastReport = statusData.report; - } - spinner.style.display = 'none'; - submitBtn.disabled = false; - submitBtn.textContent = 'Generate Brief'; - return; + } +} + +async function pollStatus(requestId, params, submitBtn, spinner) { + const logViewer = document.getElementById('logViewer'); + let polling = true; + + async function poll() { + try { + const statusResp = await fetch(`/briefs/status/${requestId}?${params}`); + if (!statusResp.ok) { + throw new Error(`Status HTTP error ${statusResp.status}`); + } + + const statusData = await statusResp.json(); + + // Render logs if available + if (logViewer && statusData.logs && Array.isArray(statusData.logs)) { + logViewer.innerHTML = statusData.logs.map(line => { + let color = 'text-text2'; + if (line.toLowerCase().includes('error')) color = 'text-red-400'; + else if (line.toLowerCase().includes('success') || line.toLowerCase().includes('complete')) color = 'text-green-400'; + else if (line.toLowerCase().includes('info')) color = 'text-blue-400'; + return `
${escapeHtml(line)}
`; + }).join(''); + logViewer.scrollTop = logViewer.scrollHeight; + } + + // Stop polling if status is 'completed' or 'failed' + if (statusData.status === 'completed' || statusData.status === 'failed') { + polling = false; + + if (statusData.status === 'completed' && statusData.report) { + // Hide loading indicators + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.add('hidden'); + }); + + // Render the report + if (window.renderBriefReport) { + renderBriefReport(statusData.report); } - } catch (err) { - logViewer.innerHTML = `
❌ Status Error: ${err.message}
`; + window.lastReport = statusData.report; + } else if (statusData.status === 'failed') { + showError('Brief generation failed. Check logs for details.'); } - if (polling) { - setTimeout(pollStatus, 5000); + + if (spinner) spinner.classList.add('hidden'); + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = ` + + + + + Generate Brief + `; } + return; + } + } catch (err) { + if (logViewer) { + logViewer.innerHTML += `
❌ Status Error: ${escapeHtml(err.message)}
`; } - pollStatus(); } - } catch (err) { - output.innerHTML = `❌ Error: ${err.message}`; - output.classList.add('error'); - submitBtn.disabled = false; - submitBtn.textContent = 'Generate Brief'; - spinner.style.display = 'none'; + + if (polling) { + setTimeout(poll, 5000); + } } -}; + + poll(); +} -document.getElementById('showJsonBtn').onclick = function () { - if (lastReport) { - document.getElementById('jsonContent').textContent = JSON.stringify(lastReport, null, 2); - document.getElementById('jsonModal').style.display = 'block'; +// Show JSON button handler +document.addEventListener('DOMContentLoaded', function() { + const showJsonBtn = document.getElementById('showJsonBtn'); + if (showJsonBtn) { + showJsonBtn.onclick = function () { + if (window.lastReport) { + const jsonContent = document.getElementById('jsonContent'); + const jsonModal = document.getElementById('jsonModal'); + if (jsonContent && jsonModal) { + jsonContent.textContent = JSON.stringify(window.lastReport, null, 2); + jsonModal.classList.remove('hidden'); + } + } + }; } -}; \ No newline at end of file +}); diff --git a/bigdata_briefs/static/scripts/load_example.js b/bigdata_briefs/static/scripts/load_example.js index e2b7baf..92ff939 100644 --- a/bigdata_briefs/static/scripts/load_example.js +++ b/bigdata_briefs/static/scripts/load_example.js @@ -1,43 +1,119 @@ // Load Example function that accepts requestId as parameter async function loadRequestId(requestId) { - // Load example data into the form - // Add token from URL param if present - const showJsonBtn = document.getElementById('showJsonBtn'); - showJsonBtn.style.display = 'none'; - lastReport = null; + // Hide empty state and show dashboard + const emptyState = document.getElementById('emptyState'); + const dashboardSection = document.getElementById('dashboardSection'); + if (emptyState) emptyState.style.display = 'none'; + if (dashboardSection) dashboardSection.classList.remove('hidden'); + + // Clear previous results + window.lastReport = null; + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + const companiesContent = document.querySelector('[data-tab-content="companies"] .tab-actual-content'); + if (overviewContent) overviewContent.innerHTML = ''; + if (companiesContent) companiesContent.innerHTML = ''; + + // Show loading indicators + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.remove('hidden'); + }); + // Add token from URL param if present const params = new URLSearchParams(); const token = getUrlParam('token'); if (token) { params.append("token", token); } + const logViewer = document.getElementById('logViewer'); + const logViewerContainer = document.getElementById('logViewerContainer'); + + // Open logs + if (logViewerContainer && logViewerContainer.classList.contains('hidden')) { + toggleProcessLogs(); + } + + try { + const statusResp = await fetch(`/briefs/status/${requestId}?${params}`); + if (!statusResp.ok) { + throw new Error(`Status HTTP error ${statusResp.status}`); + } + + const statusData = await statusResp.json(); + + // Render logs if available + if (logViewer && statusData.logs && Array.isArray(statusData.logs)) { + logViewer.innerHTML = statusData.logs.map(line => { + let color = 'text-zinc-300'; + if (line.toLowerCase().includes('error')) color = 'text-red-400'; + else if (line.toLowerCase().includes('success') || line.toLowerCase().includes('complete')) color = 'text-green-400'; + else if (line.toLowerCase().includes('info')) color = 'text-blue-400'; + return `
${escapeHtml(line)}
`; + }).join(''); + logViewer.scrollTop = logViewer.scrollHeight; + } else if (logViewer && statusData.log) { + logViewer.textContent = statusData.log; + } else if (logViewer) { + logViewer.textContent = 'No logs available.'; + } + + // Render report if completed + if (statusData.status === 'completed' && statusData.report) { + // Hide loading indicators + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.add('hidden'); + }); - const statusResp = await fetch(`/briefs/status/${requestId}?${params}`); - if (!statusResp.ok) { - throw new Error(`Status HTTP error ${statusResp.status}`); + // Update header info + const report = statusData.report; + updateHeaderInfo( + report.start_date, + report.end_date, + report.watchlist_name || report.watchlist_id + ); + + // Render the report using the new renderer + if (window.renderBriefReport) { + window.renderBriefReport(report); + } + + window.lastReport = report; + } else if (statusData.status === 'failed') { + showError('Failed to load example. The brief generation may have failed.'); + } + } catch (err) { + showError(`Error loading example: ${err.message}`); } - const statusData = await statusResp.json(); - // Render logs if available - if (statusData.logs && Array.isArray(statusData.logs)) { - logViewer.innerHTML = statusData.logs.map(line => { - let base = 'mb-1'; - let color = ''; - if (line.toLowerCase().includes('error')) color = 'text-red-400'; - else if (line.toLowerCase().includes('success')) color = 'text-green-400'; - else if (line.toLowerCase().includes('info')) color = 'text-sky-400'; - return `
${line}
`; - }).join(''); - logViewer.scrollTop = logViewer.scrollHeight; - } else if (statusData.log) { - logViewer.textContent = statusData.log; - } else { - logViewer.textContent = 'No logs yet.'; +} + +function updateHeaderInfo(startDate, endDate, companies) { + const dateRangeEl = document.getElementById('currentDateRange'); + const companiesEl = document.getElementById('currentCompanies'); + + if (dateRangeEl && startDate && endDate) { + const start = new Date(startDate).toLocaleDateString(); + const end = new Date(endDate).toLocaleDateString(); + dateRangeEl.textContent = `${start} - ${end}`; } - // Stop polling if status is 'completed' or 'failed' - if (statusData.status === 'completed') { - output.innerHTML = renderBriefReport(statusData.report) - showJsonBtn.style.display = 'inline-block'; - lastReport = statusData.report; + + if (companiesEl && companies) { + companiesEl.textContent = typeof companies === 'string' ? companies : 'Multiple companies'; } -}; \ No newline at end of file +} + +function showError(message) { + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + if (overviewContent) { + overviewContent.innerHTML = ` +
+

${escapeHtml(message)}

+
+ `; + } + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.add('hidden'); + }); +} + +// Make function globally available +window.loadRequestId = loadRequestId; diff --git a/bigdata_briefs/static/scripts/report_renderer.js b/bigdata_briefs/static/scripts/report_renderer.js index e37a4e6..aaf6bc4 100644 --- a/bigdata_briefs/static/scripts/report_renderer.js +++ b/bigdata_briefs/static/scripts/report_renderer.js @@ -1,22 +1,4 @@ -function toggleHighlights(button) { - const highlights = button.closest('.highlights'); - const isCollapsed = highlights.classList.contains('collapsed'); - const extraItems = highlights.querySelectorAll('.extra'); - - if (isCollapsed) { - highlights.classList.remove('collapsed'); - extraItems.forEach(item => { - item.classList.remove('hidden'); - }); - button.textContent = 'Show Less'; - } else { - highlights.classList.add('collapsed'); - extraItems.forEach(item => { - item.classList.add('hidden'); - }); - button.textContent = 'Show More'; - } -} +// Report Renderer - Handles rendering brief reports in tabbed interface function formatIntroduction(text) { if (!text) return ''; @@ -34,196 +16,96 @@ function formatIntroduction(text) { } function renderBriefReport(data) { - if (!data || typeof data !== 'object') return 'No data to display.'; + if (!data || typeof data !== 'object') { + showError('No data to display.'); + return; + } + + // Hide loading indicators + document.querySelectorAll('.loading-indicator').forEach(indicator => { + indicator.classList.add('hidden'); + }); + + // Render Overview tab + renderOverviewTab(data); + + // Render Company Reports tab + renderCompanyReportsTab(data); +} + +function renderOverviewTab(data) { + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + if (!overviewContent) return; let html = ''; - // Header section with watchlist badge and date range - html += '
'; - html += `
- - ${escapeHtml(data.watchlist_name || data.watchlist_id)} - - 📅 - ${escapeHtml(data.end_date)} + // Watchlist and date info + html += '
'; + html += `
+ ${escapeHtml(data.watchlist_name || data.watchlist_id || 'Unknown Watchlist')}
`; - - html += `
- - + html += `
+ Period: ${escapeHtml(data.start_date || '')} to ${escapeHtml(data.end_date || '')}
`; + html += '
'; // Title - html += `

${escapeHtml(data.report_title)}

`; - html += '
'; + if (data.report_title) { + html += `
+

${escapeHtml(data.report_title)}

+
`; + } // Introduction section if (data.introduction) { - html += `
-

- Highlights -

-
+ html += `
+

Summary

+
${formatIntroduction(data.introduction)}
`; + } else { + html += `
+

No summary available for this brief.

+
`; } - // Entity reports as cards - if (Array.isArray(data.entity_reports) && data.entity_reports.length > 0) { - html += '
'; - - data.entity_reports.forEach(entity => { - const entityName = (entity.entity_info && (entity.entity_info.name || entity.entity_info.id)) - ? escapeHtml(entity.entity_info.name || entity.entity_info.id) - : escapeHtml(entity.entity_id); - - // Extract entity metadata if available - const ticker = entity.entity_info?.ticker || entity.entity_info?.symbol || ''; - const country = entity.entity_info?.country || entity.entity_info?.location || ''; - const industry = entity.entity_info?.industry || entity.entity_info?.sector || ''; - - const highlights = Array.isArray(entity.content) ? entity.content : []; - - // Build card: Changed to a dark, blurred background with refined padding and borders. - html += `
`; - - // Company header section - html += '
'; - // Updated typography for the entity name. - html += `
${entityName}
`; - - html += '
'; - if (ticker) { - // Polished ticker style. - html += `${escapeHtml(ticker)}`; - } - if (country) { - // Replaced emoji with a cleaner SVG icon. - html += ` - ${escapeHtml(country)} - `; - } - if (industry) { - // Polished industry tag style. - html += `${escapeHtml(industry)}`; - } - html += '
'; - html += '
'; - - // Divider for better separation. - html += '
'; - - // Highlights section - if (highlights.length > 0) { - const hasMore = highlights.length > 2; - html += `
`; - - highlights.forEach((bp, i) => { - const isExtra = i >= 2; - // Updated highlight box style to match the dark theme. - html += `
`; - html += `

`; // Set base text color for highlights. - - // Source indicator with a restyled tooltip for the dark theme. - if (Array.isArray(bp.sources) && bp.sources.length > 0) { - let tooltipContent = ''; - - // Define SVG icons for a cleaner look (Heroicons) - const iconSource = ``; - const iconTitle = ``; - const iconUrl = ``; - - bp.sources.forEach((sourceId, sourceIndex) => { - // Use optional chaining and provide a default empty object for robustness - const sourceMetadata = data.source_metadata?.[sourceId] || {}; - - // Destructure properties with default values - const { - text = "No text available", - headline = 'No headline available', - source_name: sourceName = 'Unknown Source', - url - } = sourceMetadata; - - // Use a container for each source to handle spacing, removing the need for


- // This creates a more modern, card-based layout - tooltipContent += ` -
`; - - if (sourceName !== 'Unknown Source') { - tooltipContent += ` -
- ${iconSource} - ${escapeHtml(sourceName)} -
-
-
-
${iconTitle}
- ${escapeHtml(text)} -
`; - - if (url) { - tooltipContent += ` -
-
${iconUrl}
- - ${escapeHtml(url)} - -
`; - } - tooltipContent += `
`; // Closes space-y-2 - } else { - // A styled fallback for sources without metadata - tooltipContent += ` -
- Source reference: ${escapeHtml(sourceId)} -
`; - } - - tooltipContent += `
`; // Closes the main container div - }); - - html += `
- ${bp.sources.length} - -
`; - } - - html += renderBoldText(escapeHtml(bp.bullet_point)); - html += '

'; - }); - - // "Show More" button with updated styling. - if (hasMore) { - html += ``; - } - - html += '
'; - } else { - html += '

No highlights available.

'; - } - - html += '
'; // End of card - }); - - html += '
'; // End of grid + // Additional metadata if available + if (data.novelty !== undefined) { + html += `
+ Novelty filter: ${data.novelty ? 'Enabled' : 'Disabled'} +
`; + } + + overviewContent.innerHTML = html; +} + +function renderCompanyReportsTab(data) { + const companiesContent = document.querySelector('[data-tab-content="companies"] .tab-actual-content'); + if (!companiesContent) return; + + // Use the company browser to render companies + if (window.companyBrowser && Array.isArray(data.entity_reports) && data.entity_reports.length > 0) { + window.companyBrowser.init(data.entity_reports); } else { - html += '

No entity reports available.

'; + companiesContent.innerHTML = ` +
+

No company reports available.

+
+ `; + } +} + +function showError(message) { + const overviewContent = document.querySelector('[data-tab-content="overview"] .tab-actual-content'); + if (overviewContent) { + overviewContent.innerHTML = ` +
+

${escapeHtml(message)}

+
+ `; } - // Add custom styles for collapsed state - html += ``; - - return html; -}; \ No newline at end of file +} + +// Make function globally available +window.renderBriefReport = renderBriefReport; diff --git a/bigdata_briefs/static/scripts/tab_controller.js b/bigdata_briefs/static/scripts/tab_controller.js new file mode 100644 index 0000000..4a2a5ec --- /dev/null +++ b/bigdata_briefs/static/scripts/tab_controller.js @@ -0,0 +1,78 @@ +// Tab Controller - Manages tab navigation similar to risk analyzer + +class TabController { + constructor() { + this.currentTab = 'overview'; + this.tabs = []; + } + + init() { + // Get all tab buttons + this.tabs = document.querySelectorAll('[data-tab]'); + + // Add click handlers + this.tabs.forEach(tab => { + tab.addEventListener('click', (e) => { + const tabName = e.currentTarget.getAttribute('data-tab'); + this.switchTab(tabName); + }); + }); + + // Set initial tab + if (this.tabs.length > 0) { + const initialTab = this.tabs[0].getAttribute('data-tab'); + this.switchTab(initialTab); + } + } + + switchTab(tabName) { + this.currentTab = tabName; + + // Update tab buttons + this.tabs.forEach(tab => { + const tabId = tab.getAttribute('data-tab'); + if (tabId === tabName) { + // Active tab + tab.classList.remove('border-transparent', 'text-zinc-400', 'hover:text-zinc-200'); + tab.classList.add('border-blue-500', 'text-blue-400', 'bg-blue-500/10'); + } else { + // Inactive tab + tab.classList.remove('border-blue-500', 'text-blue-400', 'bg-blue-500/10'); + tab.classList.add('border-transparent', 'text-zinc-400', 'hover:text-zinc-200'); + } + }); + + // Update tab content + const tabContents = document.querySelectorAll('[data-tab-content]'); + tabContents.forEach(content => { + const contentId = content.getAttribute('data-tab-content'); + if (contentId === tabName) { + content.classList.remove('hidden'); + } else { + content.classList.add('hidden'); + } + }); + + // Trigger custom event for tab change + const event = new CustomEvent('tabChanged', { detail: { tab: tabName } }); + document.dispatchEvent(event); + } + + reset() { + if (this.tabs.length > 0) { + const firstTab = this.tabs[0].getAttribute('data-tab'); + this.switchTab(firstTab); + } + } + + getCurrentTab() { + return this.currentTab; + } +} + +// Initialize tab controller when DOM is ready +window.tabController = new TabController(); +document.addEventListener('DOMContentLoaded', function() { + window.tabController.init(); +}); + diff --git a/bigdata_briefs/static/scripts/topics_selector.js b/bigdata_briefs/static/scripts/topics_selector.js index 1bd6b01..771853b 100644 --- a/bigdata_briefs/static/scripts/topics_selector.js +++ b/bigdata_briefs/static/scripts/topics_selector.js @@ -1,3 +1,70 @@ +// Topic Presets +const TOPIC_PRESETS = { + finance: [ + "What key takeaways emerged from {company}'s latest earnings report?", + "What notable changes in {company}'s financial performance metrics have been reported recently?", + "Has {company} revised its financial or operational guidance for upcoming periods?", + "What significant strategic initiatives or business pivots has {company} announced recently?", + "What material acquisition, merger, or divestiture activities involve {company} currently?", + "What executive leadership changes have been announced at {company} recently?", + "What significant contract wins, losses, or renewals has {company} recently announced?", + "What significant new product launches or pipeline developments has {company} announced?", + "What material operational disruptions or capacity changes is {company} experiencing currently?", + "How are supply chain conditions affecting {company}'s operations and outlook?", + "What production milestones or efficiency improvements has {company} achieved recently?", + "What cost-cutting measures or expense management initiatives has {company} recently disclosed?", + "What notable market share shifts has {company} experienced recently?", + "How is {company} responding to new competitive threats or significant competitor actions?", + "What significant new product launches or pipeline developments has {company} announced?", + "What specific regulatory developments are materially affecting {company}?", + "How are current macroeconomic factors affecting {company}'s performance and outlook?", + "What material litigation developments involve {company} currently?", + "What industry-specific trends or disruptions are directly affecting {company}?", + "What significant capital allocation decisions has {company} announced recently?", + "What changes to dividends, buybacks, or other shareholder return programs has {company} announced?", + "What debt issuance, refinancing, or covenant changes has {company} recently announced?", + "Have there been any credit rating actions or outlook changes for {company} recently?", + "What shifts in the prevailing narrative around {company} are emerging among influential investors?", + "What significant events could impact {company}'s performance in the near term?", + "What unexpected disclosures or unusual trading patterns has {company} experienced recently?", + "Is there any activist investor involvement or significant shareholder actions affecting {company}?" + ], + all: [ + "What are the most important developments for {company}?" + ], + esg: [ + "What environmental initiatives or sustainability goals has {company} announced or achieved?", + "What climate-related risks or opportunities is {company} facing?", + "How is {company} managing its carbon footprint and environmental impact?", + "What social responsibility initiatives or community engagement programs has {company} launched?", + "What diversity, equity, and inclusion efforts has {company} implemented?", + "What labor practices, employee relations, or workplace safety issues affect {company}?", + "What governance changes, board composition updates, or executive compensation changes has {company} made?", + "What ethical concerns, controversies, or compliance issues involve {company}?", + "What ESG-related regulations or standards is {company} responding to?", + "What stakeholder engagement or ESG reporting updates has {company} provided?" + ], + custom: [] +}; + +// Handle topic preset changes +function handleTopicPresetChange() { + const presetSelect = document.getElementById('topicPreset'); + const selectedPreset = presetSelect.value; + + if (selectedPreset === 'custom') { + // Keep current topics + return; + } + + // Load preset topics + if (TOPIC_PRESETS[selectedPreset]) { + topic_sentences = [...TOPIC_PRESETS[selectedPreset]]; + renderSentences(); + } +} +window.handleTopicPresetChange = handleTopicPresetChange; + document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('add-form'); const addButton = document.getElementById('add-button'); @@ -9,11 +76,12 @@ document.addEventListener('DOMContentLoaded', () => { // Function to render the list of topic_sentences const renderSentences = () => { // Clear the current list to avoid duplicates + if (!listContainer) return; listContainer.innerHTML = ''; // If there are no topic_sentences, display a message - if (topic_sentences.length === 0) { - listContainer.innerHTML = `

No keyword lines added yet.

`; + if (!topic_sentences || topic_sentences.length === 0) { + listContainer.innerHTML = `

No topics added yet. Add a topic above.

`; return; } @@ -24,17 +92,18 @@ document.addEventListener('DOMContentLoaded', () => { // Create the main container for the list item const listItem = document.createElement('div'); - listItem.className = 'flex justify-between items-center bg-white p-4 border border-gray-200 rounded-md shadow-sm text-xs'; + listItem.className = 'flex justify-between items-center bg-zinc-900/50 p-3 border border-zinc-700 rounded-lg text-sm'; // Create the text element const sentenceText = document.createElement('span'); sentenceText.textContent = sentence; - sentenceText.className = 'text-gray-700'; + sentenceText.className = 'text-zinc-300 flex-1 mr-3'; // Create the delete button const deleteButton = document.createElement('button'); deleteButton.textContent = 'Delete'; - deleteButton.className = 'bg-red-500 text-white font-semibold px-4 py-1 rounded-md hover:bg-red-600 transition-colors'; + deleteButton.type = 'button'; + deleteButton.className = 'bg-red-600/80 text-white font-medium px-3 py-1.5 rounded-md hover:bg-red-600 transition-colors text-xs'; // Add an event listener to the delete button deleteButton.addEventListener('click', () => { @@ -52,11 +121,26 @@ document.addEventListener('DOMContentLoaded', () => { // Function to add a new sentence const addSentence = () => { + if (!input) return; const newSentence = input.value.trim(); // Get value and remove whitespace if (newSentence) { // Only add if the input is not empty + // Validate that it contains {company} + if (!newSentence.includes('{company}')) { + alert('Topic must include {company} placeholder'); + return; + } + if (!topic_sentences) { + topic_sentences = []; + } topic_sentences.push(newSentence); input.value = ''; // Clear the input field renderSentences(); // Re-render the list + + // Switch to custom preset if not already + const presetSelect = document.getElementById('topicPreset'); + if (presetSelect && presetSelect.value !== 'custom') { + presetSelect.value = 'custom'; + } } }; @@ -67,21 +151,35 @@ document.addEventListener('DOMContentLoaded', () => { renderSentences(); // Re-render the list }; + // Make renderSentences available globally + window.renderSentences = renderSentences; + // --- EVENT LISTENERS --- // Listen for form submission (e.g., pressing Enter) - form.addEventListener('submit', (event) => { - event.preventDefault(); // Prevent page reload - addSentence(); - }); + if (form) { + form.addEventListener('submit', (event) => { + event.preventDefault(); // Prevent page reload + addSentence(); + }); + } // Listen for clicks on the add button - addButton.addEventListener('click', (event) => { - event.preventDefault(); // Prevent form submission - addSentence(); - }); + if (addButton) { + addButton.addEventListener('click', (event) => { + event.preventDefault(); // Prevent form submission + addSentence(); + }); + } // --- INITIAL RENDER --- // Render the initial list when the page loads - renderSentences(); -}); \ No newline at end of file + if (listContainer) { + renderSentences(); + } +}); + +// Make topic_sentences accessible globally +if (typeof topic_sentences === 'undefined') { + window.topic_sentences = []; +} diff --git a/bigdata_briefs/static/scripts/visualization.js b/bigdata_briefs/static/scripts/visualization.js index 6607d36..1e17a3e 100644 --- a/bigdata_briefs/static/scripts/visualization.js +++ b/bigdata_briefs/static/scripts/visualization.js @@ -1,56 +1,73 @@ // Info modal content for each label const infoContents = { topics: `Topics:
Specify the topics you want to analyze. Each topic must include the {company} placeholder which will be replaced with actual company names during analysis. You can specify multiple topics, one per line.
Examples: "What key takeaways emerged from {company}'s latest earnings report?"`, - companies: `Company Universe:
The portfolio of companies you want to create the brief for. You have several input options:
  • Select one of the public watchlists using the dropdown menu
  • Write list of RavenPack entity IDs (e.g., 4A6F00, D8442A)
  • Input a watchlist ID (e.g., 44118802-9104-4265-b97a-2e6d88d74893 )

Watchlists can be created programmatically using the Bigdata.com SDK or through the Bigdata app.`, - start_date: `Start/End Date:
The start and end of the time sample during which you want to generate the brief. Format: YYYY-MM-DD.`, - novelty: 'Novelty:
If set to true, the analysis will focus on novel events that have not been widely reported before, helping to identify emerging risks. If false, all relevant events will be considered, including those that have been frequently reported.', - sources: `Sources:
Optionally, you can filter the analysis to include only events from specific sources. You can provide a list of RavenPack entity IDs separated by commas (e.g., 9D69F1, B5235B). If left empty, events from all sources will be considered.`, + companies: `Company Universe:
The portfolio of companies you want to create the brief for. You have several input options:
  • Select one of the public watchlists using the dropdown menu
  • Upload a CSV file with RavenPack entity IDs (one per line)
  • Manually enter a list of RavenPack entity IDs separated by commas (e.g., 4A6F00, D8442A)
  • Input a watchlist ID (e.g., 44118802-9104-4265-b97a-2e6d88d74893)

Watchlists can be created programmatically using the Bigdata.com SDK or through the Bigdata app.`, + start_date: `Start Date:
The start of the time period for which you want to generate the brief. Format: YYYY-MM-DD.`, + end_date: `End Date:
The end of the time period for which you want to generate the brief. Format: YYYY-MM-DD.`, + novelty: 'Novelty Filter:
If enabled, the brief will focus on novel information that has not been included in previously generated briefs. This helps avoid redundant content and highlights new developments.', + sources: `Sources:
Optionally, you can filter the brief to include only events from specific sources. You can provide a list of RavenPack entity IDs separated by commas (e.g., 9D69F1, B5235B). If left empty, events from all sources will be considered.`, + source_rank_boost: `Source Rank Boost (0-10):
Controls how much the source rank influences relevance. Set to 0 to ignore source rank, or up to 10 for maximum effect, boosting chunks from premium sources.`, + freshness_boost: `Freshness Boost (0-10):
Controls the influence of document timestamp on relevance. Set to 0 to ignore publishing time (useful for point-in-time research), or up to 10 to heavily prioritize the most recent documents.`, + includeTitleSummary: `Include Title and Summary:
When enabled, the brief will include a title and introduction/summary section. This feature is currently being prepared for backend support.`, load_example: `Load Example:
By clicking this button you will load an example output that is preloaded. By using it you can get an idea of the type of output you can expect from this workflow without waiting. The input data for the example is:

Start date: 2025-10-01 00:00:00
End date: 2025-10-07 00:00:00
Topics: Default topics list
`, }; -document.addEventListener('DOMContentLoaded', function () { - const dragbar = document.getElementById('dragbar'); - const sidebar = document.getElementById('sidebar'); - const outputarea = document.getElementById('outputarea'); - let dragging = false; +// Toggle advanced options +function toggleAdvancedOptions() { + const adv = document.getElementById('advanced-options'); + const btnIcon = document.getElementById('advancedOptionsIcon'); + if (!adv || !btnIcon) return; + + if (adv.classList.contains('hidden')) { + adv.classList.remove('hidden'); + btnIcon.style.transform = 'rotate(180deg)'; + } else { + adv.classList.add('hidden'); + btnIcon.style.transform = 'rotate(0deg)'; + } +} - dragbar.addEventListener('mousedown', function (e) { - dragging = true; - document.body.classList.add('cursor-ew-resize'); - document.body.style.userSelect = 'none'; - }); +// Make function globally available +window.toggleAdvancedOptions = toggleAdvancedOptions; - document.addEventListener('mousemove', function (e) { - if (!dragging) return; - const minSidebar = 250; - const maxSidebar = 600; - let newWidth = Math.min(Math.max(e.clientX - sidebar.getBoundingClientRect().left, minSidebar), maxSidebar); - sidebar.style.width = newWidth + 'px'; - // outputarea will flex to fill remaining space - }); +// Toggle process logs +function toggleProcessLogs() { + const container = document.getElementById('logViewerContainer'); + const icon = document.getElementById('logsIcon'); + if (!container || !icon) return; + + if (container.classList.contains('hidden')) { + container.classList.remove('hidden'); + icon.style.transform = 'rotate(180deg)'; + } else { + container.classList.add('hidden'); + icon.style.transform = 'rotate(0deg)'; + } +} - document.addEventListener('mouseup', function (e) { - if (dragging) { - dragging = false; - document.body.classList.remove('cursor-ew-resize'); - document.body.style.userSelect = ''; - } - }); -}); +window.toggleProcessLogs = toggleProcessLogs; function showInfoModal(label) { let container = document.getElementById('infoModalsContainer'); + const content = infoContents[label] || 'No info available.'; container.innerHTML = ` -
-
- -
${infoContents[label] || 'No info available.'}
-
For a complete list of parameters and their descriptions, refer to the API documentation.
+
+
+ +
${content}
+
For a complete list of parameters and their descriptions, refer to the API documentation.
`; } +// Make function globally available +window.showInfoModal = showInfoModal; + function showDocumentModal(document_id) { let container = document.getElementById('infoModalsContainer'); container.innerHTML = ` diff --git a/bigdata_briefs/templates/api/base.html.jinja b/bigdata_briefs/templates/api/base.html.jinja index 7bf67d7..c3cfb9f 100644 --- a/bigdata_briefs/templates/api/base.html.jinja +++ b/bigdata_briefs/templates/api/base.html.jinja @@ -4,52 +4,108 @@ Bigdata Research Services + - - - - + + + - - -