Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>System Data Viewer</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; color: #333; } | |
| .container { max-width: 1200px; margin: 0 auto; padding: 20px; } | |
| .header { text-align: center; margin-bottom: 40px; color: white; } | |
| .header h1 { font-size: 2.5em; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } | |
| .header p { font-size: 1.2em; opacity: 0.9; } | |
| .controls { background: rgba(255, 255, 255, 0.95); border-radius: 15px; padding: 30px; margin-bottom: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); backdrop-filter: blur(10px); } | |
| .control-group { margin-bottom: 20px; } | |
| .control-group label { display: block; font-weight: 600; margin-bottom: 8px; color: #555; font-size: 1.1em; } | |
| select { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #ccc; font-size: 1em; background-color: white; } | |
| .radio-group { display: flex; gap: 20px; flex-wrap: wrap; } | |
| .radio-option { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 15px; border-radius: 8px; transition: all 0.3s ease; background: rgba(102, 126, 234, 0.1); } | |
| .radio-option:hover { background: rgba(102, 126, 234, 0.2); transform: translateY(-2px); } | |
| .radio-option input[type="radio"] { margin: 0; } | |
| /* AGGIUNTA: Stile per Immagine Evento */ | |
| #imageContainer { display: none; margin-bottom: 30px; text-align: center; } | |
| #eventImage { max-width: 100%; max-height: 450px; border-radius: 15px; border: 5px solid white; box-shadow: 0 10px 25px rgba(0,0,0,0.3); } | |
| .sdg-nav-container { background: rgba(255, 255, 255, 0.95); border-radius: 15px; padding: 20px; margin-bottom: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); backdrop-filter: blur(10px); display: none; } | |
| .sdg-nav-container.visible { display: block; } | |
| .sdg-nav-title { font-weight: 600; margin-bottom: 15px; color: #555; font-size: 1.1em; } | |
| .sdg-nav { display: flex; gap: 10px; flex-wrap: wrap; } | |
| .sdg-nav-item { padding: 10px 16px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 8px; cursor: pointer; font-size: 0.95em; font-weight: 500; transition: all 0.3s ease; border: none; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); } | |
| .sdg-nav-item:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5); filter: brightness(1.1); } | |
| .sdg-nav-item:active { transform: translateY(0); } | |
| .content { background: rgba(255, 255, 255, 0.95); border-radius: 15px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); backdrop-filter: blur(10px); min-height: 400px; } | |
| /* AGGIUNTA: Stile Sezione Feedback */ | |
| .feedback-section { background: rgba(255, 255, 255, 0.95); border-radius: 15px; padding: 30px; margin-top: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); backdrop-filter: blur(10px); } | |
| .feedback-input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #ccc; margin-bottom: 15px; font-family: inherit; font-size: 1em; } | |
| .btn-send { background: linear-gradient(135deg, #28a745, #20c997); color: white; border: none; padding: 12px 25px; border-radius: 8px; cursor: pointer; font-weight: bold; width: 100%; transition: all 0.3s ease; font-size: 1.1em; } | |
| .btn-send:hover { transform: translateY(-2px); filter: brightness(1.05); } | |
| .file-section { margin-bottom: 40px; border: 2px solid #e1e5e9; border-radius: 12px; overflow: hidden; } | |
| .file-header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 20px; font-size: 1.3em; font-weight: 600; } | |
| .file-content { padding: 25px; } | |
| .summary { background: #f8f9fa; border-left: 4px solid #667eea; padding: 20px; margin: 20px 0; border-radius: 0 8px 8px 0; line-height: 1.6; white-space: pre-wrap; } | |
| .cluster { margin: 30px 0; border: 1px solid #dee2e6; border-radius: 10px; overflow: hidden; } | |
| .cluster-header { background: linear-gradient(45deg, #28a745, #20c997); color: white; padding: 15px 20px; font-weight: 600; font-size: 1.1em; } | |
| .cluster-content { padding: 20px; } | |
| .cluster-summary { background: #e8f5e8; border-radius: 8px; padding: 15px; line-height: 1.6; white-space: pre-wrap; } | |
| .qa-item { margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff; } | |
| .question { font-weight: 600; color: #495057; margin-bottom: 10px; font-size: 1.05em; } | |
| .answer { color: #6c757d; line-height: 1.5; white-space: pre-wrap; } | |
| .no-data { text-align: center; color: #6c757d; font-style: italic; padding: 40px; } | |
| .citation { display: inline; background: #007bff; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; font-weight: 600; cursor: pointer; margin: 0 2px; transition: all 0.3s ease; position: relative; } | |
| .citation:hover { background: #0056b3; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3); } | |
| .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(3px); } | |
| .modal-content { background: white; margin: 5% auto; padding: 0; border-radius: 15px; width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); animation: modalSlideIn 0.3s ease-out; } | |
| @keyframes modalSlideIn { from { transform: translateY(-50px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } | |
| .modal-header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 20px; border-radius: 15px 15px 0 0; display: flex; justify-content: space-between; align-items: center; } | |
| .modal-title { font-size: 1.2em; font-weight: 600; margin: 0; } | |
| .close { color: white; font-size: 28px; font-weight: bold; cursor: pointer; line-height: 1; transition: opacity 0.3s; } | |
| .close:hover { opacity: 0.7; } | |
| .modal-body { padding: 25px; } | |
| .context-section { margin-bottom: 20px; } | |
| .context-label { font-weight: 600; color: #495057; margin-bottom: 8px; font-size: 1.05em; } | |
| .context-content { background: #f8f9fa; padding: 15px; border-radius: 8px; line-height: 1.6; border-left: 4px solid #007bff; } | |
| .context-url { margin-top: 15px; } | |
| .context-url a { color: #007bff; text-decoration: none; word-break: break-all; font-size: 0.95em; } | |
| .context-url a:hover { text-decoration: underline; } | |
| .collapsible-header { cursor: pointer; position: relative; transition: background-color 0.2s ease; } | |
| .collapsible-header:hover { filter: brightness(1.1); } | |
| .collapsible-header::after { content: '+'; font-family: monospace; font-weight: bold; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); font-size: 1.6em; line-height: 1; transition: transform 0.3s ease; } | |
| .collapsible-header.active::after { content: '−'; transform: translateY(-50%) rotate(180deg); } | |
| .collapsible-content { | |
| display: grid; | |
| grid-template-rows: 0fr; | |
| transition: grid-template-rows 0.4s ease-out; | |
| } | |
| .collapsible-header.active + .collapsible-content { | |
| grid-template-rows: 1fr; | |
| } | |
| .collapsible-content > div { | |
| overflow: hidden; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header class="header"> | |
| <h1>Our Report </h1> | |
| <p>Select an event and view to visualize the data</p> | |
| </header> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label for="eventSelector">Select Event:</label> | |
| <select id="eventSelector"></select> | |
| </div> | |
| <div class="control-group"> | |
| <label>Arranged by: </label> | |
| <div class="radio-group"> | |
| <label class="radio-option"><input type="radio" name="viewType" value="subtopics" checked><span>Sub Topics</span></label> | |
| <label class="radio-option"><input type="radio" name="viewType" value="sdgs"><span>SDGs</span></label> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Presented as: </label> | |
| <div class="radio-group"> | |
| <label class="radio-option"><input type="radio" name="format" value="qa" checked><span>Q&A</span></label> | |
| <label class="radio-option"><input type="radio" name="format" value="summary"><span>Summary</span></label> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="imageContainer"> | |
| <img id="eventImage" src="" alt="Event visualization"> | |
| </div> | |
| <div id="sdgNavContainer" class="sdg-nav-container"> | |
| <div class="sdg-nav-title">Jump to SDG:</div> | |
| <div id="sdgNav" class="sdg-nav"></div> | |
| </div> | |
| <div class="content" id="content"></div> | |
| <div class="feedback-section"> | |
| <h3 style="margin-bottom: 15px; color: #555;">💬 Inviaci un Feedback</h3> | |
| <input type="text" id="fbUser" class="feedback-input" placeholder="Il tuo nome"> | |
| <textarea id="fbComment" class="feedback-input" rows="4" placeholder="Cosa ne pensi di questo report?"></textarea> | |
| <button class="btn-send" onclick="sendFeedback()">Invia Feedback</button> | |
| <div id="fbStatus" style="margin-top: 10px; font-weight: bold; text-align: center;"></div> | |
| </div> | |
| </div> | |
| <div id="citationModal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title">Citation Details</h3> | |
| <span class="close">×</span> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="context-section"> | |
| <div class="context-label">Context:</div> | |
| <div class="context-content" id="modalContext"></div> | |
| </div> | |
| <div class="context-section"> | |
| <div class="context-label">Title:</div> | |
| <div id="modalTitle"></div> | |
| </div> | |
| <div class="context-section context-url"> | |
| <div class="context-label">Source URL:</div> | |
| <div><a id="modalUrl" href="#" target="_blank"></a></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const folderMap = { | |
| "qa_subtopics": "./Data/Our_system/QA+Topics", | |
| "qa_sdgs": "./Data/Our_system/QA+SDGs", | |
| "summary_subtopics": "./Data/Our_system/Summary+Topics", | |
| "summary_sdgs": "./Data/Our_system/Summary+SDG" | |
| }; | |
| let currentEventBase = ''; | |
| let currentFormat = 'qa'; | |
| let currentViewType = 'subtopics'; | |
| let currentData = null; | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| await populateEventSelector(); | |
| initializeModal(); | |
| initializeCollapsibles(); | |
| const selector = document.getElementById('eventSelector'); | |
| selector.addEventListener('change', handleSelectionChange); | |
| document.querySelectorAll('input[name="format"], input[name="viewType"]').forEach(radio => { | |
| radio.addEventListener('change', handleSelectionChange); | |
| }); | |
| const contentContainer = document.getElementById('content'); | |
| contentContainer.addEventListener('click', function(event) { | |
| const citationSpan = event.target.closest('.citation'); | |
| if (citationSpan) { | |
| showCitationPopup(citationSpan); | |
| } | |
| }); | |
| handleSelectionChange(); | |
| }); | |
| // AGGIUNTA: Funzione per aggiornare l'immagine | |
| function updateEventImage(eventName) { | |
| const container = document.getElementById('imageContainer'); | |
| const img = document.getElementById('eventImage'); | |
| if (!eventName) { | |
| container.style.display = 'none'; | |
| return; | |
| } | |
| // Cerchiamo l'immagine JPG (puoi cambiare estensione se necessario) | |
| img.src = `/Images/${eventName}.jpg`; | |
| img.onload = () => { | |
| container.style.display = 'block'; | |
| }; | |
| img.onerror = () => { | |
| container.style.display = 'none'; | |
| console.log("Immagine non trovata per:", eventName); | |
| }; | |
| } | |
| // AGGIUNTA: Funzione per invio feedback | |
| async function sendFeedback() { | |
| const user = document.getElementById('fbUser').value; | |
| const comment = document.getElementById('fbComment').value; | |
| const status = document.getElementById('fbStatus'); | |
| if (!user || !comment) { | |
| alert("Per favore, inserisci nome e commento."); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('user', user); | |
| formData.append('comment', comment); | |
| try { | |
| const response = await fetch('/submit-feedback', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (response.ok) { | |
| status.style.color = "#28a745"; | |
| status.textContent = "✅ Feedback inviato con successo! Grazie."; | |
| document.getElementById('fbUser').value = ""; | |
| document.getElementById('fbComment').value = ""; | |
| } else { | |
| throw new Error(); | |
| } | |
| } catch (e) { | |
| status.style.color = "#dc3545"; | |
| status.textContent = "❌ Errore durante l'invio. Riprova."; | |
| } | |
| } | |
| async function listFilesInFolder(folderUrl) { | |
| try { | |
| try { | |
| const response = await fetch(`${folderUrl}/index.json`); | |
| if (response.ok) { | |
| const files = await response.json(); | |
| return files; | |
| } | |
| } catch (e) { | |
| console.log('index.json not found, trying directory listing'); | |
| } | |
| const url = encodeURI(folderUrl + '/'); | |
| const resp = await fetch(url, { cache: 'no-store' }); | |
| if (!resp.ok) throw new Error('No index listing'); | |
| const text = await resp.text(); | |
| const filenames = new Set(); | |
| const hrefRegex = /href=["']([^"']+)["']/gi; | |
| for (const m of text.matchAll(hrefRegex)) { | |
| try { | |
| const href = m[1]; | |
| const decoded = decodeURIComponent(href.split('/').pop()); | |
| if (/_combined_data\.json$/i.test(decoded)) filenames.add(decoded); | |
| } catch (e) {} | |
| } | |
| const rawRegex = /([A-Za-z0-9_\- ]+)_combined_data\.json/gi; | |
| for (const m of text.matchAll(rawRegex)) { | |
| filenames.add(m[0]); | |
| } | |
| return Array.from(filenames); | |
| } catch (err) { | |
| console.warn('Could not list folder', folderUrl, err); | |
| return []; | |
| } | |
| } | |
| async function populateEventSelector() { | |
| const selector = document.getElementById('eventSelector'); | |
| selector.innerHTML = '<option value="">-- Please select an event --</option>'; | |
| const allBases = new Set(); | |
| for (const folder of Object.values(folderMap)) { | |
| const files = await listFilesInFolder(folder); | |
| files.forEach(f => { | |
| const base = f.replace(/_combined_data\.json$/i, ''); | |
| if (!/^\d{4,}$/.test(base.trim())) { | |
| allBases.add(base); | |
| } | |
| }); | |
| } | |
| if (allBases.size === 0) { | |
| console.warn('No files found in any folder'); | |
| } | |
| Array.from(allBases).sort((a,b)=> a.localeCompare(b)).forEach(name => { | |
| const option = document.createElement('option'); | |
| option.value = name; | |
| option.textContent = name.replace(/_/g, ' '); | |
| selector.appendChild(option); | |
| }); | |
| } | |
| function handleSelectionChange() { | |
| currentEventBase = document.getElementById('eventSelector').value; | |
| currentFormat = document.querySelector('input[name="format"]:checked').value; | |
| currentViewType = document.querySelector('input[name="viewType"]:checked').value; | |
| // AGGIUNTA: Aggiorna immagine | |
| updateEventImage(currentEventBase); | |
| const sdgNavContainer = document.getElementById('sdgNavContainer'); | |
| if (currentViewType === 'sdgs') { | |
| sdgNavContainer.classList.add('visible'); | |
| } else { | |
| sdgNavContainer.classList.remove('visible'); | |
| } | |
| loadSelectedEventData(); | |
| } | |
| async function loadSelectedEventData() { | |
| const content = document.getElementById('content'); | |
| if (!currentEventBase) { | |
| content.innerHTML = '<div class="no-data"><h2>Please select an event from the dropdown above.</h2></div>'; | |
| return; | |
| } | |
| content.innerHTML = '<div class="no-data"><h2>📄 Loading data...</h2></div>'; | |
| const key = `${currentFormat}_${currentViewType}`; | |
| const folder = folderMap[key]; | |
| const filename = `${folder}/${currentEventBase}_combined_data.json`; | |
| try { | |
| const response = await fetch(encodeURI(filename)); | |
| if (!response.ok) throw new Error(`File not found (${response.status})`); | |
| currentData = await response.json(); | |
| renderContent(); | |
| } catch (error) { | |
| console.error(`Error loading ${filename}:`, error); | |
| content.innerHTML = `<div class="no-data"><h2>⚠ Error loading data</h2> | |
| <p>Could not load: <strong>${filename}</strong></p> | |
| <p>Make sure the file exists in the selected folder and is accessible from the browser.</p></div>`; | |
| currentData = null; | |
| } | |
| } | |
| function renderContent() { | |
| if (!currentData) return; | |
| const content = document.getElementById('content'); | |
| const clustersToRender = processFileData(currentData); | |
| if (currentViewType === 'sdgs') { | |
| selectedSDGIndex = 0; | |
| populateSDGNavigation(clustersToRender); | |
| } else { | |
| content.innerHTML = ` | |
| <div class="file-section"> | |
| <div class="file-header collapsible-header">📄 ${escapeHtml(currentData.file_name || currentEventBase)}</div> | |
| <div class="file-content collapsible-content"> | |
| <div> | |
| <div class="summary"><strong>Summary:</strong><br>${processCitationsInText(escapeHtml(currentData.summary || 'No summary available'), currentData.summary_contexts || {})}</div> | |
| ${renderClusters(clustersToRender)} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| } | |
| let selectedSDGIndex = 0; | |
| function populateSDGNavigation(clusters) { | |
| const sdgNav = document.getElementById('sdgNav'); | |
| sdgNav.innerHTML = ''; | |
| clusters.forEach((cluster, index) => { | |
| const button = document.createElement('button'); | |
| button.className = 'sdg-nav-item'; | |
| if (index === selectedSDGIndex) { | |
| button.style.background = 'linear-gradient(135deg, #28a745, #20c997)'; | |
| } | |
| button.textContent = cluster.cluster_headline || cluster.cluster_id || `SDG ${index + 1}`; | |
| button.addEventListener('click', () => selectSDG(index, clusters)); | |
| sdgNav.appendChild(button); | |
| }); | |
| renderSingleSDG(clusters[selectedSDGIndex]); | |
| } | |
| function selectSDG(index, clusters) { | |
| selectedSDGIndex = index; | |
| populateSDGNavigation(clusters); | |
| } | |
| function renderSingleSDG(cluster) { | |
| const content = document.getElementById('content'); | |
| content.innerHTML = ` | |
| <div class="file-section"> | |
| <div class="file-header">📄 ${escapeHtml(currentData.file_name || currentEventBase)}</div> | |
| <div class="file-content"> | |
| <div> | |
| <div class="summary"><strong>Summary:</strong><br>${processCitationsInText(escapeHtml(currentData.summary || 'No summary available'), currentData.summary_contexts || {})}</div> | |
| <div class="cluster"> | |
| <div class="cluster-header">🎯 ${escapeHtml(cluster.cluster_headline || cluster.cluster_id || 'Cluster')}</div> | |
| <div class="cluster-content"> | |
| <div> | |
| ${currentFormat === 'summary' ? renderSummaryContent(cluster) : renderQAContent(cluster)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function processFileData(fileData) { | |
| if (Array.isArray(fileData.clusters)) return fileData.clusters; | |
| const ignoreKeys = ['file_name', 'summary', 'summary_contexts']; | |
| const clusters = []; | |
| for (const key in fileData) { | |
| if (!fileData.hasOwnProperty(key)) continue; | |
| if (ignoreKeys.includes(key)) continue; | |
| const value = fileData[key]; | |
| if (currentFormat === 'qa') { | |
| clusters.push({ cluster_headline: key, questions_and_answers: Array.isArray(value) ? value : [] }); | |
| } else { | |
| if (Array.isArray(value)) { | |
| const firstItem = value[0] || {}; | |
| const joined = value.map(v => v.cluster_summary || v.summary || (typeof v === 'string' ? v : '')).filter(Boolean).join('\n\n'); | |
| clusters.push({ | |
| cluster_headline: key, | |
| cluster_summary: joined || 'No cluster summary available', | |
| used_contexts: firstItem.used_contexts || {} | |
| }); | |
| } else if (typeof value === 'string') { | |
| clusters.push({ cluster_headline: key, cluster_summary: value, used_contexts: {} }); | |
| } else if (typeof value === 'object' && value !== null) { | |
| clusters.push({ | |
| cluster_headline: key, | |
| cluster_summary: value.cluster_summary || value.summary || JSON.stringify(value), | |
| used_contexts: value.used_contexts || {} | |
| }); | |
| } else { | |
| clusters.push({ cluster_headline: key, cluster_summary: 'No cluster summary available', used_contexts: {} }); | |
| } | |
| } | |
| } | |
| return clusters; | |
| } | |
| function renderClusters(clusters) { | |
| if (!clusters || clusters.length === 0) { | |
| return '<div class="no-data">No content available for this view.</div>'; | |
| } | |
| return clusters.map(cluster => ` | |
| <div class="cluster"> | |
| <div class="cluster-header collapsible-header">🎯 ${escapeHtml(cluster.cluster_headline || cluster.cluster_id || 'Cluster')}</div> | |
| <div class="cluster-content collapsible-content"> | |
| <div> | |
| ${currentFormat === 'summary' ? renderSummaryContent(cluster) : renderQAContent(cluster)} | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function renderSummaryContent(cluster) { | |
| const clusterSummary = cluster.cluster_summary || cluster.summary || 'No cluster summary available'; | |
| const contexts = cluster.summary_contexts || cluster.used_contexts || {}; | |
| return `<div class="cluster-summary">${processCitationsInText(escapeHtml(clusterSummary), contexts)}</div>`; | |
| } | |
| function renderQAContent(cluster) { | |
| const qas = cluster.questions_and_answers || []; | |
| if (!qas || qas.length === 0) return '<div class="no-data">No questions and answers available</div>'; | |
| return qas.map(qa => { | |
| const question = qa.question || qa.Question || 'No question provided'; | |
| const answer = qa.updated_retrieved_answer || qa.retrieved_answer || qa.answer || qa.updated_answer || 'No answer available'; | |
| const contexts = qa.used_contexts || qa.summary_contexts || {}; | |
| return ` | |
| <div class="qa-item"> | |
| <div class="question">❓ ${escapeHtml(question)}</div> | |
| <div class="answer">${processCitationsInText(escapeHtml(answer), contexts)}</div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function processCitationsInText(text, contexts) { | |
| if (!text || !contexts) return text || ''; | |
| const citationRegex = /\[(\d+)\]/g; | |
| return text.replace(citationRegex, (match, citationId) => { | |
| const ctx = contexts[citationId]; | |
| if (ctx) { | |
| const contextData = { context: ctx.context || '', title: ctx.title || '', url: ctx.url || '' }; | |
| const encoded = encodeURIComponent(JSON.stringify(contextData)); | |
| return `<span class="citation" data-context="${encoded}">[${citationId}]</span>`; | |
| } | |
| return match; | |
| }); | |
| } | |
| function showCitationPopup(citationElement) { | |
| const contextDataStr = citationElement.getAttribute('data-context'); | |
| if (!contextDataStr) return; | |
| try { | |
| const contextData = JSON.parse(decodeURIComponent(contextDataStr)); | |
| document.getElementById('modalContext').textContent = contextData.context || 'No context available.'; | |
| document.getElementById('modalTitle').textContent = contextData.title || 'No title available.'; | |
| document.getElementById('modalUrl').textContent = contextData.url || 'No URL provided.'; | |
| document.getElementById('modalUrl').href = contextData.url || '#'; | |
| document.getElementById('citationModal').style.display = 'block'; | |
| } catch (error) { | |
| console.error('Error parsing context data:', error); | |
| } | |
| } | |
| function initializeModal() { | |
| const modal = document.getElementById('citationModal'); | |
| const closeBtn = document.querySelector('.close'); | |
| closeBtn.addEventListener('click', () => modal.style.display = 'none'); | |
| window.addEventListener('click', event => { if (event.target === modal) modal.style.display = 'none'; }); | |
| document.addEventListener('keydown', event => { if (event.key === 'Escape' && modal.style.display === 'block') modal.style.display = 'none'; }); | |
| } | |
| function initializeCollapsibles() { | |
| document.querySelector('.container').addEventListener('click', function(event) { | |
| const header = event.target.closest('.collapsible-header'); | |
| if (!header) return; | |
| header.classList.toggle('active'); | |
| }); | |
| } | |
| function escapeHtml(unsafe) { | |
| if (unsafe === null || unsafe === undefined) return ''; | |
| return String(unsafe) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| </script> | |
| </body> | |
| </html> |