GLAkavya commited on
Commit
4a0fc18
·
verified ·
1 Parent(s): da4bc90

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +401 -119
app.py CHANGED
@@ -1,27 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
2
  import random
 
 
 
3
  import gradio as gr
4
- import plotly.express as px
5
  import pandas as pd
6
- from transformers import pipeline
7
- import google.generativeai as genai
8
-
9
- # -----------------------------
10
- # Load Hugging Face Sentiment Model
11
- # -----------------------------
12
- sentiment_model = pipeline("sentiment-analysis")
13
-
14
- # -----------------------------
15
- # Configure Gemini (Key from Secrets)
16
- # -----------------------------
17
- GEMINI_KEY = os.getenv("GEMINI_API_KEY") # add in Hugging Face Secrets
18
- if GEMINI_KEY:
 
 
 
 
 
 
 
 
 
 
 
 
19
  genai.configure(api_key=GEMINI_KEY)
20
 
21
- # -----------------------------
22
- # Fake Post Generator (simulate Twitter)
23
- # -----------------------------
24
- def generate_fake_posts(hashtag, n=20):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  templates = [
26
  f"I love {hashtag}! It's amazing ❤️",
27
  f"I'm disappointed with {hashtag} 💔",
@@ -30,119 +138,293 @@ def generate_fake_posts(hashtag, n=20):
30
  f"People are talking about {hashtag} everywhere 🌍",
31
  f"{hashtag} campaign is the best thing this year 🎉",
32
  f"Super excited about {hashtag} 🔥",
33
- f"{hashtag} is the worst thing ever 😡"
 
 
34
  ]
