| """News display components for the financial dashboard.""" |
|
|
| import streamlit as st |
| import pandas as pd |
| from datetime import datetime |
| import html as html_module |
|
|
|
|
| def display_news_card(news_item: dict): |
| """Display a single news card with professional styling using Streamlit components.""" |
|
|
| |
| time_diff = datetime.now() - news_item['timestamp'] |
| if time_diff.seconds < 60: |
| time_ago = f"{time_diff.seconds}s ago" |
| elif time_diff.seconds < 3600: |
| time_ago = f"{time_diff.seconds // 60}m ago" |
| else: |
| time_ago = f"{time_diff.seconds // 3600}h ago" |
|
|
| |
| with st.container(): |
| |
| st.markdown(""" |
| <style> |
| .news-card { |
| background: linear-gradient(135deg, #1f2937 0%, #111827 100%); |
| border: 1px solid #374151; |
| border-radius: 12px; |
| padding: 20px; |
| margin-bottom: 16px; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| col1, col2 = st.columns([3, 1]) |
|
|
| with col1: |
| |
| badge_cols = st.columns([2, 1, 1, 1]) |
| with badge_cols[0]: |
| st.markdown(f"**:blue[{news_item['source']}]**") |
| with badge_cols[1]: |
| if news_item['impact'] == 'high': |
| st.markdown("π΄ **HIGH**") |
| elif news_item['impact'] == 'medium': |
| st.markdown("π‘ **MED**") |
| else: |
| st.markdown("π’ **LOW**") |
| with badge_cols[2]: |
| sentiment_emoji = {'positive': 'π', 'negative': 'π', 'neutral': 'β‘οΈ'} |
| st.markdown(f"{sentiment_emoji.get(news_item['sentiment'], 'β‘οΈ')} {news_item['sentiment'].title()}") |
| with badge_cols[3]: |
| st.markdown(f"**#{news_item['category']}**") |
|
|
| with col2: |
| st.markdown(f"[**Read More β**]({news_item['url']})") |
|
|
| |
| st.markdown(f"### {news_item.get('summary', '').strip()}") |
|
|
| |
| st.caption(f"π {time_ago}") |
|
|
| st.markdown("---") |
|
|
|
|
| def display_news_feed(df: pd.DataFrame, max_items: int = 20): |
| """Display a feed of news items.""" |
|
|
| if df.empty: |
| st.info("π No news available. Adjust your filters or refresh the feed.") |
| return |
|
|
| |
| st.markdown(""" |
| <style> |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.6; } |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| for idx, row in df.head(max_items).iterrows(): |
| display_news_card(row.to_dict()) |
|
|
|
|
| def display_news_statistics(stats: dict): |
| """Display news feed statistics in metric cards.""" |
|
|
| col1, col2, col3, col4 = st.columns(4) |
|
|
| with col1: |
| st.metric( |
| "Total Stories", |
| f"{stats['total']}", |
| help="Total news items in feed" |
| ) |
|
|
| with col2: |
| st.metric( |
| "High Impact", |
| f"{stats['high_impact']}", |
| delta=f"{(stats['high_impact']/max(stats['total'], 1)*100):.0f}%", |
| help="High-impact market-moving news" |
| ) |
|
|
| with col3: |
| st.metric( |
| "Breaking News", |
| f"{stats['breaking']}", |
| delta="LIVE" if stats['breaking'] > 0 else None, |
| help="Breaking news alerts" |
| ) |
|
|
| with col4: |
| st.metric( |
| "Last Update", |
| stats['last_update'], |
| help="Time of last news fetch" |
| ) |
|
|
|
|
| def display_category_breakdown(stats: dict): |
| """Display news breakdown by category using Streamlit components.""" |
|
|
| if 'by_category' not in stats: |
| return |
|
|
| st.markdown("### π News by Category") |
|
|
| categories = stats['by_category'] |
| total = sum(categories.values()) |
|
|
| if total == 0: |
| st.info("No categorized news available") |
| return |
|
|
| col1, col2, col3 = st.columns(3) |
|
|
| with col1: |
| macro_count = categories.get('macro', 0) |
| macro_pct = (macro_count / total) * 100 |
| with st.container(): |
| st.markdown("**:blue[π MACRO]**") |
| st.markdown(f"# {macro_count}") |
| st.caption(f"{macro_pct:.1f}% of total") |
|
|
| with col2: |
| geo_count = categories.get('geopolitical', 0) |
| geo_pct = (geo_count / total) * 100 |
| with st.container(): |
| st.markdown("**:orange[π GEOPOLITICAL]**") |
| st.markdown(f"# {geo_count}") |
| st.caption(f"{geo_pct:.1f}% of total") |
|
|
| with col3: |
| markets_count = categories.get('markets', 0) |
| markets_pct = (markets_count / total) * 100 |
| with st.container(): |
| st.markdown("**:green[πΉ MARKETS]**") |
| st.markdown(f"# {markets_count}") |
| st.caption(f"{markets_pct:.1f}% of total") |
|
|
|
|
| def display_breaking_news_banner(df: pd.DataFrame): |
| """Display breaking news banner at the top using Streamlit components.""" |
|
|
| breaking = df[df['is_breaking'] == True] if not df.empty and 'is_breaking' in df.columns else pd.DataFrame() |
|
|
| if not breaking.empty: |
| latest = breaking.iloc[0] |
|
|
| |
| with st.container(): |
| st.markdown(""" |
| <style> |
| .breaking-news-container { |
| background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); |
| border-radius: 12px; |
| padding: 20px; |
| margin-bottom: 24px; |
| border: 2px solid #fca5a5; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| col1, col2, col3 = st.columns([1, 8, 2]) |
|
|
| with col1: |
| st.markdown("# π¨") |
|
|
| with col2: |
| st.markdown(f"**:red[BREAKING NEWS β’ {latest['source'].upper()}]**") |
| st.markdown(f"## {latest.get('summary', '').strip()}") |
|
|
| with col3: |
| st.markdown(f"[**READ NOW β**]({latest['url']})") |
|
|