File size: 15,546 Bytes
adb3bbe
b560569
896ae69
f7fc39b
 
 
 
a9b7f24
d252c6d
adb3bbe
538b42b
 
f7fc39b
 
493ca9b
f7fc39b
adb3bbe
b560569
a9b7f24
adb3bbe
 
 
 
896ae69
adb3bbe
896ae69
f7fc39b
 
a9b7f24
f7fc39b
 
adb3bbe
b560569
f7fc39b
a0b418d
a9b7f24
f7fc39b
 
 
b560569
 
f7fc39b
6e2376b
f7fc39b
 
 
 
 
a9b7f24
adb3bbe
f7fc39b
a9b7f24
adb3bbe
f7fc39b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb3bbe
 
f7fc39b
adb3bbe
 
f7fc39b
adb3bbe
 
8a531f0
adb3bbe
8a531f0
4cc3230
f7fc39b
 
4cc3230
6d43d2f
adb3bbe
f7fc39b
adb3bbe
6d43d2f
4cc3230
bff5b73
f7fc39b
 
cb4dce3
 
f7fc39b
cb4dce3
b8b7e00
538b42b
adb3bbe
 
 
 
f7fc39b
adb3bbe
a9b7f24
f7fc39b
 
adb3bbe
 
a9b7f24
f7fc39b
 
 
 
 
 
a9b7f24
f7fc39b
a9b7f24
f7fc39b
 
 
 
 
a9b7f24
f7fc39b
 
 
 
 
 
 
 
 
a9b7f24
8a531f0
73e88eb
 
f7fc39b
 
73e88eb
f7fc39b
a9b7f24
 
 
 
f7fc39b
73e88eb
adb3bbe
 
 
 
 
 
 
 
f7fc39b
adb3bbe
 
 
 
 
7ab0240
adb3bbe
 
 
4cc3230
f7fc39b
4cc3230
a9b7f24
f7fc39b
 
88d3a6e
 
f7fc39b
2051c7a
 
f7fc39b
f466d89
 
f7fc39b
6d43d2f
 
f7fc39b
 
a9b7f24
adb3bbe
 
 
f7fc39b
adb3bbe
06d22e5
538b42b
 
 
f7fc39b
 
538b42b
 
bff5b73
b8b7e00
538b42b
 
adb3bbe
 
f7fc39b
 
 
 
adb3bbe
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# -*- coding: utf-8 -*-
import gradio as gr
import json
import requests # Added for API calls
import os # Added for environment variables
import urllib.parse # Added for URL encoding (though requests handles params well)

# Assuming these custom modules exist in your project directory or Python path
from Data_Fetching_and_Rendering import fetch_and_render_dashboard
from analytics_fetch_and_rendering import fetch_and_render_analytics
from mentions_dashboard import generate_mentions_dashboard

# Import the function from your utils file
from gradio_utils import get_url_user_token # Assuming gradio_utils.py is in the same directory

# Shared state for token received via POST or Bubble
token_received = {"status": False, "token": None, "client_id": None}

# --- Handlers for token reception (POST) and status ---
def receive_token(accessToken: str, client_id: str):
    """
    Called by a hidden POST mechanism to supply the OAuth code/token and client ID.
    """
    try:
        token_dict = json.loads(accessToken.replace("'", '"'))
    except json.JSONDecodeError as e:
        print(f"Error decoding accessToken (POST): {e}")
        token_received["status"] = False
        token_received["token"] = None
        token_received["client_id"] = client_id
        return "❌ Invalid token format (POST)", "", client_id
    
    token_received["status"] = True
    token_received["token"] = token_dict # This should be the dict like {"access_token": "value"}
    token_received["client_id"] = client_id
    print(f"Token (from POST) received successfully. Client ID: {client_id}")
    # Update status box, token display, client display directly
    return check_status(), show_token(), show_client()


def check_status():
    return "✅ Token available" if token_received["status"] else "❌ Waiting for token…"

