social-eq / app.py
DarleisonRodrigues's picture
Update app.py
72a738a verified
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)
@reactive.calc
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"
)
@render.ui
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"
@render.express
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"
@render.express
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"
@render.express
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"
@render.express
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")
@render_plotly
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
)
@render_plotly
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
)
@render.ui
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
@reactive.calc
def analysis_data():
return analysis_results()
@reactive.effect
@reactive.event(input.analyze_btn)
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)
@reactive.effect
@reactive.event(input.reset_btn)
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"])