import gradio as gr import pandas as pd # Needed for state variables that hold DataFrames from typing import Dict, Any, Callable # Custom CSS for professional, modern UI CUSTOM_CSS = """ /* === ROOT VARIABLES === */ :root { --primary-color: #0A66C2; --primary-hover: #004182; --secondary-color: #5E9ED6; --accent-color: #F3F2EF; --success-color: #57C4A3; --warning-color: #F5B800; --error-color: #CC1016; --text-primary: #000000DE; --text-secondary: #00000099; --text-tertiary: #00000061; --background-primary: #FFFFFF; --background-secondary: #F8F9FA; --background-tertiary: #F3F2EF; --border-color: #E5E5E5; --border-radius: 12px; --shadow-light: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.12); --shadow-heavy: 0 8px 32px rgba(0, 0, 0, 0.16); --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* === GLOBAL STYLES === */ .gradio-container { background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; min-height: 100vh; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; } .gradio-container .main { background: transparent; padding: 0; max-width: none !important; } /* === MAIN HEADER === */ .main-header { background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white; padding: 2rem 0; margin: -1rem -1rem 2rem -1rem; text-align: center; box-shadow: var(--shadow-medium); position: relative; overflow: hidden; } .main-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url('data:image/svg+xml,'); opacity: 0.3; } .main-header h1 { font-size: 2.5rem !important; font-weight: 700 !important; margin: 0 !important; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); position: relative; z-index: 1; } .main-header .subtitle { font-size: 1.1rem; opacity: 0.9; margin-top: 0.5rem; position: relative; z-index: 1; } /* === STATUS BAR === */ .status-container { background: var(--background-primary); border-radius: var(--border-radius); padding: 1rem; margin-bottom: 1.5rem; box-shadow: var(--shadow-light); border-left: 4px solid var(--primary-color); } /* === TAB CONTAINER === */ .tab-nav { background: var(--background-primary); border-radius: var(--border-radius); padding: 0.5rem; box-shadow: var(--shadow-light); margin-bottom: 1.5rem; border: none; } .tab-nav button { background: transparent !important; border: none !important; padding: 1rem 1.5rem !important; margin: 0 0.25rem !important; border-radius: 8px !important; font-weight: 600 !important; font-size: 0.95rem !important; transition: var(--transition) !important; color: var(--text-secondary) !important; position: relative; } .tab-nav button:hover { background: var(--background-secondary) !important; color: var(--text-primary) !important; transform: translateY(-2px); } .tab-nav button.selected { background: var(--primary-color) !important; color: white !important; box-shadow: var(--shadow-medium); } /* === TAB CONTENT === */ .tabitem { background: var(--background-primary); border-radius: var(--border-radius); padding: 2rem; box-shadow: var(--shadow-light); border: 1px solid var(--border-color); margin-top: 0; } /* === FORM ELEMENTS === */ .gr-form { background: var(--background-primary); border-radius: var(--border-radius); padding: 1.5rem; box-shadow: var(--shadow-light); border: 1px solid var(--border-color); } input, textarea, select { border: 2px solid var(--border-color) !important; border-radius: 8px !important; padding: 0.75rem 1rem !important; font-size: 0.95rem !important; transition: var(--transition) !important; background: var(--background-primary) !important; } input:focus, textarea:focus, select:focus { border-color: var(--primary-color) !important; box-shadow: 0 0 0 3px rgba(10, 102, 194, 0.1) !important; outline: none !important; } /* === BUTTONS === */ .gr-button { background: var(--primary-color) !important; color: white !important; border: none !important; border-radius: 8px !important; padding: 0.75rem 1.5rem !important; font-weight: 600 !important; font-size: 0.95rem !important; transition: var(--transition) !important; cursor: pointer !important; box-shadow: var(--shadow-light); } .gr-button:hover { background: var(--primary-hover) !important; transform: translateY(-2px); box-shadow: var(--shadow-medium); } .gr-button.secondary { background: var(--background-secondary) !important; color: var(--text-primary) !important; border: 2px solid var(--border-color) !important; } .gr-button.secondary:hover { background: var(--background-tertiary) !important; border-color: var(--primary-color) !important; } /* === CARDS === */ .card { background: var(--background-primary); border-radius: var(--border-radius); padding: 1.5rem; box-shadow: var(--shadow-light); border: 1px solid var(--border-color); transition: var(--transition); margin-bottom: 1rem; } .card:hover { box_shadow: var(--shadow-medium); transform: translateY(-2px); } .card-header { font-size: 1.25rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem; } .card-content { color: var(--text-secondary); line-height: 1.6; } /* === CHARTS AND PLOTS === */ .plot-container { background: var(--background-primary); border-radius: var(--border-radius); padding: 1rem; box-shadow: var(--shadow-light); border: 1px solid var(--border-color); margin: 1rem 0; } /* === MARKDOWN CONTENT === */ .markdown-content h1, .markdown-content h2, .markdown-content h3 { color: var(--text-primary); font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; } .markdown-content h1 { font-size: 2rem; border-bottom: 3px solid var(--primary-color); padding-bottom: 0.5rem; } .markdown-content h2 { font-size: 1.5rem; color: var(--primary-color); } .markdown-content h3 { font-size: 1.25rem; color: var(--secondary-color); } .markdown-content p { line-height: 1.7; color: var(--text-secondary); margin-bottom: 1rem; } .markdown-content ul, .markdown-content ol { padding-left: 1.5rem; margin-bottom: 1rem; } .markdown-content li { margin-bottom: 0.5rem; line-height: 1.6; color: var(--text-secondary); } .markdown-content code { background: var(--background-tertiary); padding: 0.2rem 0.4rem; border-radius: 4px; font-family: 'Fira Code', monospace; font-size: 0.9rem; } .markdown-content pre { background: var(--background-tertiary); padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 1rem 0; } /* === LOADING STATES === */ .loading-spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(10, 102, 194, 0.3); border-radius: 50%; border-top-color: var(--primary-color); animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* === RESPONSIVE DESIGN === */ @media (max-width: 768px) { .main-header h1 { font-size: 2rem !important; } .tabitem { padding: 1rem; } .tab-nav button { padding: 0.75rem 1rem !important; font-size: 0.9rem !important; } } /* === ANIMATIONS === */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.6s ease-out; } /* === ACCESSIBILITY === */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* === GRADIO SPECIFIC OVERRIDES === */ .gradio-container .wrap { border-radius: var(--border-radius) !important; border: 1px solid var(--border-color) !important; } .gradio-container .panel { background: var(--background-primary) !important; border-radius: var(--border-radius) !important; } .gradio-container .form { background: transparent !important; } /* === STATUS INDICATORS === */ .status-success { color: var(--success-color) !important; background: rgba(87, 196, 163, 0.1) !important; padding: 0.5rem 1rem; border-radius: 6px; border-left: 4px solid var(--success-color); } .status-warning { color: var(--warning-color) !important; background: rgba(245, 184, 0, 0.1) !important; padding: 0.5rem 1rem; border-radius: 6px; border-left: 4px solid var(--warning-color); } .status-error { color: var(--error-color) !important; background: rgba(204, 16, 22, 0.1) !important; padding: 0.5rem 1rem; border-radius: 6px; border-left: 4px solid var(--error-color); } /* === HIDE GRADIO FOOTER === */ footer { display: none !important; } .footer { display: none !important; } /* === IMPROVE SPACING === */ .block { margin-bottom: 1.5rem !important; } /* === CUSTOM COMPONENTS === */ .metric-card { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: white; padding: 1.5rem; border-radius: var(--border-radius); text-align: center; box-shadow: var(--shadow-medium); margin: 0.5rem; } .metric-value { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; } .metric-label { font-size: 0.9rem; opacity: 0.9; } """ # Custom theme custom_theme = gr.themes.Soft( primary_hue="blue", secondary_hue="sky", neutral_hue="slate", spacing_size="md", radius_size="md" ).set( body_background_fill="linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)", block_background_fill="white", block_border_width="1px", block_border_color="#e5e5e5", block_radius="12px", block_shadow="0 2px 8px rgba(0, 0, 0, 0.08)", button_primary_background_fill="#0A66C2", button_primary_background_fill_hover="#004182", button_primary_text_color="white", button_secondary_background_fill="#f8f9fa", button_secondary_background_fill_hover="#f3f2ef", input_border_color="#e5e5e5", input_border_color_focus="#0A66C2", input_border_width="2px" ) def update_report_display_enhanced(selected_report_id: str, current_token_state: dict, format_report_for_display_func): """ Updates the report header and body display when a new report is selected. This function now expects format_report_for_display to return a dict with 'header_html' and 'body_markdown'. Args: selected_report_id (str): The ID of the selected report. current_token_state (dict): The current state dictionary containing data. format_report_for_display_func (callable): The function to format report data. Returns: tuple: A tuple of gr.update objects for the header and body displays. """ # Define empty states for header and body with improved styling empty_header_html = """
📊 Comprehensive Analysis Report

