| | """ |
| | 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 |