import streamlit as st import requests import feedparser from datetime import datetime from openai import OpenAI # The OpenAI library is used, but configured for OpenRouter from openai import APIStatusError, RateLimitError, AuthenticationError, APIConnectionError from deep_translator import GoogleTranslator from bs4 import BeautifulSoup from dotenv import load_dotenv # Import for loading environment variables import os # Import for accessing environment variables # --- CONFIG ------------------------------------------------------------------ # Load environment variables from .env file load_dotenv() # --- IMPORTANT: HOW TO SET UP YOUR YOUR OPENROUTER API KEY --- # 1. Create a file named `.env` in the same directory as your Python script. # 2. Add the following line to the `.env` file: # OPENROUTER_API_KEY="YOUR_OPENROUTER_API_KEY_HERE" # 3. Replace "YOUR_OPENROUTER_API_KEY_HERE" with your actual API key from OpenRouter. # 4. Make sure to keep your `.env` file out of version control (e.g., add it to .gitignore). # -------------------------------------------------------- LANGUAGES = { "en": {"en": "English", "hi": "Hindi", "te": "Telugu"}, "hi": {"en": "अंग्रेज़ी", "hi": "हिंदी", "te": "तेలుగు"}, "te": {"en": "ఆంగ్లం", "hi": "హిందీ", "te": "తెలుగు"}, } translations = { "en": { "title": "🕰️ Timescope", "description": "Get today's headlines and explore history — all in one place.", "on_this_day": "📅 On This Day in History", "pick_date": "Pick a date", "language": "🌐 Language", "events": "Events:", "no_events": "No events found for this date.", "todays_headlines": "📢 Today's Headlines by Category", "couldnt_load_news": "❌ Couldn’t load news. Please check your internet.", "refresh_news": "🔄 Refresh News", "ask_ai": "🤖 Ask Timescope AI", "ask_placeholder": "Ask anything about history, world news, or events:", "answer": "**Answer:** ", "ai_error": "❌ AI Assistant Error:", # Changed to be more general for AI "unsupported_onthisday": "Sorry, 'On This Day' is not available in this language.", "read_more": "Read more" }, "hi": { "title": "🕰️ टाइमस्कोप", "description": "आज की सुर्खियाँ और इतिहास एक ही स्थान पर देखें।", "on_this_day": "📅 आज के दिन का इतिहास", "pick_date": "तारीख चुनें", "language": "🌐 भाषा", "events": "घटनाएँ:", "no_events": "इस तारीख के लिए कोई घटना नहीं मिली।", "todays_headlines": "📢 आज की प्रमुख खबरें (श्रेणी अनुसार)", "couldnt_load_news": "❌ खबरें लोड नहीं हो सकीं। कृपया अपना इंटरनेट जांचें।", "refresh_news": "🔄 खबरें रीफ़्रेश करें", "ask_ai": "🤖 टाइमस्कोप एआई से पूछें", "ask_placeholder": "इतिहास, विश्व समाचार या घटनाओं के बारे में कुछ भी पूछें:", "answer": "**उत्तर:** ", "ai_error": "❌ AI सहायक त्रुटि:", # Changed "unsupported_onthisday": "क्षमा करें, 'आज के दिन' इस भाषा में उपलब्ध नहीं है।", "read_more": "और पढ़ें" }, "te": { "title": "🕰️ టైమ్‌స్కోప్", "description": "ఈ రోజు ముఖ్యాంశాలు మరియు చరిత్రను ఒకే చోట అన్వేషించండి.", "on_this_day": "📅 ఈ రోజు చరిత్రలో", "pick_date": "తేదీ ఎంచుకోండి", "language": "🌐 భాష", "events": "ఈవెంట్స్:", "no_events": "ఈ తేదీకి ఈవెంట్స్ లభించలేదు.", "todays_headlines": "📢 ఈ రోజు ముఖ్యాంశాలు (వర్గం ద్వారా)", "couldnt_load_news": "❌ వార్తలు లోడ్ కాలేకపోయాయి. దయచేసి మీ ఇంటర్నెట్‌ను తనిఖీ చేయండి.", "refresh_news": "🔄 వార్తలను రీఫ్రెష్ చేయండి", "ask_ai": "🤖 టైమ్‌స్కోప్ AI ను అడగండి", "ask_placeholder": "చరిత్ర, ప్రపంచ వార్తలు లేదా ఈవెంట్స్ గురించి ఏదైనా అడగండి:", "answer": "**సమాధానం:** ", "ai_error": "❌ AI సహాయ లోపం:", # Changed "unsupported_onthisday": "క్షమించండి, 'ఈ రోజు చరిత్రలో' ఈ భాషలో అందుబాటులో లేదు.", "read_more": "ఇంకా చదవండి" } } CATEGORY_FEEDS = { "World": "http://feeds.bbci.co.uk/news/world/rss.xml", "Technology": "http://feeds.bbci.co.uk/news/technology/rss.xml", "Business": "http://feeds.bbci.co.uk/news/business/rss.xml", "Politics": "http://feeds.bbci.co.uk/news/politics/rss.xml", "Health": "http://feeds.bbci.co.uk/news/health/rss.xml", "Science": "http://feeds.bbci.co.uk/news/science_and_environment/rss.xml", "Entertainment": "http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml", "Sports": "https://feeds.bbci.co.uk/sport/rss.xml" } # --- OpenAI/OpenRouter Client Setup ------------------------------------------ openrouter_api_key = "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" # Load from .env DEFAULT_AI_MODEL = "mistralai/mistral-7b-instruct" # Recommended general-purpose model for OpenRouter # --- Initialize OpenAI client for OpenRouter if not openrouter_api_key: st.warning("OpenRouter API Key not found! AI Assistant functionality will be limited.") # Initialize with a dummy key and base_url so the app doesn't crash if key is missing. # The actual error for missing key will be caught in the try-except block for AI calls. client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key="sk-dummy-key-for-no-ai" ) else: client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=openrouter_api_key ) # --- Streamlit Page Configuration -------------------------------------------- st.set_page_config("Timescope", layout="wide") # Initialize session state for chat history and language if 'chat_history' not in st.session_state: st.session_state['chat_history'] = [] if 'lang_code' not in st.session_state: st.session_state['lang_code'] = 'en' # --- Custom CSS for fixed AI Assistant and general styling ------------------- st.markdown(""" """, unsafe_allow_html=True) # --- Language selection in sidebar --- lang_code = st.session_state.get('lang_code', 'en') # Ensure lang_code is defined before use lang_options = [LANGUAGES[lang_code][code] for code in LANGUAGES[lang_code]] lang_codes = list(LANGUAGES[lang_code].keys()) t = translations[lang_code] # Define t before using it selected_lang_display = st.sidebar.selectbox( t['language'], # Use translated label lang_options, index=lang_codes.index(lang_code), key="language_select" ) selected_lang_code = lang_codes[lang_options.index(selected_lang_display)] if selected_lang_code != lang_code: st.session_state['lang_code'] = selected_lang_code st.rerun() lang_code = st.session_state['lang_code'] t = translations[lang_code] # Update translator after potential language change # --- On This Day Feature ----------------------------------------------------- st.sidebar.header(t['on_this_day']) selected_date = st.sidebar.date_input(t['pick_date'], datetime.today()) @st.cache_data(ttl=3600) # Cache for 1 hour def fetch_on_this_day(month, day, lang_code_for_wiki): url = f"https://{lang_code_for_wiki}.wikipedia.org/api/rest_v1/feed/onthisday/events/{month}/{day}" try: res = requests.get(url, timeout=5) if res.status_code == 404: # Wikipedia API returns 404 if language is not supported for OnThisDay return None res.raise_for_status() return res.json().get("events", []) except requests.exceptions.RequestException as e: st.sidebar.error(f"Error fetching historical events: {e}") return [] except Exception as e: st.sidebar.error(f"An unexpected error occurred while fetching historical events: {e}") return [] supported_onthisday_langs = ['en', 'es', 'fr', 'de', 'ru', 'pt'] # Common languages with OTD pages if lang_code in supported_onthisday_langs: events = fetch_on_this_day(selected_date.month, selected_date.day, lang_code) else: st.sidebar.info(t['unsupported_onthisday']) events = fetch_on_this_day(selected_date.month, selected_date.day, 'en') # Fallback to English if events: st.sidebar.subheader(t['events']) for ev in events[:6]: # Limit to 6 events for brevity event_text = ev['text'] # Only translate if the fetched language was English and target is different or if fallback to English occurred if lang_code != 'en' and (lang_code not in supported_onthisday_langs or lang_code == 'en'): try: event_text = GoogleTranslator(source='en', target=lang_code).translate(event_text) except Exception as e: pass # Fallback to original text on error st.sidebar.markdown(f"**{ev['year']}** — {event_text}") if ev.get("pages"): p = ev["pages"][0] title = p["titles"]["normalized"] url = p["content_urls"]["desktop"]["page"] # Only translate if the fetched language was English and target is different or if fallback to English occurred if lang_code != 'en' and (lang_code not in supported_onthisday_langs or lang_code == 'en'): try: title = GoogleTranslator(source='en', target=lang_code).translate(title) except Exception as e: pass # Fallback to original title on error st.sidebar.markdown(f"[🔗 {title}]({url})") else: if lang_code in supported_onthisday_langs: # Only show 'no events' if the language is supported st.sidebar.info(t['no_events']) # --- Main App Title and Description ------------------------------------------ st.title(t['title']) st.markdown(t['description']) st.markdown("---") # --- News Fetching and Display Functions ------------------------------------- @st.cache_data(ttl=600) # Cache for 10 minutes def fetch_rss_entries(url): headers = {'User-Agent': 'Mozilla/5.0'} try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() feed = feedparser.parse(resp.content) return feed.entries if feed.entries else None except Exception as e: print("RSS error:", e) # For debugging in console return None def translate_text(text, target_lang): if target_lang == 'en': return text try: return GoogleTranslator(source='auto', target=target_lang).translate(text) # Auto-detect source language except Exception: return text def get_image_from_entry(entry): # Try media:content first if hasattr(entry, "media_content"): for media in entry.media_content: if "url" in media: return media["url"] # Fallback to looking in links with image type if hasattr(entry, "links"): for link in entry.links: if hasattr(link, "type") and "image" in link.type: return link.href return None def clean_html(raw_html): if not raw_html: return "" soup = BeautifulSoup(raw_html, "html.parser") return soup.get_text(separator=" ", strip=True) def display_news_cards_in_two_columns(news_items): col1, col2 = st.columns(2) card_style = ( "background-color: #add8e6; border-radius: 10px; padding: 10px; " "margin-bottom: 10px; color: #002244; font-family: sans-serif;" ) for i, item in enumerate(news_items): with col1 if i % 2 == 0 else col2: content = "" if item["image"]: content += f'' content += f"""

{item['title']}

""" if item["summary"]: content += f"""
{item["summary"]}
""" st.markdown( f"
{content}
", unsafe_allow_html=True ) st.markdown( f"[{t['read_more']}]({item['url']})", unsafe_allow_html=False ) # --- News Display Section ---------------------------------------------------- st.header(t['todays_headlines']) tabs = st.tabs(list(CATEGORY_FEEDS.keys())) for tab, (cat, feed_url) in zip(tabs, CATEGORY_FEEDS.items()): with tab: st.subheader(cat) entries = fetch_rss_entries(feed_url) if not entries: st.error(t['couldnt_load_news']) else: news_items = [] for e in entries[:8]: # Limit to 8 articles per category title = translate_text(e.title, lang_code) summary = getattr(e, "summary", getattr(e, "description", "")) summary = clean_html(summary) summary = translate_text(summary, lang_code) if summary else "" image_url = get_image_from_entry(e) news_items.append({ "title": title, "summary": summary, "image": image_url, "url": e.link }) display_news_cards_in_two_columns(news_items) # Refresh button if st.button(t['refresh_news']): st.cache_data.clear() # Clear all caches including RSS feeds st.session_state['chat_history'] = [] # Clear AI chat history too st.rerun() # --- AI Assistant Section ---------------------------------------------------- # Fixed position container for the AI assistant st.markdown("
", unsafe_allow_html=True) st.header(t['ask_ai']) # Display chat history with st.container(): st.markdown("
", unsafe_allow_html=True) if not st.session_state.chat_history: st.markdown("Say hello! I'm your Timescope Assistant, ready to help you explore news and history.") for msg in st.session_state.chat_history: role_label = "🧑‍💻 You" if msg["role"] == "user" else "🤖 Assistant" st.markdown(f"**{role_label}:** {translate_text(msg['content'], lang_code)}") # Translate history too st.markdown("
", unsafe_allow_html=True) # Input form for the AI assistant with st.form("chat_form", clear_on_submit=True): user_input = st.text_input(t['ask_placeholder'], key="assistant_input", label_visibility="collapsed", placeholder=t['ask_placeholder']) # Use translated placeholder send_button = st.form_submit_button("Send") if send_button and user_input: # Add user message to history st.session_state.chat_history.append({"role": "user", "content": user_input}) with st.spinner("Thinking..."): try: # Check if API key is truly available before making the call if not openrouter_api_key or openrouter_api_key == "sk-dummy-key-for-no-ai": st.error(f"{t['ai_error']} OpenRouter API Key is not set. Please add it to your `.env` file.") else: # Build the messages list first messages = [ {"role": "system", "content": translate_text('You are a helpful assistant about world news and history.', lang_code)} ] + [ {"role": m["role"], "content": m["content"]} for m in st.session_state.chat_history ] resp = client.chat.completions.create( model=DEFAULT_AI_MODEL, # Use the OpenRouter model messages=messages, max_tokens=1000, # Max tokens to control response length and cost ) assistant_response = resp.choices[0].message.content.strip() st.session_state.chat_history.append({"role": "assistant", "content": assistant_response}) # Store original response # No need to display directly here, it will be rerendered by st.markdown in history display st.rerun() # Rerun to update chat history display except Exception as e: if isinstance(e, RateLimitError): st.error(f"{t['ai_error']} You've hit a rate limit. Please wait a moment and try again.") st.error(f"{t['ai_error']} You've hit a rate limit. Please wait a moment and try again.") elif isinstance(e, AuthenticationError): st.error(f"{t['ai_error']} Authentication failed. Please check your OpenRouter API key in your `.env` file.") elif isinstance(e, APIConnectionError): st.error(f"{t['ai_error']} Could not connect to the AI service. Please check your internet connection or the OpenRouter API status.") elif isinstance(e, APIStatusError): # This handles the 402 error for credits as well as other API errors st.error(f"{t['ai_error']} An API error occurred: {e.status_code} - {e.response.text if hasattr(e.response, 'text') else str(e)}") else: st.error(f"{t['ai_error']} An unexpected error occurred: {e}") st.session_state.chat_history.append({"role": "assistant", "content": f"Error: {e}"}) # Add error to history st.rerun() # Rerun to display error in history st.markdown("
", unsafe_allow_html=True) # Close fixed-assistant-container