AI-Generated Insights from Your LinkedIn Data

Generated from Bubble.io
""" empty_body_markdown_no_selection = """
📋

Select a Report

Choose a report from the dropdown above to view its detailed analysis and insights.

""" empty_body_markdown_no_data = """
⚠️

Data Not Available

Analysis data is not loaded or is empty. Please try refreshing the page.

""" empty_body_markdown_not_found = lambda _id: f"""

Report Not Found

Report with ID '{_id}' was not found in the database.

""" if not selected_report_id: return gr.update(value=empty_header_html), gr.update(value=empty_body_markdown_no_selection) agentic_df = current_token_state.get("bubble_agentic_analysis_data") if agentic_df is None or agentic_df.empty: return gr.update(value=empty_header_html), gr.update(value=empty_body_markdown_no_data) selected_report_series_df = agentic_df[agentic_df['_id'] == selected_report_id] if selected_report_series_df.empty: return gr.update(value=empty_header_html), gr.update(value=empty_body_markdown_not_found(selected_report_id)) selected_report_series = selected_report_series_df.iloc[0] # Call the format_report_for_display, which now returns a dict formatted_content_parts = format_report_for_display_func(selected_report_series) # Update the two separate Gradio components return ( gr.update(value=formatted_content_parts['header_html']), gr.update(value=formatted_content_parts['body_markdown']) ) def build_main_app_ui( PLOT_ID_TO_FORMULA_KEY_MAP: Dict[str, Any], PLOT_FORMULAS: Dict[str, Any], BOMB_ICON: str, EXPLORE_ICON: str, FORMULA_ICON: str, ACTIVE_ICON: str, build_analytics_tab_plot_area_func: Callable, update_analytics_plots_figures_func: Callable, create_placeholder_plot_func: Callable, get_initial_insight_prompt_and_suggestions_func: Callable, generate_llm_response_func: Callable, build_home_tab_ui_func: Callable, create_enhanced_report_tab_func: Callable, create_enhanced_okr_tab_func: Callable, format_report_for_display_func: Callable, # New argument to pass the formatting function AGENTIC_MODULES_LOADED: bool, # ADD THIS PARAMETER get_initial_okr_display_func: Callable = None, # ADD THIS PARAMETER TOO # NEW: Added parameters for initial event binding functions initial_data_load_sequence_func: Callable = None, get_url_user_token_func: Callable = None, load_and_display_agentic_results_func: Callable = None, format_okrs_for_enhanced_display_func: Callable = None, update_report_display_enhanced_func: Callable = None ) -> tuple: """ Builds the main Gradio application UI with enhanced styling and structure. Args: (All necessary functions and constants are passed as arguments to ensure self-containment within the UI module without direct imports of non-UI related logic) AGENTIC_MODULES_LOADED (bool): Whether agentic modules are loaded get_initial_okr_display_func (callable): Function to get initial OKR display initial_data_load_sequence_func (callable): Function to handle initial data load. get_url_user_token_func (callable): Function to get URL user token. load_and_display_agentic_results_func (callable): Function to load and display agentic results. format_okrs_for_enhanced_display_func (callable): Function to format OKRs for enhanced display. update_report_display_enhanced_func (callable): Function to update enhanced report display. Returns: tuple: A tuple containing: - app (gr.Blocks): The main Gradio application object. - url_user_token_display (gr.Textbox): Hidden component for user token. - org_urn_display (gr.Textbox): Hidden component for organization URN. - status_box (gr.Textbox): Component for system status. - token_state (gr.State): Gradio state for token and data. - reconstruction_cache_st (gr.State): State for reconstructed OKR data cache. - enhanced_okr_display_html (gr.HTML): HTML component for enhanced OKR display. - tabs (gr.Tabs): The main tabs component. - report_selector_dd (gr.Dropdown): Dropdown for selecting reports. - agentic_display_outputs (list): List of Gradio components for agentic results. - analytics_tab_instance (AnalyticsTab): The analytics tab instance itself. - chat_histories_st (gr.State): State for chat histories. - current_chat_plot_id_st (gr.State): State for current chat plot ID. - plot_data_for_chatbot_st (gr.State): State for plot data for chatbot. - format_report_for_display_func (callable): The report display formatting function. """ app = gr.Blocks( theme=custom_theme, css=CUSTOM_CSS, title="LinkedIn Organization Dashboard", head=""" """ ) with app: # --- STATE MANAGEMENT --- token_state = gr.State(value={ "token": None, "client_id": None, "org_urn": None, "bubble_posts_df": pd.DataFrame(), "bubble_post_stats_df": pd.DataFrame(), "bubble_mentions_df": pd.DataFrame(), "bubble_follower_stats_df": pd.DataFrame(), "bubble_agentic_analysis_data": pd.DataFrame(), # To store agentic results from Bubble "url_user_token_temp_storage": None, "config_date_col_posts": "published_at", "config_date_col_mentions": "date", "config_date_col_followers": "date", "config_media_type_col": "media_type", "config_eb_labels_col": "li_eb_label" }) # States for analytics tab chatbot chat_histories_st = gr.State({}) current_chat_plot_id_st = gr.State(None) plot_data_for_chatbot_st = gr.State({}) # States for agentic results display orchestration_raw_results_st = gr.State(None) # KEPT for compatibility with load_and_display_agentic_results signature key_results_for_selection_st = gr.State([]) selected_key_result_ids_st = gr.State([]) # --- NEW: Session-specific cache for reconstructed OKR data --- reconstruction_cache_st = gr.State({}) # NEW: State to hold the actionable_okrs dictionary explicitly actionable_okrs_data_st = gr.State({}) # --- UI LAYOUT --- # Main Header with gr.Row(): with gr.Column(): gr.HTML("""