35
- return random.choices(templates, k=n)
36
-
37
- # -----------------------------
38
- # Run HuggingFace Analysis
39
- # -----------------------------
40
- def analyze_with_hf(posts):
41
- results = sentiment_model(posts)
42
- sentiments = []
43
- for post, res in zip(posts, results):
44
- sentiments.append({
45
- "Post": post,
46
- "Sentiment": res["label"],
47
- "Confidence": round(res["score"], 2)
48
- })
49
- return sentiments
50
-
51
- # -----------------------------
52
- # Run Gemini Advanced Analysis
53
- # -----------------------------
54
- def analyze_with_gemini(posts):
55
- if not GEMINI_KEY:
56
- return analyze_with_hf(posts) # fallback
57
-
58
- sentiments = []
59
- for post in posts:
60
- try:
61
- prompt = f"""Classify the sentiment of this social media post as Positive, Negative, or Neutral.
62
- Return only JSON with 'label' and 'confidence' between 0 and 1.
63
-
64
- Post: "{post}"
65
- """
66
- response = genai.GenerativeModel("gemini-1.5-flash").generate_content(prompt)
67
-
68
- # crude parse
69
- text = response.text.strip()
70
- if "Positive" in text:
71
- label = "POSITIVE"
72
- elif "Negative" in text:
73
- label = "NEGATIVE"
74
- else:
75
- label = "NEUTRAL"
76
-
77
- sentiments.append({
78
- "Post": post,
79
- "Sentiment": label,
80
- "Confidence": 0.95
81
- })
82
- except:
83
- sentiments.append({
84
- "Post": post,
85
- "Sentiment": "NEUTRAL",
86
- "Confidence": 0.5
87
- })
88
- return sentiments
89
-
90
- # -----------------------------
91
- # Main Function
92
- # -----------------------------
93
- def run_analysis(hashtag, n_posts, vis_type, use_gemini):
94
- posts = generate_fake_posts(hashtag, n_posts)
95
-
96
- if use_gemini:
97
- data = analyze_with_gemini(posts)
98
- source_info = f"Analyzed with Gemini AI: {len(posts)} posts"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  else:
100
- data = analyze_with_hf(posts)
101
- source_info = f"Analyzed with Hugging Face Transformers: {len(posts)} posts"
102
-
103
- df = pd.DataFrame(data)
104
-
105
- # Chart
106
- if vis_type == "Bar":
107
- fig = px.bar(df, x="Sentiment", title=f"Sentiment Distribution for {hashtag}")
108
- elif vis_type == "Line":
109
- fig = px.line(df, y="Confidence", title=f"Sentiment Rolling Trend for {hashtag}")
110
- elif vis_type == "Pie":
111
- fig = px.pie(df, names="Sentiment", title=f"Sentiment Share for {hashtag}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  else:
113
- fig = px.scatter(df, x="Sentiment", y="Confidence", title=f"Scatter of Sentiments for {hashtag}")
 
 
 
 
 
 
 
 
114
 
115
- return df, fig, source_info
 
116
 
 
 
 
117
 
118
- # -----------------------------
119
- # Gradio UI
120
- # -----------------------------
121
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="orange", secondary_hue="blue")) as demo:
122
- gr.Markdown("<h1 style='text-align:center;'>🚀 Social Media Sentiment Analyzer</h1>")
123
- gr.Markdown("<p style='text-align:center;'>Stream posts · Analyze moods · Visualize trends</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  with gr.Row():
126
- with gr.Column(scale=1):
127
- hashtag = gr.Textbox(label="Enter Hashtag", value="#gla")
128
- n_posts = gr.Slider(5, 50, value=20, step=1, label="Number of Posts")
129
- vis_type = gr.Dropdown(["Bar", "Line", "Pie", "Scatter"], label="Choose Visualization", value="Bar")
130
- use_gemini = gr.Checkbox(label="Use Gemini Advanced Analysis", value=False)
131
- run_btn = gr.Button("🔍 Run Analysis", variant="primary")
132
-
133
- with gr.Column(scale=2):
134
- output_table = gr.Dataframe(headers=["Post", "Sentiment", "Confidence"], wrap=True)
135
- output_plot = gr.Plot()
136
- source_label = gr.Label(label="Analysis Source")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  run_btn.click(
139
  fn=run_analysis,
140
- inputs=[hashtag, n_posts, vis_type, use_gemini],
141
- outputs=[output_table, output_plot, source_label]
 
 
 
 
142
  )
143
 
144
- # -----------------------------
145
- # Launch App
146
- # -----------------------------
147
  if __name__ == "__main__":
148
  demo.launch()
 
1
+ # app.py
2
+ # ------------------------------------------------------------
3
+ # Social Media Sentiment Analyzer (Gemini + HF)
4
+ # - Posts can be generated by Gemini (toggle)
5
+ # - Sentiment via Gemini or HF Transformers (toggle)
6
+ # - Pretty Plotly charts + animated background
7
+ #
8
+ # Requires (in requirements.txt):
9
+ # gradio>=4.36.1
10
+ # plotly>=5.22.0
11
+ # transformers>=4.41.2
12
+ # torch --extra-index-url https://download.pytorch.org/whl/cpu
13
+ # google-generativeai>=0.7.2
14
+ # pandas
15
+ # ------------------------------------------------------------
16
+
17
  import os
18
+ import json
19
  import random
20
+ import re
21
+ from typing import List, Tuple, Dict
22
+
23
  import gradio as gr
 
24
  import pandas as pd
25
+ import plotly.express as px
26
+
27
+ # --- Optional Gemini import (handled gracefully) ---
28
+ GEMINI_AVAILABLE = True
29
+ try:
30
+ import google.generativeai as genai # type: ignore
31
+ except Exception:
32
+ GEMINI_AVAILABLE = False
33
+
34
+ # --- Optional HF Transformers sentiment pipeline (CPU friendly) ---
35
+ HF_AVAILABLE = True
36
+ try:
37
+ from transformers import pipeline
38
+ except Exception:
39
+ HF_AVAILABLE = False
40
+
41
+
42
+ # ------------------ Config ------------------
43
+
44
+ MAX_POSTS = 50
45
+ DEFAULT_MODEL_HF = "distilbert-base-uncased-finetuned-sst-2-english"
46
+ GEMINI_MODEL_FAST = "gemini-1.5-flash"
47
+
48
+ GEMINI_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
49
+ if GEMINI_AVAILABLE and GEMINI_KEY:
50
  genai.configure(api_key=GEMINI_KEY)
51
 
52
+
53
+ # ------------------ Utilities ------------------
54
+
55
+ def clean_posts_list(text: str, n: int) -> List[str]:
56
+ """
57
+ Try to parse a JSON array of strings; if not, split lines or bullets.
58
+ Ensures length <= n and removes duplicates while keeping order.
59
+ """
60
+ text = text.strip()
61
+ posts: List[str] = []
62
+
63
+ # Try JSON array
64
+ if text.startswith("["):
65
+ try:
66
+ arr = json.loads(text)
67
+ if isinstance(arr, list):
68
+ posts = [str(x).strip() for x in arr]
69
+ except Exception:
70
+ posts = []
71
+
72
+ # If empty, try to parse as numbered/bulleted lines
73
+ if not posts:
74
+ lines = [ln.strip() for ln in re.split(r"[\r\n]+", text) if ln.strip()]
75
+ cleaned = []
76
+ for ln in lines:
77
+ ln = re.sub(r"^[\-\*\d\.\)\s]+", "", ln) # strip bullets/nums
78
+ if ln:
79
+ cleaned.append(ln)
80
+ posts = cleaned
81
+
82
+ # Deduplicate while preserving order
83
+ seen = set()
84
+ unique = []
85
+ for p in posts:
86
+ if p not in seen:
87
+ seen.add(p)
88
+ unique.append(p)
89
+
90
+ # Trim length and empty values
91
+ unique = [p for p in unique if p.strip()][:n]
92
+ return unique
93
+
94
+
95
+ def generate_posts_gemini(hashtag: str, n: int) -> Tuple[List[str], str]:
96
+ """
97
+ Ask Gemini to create n short, realistic social posts.
98
+ Returns (posts, info_message). If fails, returns ([], reason).
99
+ """
100
+ if not (GEMINI_AVAILABLE and GEMINI_KEY):
101
+ return [], "Gemini unavailable (missing package or key)."
102
+
103
+ prompt = f"""
104
+ You are a social media copy expert.
105
+
106
+ Generate {n} diverse, realistic, short social posts about the topic {hashtag}.
107
+ Constraints:
108
+ - sound like real posts/tweets (casual, short, natural)
109
+ - include some emojis and variety in sentiment (positive, negative, neutral)
110
+ - avoid hate speech, slurs, or unsafe content
111
+ - return ONLY a JSON array of strings, no extra text
112
+
113
+ Example:
114
+ ["Love {hashtag}! 🚀", "Not sure about {hashtag}… 🤔", "This {hashtag} launch was underwhelming 😕"]
115
+ """
116
+
117
+ try:
118
+ model = genai.GenerativeModel(GEMINI_MODEL_FAST)
119
+ resp = model.generate_content(prompt)
120
+ text = resp.text or ""
121
+ posts = clean_posts_list(text, n)
122
+ if posts:
123
+ return posts, f"Generated {len(posts)} posts via Gemini."
124
+ return [], "Gemini responded but parsing returned no posts."
125
+ except Exception as e:
126
+ return [], f"Gemini error: {e}"
127
+
128
+
129
+ def generate_posts_fallback(hashtag: str, n: int) -> List[str]:
130
+ """
131
+ Local lightweight fallback templates.
132
+ """
133
  templates = [
134
  f"I love {hashtag}! It's amazing ❤️",
135
  f"I'm disappointed with {hashtag} 💔",
 
138
  f"People are talking about {hashtag} everywhere 🌍",
139
  f"{hashtag} campaign is the best thing this year 🎉",
140
  f"Super excited about {hashtag} 🔥",
141
+ f"{hashtag} is the worst thing ever 😡",
142
+ f"Mixed feelings about {hashtag} today 😶‍🌫️",
143
+ f"Curious where {hashtag} goes next 👀",
144
  ]
145
+ # sample with replacement for diversity
146
+ return random.sample(templates, k=min(len(templates), n)) if n <= len(templates) else random.choices(templates, k=n)
147
+
148
+
149
+ def analyze_sentiment_hf(posts: List[str]) -> List[Dict]:
150
+ """
151
+ HF pipeline sentiment: POSITIVE/NEGATIVE with score
152
+ (Neutral simulated lightly based on score band).
153
+ """
154
+ if not HF_AVAILABLE:
155
+ # If transformers not available, return neutral placeholders
156
+ return [{"sentiment": "NEUTRAL", "confidence": 0.5} for _ in posts]
157
+
158
+ nlp = pipeline("sentiment-analysis", model=DEFAULT_MODEL_HF)
159
+ results = nlp(posts)
160
+ out = []
161
+ for r in results:
162
+ label = r["label"].upper()
163
+ score = float(r["score"])
164
+ # Project a basic neutral band to make visuals richer
165
+ if 0.45 < score < 0.55:
166
+ sent = "NEUTRAL"
167
+ conf = 0.5
168
+ else:
169
+ sent = "POSITIVE" if label.startswith("POS") else "NEGATIVE"
170
+ conf = score
171
+ out.append({"sentiment": sent, "confidence": round(conf, 2)})
172
+ return out
173
+
174
+
175
+ def analyze_sentiment_gemini(posts: List[str]) -> List[Dict]:
176
+ """
177
+ Gemini multi-class sentiment with confidence 0..1.
178
+ """
179
+ if not (GEMINI_AVAILABLE and GEMINI_KEY):
180
+ return [{"sentiment": "NEUTRAL", "confidence": 0.5} for _ in posts]
181
+
182
+ prompt = f"""
183
+ Classify sentiment of each post as one of: POSITIVE, NEGATIVE, NEUTRAL.
184
+ Return JSON array of objects with fields: sentiment, confidence (0..1).
185
+ No extra text.
186
+
187
+ Posts:
188
+ {json.dumps(posts, ensure_ascii=False, indent=2)}
189
+ Expected JSON schema:
190
+ [{{"sentiment":"POSITIVE|NEGATIVE|NEUTRAL","confidence":0.87}}, ...]
191
+ """
192
+ try:
193
+ model = genai.GenerativeModel(GEMINI_MODEL_FAST)
194
+ resp = model.generate_content(prompt)
195
+ text = resp.text or ""
196
+ # Find JSON array robustly
197
+ match = re.search(r"\[[\s\S]+\]", text)
198
+ if match:
199
+ arr = json.loads(match.group(0))
200
+ clean = []
201
+ for i, it in enumerate(arr[:len(posts)]):
202
+ s = str(it.get("sentiment", "NEUTRAL")).upper()
203
+ if s not in {"POSITIVE", "NEGATIVE", "NEUTRAL"}:
204
+ s = "NEUTRAL"
205
+ c = float(it.get("confidence", 0.5))
206
+ c = max(0.0, min(1.0, c))
207
+ clean.append({"sentiment": s, "confidence": round(c, 2)})
208
+ # If Gemini returned fewer rows, pad neutrals
209
+ while len(clean) < len(posts):
210
+ clean.append({"sentiment": "NEUTRAL", "confidence": 0.5})
211
+ return clean
212
+ except Exception:
213
+ pass
214
+ # fallback neutrals
215
+ return [{"sentiment": "NEUTRAL", "confidence": 0.5} for _ in posts]
216
+
217
+
218
+ def build_plot(df: pd.DataFrame, vis: str, hashtag: str):
219
+ """
220
+ Build nice Plotly figure.
221
+ """
222
+ vis = (vis or "Bar").lower()
223
+ # Count per sentiment
224
+ counts = df["Sentiment"].value_counts().reindex(["POSITIVE", "NEUTRAL", "NEGATIVE"], fill_value=0)
225
+ count_df = counts.reset_index()
226
+ count_df.columns = ["Sentiment", "Count"]
227
+
228
+ if vis == "pie":
229
+ fig = px.pie(
230
+ count_df, values="Count", names="Sentiment",
231
+ title=f"Sentiment Distribution for {hashtag}",
232
+ hole=0.45
233
+ )
234
+ fig.update_traces(textposition="inside", pull=[0.03, 0.03, 0.03])
235
+ elif vis == "line":
236
+ # rolling positive ratio
237
+ map_vals = df["Sentiment"].map({"POSITIVE": 1, "NEUTRAL": 0.5, "NEGATIVE": 0})
238
+ roll = map_vals.rolling(window=max(3, min(10, len(df)//3)), min_periods=1).mean()
239
+ fig = px.line(
240
+ x=list(range(1, len(df)+1)), y=roll,
241
+ labels={"x": "Post Index", "y": "Rolling Sentiment (0..1)"},
242
+ title=f"Sentiment Rolling Trend for {hashtag}"
243
+ )
244
  else:
245
+ fig = px.bar(
246
+ count_df, x="Sentiment", y="Count",
247
+ title=f"Sentiment Distribution for {hashtag}"
248
+ )
249
+
250
+ fig.update_layout(
251
+ paper_bgcolor="rgba(0,0,0,0)",
252
+ plot_bgcolor="rgba(0,0,0,0)",
253
+ font=dict(size=14),
254
+ title_x=0.02,
255
+ hovermode="x unified",
256
+ margin=dict(l=40, r=20, t=60, b=40),
257
+ )
258
+ return fig
259
+
260
+
261
+ # ------------------ Main callback ------------------
262
+
263
+ def run_analysis(
264
+ hashtag: str,
265
+ n_posts: int,
266
+ vis_type: str,
267
+ use_gemini_posts: bool,
268
+ use_gemini_analysis: bool
269
+ ):
270
+ hashtag = hashtag.strip()
271
+ if not hashtag:
272
+ return (
273
+ gr.update(value=pd.DataFrame([])),
274
+ gr.update(value=None),
275
+ "⚠️ Please enter a hashtag.",
276
+ "—", 0, 0
277
+ )
278
+
279
+ n_posts = max(5, min(MAX_POSTS, int(n_posts or 20)))
280
+
281
+ # 1) Generate posts
282
+ posts = []
283
+ info_posts = ""
284
+ gemini_count = 0
285
+
286
+ if use_gemini_posts:
287
+ posts, info_posts = generate_posts_gemini(hashtag, n_posts)
288
+ gemini_count = len(posts)
289
+
290
+ if len(posts) < n_posts:
291
+ # Top up with fallback to avoid looking repetitive if Gemini returned few
292
+ remaining = n_posts - len(posts)
293
+ posts += generate_posts_fallback(hashtag, remaining)
294
+ info_posts += f" | Fallback added: {remaining}"
295
+
296
+ # 2) Sentiment
297
+ if use_gemini_analysis:
298
+ analysis = analyze_sentiment_gemini(posts)
299
+ analysis_engine = "Gemini"
300
  else:
301
+ analysis = analyze_sentiment_hf(posts)
302
+ analysis_engine = "HF Transformers"
303
+
304
+ # 3) DataFrame
305
+ df = pd.DataFrame({
306
+ "Post": posts,
307
+ "Sentiment": [a["sentiment"] for a in analysis],
308
+ "Confidence": [a["confidence"] for a in analysis],
309
+ })
310
 
311
+ # 4) Plot
312
+ fig = build_plot(df, vis_type, hashtag)
313
 
314
+ # 5) Status
315
+ status = f"Generated {len(posts)} posts · {gemini_count} via Gemini · Analyzed with {analysis_engine}"
316
+ return df, fig, status, analysis_engine, gemini_count, len(posts) - gemini_count
317
 
318
+
319
+ # ------------------ UI ------------------
320
+
321
+ THEME = gr.themes.Soft(
322
+ primary_hue="indigo",
323
+ neutral_hue="slate",
324
+ ).set(
325
+ button_primary_background_fill="*primary_600",
326
+ button_primary_background_fill_hover="*primary_700",
327
+ )
328
+
329
+ CUSTOM_CSS = """
330
+ /* Starry gradient background */
331
+ body { background: radial-gradient(1200px 600px at 60% -10%, rgba(0,255,255,0.15), transparent 60%),
332
+ radial-gradient(900px 400px at 20% -10%, rgba(255,0,255,0.12), transparent 60%),
333
+ linear-gradient(160deg, #0b1020, #100a25 35%, #0a0f2d 70%); }
334
+ #root { background: transparent !important; }
335
+
336
+ .starfield, .planet {
337
+ position: fixed; inset: 0; pointer-events:none; z-index: -1;
338
+ }
339
+ .starfield::before, .starfield::after {
340
+ content: ""; position: absolute; inset: 0;
341
+ background-image:
342
+ radial-gradient(2px 2px at 20% 30%, rgba(255,255,255,.6) 50%, transparent 51%),
343
+ radial-gradient(1.5px 1.5px at 70% 60%, rgba(255,255,255,.4) 50%, transparent 51%),
344
+ radial-gradient(1.7px 1.7px at 40% 80%, rgba(255,255,255,.5) 50%, transparent 51%),
345
+ radial-gradient(1.4px 1.4px at 90% 20%, rgba(255,255,255,.35) 50%, transparent 51%);
346
+ animation: twinkle 6s infinite ease-in-out alternate;
347
+ }
348
+ @keyframes twinkle { from {opacity:.4} to {opacity:1} }
349
+ .planet::before{
350
+ content:""; position:absolute; width:220px; height:220px; right:8%; top:12%;
351
+ background: radial-gradient(circle at 30% 30%, #4ef4d7, #2b7dff 40%, #2339a1 70%, #0d1130 80%);
352
+ border-radius:50%; filter: blur(0.3px) drop-shadow(0 0 18px rgba(70,180,255,.25));
353
+ animation: floaty 10s ease-in-out infinite;
354
+ }
355
+ @keyframes floaty { 50% { transform: translateY(12px) translateX(-8px) } }
356
+
357
+ .gradio-container { max-width: 1100px !important; margin: 0 auto; }
358
+ .header-title { font-size: 2.1rem; font-weight: 800; letter-spacing: .5px; color: #d9f0ff; }
359
+ .header-sub { color: #bcd7ff; opacity: .85; }
360
+
361
+ .card {
362
+ border: 1px solid rgba(255,255,255,.08);
363
+ background: rgba(255,255,255,.05);
364
+ backdrop-filter: blur(10px);
365
+ border-radius: 18px;
366
+ transition: transform .2s ease, box-shadow .2s ease;
367
+ }
368
+ .card:hover { transform: translateY(-2px); box-shadow: 0 18px 40px rgba(0,0,0,.25); }
369
+
370
+ label, .label { color:#eaf2ff !important; font-weight:600; }
371
+
372
+ .status-badge{
373
+ padding:.4rem .7rem; border-radius:999px; background:rgba(0,0,0,.35); color:#e6f7ff;
374
+ display:inline-flex; gap:.5rem; align-items:center; border:1px solid rgba(255,255,255,.12)
375
+ }
376
+ """
377
+
378
+ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Social Media Sentiment Analyzer") as demo:
379
+ gr.HTML('<div class="starfield"></div><div class="planet"></div>')
380
+ gr.Markdown(
381
+ """
382
+ <div class="header-title">🚀 Social Media Sentiment Analyzer</div>
383
+ <div class="header-sub">Stream-like posts • Analyze moods • Visualize trends — with Gemini & HF</div>
384
+ """
385
+ )
386
 
387
  with gr.Row():
388
+ with gr.Column(scale=5, elem_classes=["card"]):
389
+ hashtag = gr.Textbox(label="Enter Hashtag", placeholder="#YourTopic", value="#gla university")
390
+ n_posts = gr.Slider(5, MAX_POSTS, value=20, step=1, label="Number of Posts (max 50)")
391
+ vis_type = gr.Dropdown(["Bar", "Pie", "Line"], value="Bar", label="Choose Visualization")
392
+ use_gemini_posts = gr.Checkbox(value=True, label="Generate Posts with Gemini")
393
+ use_gemini_analysis = gr.Checkbox(value=False, label="Use Gemini for Sentiment (else HF)")
394
+
395
+ run_btn = gr.Button("🔎 Run Analysis", variant="primary")
396
+
397
+ status = gr.Markdown("Ready.")
398
+ stats_row = gr.Markdown("", visible=False)
399
+
400
+ with gr.Column(scale=7, elem_classes=["card"]):
401
+ posts_table = gr.Dataframe(
402
+ headers=["Post", "Sentiment", "Confidence"], wrap=True, height=420, interactive=False
403
+ )
404
+ plot = gr.Plot(label="Visualization")
405
+
406
+ hidden_engine = gr.State(value="—")
407
+ hidden_gemini_count = gr.State(value=0)
408
+ hidden_fallback_count = gr.State(value=0)
409
+
410
+ def _status_text(status_str, engine, gcount, fcount):
411
+ stats_md = f"""
412
+ <span class="status-badge">🔧 Engine: <b>{engine}</b></span>
413
+ <span class="status-badge">✨ Gemini posts: <b>{gcount}</b></span>
414
+ <span class="status-badge">🧩 Fallback posts: <b>{fcount}</b></span>
415
+ """
416
+ return gr.update(value=f"**{status_str}**"), gr.update(value=stats_md, visible=True)
417
 
418
  run_btn.click(
419
  fn=run_analysis,
420
+ inputs=[hashtag, n_posts, vis_type, use_gemini_posts, use_gemini_analysis],
421
+ outputs=[posts_table, plot, status, hidden_engine, hidden_gemini_count, hidden_fallback_count]
422
+ ).then(
423
+ fn=_status_text,
424
+ inputs=[status, hidden_engine, hidden_gemini_count, hidden_fallback_count],
425
+ outputs=[status, stats_row]
426
  )
427
 
428
+ # ------------------ Launch ------------------
 
 
429
  if __name__ == "__main__":
430
  demo.launch()