riazmo commited on
Commit
2ad9e40
Β·
verified Β·
1 Parent(s): 2759537

Upload app_streamlit.py

Browse files
Files changed (1) hide show
  1. app_streamlit.py +751 -0
app_streamlit.py ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Spaces - Review Intelligence System (Streamlit)
3
+ Complete app with URL input, progress tracking, and interactive dashboard
4
+ """
5
+
6
+ import streamlit as st
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import os
11
+ from datetime import datetime
12
+ from typing import List, Dict, Optional
13
+ import time
14
+
15
+ from gradio_pipeline import GradioPipeline
16
+
17
+
18
+ # ============================================================================
19
+ # PAGE CONFIGURATION
20
+ # ============================================================================
21
+
22
+ st.set_page_config(
23
+ page_title="Review Intelligence System",
24
+ page_icon="🎯",
25
+ layout="wide",
26
+ initial_sidebar_state="expanded"
27
+ )
28
+
29
+ # Custom CSS
30
+ st.markdown("""
31
+ <style>
32
+ .main {
33
+ padding: 0rem 1rem;
34
+ }
35
+ .stMetric {
36
+ background-color: #f0f2f6;
37
+ padding: 15px;
38
+ border-radius: 5px;
39
+ }
40
+ .big-font {
41
+ font-size: 24px !important;
42
+ font-weight: bold;
43
+ }
44
+ .success-box {
45
+ padding: 20px;
46
+ border-radius: 10px;
47
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
48
+ color: white;
49
+ margin: 20px 0;
50
+ }
51
+ </style>
52
+ """, unsafe_allow_html=True)
53
+
54
+
55
+ # ============================================================================
56
+ # SESSION STATE INITIALIZATION
57
+ # ============================================================================
58
+
59
+ if 'processing_complete' not in st.session_state:
60
+ st.session_state.processing_complete = False
61
+
62
+ if 'results' not in st.session_state:
63
+ st.session_state.results = None
64
+
65
+ if 'insights' not in st.session_state:
66
+ st.session_state.insights = None
67
+
68
+ if 'scraped_count' not in st.session_state:
69
+ st.session_state.scraped_count = 0
70
+
71
+
72
+ # ============================================================================
73
+ # PROCESSING FUNCTIONS
74
+ # ============================================================================
75
+
76
+ def process_reviews_streamlit(app_store_urls: str, play_store_urls: str,
77
+ hf_api_key: str, review_limit: int):
78
+ """
79
+ Process reviews with Streamlit progress tracking
80
+ """
81
+
82
+ # Validate inputs
83
+ if not hf_api_key or not hf_api_key.strip():
84
+ st.error("❌ Please provide your HuggingFace API key")
85
+ return False
86
+
87
+ if not app_store_urls.strip() and not play_store_urls.strip():
88
+ st.error("❌ Please provide at least one App Store or Play Store URL")
89
+ return False
90
+
91
+ try:
92
+ # Set API key
93
+ os.environ['HUGGINGFACE_API_KEY'] = hf_api_key.strip()
94
+
95
+ # Progress indicators
96
+ progress_bar = st.progress(0)
97
+ status_text = st.empty()
98
+
99
+ # Initialize pipeline
100
+ status_text.text("πŸš€ Initializing pipeline...")
101
+ progress_bar.progress(5)
102
+ pipeline = GradioPipeline(review_limit=review_limit)
103
+
104
+ # Parse URLs
105
+ app_urls = [url.strip() for url in app_store_urls.split('\n') if url.strip()]
106
+ play_urls = [url.strip() for url in play_store_urls.split('\n') if url.strip()]
107
+
108
+ # Stage 0: Scraping
109
+ status_text.text("πŸ•·οΈ Scraping reviews from stores...")
110
+ progress_bar.progress(10)
111
+
112
+ scraped_count = 0
113
+ total_apps = len(app_urls) + len(play_urls)
114
+
115
+ for i, app_id in enumerate(app_urls, 1):
116
+ status_text.text(f"🍎 Scraping App Store ({i}/{total_apps}): {app_id}")
117
+ reviews = pipeline.scraper.scrape_app_store_rss(app_id, country="ae", limit=review_limit)
118
+ saved = pipeline.scraper.save_reviews_to_db(reviews)
119
+ scraped_count += saved
120
+ progress_bar.progress(10 + int(20 * i / total_apps))
121
+ time.sleep(1)
122
+
123
+ for i, package in enumerate(play_urls, 1):
124
+ status_text.text(f"πŸ€– Scraping Play Store ({i}/{total_apps}): {package}")
125
+ reviews = pipeline.scraper.scrape_play_store_api(package, country="ae", limit=review_limit)
126
+ saved = pipeline.scraper.save_reviews_to_db(reviews)
127
+ scraped_count += saved
128
+ progress_bar.progress(10 + int(20 * (len(app_urls) + i) / total_apps))
129
+ time.sleep(1)
130
+
131
+ if scraped_count == 0:
132
+ st.warning("⚠️ No reviews scraped. Please check your URLs and try again.")
133
+ progress_bar.empty()
134
+ status_text.empty()
135
+ return False
136
+
137
+ st.session_state.scraped_count = scraped_count
138
+
139
+ # Stage 1-3: Processing
140
+ status_text.text("πŸ€– Processing reviews with AI models...")
141
+ progress_bar.progress(30)
142
+
143
+ reviews = pipeline.db.get_pending_reviews(limit=review_limit)
144
+ total_reviews = len(reviews)
145
+
146
+ processed_states = []
147
+
148
+ for i, review in enumerate(reviews, 1):
149
+ review_id = review.get('review_id', 'unknown')[:20]
150
+ status_text.text(f"πŸ€– Processing review {i}/{total_reviews}: {review_id}...")
151
+ progress_bar.progress(30 + int(60 * i / total_reviews))
152
+
153
+ try:
154
+ from langgraph_state import create_initial_state
155
+ state = create_initial_state(review)
156
+ config = {"configurable": {"thread_id": f"review_{review.get('review_id')}"}}
157
+ final_state = pipeline.review_graph.invoke(state, config=config)
158
+ processed_states.append(dict(final_state))
159
+ except Exception as e:
160
+ st.warning(f"⚠️ Error processing review: {str(e)}")
161
+ continue
162
+
163
+ if len(processed_states) == 0:
164
+ st.error("❌ No reviews were processed successfully.")
165
+ progress_bar.empty()
166
+ status_text.empty()
167
+ return False
168
+
169
+ # Stage 4: Batch Analysis
170
+ status_text.text("πŸ“Š Generating batch insights...")
171
+ progress_bar.progress(90)
172
+
173
+ insights = pipeline.analyze_batch(processed_states)
174
+
175
+ # Store in session state
176
+ st.session_state.results = processed_states
177
+ st.session_state.insights = insights
178
+ st.session_state.processing_complete = True
179
+
180
+ # Complete
181
+ progress_bar.progress(100)
182
+ status_text.text("βœ… Analysis complete!")
183
+ time.sleep(1)
184
+ progress_bar.empty()
185
+ status_text.empty()
186
+
187
+ return True
188
+
189
+ except Exception as e:
190
+ st.error(f"❌ Error during processing: {str(e)}")
191
+ import traceback
192
+ st.code(traceback.format_exc())
193
+ return False
194
+
195
+
196
+ # ============================================================================
197
+ # VISUALIZATION FUNCTIONS
198
+ # ============================================================================
199
+
200
+ def create_summary_section(scraped_count: int, results: List[Dict], insights: Dict):
201
+ """Create summary metrics section"""
202
+
203
+ total = len(results)
204
+ positive = insights.get('sentiment_distribution', {}).get('POSITIVE', 0)
205
+ neutral = insights.get('sentiment_distribution', {}).get('NEUTRAL', 0)
206
+ negative = insights.get('sentiment_distribution', {}).get('NEGATIVE', 0)
207
+ critical = insights.get('priority_distribution', {}).get('critical', 0)
208
+ churn_risk = insights.get('churn_risk', 0)
209
+
210
+ # Success header
211
+ st.markdown(
212
+ f"""
213
+ <div class="success-box">
214
+ <h1 style="margin: 0;">βœ… Analysis Complete!</h1>
215
+ <p style="margin: 10px 0 0 0; font-size: 1.2em; opacity: 0.9;">
216
+ Review Intelligence System Results
217
+ </p>
218
+ </div>
219
+ """,
220
+ unsafe_allow_html=True
221
+ )
222
+
223
+ # Metrics
224
+ col1, col2, col3, col4, col5 = st.columns(5)
225
+
226
+ with col1:
227
+ st.metric("Total Reviews", total, f"Scraped: {scraped_count}")
228
+
229
+ with col2:
230
+ pos_pct = (positive / total * 100) if total > 0 else 0
231
+ st.metric("Positive", positive, f"{pos_pct:.1f}%")
232
+
233
+ with col3:
234
+ neg_pct = (negative / total * 100) if total > 0 else 0
235
+ st.metric("Negative", negative, f"{neg_pct:.1f}%")
236
+
237
+ with col4:
238
+ st.metric("Critical Issues", critical, "🚨" if critical > 0 else "βœ…")
239
+
240
+ with col5:
241
+ delta_color = "inverse" if churn_risk > 30 else "normal"
242
+ st.metric("Churn Risk", f"{churn_risk:.1f}%",
243
+ "⚠️ High" if churn_risk > 30 else "βœ… Low")
244
+
245
+ # Recommendations
246
+ st.markdown("### πŸ’‘ Key Recommendations")
247
+ for rec in insights.get('recommendations', []):
248
+ st.info(rec)
249
+
250
+
251
+ def create_sentiment_chart(insights: Dict):
252
+ """Create sentiment distribution donut chart"""
253
+ sentiment_dist = insights.get('sentiment_distribution', {})
254
+
255
+ labels = list(sentiment_dist.keys())
256
+ values = list(sentiment_dist.values())
257
+ colors = ['#2ca02c', '#ff7f0e', '#d62728']
258
+
259
+ fig = go.Figure(data=[go.Pie(
260
+ labels=labels,
261
+ values=values,
262
+ hole=0.5,
263
+ marker_colors=colors,
264
+ textinfo='label+percent',
265
+ textposition='outside',
266
+ textfont_size=14
267
+ )])
268
+
269
+ fig.update_layout(
270
+ title="😊 Sentiment Distribution",
271
+ showlegend=True,
272
+ height=400
273
+ )
274
+
275
+ return fig
276
+
277
+
278
+ def create_priority_chart(insights: Dict):
279
+ """Create priority distribution bar chart"""
280
+ priority_dist = insights.get('priority_distribution', {})
281
+
282
+ priority_order = ['critical', 'high', 'medium', 'low']
283
+ labels = [p for p in priority_order if p in priority_dist]
284
+ values = [priority_dist.get(p, 0) for p in labels]
285
+ colors = ['#d62728', '#ff7f0e', '#1f77b4', '#2ca02c']
286
+
287
+ fig = go.Figure(data=[go.Bar(
288
+ x=labels,
289
+ y=values,
290
+ marker_color=colors[:len(labels)],
291
+ text=values,
292
+ textposition='auto'
293
+ )])
294
+
295
+ fig.update_layout(
296
+ title="🎯 Priority Levels",
297
+ xaxis_title="Priority",
298
+ yaxis_title="Count",
299
+ height=400
300
+ )
301
+
302
+ return fig
303
+
304
+
305
+ def create_department_chart(insights: Dict):
306
+ """Create department routing horizontal bar chart"""
307
+ dept_dist = insights.get('department_distribution', {})
308
+
309
+ labels = list(dept_dist.keys())
310
+ values = list(dept_dist.values())
311
+
312
+ fig = go.Figure(data=[go.Bar(
313
+ x=values,
314
+ y=labels,
315
+ orientation='h',
316
+ marker_color='#667eea',
317
+ text=values,
318
+ textposition='auto'
319
+ )])
320
+
321
+ fig.update_layout(
322
+ title="🏒 Department Routing",
323
+ xaxis_title="Number of Issues",
324
+ yaxis_title="Department",
325
+ height=400
326
+ )
327
+
328
+ return fig
329
+
330
+
331
+ def create_emotion_chart(insights: Dict):
332
+ """Create emotion distribution chart"""
333
+ emotion_dist = insights.get('emotion_distribution', {})
334
+
335
+ labels = list(emotion_dist.keys())
336
+ values = list(emotion_dist.values())
337
+
338
+ fig = px.bar(
339
+ x=labels,
340
+ y=values,
341
+ labels={'x': 'Emotion', 'y': 'Count'},
342
+ color=values,
343
+ color_continuous_scale='Viridis'
344
+ )
345
+
346
+ fig.update_layout(
347
+ title="😊 Emotional Analysis",
348
+ xaxis_title="Emotion Type",
349
+ yaxis_title="Number of Reviews",
350
+ height=300,
351
+ showlegend=False
352
+ )
353
+
354
+ return fig
355
+
356
+
357
+ def create_reviews_dataframe(results: List[Dict]) -> pd.DataFrame:
358
+ """Create DataFrame for reviews table"""
359
+
360
+ df_data = []
361
+ for review in results:
362
+ df_data.append({
363
+ 'Review ID': review.get('review_id', 'N/A')[:20],
364
+ 'Rating': review.get('rating', 0),
365
+ 'Review': (review.get('review_text', 'N/A') or '')[:100] + '...',
366
+ 'Sentiment': review.get('stage3_final_sentiment', 'N/A'),
367
+ 'Type': review.get('stage1_llm1_type', 'N/A'),
368
+ 'Department': review.get('stage1_llm1_department', 'N/A'),
369
+ 'Priority': review.get('stage1_llm1_priority', 'N/A'),
370
+ 'Emotion': review.get('stage1_llm2_emotion', 'N/A'),
371
+ 'Needs Review': '🚨 Yes' if review.get('stage3_needs_human_review') else 'βœ… No'
372
+ })
373
+
374
+ return pd.DataFrame(df_data)
375
+
376
+
377
+ # ============================================================================
378
+ # MAIN APP
379
+ # ============================================================================
380
+
381
+ def main():
382
+ """Main Streamlit app"""
383
+
384
+ # Title
385
+ st.title("🎯 Review Intelligence System")
386
+ st.markdown("### Multi-Stage AI Analysis for App Store & Play Store Reviews")
387
+ st.markdown("Powered by **LangGraph** + **HuggingFace** β€’ 4-Stage Processing Pipeline")
388
+ st.markdown("---")
389
+
390
+ # Sidebar - Input or View Mode
391
+ with st.sidebar:
392
+ st.header("πŸŽ›οΈ Control Panel")
393
+
394
+ if st.session_state.processing_complete:
395
+ st.success("βœ… Analysis Complete!")
396
+ if st.button("πŸ”„ Start New Analysis", use_container_width=True):
397
+ st.session_state.processing_complete = False
398
+ st.session_state.results = None
399
+ st.session_state.insights = None
400
+ st.rerun()
401
+ else:
402
+ st.info("πŸ‘ˆ Enter URLs below to start")
403
+
404
+ # Main content - Input or Results
405
+ if not st.session_state.processing_complete:
406
+ # INPUT MODE
407
+ show_input_form()
408
+ else:
409
+ # RESULTS MODE
410
+ show_results_dashboard()
411
+
412
+
413
+ def show_input_form():
414
+ """Show input form for URLs and API key"""
415
+
416
+ st.markdown("### πŸ“ Step 1: Enter Store URLs")
417
+
418
+ col1, col2 = st.columns(2)
419
+
420
+ with col1:
421
+ st.markdown("#### 🍎 App Store IDs")
422
+ st.markdown(
423
+ """
424
+ **Format:** Just paste the app ID
425
+ - Example: `1158907446` (UAE)
426
+ - Example: `1234567890` (US)
427
+ """
428
+ )
429
+ app_store_urls = st.text_area(
430
+ "App Store IDs (one per line)",
431
+ placeholder="1158907446\n1234567890",
432
+ height=150,
433
+ key="app_urls"
434
+ )
435
+
436
+ with col2:
437
+ st.markdown("#### πŸ€– Play Store Packages")
438
+ st.markdown(
439
+ """
440
+ **Format:** Package name
441
+ - Example: `com.yas.app`
442
+ - Example: `com.company.app`
443
+ """
444
+ )
445
+ play_store_urls = st.text_area(
446
+ "Play Store Package Names (one per line)",
447
+ placeholder="com.yas.app\ncom.company.app",
448
+ height=150,
449
+ key="play_urls"
450
+ )
451
+
452
+ st.markdown("---")
453
+ st.markdown("### πŸ”‘ Step 2: Configure Settings")
454
+
455
+ col1, col2 = st.columns([2, 1])
456
+
457
+ with col1:
458
+ hf_api_key = st.text_input(
459
+ "πŸ”‘ HuggingFace API Key",
460
+ type="password",
461
+ placeholder="hf_...",
462
+ help="Get your key from: https://huggingface.co/settings/tokens",
463
+ key="hf_key"
464
+ )
465
+
466
+ with col2:
467
+ review_limit = st.slider(
468
+ "πŸ“Š Reviews per App",
469
+ min_value=5,
470
+ max_value=100,
471
+ value=20,
472
+ step=5,
473
+ help="More reviews = longer processing time",
474
+ key="review_limit"
475
+ )
476
+
477
+ st.markdown("---")
478
+
479
+ # Submit button
480
+ col1, col2, col3 = st.columns([1, 1, 1])
481
+
482
+ with col2:
483
+ if st.button("πŸš€ Start Analysis", use_container_width=True, type="primary"):
484
+ with st.spinner("Processing..."):
485
+ success = process_reviews_streamlit(
486
+ app_store_urls,
487
+ play_store_urls,
488
+ hf_api_key,
489
+ review_limit
490
+ )
491
+
492
+ if success:
493
+ st.balloons()
494
+ st.rerun()
495
+
496
+ # Documentation
497
+ with st.expander("πŸ“š How to Use"):
498
+ st.markdown("""
499
+ ### πŸ“– Quick Guide
500
+
501
+ **1. Get HuggingFace API Key:**
502
+ - Visit: https://huggingface.co/settings/tokens
503
+ - Create new token (Read access)
504
+ - Copy token (starts with `hf_`)
505
+
506
+ **2. Enter URLs:**
507
+ - **App Store**: Just the ID number (e.g., `1234567890`)
508
+ - **Play Store**: Package name (e.g., `com.company.app`)
509
+ - One per line
510
+
511
+ **3. Click Start:**
512
+ - Watch progress bar
513
+ - Wait for completion (~7 sec per review)
514
+ - View results automatically
515
+
516
+ ### πŸ—οΈ What Happens:
517
+ - πŸ•·οΈ **Stage 0**: Scrapes reviews from stores
518
+ - πŸ€– **Stage 1**: Classifies with 3 AI models (Type, Department, Priority)
519
+ - 😊 **Stage 2**: Analyzes sentiment with dual BERT models
520
+ - πŸ“Š **Stage 3**: Synthesizes insights and recommendations
521
+ - πŸ’‘ **Stage 4**: Generates batch analytics
522
+
523
+ ### ⚑ Performance:
524
+ - ~7 seconds per review
525
+ - 7 AI models working together
526
+ - Parallel execution for speed
527
+ """)
528
+
529
+
530
+ def show_results_dashboard():
531
+ """Show results dashboard with charts and tables"""
532
+
533
+ results = st.session_state.results
534
+ insights = st.session_state.insights
535
+ scraped_count = st.session_state.scraped_count
536
+
537
+ # Summary section
538
+ create_summary_section(scraped_count, results, insights)
539
+
540
+ st.markdown("---")
541
+
542
+ # Tabs for different views
543
+ tab1, tab2, tab3, tab4 = st.tabs([
544
+ "πŸ“Š Sentiment Analysis",
545
+ "🚨 Critical Issues",
546
+ "πŸ“‹ All Reviews",
547
+ "πŸ“₯ Export"
548
+ ])
549
+
550
+ # TAB 1: Sentiment Analysis
551
+ with tab1:
552
+ st.header("πŸ“Š Sentiment Analysis Overview")
553
+
554
+ col1, col2 = st.columns(2)
555
+
556
+ with col1:
557
+ fig_sentiment = create_sentiment_chart(insights)
558
+ st.plotly_chart(fig_sentiment, use_container_width=True)
559
+
560
+ with col2:
561
+ fig_priority = create_priority_chart(insights)
562
+ st.plotly_chart(fig_priority, use_container_width=True)
563
+
564
+ st.markdown("### 🏒 Department Routing")
565
+ fig_dept = create_department_chart(insights)
566
+ st.plotly_chart(fig_dept, use_container_width=True)
567
+
568
+ st.markdown("### 😊 Emotional Analysis")
569
+ fig_emotion = create_emotion_chart(insights)
570
+ st.plotly_chart(fig_emotion, use_container_width=True)
571
+
572
+ # TAB 2: Critical Issues
573
+ with tab2:
574
+ st.header("🚨 Critical Issues Requiring Attention")
575
+
576
+ # Filter critical reviews
577
+ critical_reviews = [
578
+ r for r in results
579
+ if (r.get('stage1_llm1_priority') == 'critical' or
580
+ r.get('stage3_needs_human_review') or
581
+ (r.get('stage3_final_sentiment') == 'NEGATIVE' and r.get('rating', 5) <= 2))
582
+ ]
583
+
584
+ if len(critical_reviews) == 0:
585
+ st.success("βœ… No critical issues found! All reviews are in good shape.")
586
+ else:
587
+ st.warning(f"Found {len(critical_reviews)} critical issues")
588
+
589
+ for review in critical_reviews:
590
+ with st.expander(
591
+ f"⚠️ {review.get('review_id', 'Unknown')[:30]} - "
592
+ f"Rating: {review.get('rating', 'N/A')}/5"
593
+ ):
594
+ col1, col2 = st.columns([2, 1])
595
+
596
+ with col1:
597
+ st.markdown("**Review Text:**")
598
+ st.write(review.get('review_text', 'No text available'))
599
+
600
+ st.markdown("**Reasoning:**")
601
+ st.info(review.get('stage3_reasoning', 'No reasoning available'))
602
+
603
+ with col2:
604
+ st.markdown("**Classification:**")
605
+ st.write(f"πŸ“Œ Type: {review.get('stage1_llm1_type', 'N/A')}")
606
+ st.write(f"🏒 Department: {review.get('stage1_llm1_department', 'N/A')}")
607
+ st.write(f"🎯 Priority: {review.get('stage1_llm1_priority', 'N/A')}")
608
+ st.write(f"πŸ˜” Emotion: {review.get('stage1_llm2_emotion', 'N/A')}")
609
+ st.write(f"πŸ’­ Sentiment: {review.get('stage3_final_sentiment', 'N/A')}")
610
+
611
+ st.markdown("**Action:**")
612
+ st.error(review.get('stage3_action_recommendation', 'No action specified'))
613
+
614
+ # TAB 3: All Reviews
615
+ with tab3:
616
+ st.header("πŸ“‹ Detailed Review Analysis")
617
+
618
+ # Create DataFrame
619
+ df = create_reviews_dataframe(results)
620
+
621
+ # Filters
622
+ col1, col2, col3 = st.columns(3)
623
+
624
+ with col1:
625
+ sentiment_filter = st.multiselect(
626
+ "Filter by Sentiment",
627
+ options=df['Sentiment'].unique(),
628
+ default=df['Sentiment'].unique()
629
+ )
630
+
631
+ with col2:
632
+ dept_filter = st.multiselect(
633
+ "Filter by Department",
634
+ options=df['Department'].unique(),
635
+ default=df['Department'].unique()
636
+ )
637
+
638
+ with col3:
639
+ priority_filter = st.multiselect(
640
+ "Filter by Priority",
641
+ options=df['Priority'].unique(),
642
+ default=df['Priority'].unique()
643
+ )
644
+
645
+ # Apply filters
646
+ filtered_df = df[
647
+ (df['Sentiment'].isin(sentiment_filter)) &
648
+ (df['Department'].isin(dept_filter)) &
649
+ (df['Priority'].isin(priority_filter))
650
+ ]
651
+
652
+ st.info(f"Showing {len(filtered_df)} of {len(df)} reviews")
653
+
654
+ # Display table
655
+ st.dataframe(
656
+ filtered_df,
657
+ use_container_width=True,
658
+ height=600
659
+ )
660
+
661
+ # TAB 4: Export
662
+ with tab4:
663
+ st.header("πŸ“₯ Export Results")
664
+
665
+ st.markdown("### Download Options")
666
+
667
+ col1, col2 = st.columns(2)
668
+
669
+ with col1:
670
+ st.markdown("#### πŸ“Š CSV Export")
671
+ st.write("Download complete analysis with all classifications")
672
+
673
+ df = create_reviews_dataframe(results)
674
+ csv = df.to_csv(index=False)
675
+
676
+ st.download_button(
677
+ label="πŸ“₯ Download CSV Report",
678
+ data=csv,
679
+ file_name=f"review_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
680
+ mime="text/csv",
681
+ use_container_width=True
682
+ )
683
+
684
+ with col2:
685
+ st.markdown("#### πŸ“‹ JSON Export")
686
+ st.write("Download raw data with all details")
687
+
688
+ import json
689
+ json_data = json.dumps({
690
+ 'results': results,
691
+ 'insights': insights,
692
+ 'scraped_count': scraped_count,
693
+ 'export_date': datetime.now().isoformat()
694
+ }, indent=2)
695
+
696
+ st.download_button(
697
+ label="πŸ“₯ Download JSON Data",
698
+ data=json_data,
699
+ file_name=f"review_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
700
+ mime="application/json",
701
+ use_container_width=True
702
+ )
703
+
704
+ st.markdown("---")
705
+ st.markdown("### πŸ“Š Summary Statistics")
706
+
707
+ col1, col2, col3 = st.columns(3)
708
+
709
+ with col1:
710
+ st.metric("Total Reviews Analyzed", len(results))
711
+
712
+ with col2:
713
+ positive = insights.get('sentiment_distribution', {}).get('POSITIVE', 0)
714
+ total = len(results)
715
+ pct = (positive / total * 100) if total > 0 else 0
716
+ st.metric("Positive Rate", f"{pct:.1f}%")
717
+
718
+ with col3:
719
+ critical = insights.get('priority_distribution', {}).get('critical', 0)
720
+ st.metric("Critical Issues", critical)
721
+
722
+
723
+ # ============================================================================
724
+ # FOOTER
725
+ # ============================================================================
726
+
727
+ def show_footer():
728
+ """Show footer with credits"""
729
+ st.markdown("---")
730
+ st.markdown(
731
+ """
732
+ <div style='text-align: center'>
733
+ <p>πŸ€– Powered by Multi-Stage AI Pipeline |
734
+ Stage 1: Classification (Qwen, Mistral, Llama) |
735
+ Stage 2: Sentiment (Twitter-BERT) |
736
+ Stage 3: Finalization (Llama 70B) |
737
+ Stage 4: Batch Analysis</p>
738
+ <p>Built with ❀️ using LangGraph + HuggingFace + Streamlit</p>
739
+ </div>
740
+ """,
741
+ unsafe_allow_html=True
742
+ )
743
+
744
+
745
+ # ============================================================================
746
+ # RUN APP
747
+ # ============================================================================
748
+
749
+ if __name__ == "__main__":
750
+ main()
751
+ show_footer()