Spaces:
Build error
Build error
| import os | |
| import firebase_admin | |
| from firebase_admin import credentials, firestore | |
| from dotenv import load_dotenv, find_dotenv | |
| from datetime import date | |
| import logging | |
| import json | |
| import streamlit as st | |
| # Set up logging configuration. | |
| logging.basicConfig( | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| level=logging.INFO | |
| ) | |
| # --------------- Firebase Initialization Functions --------------- | |
| def initialize_environment(): | |
| """ | |
| Loads environment variables from the .env file. | |
| """ | |
| logging.info("Loading ") | |
| dotenv_path = find_dotenv() | |
| load_dotenv(dotenv_path) | |
| logging.info(f"Environment variables loaded from {dotenv_path}") | |
| def initialize_firebase(): | |
| """ | |
| Initializes Firebase using credentials. | |
| Returns a Firestore client. | |
| """ | |
| if not firebase_admin._apps: | |
| data = json.loads(os.getenv("firebase_detailed_cred")) | |
| cred = credentials.Certificate(data) | |
| firebase_admin.initialize_app(cred) | |
| logging.info("Firebase initialized using provided credentials.") | |
| else: | |
| logging.info("Firebase already initialized.") | |
| return firestore.client() | |
| # # --------------- Firestore Query Function --------------- | |
| # def fetch_news_firestore(start_date: str, end_date: str, categories, keyword, db_client): | |
| # """ | |
| # Queries Firestore for news articles in the "ProcessedNews" collection that | |
| # match the provided date range and categories. | |
| # Performs client-side filtering for keywords. | |
| # Limits the results to 100 articles. | |
| # """ | |
| # logging.info(f"Querying Firestore for articles from {start_date} to {end_date}.") | |
| # query = db_client.collection("ProcessedNews") \ | |
| # .where("Date", ">=", start_date) \ | |
| # .where("Date", "<=", end_date) \ | |
| # .limit(100) | |
| # if categories: | |
| # logging.info(f"Filtering by categories: {categories}") | |
| # query = query.where("category", "in", categories) | |
| # docs = query.stream() | |
| # results = [] | |
| # for doc in docs: | |
| # data = doc.to_dict() | |
| # results.append(data) | |
| # logging.info(f"Retrieved {len(results)} articles from Firestore before keyword filtering.") | |
| # if keyword: | |
| # kw = keyword.lower() | |
| # results = [ | |
| # article for article in results | |
| # if kw in article.get("title", "").lower() or kw in article.get("summary", "").lower() | |
| # ] | |
| # logging.info(f"{len(results)} articles remain after keyword filtering with keyword '{keyword}'.") | |
| # return results | |
| from firebase_admin.firestore import FieldFilter | |
| from datetime import datetime | |
| def fetch_news_firestore(start_date: str, end_date: str, categories, keyword, db_client): | |
| """ | |
| Queries Firestore for news articles using correct filter syntax and date handling, | |
| and removes duplicate articles based on the "title" field. | |
| """ | |
| logging.info(f"Querying Firestore for articles from {start_date} to {end_date}.") | |
| # Convert input dates to datetime objects | |
| try: | |
| start_date_dt = datetime.strptime(start_date, '%Y-%m-%d') | |
| end_date_dt = datetime.strptime(end_date, '%Y-%m-%d') | |
| except ValueError as e: | |
| logging.error(f"Invalid date format: {e}. Use YYYY-MM-DD.") | |
| return [] | |
| # Assuming 'Date' is stored as a string in Firestore. Adjust if using Timestamp. | |
| start_date_str = start_date_dt.strftime('%Y-%m-%d') | |
| end_date_str = end_date_dt.strftime('%Y-%m-%d') | |
| # Build query with FieldFilter | |
| query = db_client.collection("ProcessedNews") | |
| query = query.where(filter=FieldFilter("Date", ">=", start_date_str)) | |
| query = query.where(filter=FieldFilter("Date", "<=", end_date_str)) | |
| query = query.limit(100) | |
| if categories: | |
| logging.info(f"Filtering by categories: {categories}") | |
| query = query.where(filter=FieldFilter("category", "in", categories)) | |
| docs = query.stream() | |
| results = [doc.to_dict() for doc in docs] | |
| logging.info(f"Retrieved {len(results)} articles from Firestore before keyword filtering.") | |
| # Keyword filtering remains the same | |
| if keyword: | |
| kw = keyword.lower() | |
| results = [ | |
| article for article in results | |
| if kw in article.get("title", "").lower() or kw in article.get("summary", "").lower() | |
| ] | |
| logging.info(f"{len(results)} articles remain after keyword filtering.") | |
| # Remove duplicate articles based on title: | |
| unique_results = [] | |
| seen_titles = set() | |
| for article in results: | |
| title = article.get("title", "").strip() | |
| # Only add article if title is non-empty and hasn't been seen yet | |
| if title and title not in seen_titles: | |
| seen_titles.add(title) | |
| unique_results.append(article) | |
| logging.info(f"{len(unique_results)} unique articles remain after duplicate removal.") | |
| return unique_results | |
| # --------------- Streamlit UI & Styles --------------- | |
| def local_css(css_text): | |
| st.markdown(f'<style>{css_text}</style>', unsafe_allow_html=True) | |
| light_mode_styles = """ | |
| /* Global Styles for Light Mode */ | |
| body, .stApp { | |
| background-color: #f8f9fa !important; | |
| color: #212529 !important; | |
| font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
| } | |
| /* Overall text decoration */ | |
| h1, h2, h3, h4, h5, h6 { | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.1); | |
| font-weight: 700; | |
| } | |
| .container h2 { | |
| text-align: center; | |
| text-transform: uppercase; | |
| color: #FF7E5F; | |
| border-bottom: 2px solid #FF7E5F; | |
| padding-bottom: 0.5rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| p { | |
| font-size: 1rem; | |
| line-height: 1.6; | |
| margin-bottom: 1rem; | |
| } | |
| a { | |
| text-decoration: underline; | |
| transition: color 0.2s ease; | |
| } | |
| a:hover { | |
| color: #FF7E5F; | |
| } | |
| /* Main Container */ | |
| .container { | |
| max-width: 800px; | |
| margin: 2rem auto; | |
| padding: 0 1rem; | |
| } | |
| /* Hero Section with Brighter Orange */ | |
| .hero { | |
| background: linear-gradient(135deg, rgba(255,140,0,0.9), rgba(255,69,0,0.9)), | |
| url('https://images.unsplash.com/photo-1529257414775-5d6c2509fbe6?fit=crop&w=1600&q=80'); | |
| background-size: cover; | |
| background-position: center; | |
| border-radius: 12px; | |
| padding: 6rem 2rem; | |
| text-align: center; | |
| color: #fff; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.2); | |
| animation: fadeIn 1s ease-in-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .hero h1 { | |
| font-size: 3rem; | |
| margin-bottom: 1.5rem; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .hero p { | |
| font-size: 1.3rem; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| line-height: 1.6; | |
| } | |
| /* News Cards with Enhanced Shadow & Animation */ | |
| .news-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .news-card { | |
| background-color: #fff; | |
| color: #212529; | |
| border-radius: 8px; | |
| border-left: 4px solid #FF7E5F; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
| padding: 1.5rem; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| margin-bottom: 1rem; | |
| } | |
| .news-card:hover { | |
| transform: translateY(-5px) scale(1.03); | |
| box-shadow: 0 8px 16px rgba(255,126,95,0.3); | |
| } | |
| .news-card h2 { | |
| font-size: 1.2rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .news-card .meta { | |
| font-size: 0.9rem; | |
| color: #6c757d; | |
| margin-bottom: 0.8rem; | |
| } | |
| .news-card p { | |
| font-size: 0.95rem; | |
| line-height: 1.4; | |
| } | |
| .news-card a { | |
| color: #007bff; | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| """ | |
| # --------------- Main App --------------- | |
| def main(): | |
| st.set_page_config(page_title="Modern News Feed", layout="centered") | |
| st.sidebar.title("Refine Your Feed") | |
| local_css(light_mode_styles) | |
| # Initialize Firebase and environment variables once. | |
| if "db_client" not in st.session_state: | |
| initialize_environment() | |
| st.session_state.db_client = initialize_firebase() | |
| db_client = st.session_state.db_client | |
| # Sidebar filters. | |
| today_date = date.today() | |
| default_start = today_date.replace(day=1) | |
| default_end = today_date | |
| date_range = st.sidebar.date_input("Select Date Range", [default_start, default_end]) | |
| if len(date_range) != 2: | |
| st.sidebar.error("Please select both a start and an end date.") | |
| return | |
| start_date_obj, end_date_obj = date_range | |
| start_date_str = start_date_obj.strftime("%Y-%m-%d") | |
| end_date_str = end_date_obj.strftime("%Y-%m-%d") | |
| categories_list = ["World", "Sports", "Business", "Sci/Tech", "Politics", "Entertainment","Others"] | |
| selected_categories = st.sidebar.multiselect("Select Categories", options=categories_list) | |
| keyword = st.sidebar.text_input("Search Keywords") | |
| # Two sidebar buttons. | |
| col1, col2 = st.sidebar.columns(2) | |
| with col1: | |
| get_news = st.button("Get News") | |
| with col2: | |
| clear_news = st.button("Clear News") | |
| # When "Get News" is pressed, query Firestore. | |
| if get_news: | |
| logging.info("Get News button pressed.") | |
| st.session_state.news_pressed = True | |
| with st.spinner("Fetching articles..."): | |
| articles = fetch_news_firestore(start_date_str, end_date_str, selected_categories, keyword, db_client) | |
| st.session_state.articles = articles | |
| logging.info(f"{len(articles)} articles stored in session state after filtering.") | |
| # When "Clear News" is pressed, clear the session. | |
| if clear_news: | |
| logging.info("Clear News button pressed. Clearing articles and flags.") | |
| st.session_state.pop("articles", None) | |
| st.session_state.pop("news_pressed", None) | |
| # --------------- MAIN CONTENT --------------- | |
| st.markdown("<div class='container'>", unsafe_allow_html=True) | |
| # If Get News has been pressed, show results or an info message. | |
| if st.session_state.get("news_pressed", False): | |
| articles_to_show = st.session_state.get("articles", []) | |
| if articles_to_show: | |
| st.markdown("## Explore the Stories That Matter") | |
| st.success(f"Found {len(articles_to_show)} articles") | |
| st.markdown("<div class='news-container'>", unsafe_allow_html=True) | |
| for article in articles_to_show: | |
| st.markdown(f""" | |
| <div class="news-card"> | |
| <h2>{article.get("title", "No Title")}</h2> | |
| <div class="meta"> | |
| <strong>Date:</strong> {article.get("Date", "Unknown")} | |
| <strong>Category:</strong> {article.get("category", "Uncategorized")} | |
| </div> | |
| <p>{article.get("summary", "No Summary Available")}</p> | |
| {'<a href="' + article.get("url", "#") + '" target="_blank">Read More</a>' if article.get("url") else ""} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| else: | |
| st.info("No news meet the requirement. Please adjust your filters and try again.") | |
| logging.info("No articles found matching the filters.") | |
| else: | |
| # If Get News hasn't been pressed, show the hero section. | |
| st.markdown(""" | |
| <div class="hero"> | |
| <h1>News Spotlight</h1> | |
| <p>Discover fresh stories and insights that ignite your curiosity. Your journey into groundbreaking news starts here.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |