Decoo's picture
Fixed codes and added Docker
36b221d
<!DOCTYPE html>
<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">&times;</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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
</script>
</body>
</html>