🚀 LinkedIn Organization Dashboard

Advanced Analytics & AI-Powered Insights for LinkedIn Organizations
""") # Hidden components for token management url_user_token_display = gr.Textbox(label="User Token (Hidden)", interactive=False, visible=False) org_urn_display = gr.Textbox(label="Org URN (Hidden)", interactive=False, visible=False) # Status section with better styling with gr.Row(): with gr.Column(): gr.HTML('
') status_box = gr.Textbox( label="🔄 System Status", interactive=False, value="🔄 Initializing dashboard...", elem_classes=["status-display"] ) gr.HTML('
') # Instantiate AnalyticsTab here, after all its required gr.State components are defined from services.analytics_tab_module import AnalyticsTab # Local import to ensure it's loaded within context analytics_icons = {'bomb': BOMB_ICON, 'explore': EXPLORE_ICON, 'formula': FORMULA_ICON, 'active': ACTIVE_ICON} analytics_tab_instance = AnalyticsTab( token_state=token_state, chat_histories_st=chat_histories_st, current_chat_plot_id_st=current_chat_plot_id_st, plot_data_for_chatbot_st=plot_data_for_chatbot_st, plot_id_to_formula_map=PLOT_ID_TO_FORMULA_KEY_MAP, plot_formulas_data=PLOT_FORMULAS, icons=analytics_icons, fn_build_plot_area=build_analytics_tab_plot_area_func, fn_update_plot_figures=update_analytics_plots_figures_func, fn_create_placeholder_plot=create_placeholder_plot_func, fn_get_initial_insight=get_initial_insight_prompt_and_suggestions_func, fn_generate_llm_response=generate_llm_response_func ) with gr.Tabs(elem_classes=["tab-nav"]) as tabs: # --- HOME TAB --- with gr.TabItem("🏠 Home", id="tab_home", elem_classes=["tabitem"]): # Call the new function from ui_generators to build the Home tab content btn_graphs, btn_reports, btn_okr, btn_help = build_home_tab_ui_func() # Link buttons to tab selection btn_graphs.click(fn=lambda: gr.update(selected="tab_analytics_module"), outputs=tabs) btn_reports.click(fn=lambda: gr.update(selected="tab_agentic_report"), outputs=tabs) btn_okr.click(fn=lambda: gr.update(selected="tab_agentic_okrs"), outputs=tabs) # btn_help.click(fn=lambda: gr.update(selected="tab_help"), outputs=tabs) # Uncomment if you add a help tab # Analytics Tab analytics_tab_instance.create_tab_ui() # This is the "Graphs" tab, assuming its ID is "tab_analytics_module" # --- AGENTIC ANALYSIS REPORT TAB --- with gr.TabItem("📊 Analysis Reports", id="tab_agentic_report", visible=True, elem_classes=["tabitem"]): # Set visible to True to allow `create_enhanced_report_tab_func` to return components # The create_enhanced_report_tab function handles the CSS and HTML structure agentic_pipeline_status_md, report_selector_dd, report_header_html_display, report_body_markdown_display = \ create_enhanced_report_tab_func(True) # Pass True to create elements # --- AGENTIC OKRS TAB --- with gr.TabItem("🎯 OKRs & Tasks", id="tab_agentic_okrs", visible=True, elem_classes=["tabitem"]): # Set visible to True to allow components to be created gr.HTML("""
🎯 AI Generated OKRs and Actionable Tasks

