Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import json | |
| import os | |
| from datetime import datetime | |
| import urllib.parse | |
| # Data folder paths | |
| DATA_BASE = "Viewer/Data/Our_system" | |
| FOLDER_MAP = { | |
| "qa_subtopics": f"{DATA_BASE}/QA+Topics", | |
| "qa_sdgs": f"{DATA_BASE}/QA+SDGs", | |
| "summary_subtopics": f"{DATA_BASE}/Summary+Topics", | |
| "summary_sdgs": f"{DATA_BASE}/Summary+SDG" | |
| } | |
| def get_available_events(): | |
| """Get all available events from JSON files""" | |
| events = set() | |
| for folder in FOLDER_MAP.values(): | |
| if os.path.exists(folder): | |
| for file in os.listdir(folder): | |
| if file.endswith('_combined_data.json'): | |
| event_name = file.replace('_combined_data.json', '') | |
| if not event_name.isdigit(): | |
| events.add(event_name) | |
| return sorted(list(events)) | |
| def load_event_data(event_name, format_type, view_type): | |
| """Load and render event data as HTML""" | |
| if not event_name: | |
| return '<div style="text-align: center; padding: 40px; color: #6c757d;"><h2>Please select an event from the dropdown above.</h2></div>' | |
| key = f"{format_type}_{view_type}" | |
| folder = FOLDER_MAP.get(key) | |
| filename = f"{folder}/{event_name}_combined_data.json" | |
| try: | |
| with open(filename, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| return render_data_as_html(data, event_name, format_type, view_type) | |
| except FileNotFoundError: | |
| return f'<div style="text-align: center; padding: 40px;"><h2>β File not found</h2><p>{filename}</p></div>' | |
| except Exception as e: | |
| return f'<div style="text-align: center; padding: 40px;"><h2>β Error</h2><p>{str(e)}</p></div>' | |
| def render_data_as_html(data, event_name, format_type, view_type): | |
| """Render JSON data as HTML""" | |
| # Estrai informazioni | |
| file_name = data.get('file_name', event_name) | |
| summary = data.get('summary', 'No summary available') | |
| summary_contexts = data.get('summary_contexts', {}) | |
| # Process clusters | |
| clusters = process_clusters(data, format_type) | |
| # Genera un ID unico per questa istanza | |
| unique_id = f"viewer_{abs(hash(event_name + format_type + view_type)) % 100000}" | |
| # Render HTML con modal | |
| html_parts = [f''' | |
| <div id="{unique_id}_container"> | |
| <!-- Modal for citations --> | |
| <div id="{unique_id}_modal" class="citation-modal" style="display: none; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(3px);"> | |
| <div class="citation-modal-content" style="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: slideIn 0.3s ease-out;"> | |
| <div style="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;"> | |
| <h3 style="font-size: 1.2em; font-weight: 600; margin: 0;">Citation Details</h3> | |
| <span class="citation-close" style="color: white; font-size: 28px; font-weight: bold; cursor: pointer; line-height: 1; transition: opacity 0.3s;">×</span> | |
| </div> | |
| <div style="padding: 25px;"> | |
| <div style="margin-bottom: 20px;"> | |
| <div style="font-weight: 600; color: #495057; margin-bottom: 8px; font-size: 1.05em;">Context:</div> | |
| <div class="citation-context" style="background: #f8f9fa; padding: 15px; border-radius: 8px; line-height: 1.6; border-left: 4px solid #007bff;"></div> | |
| </div> | |
| <div style="margin-bottom: 20px;"> | |
| <div style="font-weight: 600; color: #495057; margin-bottom: 8px; font-size: 1.05em;">Title:</div> | |
| <div class="citation-title"></div> | |
| </div> | |
| <div style="margin-top: 15px;"> | |
| <div style="font-weight: 600; color: #495057; margin-bottom: 8px; font-size: 1.05em;">Source URL:</div> | |
| <div><a class="citation-url" href="#" target="_blank" style="color: #007bff; text-decoration: none; word-break: break-all; font-size: 0.95em;"></a></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="margin-bottom: 40px; border: 2px solid #e1e5e9; border-radius: 12px; overflow: hidden;"> | |
| <div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 20px; font-size: 1.3em; font-weight: 600;"> | |
| π {escape_html(file_name)} | |
| </div> | |
| <div style="padding: 25px;"> | |
| <div style="background: #f8f9fa; border-left: 4px solid #667eea; padding: 20px; margin: 20px 0; border-radius: 0 8px 8px 0; line-height: 1.6;"> | |
| <strong>Summary:</strong><br>{process_citations(escape_html(summary), summary_contexts, unique_id)} | |
| </div> | |
| '''] | |
| # Render clusters | |
| for cluster in clusters: | |
| html_parts.append(render_cluster(cluster, format_type, unique_id)) | |
| html_parts.append(f''' | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes slideIn {{ | |
| from {{ transform: translateY(-50px); opacity: 0; }} | |
| to {{ transform: translateY(0); opacity: 1; }} | |
| }} | |
| .citation-close:hover {{ | |
| opacity: 0.7; | |
| }} | |
| .citation-badge {{ | |
| 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; | |
| }} | |
| .citation-badge:hover {{ | |
| background: #0056b3; | |
| transform: translateY(-1px); | |
| box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3); | |
| }} | |
| </style> | |
| <script> | |
| (function() {{ | |
| const container = document.getElementById('{unique_id}_container'); | |
| const modal = container.querySelector('#{unique_id}_modal'); | |
| const closeBtn = modal.querySelector('.citation-close'); | |
| // Close on X button | |
| closeBtn.addEventListener('click', function() {{ | |
| modal.style.display = 'none'; | |
| }}); | |
| // Close on outside click | |
| modal.addEventListener('click', function(event) {{ | |
| if (event.target === modal) {{ | |
| modal.style.display = 'none'; | |
| }} | |
| }}); | |
| // Close on ESC | |
| document.addEventListener('keydown', function(event) {{ | |
| if (event.key === 'Escape' && modal.style.display === 'block') {{ | |
| modal.style.display = 'none'; | |
| }} | |
| }}); | |
| // Handle citation clicks | |
| container.addEventListener('click', function(event) {{ | |
| const target = event.target; | |
| if (target.classList && target.classList.contains('citation-badge')) {{ | |
| const contextData = target.getAttribute('data-context'); | |
| const titleData = target.getAttribute('data-title'); | |
| const urlData = target.getAttribute('data-url'); | |
| if (contextData || titleData || urlData) {{ | |
| const contextEl = modal.querySelector('.citation-context'); | |
| const titleEl = modal.querySelector('.citation-title'); | |
| const urlEl = modal.querySelector('.citation-url'); | |
| contextEl.textContent = decodeURIComponent(contextData || 'No context available.'); | |
| titleEl.textContent = decodeURIComponent(titleData || 'No title available.'); | |
| urlEl.textContent = decodeURIComponent(urlData || 'No URL provided.'); | |
| urlEl.href = decodeURIComponent(urlData || '#'); | |
| modal.style.display = 'block'; | |
| }} | |
| }} | |
| }}); | |
| }})(); | |
| </script> | |
| ''') | |
| return ''.join(html_parts) | |
| def process_clusters(data, format_type): | |
| """Extract clusters from data""" | |
| if 'clusters' in data and isinstance(data['clusters'], list): | |
| return data['clusters'] | |
| ignore_keys = ['file_name', 'summary', 'summary_contexts'] | |
| clusters = [] | |
| for key, value in data.items(): | |
| if key in ignore_keys: | |
| continue | |
| if format_type == 'qa': | |
| clusters.append({ | |
| 'cluster_headline': key, | |
| 'questions_and_answers': value if isinstance(value, list) else [] | |
| }) | |
| else: | |
| if isinstance(value, list): | |
| summary_text = '\n\n'.join([ | |
| v.get('cluster_summary') or v.get('summary', '') | |
| for v in value if isinstance(v, dict) | |
| ]) | |
| clusters.append({ | |
| 'cluster_headline': key, | |
| 'cluster_summary': summary_text or 'No summary', | |
| 'used_contexts': value[0].get('used_contexts', {}) if value else {} | |
| }) | |
| elif isinstance(value, dict): | |
| clusters.append({ | |
| 'cluster_headline': key, | |
| 'cluster_summary': value.get('cluster_summary') or value.get('summary', 'No summary'), | |
| 'used_contexts': value.get('used_contexts', {}) | |
| }) | |
| return clusters | |
| def render_cluster(cluster, format_type, unique_id): | |
| """Render a single cluster as HTML""" | |
| header = cluster.get('cluster_headline', 'Cluster') | |
| html = f''' | |
| <div style="margin: 30px 0; border: 1px solid #dee2e6; border-radius: 10px; overflow: hidden;"> | |
| <details> | |
| <summary style="background: linear-gradient(45deg, #28a745, #20c997); color: white; padding: 15px 20px; font-weight: 600; font-size: 1.1em; cursor: pointer;"> | |
| π― {escape_html(header)} | |
| </summary> | |
| <div style="padding: 20px;"> | |
| ''' | |
| if format_type == 'summary': | |
| summary_text = cluster.get('cluster_summary', 'No summary') | |
| contexts = cluster.get('used_contexts', {}) | |
| html += f'<div style="background: #e8f5e8; border-radius: 8px; padding: 15px; line-height: 1.6;">{process_citations(escape_html(summary_text), contexts, unique_id)}</div>' | |
| else: | |
| qas = cluster.get('questions_and_answers', []) | |
| if not qas: | |
| html += '<div style="text-align: center; color: #6c757d; font-style: italic;">No Q&A available</div>' | |
| else: | |
| for qa in qas: | |
| question = qa.get('question') or qa.get('Question', 'No question') | |
| answer = qa.get('updated_retrieved_answer') or qa.get('retrieved_answer') or qa.get('answer', 'No answer') | |
| contexts = qa.get('used_contexts') or qa.get('summary_contexts', {}) | |
| html += f''' | |
| <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;"> | |
| <div style="font-weight: 600; color: #495057; margin-bottom: 10px; font-size: 1.05em;"> | |
| β {escape_html(question)} | |
| </div> | |
| <div style="color: #6c757d; line-height: 1.5;"> | |
| {process_citations(escape_html(answer), contexts, unique_id)} | |
| </div> | |
| </div> | |
| ''' | |
| html += '</div></details></div>' | |
| return html | |
| def process_citations(text, contexts, unique_id): | |
| """Add citation spans with popup data""" | |
| import re | |
| def replace_citation(match): | |
| cit_id = match.group(1) | |
| ctx = contexts.get(cit_id, {}) | |
| if not ctx: | |
| return match.group(0) | |
| context_text = urllib.parse.quote(ctx.get('context', '')[:500]) | |
| title_text = urllib.parse.quote(ctx.get('title', '')) | |
| url_text = urllib.parse.quote(ctx.get('url', '')) | |
| return f'<span class="citation-badge" data-context="{context_text}" data-title="{title_text}" data-url="{url_text}">[{cit_id}]</span>' | |
| return re.sub(r'\[(\d+)\]', replace_citation, text) | |
| def escape_html(text): | |
| """Escape HTML special characters""" | |
| if not text: | |
| return '' | |
| return str(text).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') | |
| def save_feedback(event, format_type, view_type, rating, comment): | |
| """Save feedback to JSON file""" | |
| if not event: | |
| return "β οΈ Please select an event first" | |
| feedback_dir = "feedback" | |
| os.makedirs(feedback_dir, exist_ok=True) | |
| feedback_file = os.path.join(feedback_dir, "feedback.json") | |
| if os.path.exists(feedback_file): | |
| with open(feedback_file, 'r', encoding='utf-8') as f: | |
| feedbacks = json.load(f) | |
| else: | |
| feedbacks = [] | |
| feedbacks.append({ | |
| "timestamp": datetime.now().isoformat(), | |
| "event": event, | |
| "format": format_type, | |
| "view": view_type, | |
| "rating": rating, | |
| "comment": comment | |
| }) | |
| with open(feedback_file, 'w', encoding='utf-8') as f: | |
| json.dump(feedbacks, f, indent=2, ensure_ascii=False) | |
| return "β Thank you for your feedback!" | |
| # Get events | |
| events = get_available_events() | |
| # CSS personalizzato con font originale | |
| custom_css = """ | |
| * { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .gradio-container { | |
| max-width: 1200px !important; | |
| } | |
| """ | |
| # Create Gradio interface | |
| with gr.Blocks(title="Report Viewer", theme=gr.themes.Soft(), css=custom_css) as demo: | |
| gr.Markdown(""" | |
| <div style="text-align: center; color: white; margin-bottom: 30px;"> | |
| <h1 style="font-size: 2.5em; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">π Our Report Viewer</h1> | |
| <p style="font-size: 1.2em; opacity: 0.9;">Explore events organized by topics or SDGs</p> | |
| </div> | |
| """) | |
| with gr.Tab("π Viewer"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| event_dropdown = gr.Dropdown( | |
| choices=events, | |
| label="Select Event", | |
| value=None | |
| ) | |
| with gr.Column(scale=1): | |
| view_radio = gr.Radio( | |
| choices=["subtopics", "sdgs"], | |
| label="Arranged by", | |
| value="subtopics" | |
| ) | |
| with gr.Column(scale=1): | |
| format_radio = gr.Radio( | |
| choices=["qa", "summary"], | |
| label="Presented as", | |
| value="qa" | |
| ) | |
| content_html = gr.HTML( | |
| '<div style="text-align: center; padding: 40px; color: #6c757d;"><h2>Please select an event from the dropdown above.</h2></div>' | |
| ) | |
| # Update on any change | |
| event_dropdown.change( | |
| load_event_data, | |
| inputs=[event_dropdown, format_radio, view_radio], | |
| outputs=content_html | |
| ) | |
| format_radio.change( | |
| load_event_data, | |
| inputs=[event_dropdown, format_radio, view_radio], | |
| outputs=content_html | |
| ) | |
| view_radio.change( | |
| load_event_data, | |
| inputs=[event_dropdown, format_radio, view_radio], | |
| outputs=content_html | |
| ) | |
| with gr.Tab("π¬ Feedback"): | |
| gr.Markdown("### Leave your feedback on the results") | |
| with gr.Row(): | |
| fb_event = gr.Dropdown(choices=events, label="Event") | |
| fb_format = gr.Radio(choices=["qa", "summary"], label="Format", value="qa") | |
| fb_view = gr.Radio(choices=["subtopics", "sdgs"], label="View", value="subtopics") | |
| fb_rating = gr.Slider(minimum=1, maximum=5, step=1, label="Rating (1-5 stars)", value=3) | |
| fb_comment = gr.Textbox(label="Comment (optional)", lines=5, placeholder="Write your feedback here...") | |
| fb_submit = gr.Button("Submit Feedback", variant="primary") | |
| fb_output = gr.Textbox(label="Status", interactive=False) | |
| fb_submit.click( | |
| save_feedback, | |
| inputs=[fb_event, fb_format, fb_view, fb_rating, fb_comment], | |
| outputs=fb_output | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |