File size: 11,800 Bytes
575b933
f9d8231
b560569
575b933
b0464a9
87a87e7
f7fc39b
575b933
 
 
 
4ad44b9
575b933
 
 
 
2a3b22e
575b933
 
 
 
 
 
9d99925
b0464a9
2a3b22e
 
 
51b5851
 
 
 
 
 
 
 
 
 
 
 
 
 
575b933
b0464a9
a342a6b
b0464a9
a342a6b
 
575b933
 
a342a6b
 
 
 
 
 
575b933
 
 
 
 
 
 
 
a342a6b
575b933
 
 
a342a6b
b0464a9
2a3b22e
adb3bbe
a342a6b
179ea1f
a342a6b
67742c4
a342a6b
575b933
a342a6b
575b933
51b5851
 
 
575b933
67742c4
adb3bbe
a342a6b
 
575b933
 
f9d8231
179ea1f
a342a6b
 
575b933
 
4ad44b9
575b933
a342a6b
4ad44b9
a342a6b
51b5851
4ad44b9
adb3bbe
 
2a3b22e
a342a6b
575b933
51b5851
 
575b933
51b5851
575b933
a342a6b
 
2a3b22e
4ad44b9
2a3b22e
a342a6b
 
2a3b22e
51b5851
 
 
575b933
a342a6b
4ad44b9
51b5851
 
 
 
 
 
575b933
51b5851
 
 
 
 
575b933
51b5851
 
 
 
4ad44b9
51b5851
4ad44b9
a342a6b
 
faf26ff
575b933
adb3bbe
a342a6b
575b933
a342a6b
 
 
575b933
a342a6b
575b933
adb3bbe
4ad44b9
a342a6b
 
 
adb3bbe
06d22e5
538b42b
a342a6b
 
575b933
4ad44b9
 
a342a6b
 
 
575b933
 
a342a6b
 
 
 
 
 
575b933
a342a6b
 
 
 
 
538b42b
575b933
adb3bbe
a342a6b
575b933
 
 
 
 
a342a6b
 
 
 
575b933
 
a342a6b
 
 
575b933
51b5851
 
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# app.py
# -- coding: utf-8 --
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')

# --- Helper function to load HTML animation ---
def get_sync_animation_html():
    """Loads the HTML content for the sync animation."""
    try:
        # Ensure this path is correct relative to where app.py is run
        with open("sync_animation.html", "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        logging.error("sync_animation.html not found. Please ensure it's in the same directory as app.py.")
        return "<p style='text-align:center; color: red;'>Animation file not found. Syncing...</p>"
    except Exception as e:
        logging.error(f"Error loading sync_animation.html: {e}")
        return f"<p style='text-align:center; color: red;'>Error loading animation: {e}. Syncing...</p>"

# --- 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(),
        "bubble_operations_log_df": pd.DataFrame(), # Added from your state_manager logic
        "mentions_should_sync_now": False, # Added from your state_manager logic
        "fs_should_sync_now": False, # Added from your state_manager logic
        "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) # Ensure this function handles potentially empty dfs
        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)
            
            # This HTML component will now display the animation during sync
            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"
            )
            # Note: The logic for also triggering on url_user_token_display.change has been removed
            # as org_urn_display.change should be sufficient if get_url_user_token updates both,
            # or if org_urn is the primary trigger. If issues arise, this can be revisited.

            # When Sync button is clicked:
            sync_data_btn.click(
                fn=lambda: get_sync_animation_html(), # Step 1: Show animation
                inputs=None,
                outputs=[sync_status_html_output],
                show_progress=False # Animation is its own progress indicator
            ).then(
                fn=sync_all_linkedin_data_orchestrator, # Step 2: Run the actual sync
                inputs=[token_state],
                # sync_all_linkedin_data_orchestrator must return:
                # 1. HTML string for status (replaces animation)
                # 2. Updated token_state
                outputs=[sync_status_html_output, token_state], 
                show_progress=False # Rely on animation; or set to "full" if you want Gradio's default too
            ).then(
                fn=process_and_store_bubble_token, # Step 3: Re-check sync status and update button
                inputs=[url_user_token_display, org_urn_display, token_state], 
                outputs=[status_box, token_state, sync_data_btn], 
                show_progress=False 
            ).then(
                fn=display_main_dashboard, # Step 4: 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
    # Ensure share=False for local testing unless you intend to share it via Gradio's link
    app.launch(server_name="0.0.0.0", server_port=7860, debug=True, share=False)