Based on AI analysis, the agent has proposed the following OKRs and actionable tasks from Bubble.io data.

""") # The `AGENTIC_MODULES_LOADED` check should control visibility from app.py, not prevent creation here # to avoid NoneType errors during UI building. if not AGENTIC_MODULES_LOADED: # This check is for conditional display, not creation gr.HTML("""
🔴

Module Loading Error

Agentic modules could not be loaded. This tab is currently unavailable.

""") # Create dummy components to maintain compatibility enhanced_okr_display_html = gr.HTML("""
🔴

OKR Display Unavailable

Agentic modules are not loaded.

""") # Create hidden old components for compatibility with gr.Column(visible=False): gr.Markdown("### Suggested Key Results (OLD UI - HIDDEN)") key_results_cbg = gr.CheckboxGroup(label="Select Key Results", choices=[], value=[], interactive=True) gr.Markdown("### Detailed OKRs and Tasks (OLD UI - HIDDEN)") okr_detail_display_md = gr.Markdown("I dettagli OKR appariranno qui.") else: # Keep the old components but make them invisible to maintain load_and_display_agentic_results signature with gr.Column(visible=False): gr.Markdown("### Suggested Key Results (OLD UI - HIDDEN)") key_results_cbg = gr.CheckboxGroup(label="Select Key Results", choices=[], value=[], interactive=True) gr.Markdown("### Detailed OKRs and Tasks (OLD UI - HIDDEN)") okr_detail_display_md = gr.Markdown("I dettagli OKR appariranno qui.") # NEW: Add the enhanced OKR display HTML component enhanced_okr_display_html = create_enhanced_okr_tab_func() # Add footer with enhanced styling gr.HTML("""

