File size: 10,467 Bytes
b560569
575b933
b0464a9
87a87e7
f7fc39b
575b933
 
 
 
4ad44b9
575b933
 
 
 
2a3b22e
575b933
 
 
 
 
 
9d99925
b0464a9
2a3b22e
 
 
575b933
b0464a9
a342a6b
b0464a9
a342a6b
0612e1d
575b933
 
0612e1d
a342a6b
 
 
 
 
575b933
 
 
 
 
 
 
 
a342a6b
575b933
 
 
a342a6b
b0464a9
2a3b22e
adb3bbe
a342a6b
179ea1f
0612e1d
67742c4
a342a6b
575b933
a342a6b
575b933
0612e1d
67742c4
adb3bbe
a342a6b
0612e1d
575b933
 
f9d8231
179ea1f
0612e1d
a342a6b
575b933
0612e1d
 
 
 
4ad44b9
0612e1d
 
 
adb3bbe
 
2a3b22e
a342a6b
0612e1d
 
575b933
a342a6b
0612e1d
2a3b22e
4ad44b9
2a3b22e
a342a6b
 
2a3b22e
0612e1d
 
 
 
 
 
 
 
4ad44b9
0612e1d
575b933
0612e1d
 
575b933
0612e1d
 
 
 
4ad44b9
6f117a4
4ad44b9
a342a6b
 
faf26ff
575b933
adb3bbe
a342a6b
0612e1d
 
 
 
 
 
575b933
adb3bbe
4ad44b9
a342a6b
 
 
adb3bbe
06d22e5
538b42b
a342a6b
 
575b933
4ad44b9
 
a342a6b
 
 
575b933
 
a342a6b
 
 
 
 
 
575b933
a342a6b
 
 
 
 
538b42b
575b933
adb3bbe
0612e1d
575b933
 
 
 
 
a342a6b
 
 
 
575b933
0612e1d
a342a6b
 
 
6f117a4
0612e1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import gradio as gr
import pandas as pd
import os
import logging

# --- Module Imports ---
# Functions from your existing/provided custom modules
from analytics_fetch_and_rendering import fetch_and_render_analytics # Assuming this exists
from gradio_utils import get_url_user_token # For fetching URL parameters

# Functions from newly created/refactored modules
from config import (
    LINKEDIN_CLIENT_ID_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR,
    BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR
)
from state_manager import process_and_store_bubble_token
from sync_logic import sync_all_linkedin_data_orchestrator
from ui_generators import (
    display_main_dashboard,
    run_mentions_tab_display,
    run_follower_stats_tab_display
)

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# --- Guarded Analytics Fetch ---
def guarded_fetch_analytics(token_state):
    """Guarded call to fetch_and_render_analytics, ensuring token and basic data structures."""
    if not token_state or not token_state.get("token"):
        logging.warning("Analytics fetch: Access denied. No token.")
        # Ensure the number of returned Nones matches the expected number of outputs for the plots
        return ("❌ Access denied. No token.", None, None, None, None, None, None, None)

    # Ensure DataFrames are passed, even if empty, to avoid errors in the analytics function
    posts_df_analytics = token_state.get("bubble_posts_df", pd.DataFrame())
    mentions_df_analytics = token_state.get("bubble_mentions_df", pd.DataFrame())
    follower_stats_df_analytics = token_state.get("bubble_follower_stats_df", pd.DataFrame())

    logging.info("Calling fetch_and_render_analytics with current token_state data.")
    try:
        return fetch_and_render_analytics(
            token_state.get("client_id"),
            token_state.get("token"),
            token_state.get("org_urn"),
            posts_df_analytics,
            mentions_df_analytics,
            follower_stats_df_analytics
        )
    except Exception as e:
        logging.error(f"Error in guarded_fetch_analytics calling fetch_and_render_analytics: {e}", exc_info=True)
        return (f"❌ Error fetching analytics: {e}", None, None, None, None, None, None, None)