def show_token(): # Shows access_token if available
    if token_received["status"] and token_received["token"] and isinstance(token_received["token"], dict):
        return token_received["token"].get("access_token", "Access token key missing in dict")
    elif token_received["status"] and token_received["token"]: # If token is a raw string (should not happen with new logic)
        return str(token_received["token"]) # Fallback, but ideally token_received["token"] is always a dict if status is True
    return ""

def show_client():
    return token_received["client_id"] if token_received["status"] and token_received["client_id"] else ""

# --- Function to fetch LinkedIn Token from Bubble.io ---
def fetch_linkedin_token_from_bubble(url_user_token_str):
    """
    Fetches LinkedIn access token from Bubble.io API using the state value (url_user_token_str).
    The token is expected in a 'Raw_text' field as a JSON string, which is then parsed.
    Updates the global token_received state if successful.
    Returns status messages for UI update.
    """
    # Initial UI states (in case of early exit or error)
    current_status = check_status()
    current_token_display = show_token()
    current_client_display = show_client()

    bubble_api_key = os.environ.get("Bubble_API")
    if not bubble_api_key:
        error_msg = "❌ Bubble API Error: The 'Bubble_API' environment variable is not set."
        print(error_msg)
        return error_msg, current_status, current_token_display, current_client_display

    if not url_user_token_str or "not found" in url_user_token_str or "Could not access" in url_user_token_str:
        return f"ℹ️ No valid user token from URL to query Bubble. ({url_user_token_str})", current_status, current_token_display, current_client_display

    base_url = "https://app.ingaze.ai/version-test/api/1.1/obj/Linkedin_access"
    constraints = [{"key": "state", "constraint_type": "equals", "value": url_user_token_str}]
    params = {'constraints': json.dumps(constraints)}
    headers = {"Authorization": f"Bearer {bubble_api_key}"} 
    
    bubble_api_status_msg = f"Attempting to fetch token from Bubble for state: {url_user_token_str}..."
    print(bubble_api_status_msg)

    response = None 
    try:
        response = requests.get(base_url, params=params, headers=headers, timeout=15) # Increased timeout slightly
        response.raise_for_status() 
        
        data = response.json()
        results = data.get("response", {}).get("results", [])
        
        if results:
            raw_text_from_bubble = results[0].get("Raw_text", None)
            parsed_token_dict = None 

            if raw_text_from_bubble and isinstance(raw_text_from_bubble, str):
                try:
                    parsed_token_dict = json.loads(raw_text_from_bubble)
                    if not isinstance(parsed_token_dict, dict):
                        bubble_api_status_msg = (f"⚠️ Bubble API: 'Raw_text' field did not contain a valid JSON dictionary string. "
                                                 f"Content type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
                        print(bubble_api_status_msg)
                        parsed_token_dict = None 
                    # If it is a dict, parsed_token_dict is now the token dictionary itself
                except json.JSONDecodeError as e:
                    bubble_api_status_msg = (f"⚠️ Bubble API: Error decoding 'Raw_text' JSON string: {e}. "
                                             f"Content: {raw_text_from_bubble}")
                    print(bubble_api_status_msg)
                    parsed_token_dict = None 
            elif raw_text_from_bubble: # It exists but is not a string
                 bubble_api_status_msg = (f"⚠️ Bubble API: 'Raw_text' field was not a string. "
                                         f"Type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
                 print(bubble_api_status_msg)


            if parsed_token_dict and "access_token" in parsed_token_dict:
                token_received["status"] = True
                token_received["token"] = parsed_token_dict # Store the entire parsed dictionary
                token_received["client_id"] = f"Bubble (state: {url_user_token_str})"
                bubble_api_status_msg = f"✅ LinkedIn Token successfully fetched and parsed from Bubble 'Raw_text' for state: {url_user_token_str}"
                print(bubble_api_status_msg)
            elif raw_text_from_bubble and not parsed_token_dict:
                # Error message already set by parsing logic if raw_text_from_bubble existed but parsing failed.
                # If bubble_api_status_msg wasn't set by specific parsing errors, use a general one.
                if not bubble_api_status_msg.startswith("⚠️"): # Avoid overwriting specific parsing error
                    bubble_api_status_msg = f"⚠️ Bubble API: 'Raw_text' found but could not be parsed into a valid token dictionary for state: {url_user_token_str}."
                print(bubble_api_status_msg)
            elif not raw_text_from_bubble:
                 bubble_api_status_msg = (f"⚠️ Bubble API: Token field ('Raw_text') "
                                         f"not found or is null in response for state: {url_user_token_str}. Result: {results[0]}")
                 print(bubble_api_status_msg)
            elif parsed_token_dict and "access_token" not in parsed_token_dict: # Parsed OK, but missing the crucial key
                 bubble_api_status_msg = (f"⚠️ Bubble API: 'access_token' key missing in parsed 'Raw_text' dictionary for state: {url_user_token_str}. Parsed: {parsed_token_dict}")
                 print(bubble_api_status_msg)
            # If none of the above, the initial bubble_api_status_msg will be used or an error below will catch it.

        else: # No results from Bubble for the given state
            bubble_api_status_msg = f"❌ Bubble API: No results found for state: {url_user_token_str}"
            print(bubble_api_status_msg)
            
    except requests.exceptions.HTTPError as http_err:
        error_details = response.text if response else "No response content"
        bubble_api_status_msg = f"❌ Bubble API HTTP error: {http_err} - Response: {error_details}"
        print(bubble_api_status_msg)
    except requests.exceptions.Timeout:
        bubble_api_status_msg = "❌ Bubble API Request timed out."
        print(bubble_api_status_msg)
    except requests.exceptions.RequestException as req_err:
        bubble_api_status_msg = f"❌ Bubble API Request error: {req_err}"
        print(bubble_api_status_msg)
    except json.JSONDecodeError as json_err: # Error decoding the main Bubble response, not Raw_text
        error_details = response.text if response else "No response content"
        bubble_api_status_msg = f"❌ Bubble API main response JSON decode error: {json_err}. Response: {error_details}"
        print(bubble_api_status_msg)
    except Exception as e:
        bubble_api_status_msg = f"❌ An unexpected error occurred while fetching from Bubble: {str(e)}"
        print(bubble_api_status_msg)

    # Return values to update all relevant UI components
    return bubble_api_status_msg, check_status(), show_token(), show_client()


# --- Guarded fetch functions (using token from POST or Bubble) ---
# These functions expect token_received["token"] to be a dictionary 
# like {"access_token": "actual_token_value", ...}
def guarded_fetch_dashboard():
    if not token_received["status"]:
        return "<p style='color:red; text-align:center;'>❌ Access denied. No token available. Please send token first or ensure URL token is valid.</p>"
    html = fetch_and_render_dashboard(
        token_received["client_id"],
        token_received["token"] 
    )
    return html

def guarded_fetch_analytics():
    if not token_received["status"]:
        return (
            "<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>",
            None, None, None, None, None, None, None 
        )
    count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics = fetch_and_render_analytics(
        token_received["client_id"],
        token_received["token"] 
    )
    return count_md, plot, growth_plot, avg_post_eng_rate, interaction_metrics, eb_metrics, mentions_vol_metrics, mentions_sentiment_metrics

def run_mentions_and_load():
    if not token_received["status"]:
        return ("<p style='color:red; text-align:center;'>❌ Access denied. No token available.</p>", None)
    html, fig = generate_mentions_dashboard(
        token_received["client_id"],
        token_received["token"] 
    )
    return html, fig

# --- Build the Gradio UI ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
               title="LinkedIn Post Viewer & Analytics") as app:
    gr.Markdown("# 🚀 LinkedIn Organization Post Viewer & Analytics")
    gr.Markdown("Token can be supplied via URL parameter (for Bubble.io lookup) or hidden POST. Then explore dashboard and analytics.")

    # Hidden elements: simulate POST endpoint for OAuth token
    hidden_token_input = gr.Textbox(visible=False, elem_id="hidden_token") 
    hidden_client_input = gr.Textbox(visible=False, elem_id="hidden_client_id") 
    hidden_btn = gr.Button(visible=False, elem_id="hidden_btn")

    # --- Display elements ---
    url_user_token_display = gr.Textbox(
        label="User Token (from URL - Hidden)", 
        interactive=False, 
        placeholder="Attempting to load from URL...",
        visible=False 
    )
    
    bubble_status_display = gr.Textbox(label="Bubble API Call Status", interactive=False, placeholder="Waiting for URL token...")

    status_box = gr.Textbox(label="Overall Token Status", interactive=False) 
    token_display = gr.Textbox(label="Access Token (Active)", interactive=False)
    client_display = gr.Textbox(label="Client ID (Active)", interactive=False)

    # --- Load URL parameter on app start & Link to Bubble Fetch ---
    app.load(
        fn=get_url_user_token, 
        inputs=None, 
        outputs=[url_user_token_display] 
    )

    url_user_token_display.change(
        fn=fetch_linkedin_token_from_bubble,
        inputs=[url_user_token_display],
        outputs=[bubble_status_display, status_box, token_display, client_display]
    )

    hidden_btn.click(
        fn=receive_token,
        inputs=[hidden_token_input, hidden_client_input],
        outputs=[status_box, token_display, client_display] 
    )
    
    app.load(fn=check_status, outputs=status_box)
    app.load(fn=show_token, outputs=token_display)
    app.load(fn=show_client, outputs=client_display)
    
    timer = gr.Timer(2.0) 
    timer.tick(fn=check_status, outputs=status_box)
    timer.tick(fn=show_token, outputs=token_display)
    timer.tick(fn=show_client, outputs=client_display)

    # Tabs for functionality
    with gr.Tabs():
        with gr.TabItem("1️⃣ Dashboard"):
            gr.Markdown("View your organization's recent posts and their engagement statistics.")
            fetch_dashboard_btn = gr.Button("📊 Fetch Posts & Stats", variant="primary")
            dashboard_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for token...</p>")
            fetch_dashboard_btn.click(
                fn=guarded_fetch_dashboard,
                inputs=[],
                outputs=[dashboard_html]
            )

        with gr.TabItem("2️⃣ Analytics"):
            gr.Markdown("View follower count and monthly gains for your organization.")
            fetch_analytics_btn = gr.Button("📈 Fetch Follower Analytics", variant="primary")
            
            follower_count = gr.Markdown("<p style='text-align: center; color: #555;'>Waiting for token...</p>")
            
            with gr.Row():
                follower_plot = gr.Plot(visible=True) 
                growth_rate_plot = gr.Plot(visible=True)

            with gr.Row():
                post_eng_rate_plot = gr.Plot(visible=True)

            with gr.Row():
                interaction_data = gr.Plot(visible=True)

            with gr.Row():
                eb_data = gr.Plot(visible=True)

            with gr.Row():
                mentions_vol_data = gr.Plot(visible=True)
                mentions_sentiment_data = gr.Plot(visible=True)
            
            fetch_analytics_btn.click(
                fn=guarded_fetch_analytics,
                inputs=[],
                outputs=[follower_count, follower_plot, growth_rate_plot, post_eng_rate_plot, interaction_data, eb_data, mentions_vol_data, mentions_sentiment_data]
            )

        with gr.TabItem("3️⃣ Mentions"):
            gr.Markdown("Analyze sentiment of recent posts that mention your organization.")
            fetch_mentions_btn = gr.Button("🧠 Fetch Mentions & Sentiment", variant="primary")
            mentions_html = gr.HTML(value="<p style='text-align: center; color: #555;'>Waiting for token...</p>")
            mentions_plot = gr.Plot(visible=True)
            fetch_mentions_btn.click(
                fn=run_mentions_and_load,
                inputs=[],
                outputs=[mentions_html, mentions_plot]
            )

# Launch the app
if __name__ == "__main__":
    # Ensure the 'Bubble_API' environment variable is set where this app is run.
    # For local testing, you can set it in your terminal before running:
    # export Bubble_API="YOUR_ACTUAL_BUBBLE_API_KEY"
    # python app.py
    app.launch(server_name="0.0.0.0", server_port=7860, share=True)