| """ |
| Content display components for sentiment visualization |
| Creates formatted cards and displays for content and comments |
| """ |
| import streamlit as st |
| import pandas as pd |
| from datetime import datetime |
|
|
|
|
| class ContentCards: |
| """ |
| Creates content display components |
| """ |
|
|
| @staticmethod |
| def display_content_card(content_row, rank=None): |
| """ |
| Display a formatted content card |
| |
| Args: |
| content_row: Series containing content information |
| rank: Optional rank number to display |
| """ |
| with st.container(): |
| |
| col1, col2 = st.columns([3, 1]) |
|
|
| with col1: |
| |
| if rank: |
| st.markdown(f"### π’ #{rank} - Content") |
| else: |
| st.markdown("### π Content") |
|
|
| |
| description = content_row.get('content_description', 'No description available') |
| if pd.notna(description) and description: |
| st.markdown(f"**Description:** {description[:200]}..." if len(str(description)) > 200 else f"**Description:** {description}") |
| else: |
| st.markdown("**Description:** *No description available*") |
|
|
| |
| if 'permalink_url' in content_row and pd.notna(content_row['permalink_url']): |
| st.markdown(f"π [View Content]({content_row['permalink_url']})") |
|
|
| with col2: |
| |
| if 'thumbnail_url' in content_row and pd.notna(content_row['thumbnail_url']): |
| try: |
| st.image(content_row['thumbnail_url'], use_container_width=True) |
| except Exception as e: |
| |
| st.markdown("*πΌοΈ Thumbnail unavailable*") |
|
|
| |
| st.metric("Total Comments", int(content_row.get('total_comments', 0))) |
|
|
| if 'negative_percentage' in content_row: |
| neg_pct = content_row['negative_percentage'] |
| st.metric( |
| "Negative %", |
| f"{neg_pct:.1f}%", |
| delta=None, |
| delta_color="inverse" |
| ) |
|
|
| if 'reply_required_count' in content_row: |
| st.metric("Replies Needed", int(content_row['reply_required_count'])) |
|
|
| |
| with st.expander("π View Detailed Statistics"): |
| detail_col1, detail_col2, detail_col3 = st.columns(3) |
|
|
| with detail_col1: |
| st.write("**Content ID:**", content_row.get('content_sk', 'N/A')) |
| if 'dominant_sentiment' in content_row: |
| st.write("**Dominant Sentiment:**", content_row['dominant_sentiment'].title()) |
|
|
| with detail_col2: |
| if 'negative_count' in content_row: |
| st.write("**Negative Count:**", int(content_row['negative_count'])) |
|
|
| with detail_col3: |
| if 'total_comments' in content_row: |
| positive_count = int(content_row['total_comments']) - int(content_row.get('negative_count', 0)) |
| st.write("**Positive/Neutral:**", positive_count) |
|
|
| st.markdown("---") |
|
|
| @staticmethod |
| def display_comment_card(comment_row, show_original=False): |
| """ |
| Display a formatted comment card |
| |
| Args: |
| comment_row: Series containing comment information |
| show_original: Whether to show original text for translated comments |
| """ |
| with st.container(): |
| |
| col1, col2, col3 = st.columns([2, 1, 1]) |
|
|
| with col1: |
| author = comment_row.get('author_name', 'Unknown') |
| st.markdown(f"**π€ {author}**") |
|
|
| with col2: |
| if 'comment_timestamp' in comment_row and pd.notna(comment_row['comment_timestamp']): |
| timestamp = pd.to_datetime(comment_row['comment_timestamp']) |
| st.markdown(f"*π
{timestamp.strftime('%Y-%m-%d %H:%M')}*") |
|
|
| with col3: |
| platform = comment_row.get('platform', 'unknown') |
| st.markdown(f"*π {platform.title()}*") |
|
|
| |
| display_text = comment_row.get('display_text', comment_row.get('original_text', 'No text available')) |
| st.markdown(f"π¬ {display_text}") |
|
|
| |
| badge_col1, badge_col2, badge_col3 = st.columns([2, 2, 1]) |
|
|
| with badge_col1: |
| sentiment = comment_row.get('sentiment_polarity', 'unknown') |
| sentiment_emoji = { |
| 'very_positive': 'π', |
| 'positive': 'π', |
| 'neutral': 'π', |
| 'negative': 'π', |
| 'very_negative': 'π ' |
| }.get(sentiment, 'β') |
| st.markdown(f"**Sentiment:** {sentiment_emoji} {sentiment.replace('_', ' ').title()}") |
|
|
| with badge_col2: |
| intent = comment_row.get('intent', 'unknown') |
| st.markdown(f"**Intent:** {intent}") |
|
|
| with badge_col3: |
| if comment_row.get('requires_reply', False): |
| st.markdown("**β οΈ Reply Required**") |
|
|
| |
| if show_original and comment_row.get('is_english') == False: |
| with st.expander("π View Original Text"): |
| original_text = comment_row.get('original_text', 'Not available') |
| detected_lang = comment_row.get('detected_language', 'Unknown') |
| st.markdown(f"**Language:** {detected_lang}") |
| st.markdown(f"**Original:** {original_text}") |
|
|
| |
| with st.expander("βΉοΈ More Details"): |
| detail_col1, detail_col2 = st.columns(2) |
|
|
| with detail_col1: |
| st.write("**Comment ID:**", comment_row.get('comment_id', 'N/A')) |
| st.write("**Channel:**", comment_row.get('channel_name', 'N/A')) |
| st.write("**Confidence:**", comment_row.get('sentiment_confidence', 'N/A')) |
|
|
| with detail_col2: |
| if 'content_description' in comment_row and pd.notna(comment_row['content_description']): |
| content_desc = comment_row['content_description'] |
| st.write("**Content:**", content_desc[:50] + "..." if len(str(content_desc)) > 50 else content_desc) |
| if 'permalink_url' in comment_row and pd.notna(comment_row['permalink_url']): |
| st.markdown(f"[View Content]({comment_row['permalink_url']})") |
|
|
| st.markdown("---") |
|
|
| @staticmethod |
| def display_metric_cards(metrics_dict): |
| """ |
| Display a row of metric cards |
| |
| Args: |
| metrics_dict: Dictionary of metrics {label: value} |
| """ |
| cols = st.columns(len(metrics_dict)) |
|
|
| for idx, (label, value) in enumerate(metrics_dict.items()): |
| with cols[idx]: |
| if isinstance(value, dict) and 'value' in value: |
| |
| st.metric( |
| label, |
| value['value'], |
| delta=value.get('delta'), |
| delta_color=value.get('delta_color', 'normal') |
| ) |
| else: |
| |
| st.metric(label, value) |
|
|
| @staticmethod |
| def display_summary_stats(df): |
| """ |
| Display summary statistics in a formatted layout |
| |
| Args: |
| df: Sentiment dataframe |
| """ |
| st.markdown("### π Summary Statistics") |
|
|
| col1, col2, col3, col4 = st.columns(4) |
|
|
| with col1: |
| st.metric("Total Comments", len(df)) |
|
|
| with col2: |
| unique_contents = df['content_sk'].nunique() if 'content_sk' in df.columns else 0 |
| st.metric("Unique Contents", unique_contents) |
|
|
| with col3: |
| reply_required = df['requires_reply'].sum() if 'requires_reply' in df.columns else 0 |
| st.metric("Replies Needed", int(reply_required)) |
|
|
| with col4: |
| negative_sentiments = ['negative', 'very_negative'] |
| negative_count = df['sentiment_polarity'].isin(negative_sentiments).sum() |
| negative_pct = (negative_count / len(df) * 100) if len(df) > 0 else 0 |
| st.metric("Negative %", f"{negative_pct:.1f}%") |
|
|
| @staticmethod |
| def display_filter_summary(applied_filters): |
| """ |
| Display summary of applied filters |
| |
| Args: |
| applied_filters: Dictionary of applied filters |
| """ |
| if not any(applied_filters.values()): |
| return |
|
|
| st.markdown("### π Applied Filters") |
|
|
| filter_text = [] |
| for filter_name, filter_value in applied_filters.items(): |
| if filter_value and len(filter_value) > 0: |
| filter_text.append(f"**{filter_name.title()}:** {', '.join(map(str, filter_value))}") |
|
|
| if filter_text: |
| st.info(" | ".join(filter_text)) |
|
|
| @staticmethod |
| def display_health_indicator(negative_pct): |
| """ |
| Display sentiment health indicator |
| |
| Args: |
| negative_pct: Percentage of negative sentiments |
| """ |
| if negative_pct < 10: |
| status = "Excellent" |
| color = "green" |
| emoji = "β
" |
| elif negative_pct < 20: |
| status = "Good" |
| color = "lightgreen" |
| emoji = "π" |
| elif negative_pct < 30: |
| status = "Fair" |
| color = "orange" |
| emoji = "β οΈ" |
| elif negative_pct < 50: |
| status = "Poor" |
| color = "darkorange" |
| emoji = "β‘" |
| else: |
| status = "Critical" |
| color = "red" |
| emoji = "π¨" |
|
|
| st.markdown( |
| f""" |
| <div style='padding: 10px; border-radius: 5px; background-color: {color}; color: white; text-align: center;'> |
| <h3>{emoji} Sentiment Health: {status}</h3> |
| <p>Negative Sentiment: {negative_pct:.1f}%</p> |
| </div> |
| """, |
| unsafe_allow_html=True |
| ) |
|
|
| @staticmethod |
| def display_pagination_controls(total_items, items_per_page, current_page): |
| """ |
| Display pagination controls |
| |
| Args: |
| total_items: Total number of items |
| items_per_page: Number of items per page |
| current_page: Current page number |
| |
| Returns: |
| int: New current page |
| """ |
| total_pages = (total_items - 1) // items_per_page + 1 |
|
|
| col1, col2, col3 = st.columns([1, 2, 1]) |
|
|
| with col1: |
| if st.button("β¬
οΈ Previous", disabled=(current_page <= 1)): |
| current_page -= 1 |
|
|
| with col2: |
| st.markdown(f"<center>Page {current_page} of {total_pages}</center>", unsafe_allow_html=True) |
|
|
| with col3: |
| if st.button("Next β‘οΈ", disabled=(current_page >= total_pages)): |
| current_page += 1 |
|
|
| return current_page |