© 2024 LinkedIn Organization Dashboard - Powered by AI & Advanced Analytics

🚀 Built with Gradio • 🔗 LinkedIn API • 🤖 AI Analytics • ☁️ Bubble.io Integration

""") # Ensure agentic_display_outputs correctly maps to the newly created components # This list must match the outputs of load_and_display_agentic_results # These variables are now defined within the `with app:` block agentic_display_outputs = [ agentic_pipeline_status_md, # 0: Status Markdown (hidden) report_selector_dd, # 1: Dropdown for selecting reports key_results_cbg if AGENTIC_MODULES_LOADED else gr.State([]), # Ensure it's a component or dummy state okr_detail_display_md if AGENTIC_MODULES_LOADED else gr.State(None), # Ensure it's a component or dummy state orchestration_raw_results_st, # 4: Raw results state selected_key_result_ids_st, # 5: Selected KR IDs state (kept hidden) key_results_for_selection_st, # 6: All KRs for selection state (kept hidden) report_header_html_display, # 7: New HTML output for header report_body_markdown_display, # 8: New Markdown output for body reconstruction_cache_st, # 9: Reconstruction cache state enhanced_okr_display_html, # 10: The enhanced HTML display for OKRs actionable_okrs_data_st # 11: NEW: The actionable_okrs dictionary state ] # Event handlers (moved here to be within the gr.Blocks context) if get_url_user_token_func: # Check if the function is provided app.load(fn=get_url_user_token_func, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False) if AGENTIC_MODULES_LOADED and update_report_display_enhanced_func: # Check if function is provided report_selector_dd.change( fn=lambda sr_id, c_state: update_report_display_enhanced_func(sr_id, c_state, format_report_for_display_func), inputs=[report_selector_dd, token_state], outputs=[agentic_display_outputs[7], agentic_display_outputs[8]], # report_header_html_display, report_body_markdown_display show_progress="minimal" ) if initial_data_load_sequence_func: # Check if function is provided initial_load_event = org_urn_display.change( fn=initial_data_load_sequence_func, inputs=[url_user_token_display, org_urn_display, token_state], outputs=[status_box, token_state], show_progress="full" ) # Chain the loading events if analytics_tab_instance: initial_load_event.then( fn=analytics_tab_instance._refresh_analytics_graphs_ui, inputs=[token_state, analytics_tab_instance.date_filter_selector, analytics_tab_instance.custom_start_date_picker, analytics_tab_instance.custom_end_date_picker, chat_histories_st], # Changed from chat_histories_st_returned outputs=analytics_tab_instance.graph_refresh_outputs_list, show_progress="full" ) if AGENTIC_MODULES_LOADED and load_and_display_agentic_results_func: initial_load_event.then( fn=load_and_display_agentic_results_func, inputs=[token_state, reconstruction_cache_st], outputs=agentic_display_outputs, show_progress="minimal" ) if AGENTIC_MODULES_LOADED and format_okrs_for_enhanced_display_func: initial_load_event.then( fn=format_okrs_for_enhanced_display_func, inputs=[reconstruction_cache_st], outputs=[enhanced_okr_display_html], show_progress="minimal" ) return (app, url_user_token_display, org_urn_display, status_box, token_state, reconstruction_cache_st, enhanced_okr_display_html, tabs, report_selector_dd, agentic_display_outputs, analytics_tab_instance, chat_histories_st, # Returned directly as defined in this function current_chat_plot_id_st, plot_data_for_chatbot_st, # Returned directly as defined in this function format_report_for_display_func) # Return format_report_for_display_func as well