Spaces:
Sleeping
Sleeping
| import faicons as fa | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| from datetime import datetime | |
| import requests | |
| from transformers import pipeline | |
| import asyncio | |
| from shiny import reactive, render | |
| from shiny.express import input, ui | |
| from shinywidgets import render_plotly | |
| # Initialize sentiment models (cached) | |
| def sentiment_analyzer(): | |
| try: | |
| return pipeline("sentiment-analysis", | |
| model="cardiffnlp/twitter-roberta-base-sentiment-latest", | |
| return_all_scores=True) | |
| except: | |
| return pipeline("sentiment-analysis", return_all_scores=True) | |
| # Sample data for demo | |
| sample_reviews = [ | |
| "This product is absolutely amazing! Love everything about it.", | |
| "Terrible quality, completely disappointed with my purchase.", | |
| "It's okay, nothing special but does the job.", | |
| "Excellent customer service and fast shipping!", | |
| "Worst experience ever, would not recommend.", | |
| "Great value for money, very satisfied.", | |
| "Poor packaging, item arrived damaged." | |
| ] | |
| # Page setup | |
| ui.page_opts(title="π― Social Listening AI", fillable=True) | |
| # Sidebar with input controls | |
| with ui.sidebar(open="desktop", width="350px"): | |
| ui.h4("π Analysis Configuration") | |
| ui.input_radio_buttons( | |
| "input_type", | |
| "Input Method:", | |
| {"text": "π Text Input", "url": "π URL Analysis", "sample": "π― Sample Data"}, | |
| selected="text", | |
| inline=False | |
| ) | |
| ui.input_select( | |
| "model_choice", | |
| "π€ Sentiment Model:", | |
| { | |
| "roberta": "RoBERTa Twitter (Recommended)", | |
| "bert": "BERT Base", | |
| "distilbert": "DistilBERT (Faster)", | |
| "vader": "VADER (Rule-based)" | |
| }, | |
| selected="roberta" | |
| ) | |
| def dynamic_input(): | |
| if input.input_type() == "text": | |
| return ui.input_text_area( | |
| "text_input", | |
| "Enter text to analyze:", | |
| placeholder="Paste product reviews, social media posts, or any text...", | |
| rows=8, | |
| width="100%" | |
| ) | |
| elif input.input_type() == "url": | |
| return ui.div( | |
| ui.input_text( | |
| "url_input", | |
| "Enter URL:", | |
| placeholder="https://example.com/reviews", | |
| width="100%" | |
| ), | |
| ui.p("π Demo: URL content will be simulated", class_="text-muted small") | |
| ) | |
| else: | |
| return ui.div( | |
| ui.p("π― Using sample restaurant reviews for demo", class_="text-info small"), | |
| ui.input_numeric("sample_count", "Number of samples:", value=5, min=1, max=len(sample_reviews)) | |
| ) | |
| ui.input_action_button( | |
| "analyze_btn", | |
| "π Analyze Sentiment", | |
| class_="btn-primary w-100 mt-3" | |
| ) | |
| ui.input_action_button( | |
| "reset_btn", | |
| "π Reset", | |
| class_="btn-outline-secondary w-100 mt-2" | |
| ) | |
| # Main content area | |
| ICONS = { | |
| "thumbs-up": fa.icon_svg("thumbs-up"), | |
| "thumbs-down": fa.icon_svg("thumbs-down"), | |
| "minus": fa.icon_svg("minus"), | |
| "chart": fa.icon_svg("chart-simple"), | |
| "ellipsis": fa.icon_svg("ellipsis"), | |
| "brain": fa.icon_svg("brain"), | |
| } | |
| # Value boxes for key metrics | |
| with ui.layout_columns(fill=False): | |
| with ui.value_box(showcase=ICONS["thumbs-up"], theme="success"): | |
| "Positive Sentiment" | |
| def positive_count(): | |
| data = analysis_data() | |
| if data is not None: | |
| pos = len([r for r in data if r['sentiment'] == 'POSITIVE']) | |
| total = len(data) | |
| f"{pos} ({pos/total*100:.1f}%)" | |
| else: | |
| "No data" | |
| with ui.value_box(showcase=ICONS["thumbs-down"], theme="danger"): | |
| "Negative Sentiment" | |
| def negative_count(): | |
| data = analysis_data() | |
| if data is not None: | |
| neg = len([r for r in data if r['sentiment'] == 'NEGATIVE']) | |
| total = len(data) | |
| f"{neg} ({neg/total*100:.1f}%)" | |
| else: | |
| "No data" | |
| with ui.value_box(showcase=ICONS["minus"], theme="secondary"): | |
| "Neutral Sentiment" | |
| def neutral_count(): | |
| data = analysis_data() | |
| if data is not None: | |
| neu = len([r for r in data if r['sentiment'] == 'NEUTRAL']) | |
| total = len(data) | |
| f"{neu} ({neu/total*100:.1f}%)" | |
| else: | |
| "No data" | |
| with ui.value_box(showcase=ICONS["brain"]): | |
| "Avg Confidence" | |
| def avg_confidence(): | |
| data = analysis_data() | |
| if data is not None: | |
| avg_conf = np.mean([r['confidence'] for r in data]) | |
| f"{avg_conf:.3f}" | |
| else: | |
| "N/A" | |
| # Charts and detailed results | |
| with ui.layout_columns(col_widths=[6, 6, 12]): | |
| with ui.card(full_screen=True): | |
| ui.card_header("π Sentiment Distribution") | |
| def sentiment_pie(): | |
| data = analysis_data() | |
| if data is None: | |
| return go.Figure().add_annotation( | |
| text="No data to display<br>Click 'Analyze Sentiment' to start", | |
| xref="paper", yref="paper", x=0.5, y=0.5, | |
| showarrow=False, font_size=16 | |
| ) | |
| # Count sentiments | |
| sentiments = [r['sentiment'] for r in data] | |
| sentiment_counts = pd.Series(sentiments).value_counts() | |
| fig = px.pie( | |
| values=sentiment_counts.values, | |
| names=sentiment_counts.index, | |
| color_discrete_map={ | |
| 'POSITIVE': '#10b981', | |
| 'NEGATIVE': '#ef4444', | |
| 'NEUTRAL': '#6b7280' | |
| } | |
| ) | |
| fig.update_traces(textposition='inside', textinfo='percent+label') | |
| return fig | |
| with ui.card(full_screen=True): | |
| with ui.card_header(class_="d-flex justify-content-between align-items-center"): | |
| "π― Confidence Scores" | |
| with ui.popover(title="Chart Options", placement="top"): | |
| ICONS["ellipsis"] | |
| ui.input_radio_buttons( | |
| "conf_chart_type", | |
| "Chart Type:", | |
| ["bar", "scatter", "box"], | |
| selected="bar", | |
| inline=True | |
| ) | |
| def confidence_chart(): | |
| data = analysis_data() | |
| if data is None: | |
| return go.Figure().add_annotation( | |
| text="No data available", | |
| xref="paper", yref="paper", x=0.5, y=0.5, | |
| showarrow=False, font_size=16 | |
| ) | |
| df = pd.DataFrame(data) | |
| chart_type = input.conf_chart_type() | |
| color_map = { | |
| 'POSITIVE': '#10b981', | |
| 'NEGATIVE': '#ef4444', | |
| 'NEUTRAL': '#6b7280' | |
| } | |
| if chart_type == "bar": | |
| fig = px.bar(df, x='sentence_id', y='confidence', color='sentiment', | |
| color_discrete_map=color_map, | |
| labels={'confidence': 'Confidence Score', 'sentence_id': 'Item'}) | |
| elif chart_type == "scatter": | |
| fig = px.scatter(df, x='sentence_id', y='confidence', color='sentiment', | |
| color_discrete_map=color_map, size='confidence', | |
| labels={'confidence': 'Confidence Score', 'sentence_id': 'Item'}) | |
| else: # box plot | |
| fig = px.box(df, x='sentiment', y='confidence', color='sentiment', | |
| color_discrete_map=color_map, | |
| labels={'confidence': 'Confidence Score'}) | |
| fig.update_layout(showlegend=True) | |
| return fig | |
| with ui.card(full_screen=True): | |
| with ui.card_header(class_="d-flex justify-content-between align-items-center"): | |
| "π Detailed Analysis Results" | |
| with ui.popover(title="Filter Options"): | |
| ICONS["ellipsis"] | |
| ui.input_checkbox_group( | |
| "sentiment_filter", | |
| "Show sentiments:", | |
| ["POSITIVE", "NEGATIVE", "NEUTRAL"], | |
| selected=["POSITIVE", "NEGATIVE", "NEUTRAL"], | |
| inline=True | |
| ) | |
| def detailed_results(): | |
| data = analysis_data() | |
| if data is None: | |
| return ui.div( | |
| ui.div( | |
| ui.h5("π― Ready for Analysis", class_="text-center text-muted"), | |
| ui.p("Configure your input method and click 'Analyze Sentiment' to get started!", | |
| class_="text-center text-muted"), | |
| ui.div("πππ", class_="text-center", style="font-size: 3em; opacity: 0.3;"), | |
| class_="py-5" | |
| ) | |
| ) | |
| # Filter by selected sentiments | |
| filtered_data = [r for r in data if r['sentiment'] in input.sentiment_filter()] | |
| if not filtered_data: | |
| return ui.div( | |
| ui.p("No results match the selected filters.", class_="text-center text-muted py-3") | |
| ) | |
| items = [] | |
| for i, result in enumerate(filtered_data): | |
| sentiment = result['sentiment'] | |
| confidence = result['confidence'] | |
| text = result['text'] | |
| # Style based on sentiment | |
| if sentiment == 'POSITIVE': | |
| icon = "π" | |
| badge_class = "bg-success" | |
| border_class = "border-success" | |
| elif sentiment == 'NEGATIVE': | |
| icon = "π" | |
| badge_class = "bg-danger" | |
| border_class = "border-danger" | |
| else: | |
| icon = "π" | |
| badge_class = "bg-secondary" | |
| border_class = "border-secondary" | |
| items.append( | |
| ui.div( | |
| ui.div( | |
| ui.div( | |
| f"{icon} {sentiment}", | |
| class_="fw-bold" | |
| ), | |
| ui.span( | |
| f"{confidence:.3f}", | |
| class_=f"badge {badge_class}" | |
| ), | |
| class_="d-flex justify-content-between align-items-center mb-2" | |
| ), | |
| ui.p(text, class_="mb-0 text-dark"), | |
| class_=f"border {border_class} rounded p-3 mb-3 bg-light" | |
| ) | |
| ) | |
| return ui.div(*items) | |
| # CSS styling | |
| ui.tags.style(""" | |
| .value-box { border-radius: 12px; } | |
| .card { border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } | |
| .sidebar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } | |
| .sidebar .form-control, .sidebar .form-select { border-radius: 8px; } | |
| .btn { border-radius: 8px; font-weight: 500; } | |
| .bg-light { background: #f8fafc !important; } | |
| """) | |
| # Reactive calculations and effects | |
| analysis_results = reactive.Value(None) | |
| def get_sample_text(): | |
| """Get sample reviews based on user selection""" | |
| count = input.sample_count() if hasattr(input, 'sample_count') else 5 | |
| return " ".join(sample_reviews[:count]) | |
| def fetch_url_content(url): | |
| """Simulate fetching content from URL""" | |
| return f"""Product review analysis from {url}: | |
| Amazing product quality! Really impressed with the craftsmanship and attention to detail. | |
| The shipping was incredibly fast and packaging was perfect. | |
| However, the price point is quite high compared to competitors. | |
| Customer service team was very helpful when I had questions. | |
| Overall, I'm very satisfied and would recommend this to others. | |
| The only downside is the limited color options available. | |
| Great experience overall, will definitely purchase again!""" | |
| def analyze_sentiment(text): | |
| """Perform sentiment analysis on text""" | |
| try: | |
| # Split into sentences | |
| sentences = re.split(r'[.!?]+', text) | |
| sentences = [s.strip() for s in sentences if s.strip() and len(s.strip()) > 10] | |
| if not sentences: | |
| return None | |
| # Get analyzer | |
| analyzer = sentiment_analyzer() | |
| results = [] | |
| for i, sentence in enumerate(sentences): | |
| try: | |
| # Analyze sentiment | |
| sentiment_scores = analyzer(sentence)[0] if isinstance(analyzer(sentence), list) else analyzer(sentence) | |
| # Get best prediction | |
| if isinstance(sentiment_scores, list): | |
| best_pred = max(sentiment_scores, key=lambda x: x['score']) | |
| else: | |
| best_pred = sentiment_scores | |
| # Normalize label | |
| label = best_pred['label'].upper() | |
| if 'POS' in label or 'POSITIVE' in label: | |
| label = 'POSITIVE' | |
| elif 'NEG' in label or 'NEGATIVE' in label: | |
| label = 'NEGATIVE' | |
| else: | |
| label = 'NEUTRAL' | |
| results.append({ | |
| 'sentence_id': i + 1, | |
| 'text': sentence, | |
| 'sentiment': label, | |
| 'confidence': best_pred['score'] | |
| }) | |
| except Exception as e: | |
| # Fallback for problematic sentences | |
| results.append({ | |
| 'sentence_id': i + 1, | |
| 'text': sentence, | |
| 'sentiment': 'NEUTRAL', | |
| 'confidence': 0.5 | |
| }) | |
| return results | |
| except Exception as e: | |
| print(f"Analysis error: {e}") | |
| return None | |
| def analysis_data(): | |
| return analysis_results() | |
| def perform_analysis(): | |
| # Get input text based on method | |
| if input.input_type() == "text": | |
| text = input.text_input() if hasattr(input, 'text_input') else "" | |
| elif input.input_type() == "url": | |
| url = input.url_input() if hasattr(input, 'url_input') else "" | |
| text = fetch_url_content(url) if url else "" | |
| else: # sample | |
| text = get_sample_text() | |
| if not text.strip(): | |
| return | |
| # Perform analysis | |
| results = analyze_sentiment(text) | |
| analysis_results.set(results) | |
| def reset_analysis(): | |
| analysis_results.set(None) | |
| ui.update_text_area("text_input", value="") | |
| ui.update_text("url_input", value="") | |
| ui.update_numeric("sample_count", value=5) | |
| ui.update_checkbox_group("sentiment_filter", selected=["POSITIVE", "NEGATIVE", "NEUTRAL"]) |