Spaces:
Running
Running
""" | |
Generates HTML content and Matplotlib plots for the Gradio UI tabs, | |
and UI components for the Analytics tab. | |
""" | |
import pandas as pd | |
import logging | |
import matplotlib.pyplot as plt | |
import matplotlib # To ensure backend is switched before any plt import from other modules if app structure changes | |
import gradio as gr # Added for UI components | |
# Switch backend for Matplotlib to Agg for Gradio compatibility | |
matplotlib.use('Agg') | |
# Assuming config.py contains all necessary constants | |
from config import ( | |
BUBBLE_POST_DATE_COLUMN_NAME, BUBBLE_MENTIONS_DATE_COLUMN_NAME, BUBBLE_MENTIONS_ID_COLUMN_NAME, | |
FOLLOWER_STATS_TYPE_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, | |
FOLLOWER_STATS_PAID_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN_DT, UI_DATE_FORMAT, UI_MONTH_FORMAT | |
) | |
# Configure logging for this module if not already configured at app level | |
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(module)s - %(message)s') | |
# --- Constants for Button Icons/Text --- | |
# These are also defined/imported in app.py, ensure consistency | |
BOMB_ICON = "💣" | |
EXPLORE_ICON = "🧭" | |
FORMULA_ICON = "ƒ" | |
ACTIVE_ICON = "❌ Close" # Ensure this matches app.py | |
def build_home_tab_ui(): | |
""" | |
Costruisce l'intera interfaccia utente per la scheda Home, inclusi l'intestazione, | |
la panoramica, le schede delle funzionalità e i pulsanti di navigazione. | |
Returns: | |
tuple: Una tupla contenente i componenti Gradio Button per | |
Grafici, Report e Tabella OKR, consentendo ad app.py di | |
associare gestori di click per la navigazione tra le schede. | |
""" | |
# CSS personalizzato per uno stile migliorato | |
css_styles = """ | |
<style> | |
.hero-section { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
padding: 60px 40px; | |
border-radius: 24px; | |
margin-bottom: 40px; | |
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.25); | |
color: white; | |
text-align: center; | |
position: relative; | |
overflow: hidden; | |
} | |
.hero-section::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.05)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.05)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.03)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>'); | |
opacity: 0.1; | |
} | |
.hero-content { | |
position: relative; | |
z-index: 1; | |
} | |
.hero-title { | |
font-size: 3.2em; | |
font-weight: 800; | |
margin-bottom: 24px; | |
/* Applica gradiente e riempimento del testo al testo, ma non all'emoji */ | |
background: linear-gradient(45deg, #ffffff, #e0e7ff); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
background-clip: text; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
line-height: 1.2; | |
} | |
/* NUOVO CSS: Stile per l'emoji del razzo */ | |
.hero-title .rocket-emoji { | |
background: none; /* Rimuovi effetto di sfondo */ | |
-webkit-background-clip: unset; /* Ripristina il ritaglio dello sfondo */ | |
background-clip: unset; | |
-webkit-text-fill-color: initial; /* Ripristina il colore di riempimento del testo al valore predefinito */ | |
color: initial; /* Assicurati che il colore dell'emoji sia quello predefinito (spesso colorato) */ | |
text-shadow: none; /* Rimuovi l'ombra del testo per l'emoji se la influisce negativamente */ | |
display: inline-block; /* Assicurati che rispetti l'allineamento verticale */ | |
vertical-align: middle; /* Regola l'allineamento verticale se necessario */ | |
} | |
.hero-subtitle { | |
font-size: 1.4em; | |
line-height: 1.6; | |
margin-bottom: 16px; | |
opacity: 0.95; | |
font-weight: 300; | |
} | |
.hero-description { | |
font-size: 1.1em; | |
opacity: 0.85; | |
font-weight: 300; | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.overview-section { | |
background: #ffffff; | |
padding: 40px; | |
border-radius: 20px; | |
margin-bottom: 40px; | |
box-shadow: 0 10px 40px rgba(0,0,0,0.08); | |
border: 1px solid rgba(0,0,0,0.05); | |
} | |
.overview-header { | |
color: #1a202c; | |
margin-bottom: 24px; | |
font-size: 2.2em; | |
font-weight: 700; | |
display: flex; | |
align-items: center; | |
gap: 16px; | |
} | |
.overview-text { | |
font-size: 1.15em; | |
color: #4a5568; | |
line-height: 1.7; | |
margin-bottom: 32px; | |
font-weight: 400; | |
} | |
.features-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
gap: 24px; | |
margin-top: 32px; | |
} | |
.feature-item { | |
display: flex; | |
align-items: center; | |
padding: 20px; | |
background: linear-gradient(135deg, #f8fafc, #f1f5f9); | |
border-radius: 16px; | |
border: 1px solid rgba(0,0,0,0.06); | |
transition: all 0.3s ease; | |
} | |
.feature-item:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
} | |
.feature-icon { | |
font-size: 2em; | |
margin-right: 16px; | |
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); | |
} | |
.feature-text { | |
color: #2d3748; | |
font-weight: 500; | |
font-size: 1.05em; | |
} | |
.nav-cards-container { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
gap: 30px; | |
margin-bottom: 40px; | |
} | |
.nav-card { | |
padding: 36px; | |
border-radius: 20px; | |
min-height: 280px; | |
display: flex; | |
flex-direction: column; | |
justify-content: space-between; | |
box-shadow: 0 15px 45px rgba(0,0,0,0.12); | |
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
position: relative; | |
overflow: hidden; | |
} | |
.nav-card::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); | |
opacity: 0; | |
transition: opacity 0.3s ease; | |
} | |
.nav-card:hover::before { | |
opacity: 1; | |
} | |
.nav-card:hover { | |
transform: translateY(-8px) scale(1.02); | |
box-shadow: 0 25px 60px rgba(0,0,0,0.15); | |
} | |
.nav-card-graphs { | |
background: linear-gradient(135deg, #4CAF50, #2E7D32); | |
} | |
.nav-card-reports { | |
background: linear-gradient(135deg, #2196F3, #1565C0); | |
} | |
.nav-card-okr { | |
background: linear-gradient(135deg, #FF9800, #E65100); | |
} | |
.nav-card-help { | |
background: linear-gradient(135deg, #9C27B0, #4A148C); | |
} | |
.nav-card-header { | |
color: white; | |
margin-bottom: 20px; | |
font-size: 1.5em; | |
font-weight: 700; | |
display: flex; | |
align-items: center; | |
gap: 16px; | |
} | |
.nav-card-icon { | |
font-size: 2.2em; | |
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); | |
} | |
.nav-card-description { | |
color: rgba(255,255,255,0.95); | |
line-height: 1.7; | |
font-size: 1.08em; | |
margin-bottom: 24px; | |
font-weight: 300; | |
} | |
.nav-card-footer { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-top: 20px; | |
position: relative; | |
z-index: 1; | |
} | |
.nav-card-features { | |
color: rgba(255,255,255,0.85); | |
font-size: 0.95em; | |
line-height: 1.6; | |
} | |
.nav-card-badge { | |
background: rgba(255,255,255,0.2); | |
padding: 12px; | |
border-radius: 12px; | |
backdrop-filter: blur(10px); | |
} | |
.nav-card-badge-icon { | |
font-size: 2.2em; | |
} | |
.how-it-works-section { | |
background: linear-gradient(135deg, #f8fafc, #e2e8f0); | |
padding: 40px; | |
border-radius: 20px; | |
margin-top: 40px; | |
border: 1px solid rgba(0,0,0,0.06); | |
} | |
.how-it-works-header { | |
color: #1a202c; | |
margin-bottom: 32px; | |
font-size: 2em; | |
font-weight: 700; | |
display: flex; | |
align-items: center; | |
gap: 16px; | |
} | |
.steps-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); | |
gap: 32px; | |
} | |
.step-item { | |
display: flex; | |
align-items: flex-start; | |
gap: 20px; | |
} | |
.step-number { | |
background: linear-gradient(135deg, #667eea, #764ba2); | |
color: white; | |
border-radius: 50%; | |
width: 48px; | |
height: 48px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
flex-shrink: 0; | |
font-weight: 700; | |
font-size: 1.2em; | |
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3); | |
} | |
.step-content { | |
flex: 1; | |
} | |
.step-title { | |
color: #1a202c; | |
font-weight: 700; | |
font-size: 1.2em; | |
margin-bottom: 8px; | |
} | |
.step-description { | |
color: #4a5568; | |
font-size: 1em; | |
line-height: 1.6; | |
} | |
.button-container { | |
margin-top: 24px; | |
} | |
@media (max-width: 768px) { | |
.hero-title { | |
font-size: 2.4em; | |
} | |
.nav-cards-container { | |
grid-template-columns: 1fr; | |
} | |
.nav-card { | |
min-height: 240px; | |
padding: 28px; | |
} | |
.hero-section { | |
padding: 40px 24px; | |
} | |
.overview-section, .how-it-works-section { | |
padding: 28px; | |
} | |
} | |
</style> | |
""" | |
with gr.Column(scale=1, elem_classes="home-page-container"): | |
# Inietta CSS personalizzato | |
gr.HTML(css_styles) | |
# Sezione principale "hero" | |
# MODIFICATO: Racchiuso l'emoji del razzo in uno <span> con la classe "rocket-emoji" | |
gr.HTML(""" | |
<div class="hero-section"> | |
<div class="hero-content"> | |
<h1 class="hero-title"> | |
<span class="rocket-emoji">🚀</span> LinkedIn Employer Brand Analytics | |
</h1> | |
<p class="hero-subtitle"> | |
Trasforma la tua presenza su LinkedIn con approfondimenti basati sui dati e strategie attuabili | |
</p> | |
<p class="hero-description"> | |
Misura, analizza e migliora il tuo marchio del datore di lavoro per attrarre i migliori talenti e costruire una presenza digitale più forte | |
</p> | |
</div> | |
</div> | |
""") | |
# Sezione di panoramica con spaziatura migliorata | |
gr.HTML(""" | |
<div class="overview-section"> | |
<h2 class="overview-header"> | |
<span>📊</span> Cosa Offre Questa Dashboard | |
</h2> | |
<p class="overview-text"> | |
La nostra piattaforma di analisi completa ti aiuta a comprendere e ottimizzare le prestazioni del tuo marchio del datore di lavoro su LinkedIn con approfondimenti in tempo reale, report automatizzati e raccomandazioni basate sull'intelligenza artificiale. | |
</p> | |
<div class="features-grid"> | |
<div class="feature-item"> | |
<span class="feature-icon">📈</span> | |
<span class="feature-text">Visualizzazione dei dati in tempo reale e analisi delle tendenze</span> | |
</div> | |
<div class="feature-item"> | |
<span class="feature-icon">📋</span> | |
<span class="feature-text">Report automatici trimestrali e settimanali sulle prestazioni</span> | |
</div> | |
<div class="feature-item"> | |
<span class="feature-icon">🎯</span> | |
<span class="feature-text">OKR basati sull'IA e raccomandazioni attuabili</span> | |
</div> | |
<div class="feature-item"> | |
<span class="feature-icon">🚀</span> | |
<span class="feature-text">Approfondimenti strategici per migliorare il marchio del datore di lavoro</span> | |
</div> | |
</div> | |
</div> | |
""") | |
# Schede di navigazione con layout migliorato | |
gr.HTML(""" | |
<div class="nav-cards-container"> | |
<div class="nav-card nav-card-graphs"> | |
<div> | |
<h3 class="nav-card-header"> | |
<span class="nav-card-icon">📈</span> Grafici Interattivi | |
</h3> | |
<p class="nav-card-description"> | |
Esplora visualizzazioni dinamiche delle tue metriche di performance su LinkedIn. Tieni traccia del coinvolgimento dei post, | |
della crescita dei follower, del sentiment delle menzioni e identifica le tendenze nel tempo con grafici interattivi | |
e opzioni di filtro avanzate. | |
</p> | |
</div> | |
<div class="nav-card-footer"> | |
<div class="nav-card-features"> | |
✨ Analisi in tempo reale<br/> | |
📊 Tipi di grafici multipli<br/> | |
🔍 Filtri avanzati | |
</div> | |
<div class="nav-card-badge"> | |
<span class="nav-card-badge-icon">📊</span> | |
</div> | |
</div> | |
</div> | |
<div class="nav-card nav-card-reports"> | |
<div> | |
<h3 class="nav-card-header"> | |
<span class="nav-card-icon">📊</span> Report di Analisi | |
</h3> | |
<p class="nav-card-description"> | |
Accedi a report trimestrali e settimanali completi basati sull'analisi dell'IA. Ottieni approfondimenti dettagliati | |
sulle prestazioni del tuo marchio del datore di lavoro, analisi della concorrenza e posizionamento di mercato | |
con la generazione automatica di report. | |
</p> | |
</div> | |
<div class="nav-card-footer"> | |
<div class="nav-card-features"> | |
📋 Report automatizzati<br/> | |
🤖 Approfondimenti basati sull'IA<br/> | |
📅 Settimanali e trimestrali | |
</div> | |
<div class="nav-card-badge"> | |
<span class="nav-card-badge-icon">📄</span> | |
</div> | |
</div> | |
</div> | |
<div class="nav-card nav-card-okr"> | |
<div> | |
<h3 class="nav-card-header"> | |
<span class="nav-card-icon">🎯</span> Piano d'Azione OKR | |
</h3> | |
<p class="nav-card-description"> | |
Scopri Obiettivi e Risultati Chiave (OKR) generati dall'IA con azioni concrete. | |
Trasforma gli approfondimenti dei dati in obiettivi misurabili e iniziative strategiche per migliorare | |
efficacemente il tuo marchio del datore di lavoro. | |
</p> | |
</div> | |
<div class="nav-card-footer"> | |
<div class="nav-card-features"> | |
🎯 Obiettivi strategici<br/> | |
✅ Compiti attuabili<br/> | |
📈 Risultati misurabili | |
</div> | |
<div class="nav-card-badge"> | |
<span class="nav-card-badge-icon">🎯</span> | |
</div> | |
</div> | |
</div> | |
<div class="nav-card nav-card-help"> | |
<div> | |
<h3 class="nav-card-header"> | |
<span class="nav-card-icon">💡</span> Per Iniziare | |
</h3> | |
<p class="nav-card-description"> | |
Nuovo all'analisi del marchio del datore di lavoro? Inizia con la sezione Grafici per | |
comprendere le tue prestazioni attuali, quindi controlla i Report per un'analisi dettagliata | |
e infine esplora gli OKR per i prossimi passi attuabili. | |
</p> | |
</div> | |
<div class="nav-card-footer"> | |
<div style="background: rgba(255,255,255,0.15); padding: 16px; border-radius: 12px; width: 100%; text-align: center;"> | |
<div style="color: rgba(255,255,255,0.95); font-size: 1em;"> | |
<strong>💪 Consiglio Pro:</strong><br/> | |
<span style="font-size: 0.9em;">Il monitoraggio regolare porta a un miglioramento del 40% nelle prestazioni del marchio del datore di lavoro</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
""") | |
# Crea pulsanti con una migliore spaziatura | |
with gr.Row(equal_height=True): | |
with gr.Column(): | |
btn_graphs = gr.Button("🚀 Esplora Grafici", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
with gr.Column(): | |
btn_reports = gr.Button("📊 Visualizza Report", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
with gr.Row(equal_height=True): | |
with gr.Column(): | |
btn_okr = gr.Button("🎯 Accedi agli OKR", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
with gr.Column(): | |
btn_help = gr.Button("📚 Documentazione", variant="secondary", size="lg", | |
elem_classes="nav-button", scale=1) | |
# Sezione "Come funziona" con stile migliorato | |
gr.HTML(""" | |
<div class="how-it-works-section"> | |
<h3 class="how-it-works-header"> | |
<span>ℹ️</span> Come Funziona | |
</h3> | |
<div class="steps-grid"> | |
<div class="step-item"> | |
<div class="step-number">1</div> | |
<div class="step-content"> | |
<div class="step-title">Raccolta Dati</div> | |
<div class="step-description">Si sincronizza automaticamente con i dati della tua organizzazione LinkedIn ed elabora le metriche di coinvolgimento in tempo reale</div> | |
</div> | |
</div> | |
<div class="step-item"> | |
<div class="step-number">2</div> | |
<div class="step-content"> | |
<div class="step-title">Analisi IA</div> | |
<div class="step-description">Algoritmi avanzati analizzano tendenze, sentiment e modelli di performance per generare approfondimenti attuabili</div> | |
</div> | |
</div> | |
<div class="step-item"> | |
<div class="step-number">3</div> | |
<div class="step-content"> | |
<div class="step-title">Risultati Attuabili</div> | |
<div class="step-description">Ricevi raccomandazioni specifiche, obiettivi misurabili e piani d'azione strategici per migliorare il tuo marchio del datore di lavoro</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
""") | |
return btn_graphs, btn_reports, btn_okr, btn_help | |
def create_analytics_plot_panel(plot_label_str, plot_id_str): | |
""" | |
Creates an individual plot panel with its plot component and action buttons. | |
Plot title and action buttons are on the same row. | |
Returns the panel (Column), plot component, and button components. | |
""" | |
# Icons are defined globally or imported. For this function, ensure they are accessible. | |
# If not using from config directly here, you might need to pass them or use fixed strings. | |
# Using fixed strings as a fallback if import fails, though they should be available via app.py's import. | |
local_bomb_icon, local_explore_icon, local_formula_icon = BOMB_ICON, EXPLORE_ICON, FORMULA_ICON | |
with gr.Column(visible=True) as panel_component: # Main container for this plot | |
with gr.Row(variant="compact"): | |
gr.Markdown(f"#### {plot_label_str}") # Plot title (scale might help balance) | |
with gr.Row(elem_classes="plot-actions", scale=1): # Action buttons container, give it some min_width | |
bomb_button = gr.Button(value=local_bomb_icon, variant="secondary", size="sm", min_width=30, elem_id=f"bomb_btn_{plot_id_str}") | |
formula_button = gr.Button(value=local_formula_icon, variant="secondary", size="sm", min_width=30, elem_id=f"formula_btn_{plot_id_str}") | |
explore_button = gr.Button(value=local_explore_icon, variant="secondary", size="sm", min_width=30, elem_id=f"explore_btn_{plot_id_str}") | |
# MODIFIED: Added height to gr.Plot for consistent sizing | |
plot_component = gr.Plot(label=plot_label_str, show_label=False) # Adjust height as needed | |
logging.debug(f"Created analytics panel for: {plot_label_str} (ID: {plot_id_str}) with fixed plot height.") | |
return panel_component, plot_component, bomb_button, explore_button, formula_button | |
def build_analytics_tab_plot_area(plot_configs): | |
""" | |
Builds the main plot area for the Analytics tab, arranging plot panels into rows of two, | |
with section titles appearing before their respective plots. | |
Returns a tuple: | |
- plot_ui_objects (dict): Dictionary of plot UI objects. | |
- section_titles_map (dict): Dictionary mapping section names to their gr.Markdown title components. | |
""" | |
logging.info(f"Building plot area for {len(plot_configs)} analytics plots with interleaved section titles.") | |
plot_ui_objects = {} | |
section_titles_map = {} | |
last_rendered_section = None | |
idx = 0 | |
while idx < len(plot_configs): | |
current_plot_config = plot_configs[idx] | |
current_section_name = current_plot_config["section"] | |
# Render section title if it's new for this block of plots | |
if current_section_name != last_rendered_section: | |
if current_section_name not in section_titles_map: | |
# Create the Markdown component for the section title | |
section_md_component = gr.Markdown(f"## {current_section_name}", visible=True) | |
section_titles_map[current_section_name] = section_md_component | |
logging.debug(f"Rendered and stored Markdown for section: {current_section_name}") | |
# No 'else' needed here for visibility, as it's handled by click handlers if sections are hidden/shown. | |
# The component is created once and its visibility is controlled elsewhere. | |
last_rendered_section = current_section_name | |
with gr.Row(equal_height=True): # Row for one or two plots. equal_height=False allows plots to define their height. | |
# --- Process the first plot in the row (config1) --- | |
config1 = plot_configs[idx] | |
# Safety check for section consistency (should always pass if configs are ordered by section) | |
if config1["section"] != current_section_name: | |
logging.warning(f"Plot {config1['id']} section mismatch. Expected {current_section_name}, got {config1['section']}. This might affect layout if a new section title was expected.") | |
# If a new section starts unexpectedly, ensure its title is created if missing | |
if config1["section"] not in section_titles_map: | |
sec_md = gr.Markdown(f"### {config1['section']}", visible=True) # Create and make visible | |
section_titles_map[config1['section']] = sec_md | |
last_rendered_section = config1["section"] # Update the current section context | |
panel_col1, plot_comp1, bomb_btn1, explore_btn1, formula_btn1 = \ | |
create_analytics_plot_panel(config1["label"], config1["id"]) | |
plot_ui_objects[config1["id"]] = { | |
"plot_component": plot_comp1, "bomb_button": bomb_btn1, | |
"explore_button": explore_btn1, "formula_button": formula_btn1, | |
"label": config1["label"], "panel_component": panel_col1, # This is the gr.Column containing the plot and its actions | |
"section": config1["section"] | |
} | |
logging.debug(f"Created UI panel for plot_id: {config1['id']} in section {config1['section']}") | |
idx += 1 | |
# --- Process the second plot in the row (config2), if applicable --- | |
if idx < len(plot_configs): | |
config2 = plot_configs[idx] | |
# Only add to the same row if it's part of the same section | |
if config2["section"] == current_section_name: | |
panel_col2, plot_comp2, bomb_btn2, explore_btn2, formula_btn2 = \ | |
create_analytics_plot_panel(config2["label"], config2["id"]) | |
plot_ui_objects[config2["id"]] = { | |
"plot_component": plot_comp2, "bomb_button": bomb_btn2, | |
"explore_button": explore_btn2, "formula_button": formula_btn2, | |
"label": config2["label"], "panel_component": panel_col2, | |
"section": config2["section"] | |
} | |
logging.debug(f"Created UI panel for plot_id: {config2['id']} in same row, section {config2['section']}") | |
idx += 1 | |
# If the next plot is in a new section, it will be handled in the next iteration of the while loop, | |
# starting with a new section title and a new gr.Row. | |
logging.info(f"Finished building plot area. Total plot objects: {len(plot_ui_objects)}. Section titles created: {len(section_titles_map)}") | |
if len(plot_ui_objects) != len(plot_configs): | |
logging.error(f"MISMATCH: Expected {len(plot_configs)} plot objects, but created {len(plot_ui_objects)}.") | |
return plot_ui_objects, section_titles_map | |
def create_enhanced_report_tab(agentic_modules_loaded_status: bool): | |
""" | |
Creates an enhanced report tab with Medium-style design for optimal readability. | |
This function returns the Gradio components that will be updated. | |
""" | |
# Custom CSS for Medium-style design | |
report_css = """ | |
<style> | |
/* Medium-style Report Container */ | |
.report-container { | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 40px 20px; | |
font-family: 'Georgia', 'Charter', 'Times New Roman', serif; | |
line-height: 1.6; | |
color: #292929; | |
background: #ffffff; | |
} | |
/* Header Section */ | |
.report-header { | |
text-align: center; | |
margin-bottom: 48px; | |
padding-bottom: 24px; | |
border-bottom: 1px solid #e6e6e6; | |
} | |
.report-title { | |
font-size: 2.5rem; | |
font-weight: 700; | |
color: #1a1a1a; | |
margin-bottom: 16px; | |
letter-spacing: -0.02em; | |
} | |
.report-subtitle { | |
font-size: 1.2rem; | |
color: #6b6b6b; | |
font-weight: 400; | |
margin-bottom: 24px; | |
} | |
/* Status Badge */ | |
.status-badge { | |
display: inline-flex; | |
align-items: center; | |
padding: 8px 16px; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
border-radius: 20px; | |
font-size: 0.9rem; | |
font-weight: 500; | |
margin-bottom: 16px; | |
} | |
.status-badge::before { | |
content: "📊"; | |
margin-right: 8px; | |
} | |
/* Report Selection Area */ | |
.report-selection { | |
background: #f8f9fa; | |
padding: 32px; | |
border-radius: 12px; | |
margin-bottom: 40px; | |
border: 1px solid #e9ecef; | |
} | |
.selection-title { | |
font-size: 1.3rem; | |
font-weight: 600; | |
color: #1a1a1a; | |
margin-bottom: 16px; | |
display: flex; | |
align-items: center; | |
} | |
.selection-title::before { | |
content: "📚"; | |
margin-right: 12px; | |
font-size: 1.5rem; | |
} | |
.selection-description { | |
color: #6b6b6b; | |
margin-bottom: 24px; | |
font-size: 1.05rem; | |
} | |
/* Report Content Area */ | |
.report-content { | |
background: white; | |
padding: 0; | |
border-radius: 8px; | |
box-shadow: 0 2px 12px rgba(0,0,0,0.04); | |
border: 1px solid #e9ecef; | |
min-height: 400px; | |
} | |
/* MODIFIED: Adjusted target for the report body's content */ | |
.report-body-content { /* New class for the Markdown content container */ | |
padding: 40px; | |
font-size: 1.1rem; | |
line-height: 1.8; | |
} | |
/* Typography Enhancements for the Markdown content */ | |
.report-body-content h1 { | |
font-size: 2.2rem; | |
font-weight: 700; | |
color: #1a1a1a; | |
margin-bottom: 24px; | |
margin-top: 0; | |
letter-spacing: -0.02em; | |
} | |
.report-body-content h2 { | |
font-size: 1.8rem; | |
font-weight: 600; | |
color: #1a1a1a; | |
margin-top: 48px; | |
margin-bottom: 20px; | |
padding-bottom: 12px; | |
border-bottom: 2px solid #e6e6e6; | |
} | |
.report-body-content h3 { | |
font-size: 1.4rem; | |
font-weight: 600; | |
color: #2c2c2c; | |
margin-top: 36px; | |
margin-bottom: 16px; | |
} | |
.report-body-content p { | |
margin-bottom: 20px; | |
color: #292929; | |
font-size: 1.1rem; | |
} | |
.report-body-content ul, .report-body-content ol { | |
margin-bottom: 20px; | |
padding-left: 24px; | |
} | |
.report-body-content li { | |
margin-bottom: 8px; | |
color: #292929; | |
} | |
.report-body-content strong { | |
font-weight: 600; | |
color: #1a1a1a; | |
} | |
.report-body-content em { | |
font-style: italic; | |
color: #4a4a4a; | |
} | |
/* Quote Styling */ | |
.report-body-content blockquote { | |
border-left: 4px solid #667eea; | |
padding-left: 20px; | |
margin: 24px 0; | |
font-style: italic; | |
color: #4a4a4a; | |
background: #f8f9fa; | |
padding: 20px; | |
border-radius: 4px; | |
} | |
/* Code Styling */ | |
.report-body-content code { | |
background: #f1f3f4; | |
padding: 2px 6px; | |
border-radius: 4px; | |
font-family: 'Monaco', 'Menlo', monospace; | |
font-size: 0.9em; | |
color: #d73a49; | |
} | |
/* Empty State */ | |
.empty-state { | |
text-align: center; | |
padding: 80px 40px; | |
color: #6b6b6b; | |
} | |
.empty-state-icon { | |
font-size: 4rem; | |
margin-bottom: 24px; | |
opacity: 0.6; | |
} | |
.empty-state-title { | |
font-size: 1.5rem; | |
font-weight: 600; | |
margin-bottom: 12px; | |
color: #4a4a4a; | |
} | |
.empty-state-description { | |
font-size: 1.1rem; | |
line-height: 1.6; | |
max-width: 400px; | |
margin: 0 auto; | |
} | |
/* Responsive Design */ | |
@media (max-width: 768px) { | |
.report-container { | |
padding: 24px 16px; | |
} | |
.report-title { | |
font-size: 2rem; | |
} | |
.report-selection { | |
padding: 24px; | |
} | |
.report-body-content { /* Target the inner HTML component directly */ | |
padding: 24px; | |
} | |
.report-body-content h1 { | |
font-size: 1.8rem; | |
} | |
.report-body-content h2 { | |
font-size: 1.5rem; | |
} | |
.empty-state { | |
padding: 60px 20px; | |
} | |
} | |
/* Gradio Component Overrides */ | |
.report-content .gradio-container { | |
background: transparent !important; | |
border: none !important; | |
} | |
/* Dropdown Styling */ | |
.report-selection .gr-dropdown { | |
background: white; | |
border: 2px solid #e9ecef; | |
border-radius: 8px; | |
font-size: 1.05rem; | |
} | |
.report-selection .gr-dropdown:focus { | |
border-color: #667eea; | |
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
} | |
/* Loading Indicator: Hidden by default */ | |
.loading-indicator { | |
display: none !important; /* Hide the entire loading indicator block */ | |
} | |
</style> | |
""" | |
# Inject custom CSS | |
gr.HTML(report_css) | |
# Main container | |
with gr.Column(elem_classes=["report-container"]): | |
# Header section | |
with gr.Column(elem_classes=["report-header"]): | |
# This HTML component will display the dynamic title and subtitle | |
report_header_html_display = gr.HTML(""" | |
<div class="report-title">📊 Comprehensive Analysis Report</div> | |
<div class="report-subtitle">AI-Generated Insights from Your LinkedIn Data</div> | |
<div class="status-badge">Generated from Bubble.io</div> | |
""") | |
# Report selection section | |
with gr.Column(elem_classes=["report-selection"]): | |
gr.HTML(""" | |
<div class="selection-title">Report Library</div> | |
<div class="selection-description"> | |
Select a pre-generated report from your analysis library to view detailed insights, | |
trends, and recommendations based on your LinkedIn organization data. | |
</div> | |
""") | |
# Status indicator (now hidden by default via CSS) | |
agentic_pipeline_status_md = gr.Markdown( | |
"🔄 **Status:** Loading report data...", | |
elem_classes=["loading-indicator"] # This class now hides the element | |
) | |
# Report selector dropdown | |
with gr.Row(): | |
report_selector_dd = gr.Dropdown( | |
label="📋 Select Report", | |
choices=[], | |
interactive=True, | |
info="Choose from your available analysis reports", | |
elem_classes=["report-dropdown"] | |
) | |
# Report content display area | |
with gr.Column(elem_classes=["report-content"]): | |
# This Markdown component will display the actual report text, formatted as Markdown | |
report_body_markdown_display = gr.Markdown( | |
""" | |
<div class="empty-state"> | |
<div class="empty-state-icon">📄</div> | |
<div class="empty-state-title">No Report Selected</div> | |
<div class="empty-state-description"> | |
Please select a report from the library above to view its detailed analysis and insights. | |
</div> | |
</div> | |
""", | |
# Apply styles to the content within the Markdown component using a wrapper div | |
elem_classes=["report-body-content"] | |
) | |
# Error handling for when modules are not loaded | |
if not agentic_modules_loaded_status: | |
gr.HTML(""" | |
<div style=" | |
background: #fee; | |
border: 1px solid #fcc; | |
border-radius: 8px; | |
padding: 20px; | |
margin-top: 20px; | |
text-align: center; | |
"> | |
<div style="color: #c33; font-size: 1.2rem; font-weight: 600; margin-bottom: 8px;"> | |
⚠️ Module Loading Error | |
</div> | |
<div style="color: #666;"> | |
Agentic analysis modules could not be loaded. Please check your configuration. | |
</div> | |
</div> | |
""") | |
# Return both header HTML and body Markdown components for app.py to update | |
return agentic_pipeline_status_md, report_selector_dd, report_header_html_display, report_body_markdown_display | |