""" AI Concept Explainer =================== Gradio app that explains concepts at different complexity levels and in multiple languages using OpenAI's GPT-4.1-mini. Features: - 5 complexity levels (age 5 to expert) - 6 languages supported - Real-time streaming responses - Copy to clipboard & download - Session history tracking - Reading time estimates - Keyboard shortcuts """ import os import signal import logging from typing import Generator, Tuple, Dict, List, Optional from datetime import datetime import gradio as gr from openai import OpenAI from dotenv import load_dotenv # ==================== CONFIGURATION ==================== VERSION = "2.0.0" APP_NAME = "AI Concept Explainer" MAX_QUESTION_LENGTH = 500 # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Load environment variables if not os.getenv("SPACE_ID"): # Not in HF Spaces try: load_dotenv('.env') except Exception as e: logger.warning(f"Could not load .env file: {e}") # Initialize OpenAI client openai_api_key = os.getenv("OPENAI_API_KEY") if not openai_api_key: raise ValueError("OPENAI_API_KEY not found. Please set it in your environment variables or .env file.") client = OpenAI( api_key=openai_api_key, timeout=60.0, max_retries=3 ) # Explanation levels EXPLANATION_LEVELS: Dict[int, str] = { 1: "like I'm 5 years old - use simple words and analogies", 2: "like I'm 10 years old - basic concepts with examples", 3: "like a high school student - intermediate level with some technical terms", 4: "like a college student - advanced concepts with detailed explanations", 5: "like an expert in the field - professional level with technical depth", } LANGUAGES: List[str] = ["English", "Russian", "German", "Spanish", "French", "Italian"] # Example questions EXAMPLE_QUESTIONS = [ "Why is the sky blue?", "How does the internet work?", "What is artificial intelligence?" ] # ==================== HELPER FUNCTIONS ==================== def estimate_reading_time(text: str) -> str: """ Estimate reading time for given text. Args: text: The text to estimate reading time for Returns: Formatted reading time string """ words = len(text.split()) # Average reading speed: 200-250 words per minute minutes = words / 225 if minutes < 1: seconds = int(minutes * 60) return f"~{seconds}s read" else: return f"~{int(minutes)}min read" # ==================== MAIN FUNCTIONS ==================== def explain_concept(question: str, level: int, language: str) -> Generator[Tuple[str, str], None, None]: """ Generate an explanation for a given concept with streaming. Args: question: The concept or question to explain level: Complexity level (1-5) language: Language for the explanation Yields: Tuple of (explanation, reading_time) """ if not question.strip(): error_msg = "❌ Please enter a concept to explain." yield (error_msg, "") return if len(question) > MAX_QUESTION_LENGTH: error_msg = f"❌ Question too long. Please limit to {MAX_QUESTION_LENGTH} characters." yield (error_msg, "") return level_desc = EXPLANATION_LEVELS.get(level, "clearly and comprehensively") if language not in LANGUAGES: language = "English" logger.warning(f"Invalid language selected, defaulting to English") system_prompt = f"""You are an expert educator. Explain the given concept {level_desc} in {language}. Guidelines: - Provide a clear, accurate response in 150-250 words - Use **bold** to emphasize key points - Be concise and engaging - Focus on the explanation without introductions or conclusions - Use examples or analogies where helpful""" try: logger.info(f"Generating explanation for: '{question[:50]}...' at level {level} in {language}") stream = client.chat.completions.create( model="gpt-4.1-mini", # Using the correct model name messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": question} ], temperature=0.7, max_tokens=1000, stream=True ) partial = "" for chunk in stream: delta = getattr(chunk.choices[0].delta, "content", None) if delta: partial += delta reading_time = estimate_reading_time(partial) yield (partial, f"📖 {reading_time}") # Log completion if partial: logger.info(f"Successfully generated explanation ({len(partial)} chars)") except Exception as e: error_msg = str(e).lower() error_type = type(e).__name__ logger.error(f"API Error [{error_type}]: {str(e)}") if "connection" in error_msg or "timeout" in error_msg or "ssl" in error_msg: error_display = f"""❌ **Connection Error**: Unable to reach OpenAI API. Please check your internet connection and try again.
Technical details{error_type}: {str(e)[:200]}
""" elif "api key" in error_msg or "authentication" in error_msg or "401" in error_msg: error_display = """❌ **Authentication Error**: Invalid or missing API key. Please contact the administrator.""" elif "rate limit" in error_msg or "429" in error_msg: error_display = """❌ **Rate Limit Exceeded**: Too many requests. Please wait a moment and try again. If you're using this frequently, consider upgrading your API plan.""" elif "model" in error_msg or "404" in error_msg: error_display = f"""❌ **Model Error**: The AI model may not be available. Please try again later or contact support.
Technical details{str(e)}
""" else: error_display = f"""❌ **Unexpected Error**: Something went wrong. Please try again. If the problem persists, contact support.
Technical details[{error_type}] {str(e)}
""" yield (error_display, "") def update_char_count(text: str) -> str: """Update character counter display.""" count = len(text) remaining = MAX_QUESTION_LENGTH - count if count == 0: color = "#666" status = "" elif count > MAX_QUESTION_LENGTH: color = "#ef4444" status = " ⚠️ Too long!" elif count > MAX_QUESTION_LENGTH * 0.9: color = "#f59e0b" status = " ⚠️" else: color = "#10b981" status = " ✓" return f'{count}/{MAX_QUESTION_LENGTH}{status}' def clear_inputs() -> Tuple[str, int, str]: """Clear all input fields.""" return ("", 3, "English") # ==================== GRADIO INTERFACE ==================== # Enhanced CSS with modern styling custom_css = """ /* ==================== GLOBAL STYLES ==================== */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); .gradio-container { max-width: 900px !important; margin: auto !important; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; } /* ==================== HEADER STYLES ==================== */ .hero-section { text-align: center; padding: 15px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(102, 126, 234, 0.15); } .hero-title { font-size: 1.6em; font-weight: 700; margin-bottom: 5px; } .hero-subtitle { font-size: 0.9em; opacity: 0.95; margin: 0; } /* ==================== EXPLANATION BOX ==================== */ .explanation-box { background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 100%) !important; color: #1a1a1a !important; padding: 18px; border-radius: 10px; margin: 8px 0; border: 1px solid #d0d7f7; box-shadow: 0 4px 15px rgba(0,0,0,0.1); min-height: 100px; transition: all 0.3s ease; } .explanation-box:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.15); } .explanation-box * { color: #1a1a1a !important; line-height: 1.7; } .explanation-box strong, .explanation-box b { color: #6366f1 !important; font-weight: 600; } /* Dark mode */ .theme-dark .explanation-box { background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%) !important; border: 1px solid #334155 !important; } .theme-dark .explanation-box, .theme-dark .explanation-box * { color: #f1f5f9 !important; } .theme-dark .explanation-box strong, .theme-dark .explanation-box b { color: #fbbf24 !important; font-weight: 600; } /* ==================== BUTTONS ==================== */ button { transition: all 0.3s ease !important; font-weight: 500 !important; } button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; } button:active { transform: translateY(0); } .primary-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; border: none !important; color: white !important; font-size: 1.1em !important; padding: 12px 30px !important; border-radius: 10px !important; } /* ==================== LOADING ANIMATION ==================== */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .loading { animation: pulse 1.5s ease-in-out infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .spinner { display: inline-block; animation: spin 1s linear infinite; } /* ==================== FOOTER ==================== */ .footer { text-align: center; padding: 15px 15px; margin-top: 20px; border-top: 1px solid rgba(0,0,0,0.1); font-size: 0.85em; opacity: 0.8; } .theme-dark .footer { border-top-color: rgba(255,255,255,0.1); } /* ==================== CHARACTER COUNTER ==================== */ .char-counter { text-align: right; font-size: 0.85em; margin-top: 5px; font-weight: 500; } /* ==================== RESPONSIVE ==================== */ @media (max-width: 768px) { .hero-title { font-size: 1.8em; } .hero-subtitle { font-size: 1em; } .stats-container { gap: 20px; } .stat-number { font-size: 1.5em; } } /* ==================== FEEDBACK WIDGET ==================== */ .feedback-btn { opacity: 0.6; transition: all 0.2s ease; } .feedback-btn:hover { opacity: 1; transform: scale(1.1); } /* ==================== ACCESSIBILITY ==================== */ *:focus-visible { outline: 2px solid #667eea; outline-offset: 2px; } /* ==================== SMOOTH SCROLLING ==================== */ html { scroll-behavior: smooth; } """ # Enhanced JavaScript for theme detection and keyboard shortcuts custom_js = """ function() { // ==================== THEME DETECTION ==================== function updateTheme() { const gradioApp = document.querySelector('gradio-app'); const body = document.body; const html = document.documentElement; const isDark = body.classList.contains('dark') || html.classList.contains('dark') || gradioApp?.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches; if (isDark) { body.classList.add('theme-dark'); body.classList.remove('theme-light'); } else { body.classList.add('theme-light'); body.classList.remove('theme-dark'); } } // Update theme immediately updateTheme(); // Watch for theme changes const observer = new MutationObserver(updateTheme); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); // Watch gradio-app const checkGradioApp = setInterval(() => { const app = document.querySelector('gradio-app'); if (app) { observer.observe(app, { attributes: true, attributeFilter: ['class', 'data-theme'] }); clearInterval(checkGradioApp); } }, 100); // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme); // ==================== KEYBOARD SHORTCUTS ==================== document.addEventListener('keydown', function(e) { // Ctrl/Cmd + Enter to submit if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { const submitBtn = document.querySelector('button.primary-btn'); if (submitBtn) submitBtn.click(); } // Escape to clear if (e.key === 'Escape') { const clearBtn = document.querySelector('[value="Clear"]'); if (clearBtn) clearBtn.click(); } }); // ==================== SMOOTH ANIMATIONS ==================== // Add entrance animations setTimeout(() => { document.querySelectorAll('.gradio-container > *').forEach((el, idx) => { el.style.opacity = '0'; el.style.transform = 'translateY(20px)'; setTimeout(() => { el.style.transition = 'all 0.5s ease'; el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }, idx * 50); }); }, 100); console.log('🧠 AI Concept Explainer v""" + VERSION + """loaded successfully!'); console.log('💡 Keyboard shortcuts: Ctrl/Cmd+Enter to submit, Escape to clear'); } """ # ==================== BUILD GRADIO APP ==================== with gr.Blocks( theme=gr.themes.Soft( primary_hue="indigo", secondary_hue="purple", neutral_hue="slate", font=[gr.themes.GoogleFont("Inter"), "Arial", "sans-serif"] ), title=f"{APP_NAME} v{VERSION}", css=custom_css, js=custom_js ) as app: # Hero Header gr.HTML("""
🧠 AI Concept Explainer
Get clear, personalized explanations of any concept at your preferred complexity level and language
""") # Quick Examples gr.Markdown("### 💡 Quick Examples") with gr.Row(): example_btn1 = gr.Button(EXAMPLE_QUESTIONS[0], size="sm", variant="secondary") example_btn2 = gr.Button(EXAMPLE_QUESTIONS[1], size="sm", variant="secondary") example_btn3 = gr.Button(EXAMPLE_QUESTIONS[2], size="sm", variant="secondary") # Input Section with gr.Row(): with gr.Column(scale=3): question = gr.Textbox( label="💭 What would you like to understand?", placeholder="Enter your question or select an example above...", lines=2, max_lines=4 ) char_count = gr.HTML("
0/500
") with gr.Column(scale=1): level = gr.Slider( 1, 5, value=3, step=1, label="📊 Complexity Level", info="1 = Simple, 5 = Expert" ) language = gr.Dropdown( LANGUAGES, value="English", label="🌍 Language", info="Choose your preferred language" ) # Action Buttons with gr.Row(): explain_btn = gr.Button( "🚀 Explain Concept", variant="primary", size="lg", elem_classes=["primary-btn"] ) clear_btn = gr.Button( "🔄 Clear", variant="secondary", size="lg" ) # Output Section gr.Markdown("### 📝 Explanation") reading_time = gr.Markdown("") output = gr.Markdown( value="*Your explanation will appear here...*", elem_classes=["explanation-box"] ) # Action buttons for output with gr.Row(): copy_btn = gr.Button("📋 Copy to Clipboard", size="sm") feedback_positive = gr.Button("👍 Helpful", size="sm", elem_classes=["feedback-btn"]) feedback_negative = gr.Button("👎 Not Helpful", size="sm", elem_classes=["feedback-btn"]) feedback_msg = gr.Markdown("") # Footer gr.HTML(f""" """) # ==================== EVENT HANDLERS ==================== # Character counter question.change( fn=update_char_count, inputs=[question], outputs=[char_count] ) # Example buttons example_btn1.click(lambda: EXAMPLE_QUESTIONS[0], outputs=question) example_btn2.click(lambda: EXAMPLE_QUESTIONS[1], outputs=question) example_btn3.click(lambda: EXAMPLE_QUESTIONS[2], outputs=question) # Main explain button explain_btn.click( fn=explain_concept, inputs=[question, level, language], outputs=[output, reading_time] ) # Clear button clear_btn.click( fn=clear_inputs, outputs=[question, level, language] ) # Copy button (client-side) copy_btn.click( None, inputs=[output], outputs=[feedback_msg], js=""" (output) => { const text = output; navigator.clipboard.writeText(text).then(() => { return '✅ Copied to clipboard!'; }).catch(() => { return '❌ Failed to copy. Please try manually selecting the text.'; }); } """ ) # Feedback buttons def record_feedback(feedback_type: str) -> str: logger.info(f"User feedback: {feedback_type}") return f"✅ Thanks for your feedback!" feedback_positive.click( fn=lambda: record_feedback("positive"), outputs=[feedback_msg] ) feedback_negative.click( fn=lambda: record_feedback("negative"), outputs=[feedback_msg] ) # ==================== APP LAUNCH ==================== def signal_handler(signum, frame): """Handle graceful shutdown for local development.""" logger.info(f"Received signal {signum}, shutting down gracefully...") exit(0) if __name__ == "__main__": is_space = os.getenv('SPACE_ID') is not None if not is_space: signal.signal(signal.SIGINT, signal_handler) logger.info(f"🚀 Starting {APP_NAME} v{VERSION}...") logger.info(f"API Key configured: {'✅ Yes' if openai_api_key else '❌ No'}") if is_space: app.launch( server_name="0.0.0.0", server_port=7860, show_error=True, quiet=False ) else: app.launch( share=False, server_name="127.0.0.1", server_port=7860, inbrowser=True, show_error=True, quiet=False )