# --- Gradio UI Blocks ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
               title="LinkedIn Organization Dashboard") as app:

    # Central state for holding token, client_id, org_urn, and fetched dataframes
    token_state = gr.State(value={
        "token": None, "client_id": None, "org_urn": None,
        "bubble_posts_df": pd.DataFrame(), "fetch_count_for_api": 0,
        "bubble_mentions_df": pd.DataFrame(),
        "bubble_follower_stats_df": pd.DataFrame(),
        "url_user_token_temp_storage": None
    })

    gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
    # Hidden textboxes to capture URL parameters
    url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
    status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
    org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)

    # Load URL parameters when the Gradio app loads
    app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display], api_name="get_url_params", show_progress=False)

    # This function will run after URL params are loaded and org_urn_display changes
    def initial_load_sequence(url_token, org_urn_val, current_state):
        logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
        # Process token, fetch Bubble data, determine sync needs
        status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
        # Display initial dashboard content based on (potentially empty) Bubble data
        dashboard_content = display_main_dashboard(new_state)
        return status_msg, new_state, btn_update, dashboard_content

    with gr.Tabs():
        with gr.TabItem("1️⃣ Dashboard & Sync"):
            gr.Markdown("System checks for existing data from Bubble. The 'Sync' button activates if new data needs to be fetched from LinkedIn based on the last sync times and data availability.")
            sync_data_btn = gr.Button("πŸ”„ Sync LinkedIn Data", variant="primary", visible=False, interactive=False)
            sync_status_html_output = gr.HTML("<p style='text-align:center;'>Sync status will appear here.</p>")
            dashboard_display_html = gr.HTML("<p style='text-align:center;'>Dashboard loading...</p>")

            # Chain of events for initial load:
            org_urn_display.change(
                fn=initial_load_sequence,
                inputs=[url_user_token_display, org_urn_display, token_state],
                outputs=[status_box, token_state, sync_data_btn, dashboard_display_html],
                show_progress="full"
            )
            # Also trigger initial_load_sequence if url_user_token_display changes (e.g. if it loads after org_urn)
            # This helps ensure it runs once both are potentially available.
            # Note: `org_urn_display.change` might be sufficient if `get_url_user_token` updates both nearly simultaneously.
            # Adding this for robustness, but ensure it doesn't cause unwanted multiple runs if state isn't managed carefully.
            # Consider using a flag in token_state if multiple triggers become an issue.
            # For now, relying on org_urn_display.change as the primary trigger post-load.

            # When Sync button is clicked:
            sync_data_btn.click(
                fn=sync_all_linkedin_data_orchestrator,
                inputs=[token_state],
                outputs=[sync_status_html_output, token_state], # token_state is updated here
                show_progress="full"
            ).then(
                fn=process_and_store_bubble_token, # Re-check sync status and update button
                inputs=[url_user_token_display, org_urn_display, token_state], # Pass current token_state
                outputs=[status_box, token_state, sync_data_btn], # token_state updated again
                show_progress=False # Typically "full" for user-initiated actions, "minimal" or False for quick updates
            ).then(
                fn=display_main_dashboard, # Refresh dashboard display
                inputs=[token_state],
                outputs=[dashboard_display_html],
                show_progress=False
            )

        with gr.TabItem("2️⃣ Analytics"):
            fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch/Refresh Full Analytics", variant="primary")
            follower_count_md = gr.Markdown("Analytics data will load here...")
            with gr.Row(): follower_plot, growth_plot = gr.Plot(label="Follower Demographics"), gr.Plot(label="Follower Growth")
            with gr.Row(): eng_rate_plot = gr.Plot(label="Engagement Rate")
            with gr.Row(): interaction_plot = gr.Plot(label="Post Interactions")
            with gr.Row(): eb_plot = gr.Plot(label="Engagement Benchmark")
            with gr.Row(): mentions_vol_plot, mentions_sentiment_plot = gr.Plot(label="Mentions Volume"), gr.Plot(label="Mentions Sentiment")

            fetch_analytics_btn.click(
                fn=guarded_fetch_analytics, inputs=[token_state],
                outputs=[follower_count_md, follower_plot, growth_plot, eng_rate_plot,
                         interaction_plot, eb_plot, mentions_vol_plot, mentions_sentiment_plot],
                show_progress="full"
            )

        with gr.TabItem("3️⃣ Mentions"):
            refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
            mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
            mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
            refresh_mentions_display_btn.click(
                fn=run_mentions_tab_display, inputs=[token_state],
                outputs=[mentions_html, mentions_sentiment_dist_plot],
                show_progress="full"
            )

        with gr.TabItem("4️⃣ Follower Stats"):
            refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
            follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
            with gr.Row():
                fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
            with gr.Row():
                fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
                fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")

            refresh_follower_stats_btn.click(
                fn=run_follower_stats_tab_display, inputs=[token_state],
                outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
                show_progress="full"
            )

if __name__ == "__main__":
    # Check for essential environment variables
    if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
        logging.warning(f"WARNING: '{LINKEDIN_CLIENT_ID_ENV_VAR}' environment variable not set. The app may not function correctly for LinkedIn API calls.")
    if not os.environ.get(BUBBLE_APP_NAME_ENV_VAR) or \
       not os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR) or \
       not os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR):
        logging.warning("WARNING: One or more Bubble environment variables (BUBBLE_APP_NAME, BUBBLE_API_KEY_PRIVATE, BUBBLE_API_ENDPOINT) are not set. Bubble integration will fail.")

    try:
        import matplotlib
        logging.info(f"Matplotlib version: {matplotlib.__version__} found. Backend: {matplotlib.get_backend()}")
        # The backend is now set in ui_generators.py, which is good practice.
    except ImportError:
        logging.error("Matplotlib is not installed. Plots will not be generated. Please install it: pip install matplotlib")

    # Launch the Gradio app
    app.launch(server_name="0.0.0.0", server_port=7860, debug=True)