Spaces:
Running
Running
| """ | |
| π¨ Gradio Application for Course Creator AI | |
| Main Gradio interface for course generation. | |
| """ | |
| import gradio as gr | |
| from typing import Dict, Any, Optional, Tuple | |
| import asyncio | |
| import json | |
| import markdown | |
| import re | |
| from ..agents.simple_course_agent import SimpleCourseAgent | |
| from ..types import DifficultyLevel, GenerationOptions, LearningStyle | |
| from .components import CoursePreview | |
| from .styling import get_custom_css | |
| def format_lessons(lessons: list) -> str: | |
| """Format lessons from JSON data into HTML with dark theme and markdown support""" | |
| if not lessons: | |
| return "<div class='info'>π No lessons generated yet.</div>" | |
| # Add CSS for lesson styling | |
| css = """ | |
| <style> | |
| /* Force dark theme for all lesson elements */ | |
| .lessons-container * { | |
| background: transparent !important; | |
| color: inherit !important; | |
| } | |
| .lessons-container { | |
| padding: 1rem !important; | |
| background: #1a1a2e !important; | |
| border-radius: 12px !important; | |
| margin: 1rem 0 !important; | |
| max-height: none !important; | |
| overflow: visible !important; | |
| } | |
| .lesson-card { | |
| background: #2d2d54 !important; | |
| border: 1px solid #4a4a7a !important; | |
| border-radius: 12px !important; | |
| padding: 2rem !important; | |
| margin: 1.5rem 0 !important; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; | |
| color: #e0e7ff !important; | |
| } | |
| .lesson-card h3 { | |
| color: #667eea !important; | |
| margin-bottom: 1rem !important; | |
| font-size: 1.5rem !important; | |
| border-bottom: 2px solid #667eea !important; | |
| padding-bottom: 0.5rem !important; | |
| background: transparent !important; | |
| } | |
| .lesson-card h4 { | |
| color: #8b9dc3 !important; | |
| margin: 1.5rem 0 0.75rem 0 !important; | |
| font-size: 1.2rem !important; | |
| background: transparent !important; | |
| } | |
| .lesson-card p { | |
| color: #b8c5d6 !important; | |
| line-height: 1.6 !important; | |
| margin: 0.75rem 0 !important; | |
| font-size: 1rem !important; | |
| background: transparent !important; | |
| } | |
| .lesson-card ul { | |
| color: #e0e7ff !important; | |
| margin: 0.75rem 0 !important; | |
| padding-left: 1.5rem !important; | |
| background: transparent !important; | |
| } | |
| .lesson-card li { | |
| color: #e0e7ff !important; | |
| margin: 0.5rem 0 !important; | |
| line-height: 1.5 !important; | |
| background: transparent !important; | |
| } | |
| .lesson-content { | |
| background: #3a3a6b !important; | |
| border-radius: 8px !important; | |
| padding: 1.5rem !important; | |
| margin: 1rem 0 !important; | |
| border-left: 4px solid #667eea !important; | |
| } | |
| .lesson-content h1, .lesson-content h2, .lesson-content h3, | |
| .lesson-content h4, .lesson-content h5, .lesson-content h6 { | |
| color: #667eea !important; | |
| margin: 1rem 0 0.5rem 0 !important; | |
| background: transparent !important; | |
| } | |
| .lesson-content p { | |
| color: #e0e7ff !important; | |
| margin: 0.75rem 0 !important; | |
| line-height: 1.6 !important; | |
| background: transparent !important; | |
| } | |
| .lesson-content ul, .lesson-content ol { | |
| color: #e0e7ff !important; | |
| margin: 0.75rem 0 !important; | |
| padding-left: 1.5rem !important; | |
| background: transparent !important; | |
| } | |
| .lesson-content li { | |
| color: #e0e7ff !important; | |
| margin: 0.5rem 0 !important; | |
| background: transparent !important; | |
| } | |
| .lesson-content strong { | |
| color: #8b9dc3 !important; | |
| } | |
| .lesson-content em { | |
| color: #b8c5d6 !important; | |
| } | |
| .lesson-content code { | |
| background: #4a4a7a !important; | |
| color: #e0e7ff !important; | |
| padding: 0.2rem 0.4rem; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| } | |
| .lesson-content pre { | |
| background: #4a4a7a !important; | |
| color: #e0e7ff !important; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 1rem 0; | |
| } | |
| .lesson-card ul { | |
| color: #e0e7ff !important; | |
| margin: 0.75rem 0; | |
| padding-left: 1.5rem; | |
| } | |
| .lesson-card li { | |
| color: #e0e7ff !important; | |
| margin: 0.5rem 0; | |
| line-height: 1.5; | |
| } | |
| .lesson-image { | |
| margin: 1.5rem 0; | |
| text-align: center; | |
| } | |
| .image-placeholder { | |
| background: #4a4a7a; | |
| border: 2px dashed #667eea; | |
| border-radius: 8px; | |
| padding: 2rem; | |
| text-align: center; | |
| color: #b8c5d6; | |
| } | |
| .image-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| } | |
| .image-description { | |
| font-size: 1.1rem; | |
| margin-bottom: 0.5rem; | |
| color: #e0e7ff; | |
| } | |
| .image-note { | |
| font-size: 0.9rem; | |
| font-style: italic; | |
| opacity: 0.7; | |
| } | |
| .duration-info { | |
| background: #4a4a7a !important; | |
| color: #e0e7ff !important; | |
| padding: 0.5rem 1rem !important; | |
| border-radius: 20px !important; | |
| display: inline-block !important; | |
| margin-bottom: 1rem !important; | |
| font-size: 0.9rem !important; | |
| } | |
| /* Ultimate override for any stubborn white backgrounds */ | |
| .lessons-container .lesson-card, | |
| .lessons-container .lesson-card *, | |
| .lessons-container .lesson-content, | |
| .lessons-container .lesson-content * { | |
| background-color: transparent !important; | |
| } | |
| .lessons-container .lesson-card { | |
| background: #2d2d54 !important; | |
| } | |
| .lessons-container .lesson-content { | |
| background: #3a3a6b !important; | |
| } | |
| </style> | |
| """ | |
| html = css + "<div class='lessons-container'>" | |
| for i, lesson in enumerate(lessons, 1): | |
| title = lesson.get("title", f"Lesson {i}") | |
| content = lesson.get("content", "") | |
| duration = lesson.get("duration", "") | |
| objectives = lesson.get("objectives", []) | |
| key_takeaways = lesson.get("key_takeaways", []) | |
| image_description = lesson.get("image_description", "") | |
| # Convert markdown content to HTML | |
| if content: | |
| try: | |
| # Create markdown instance with extensions | |
| import markdown | |
| md = markdown.Markdown(extensions=['extra', 'codehilite']) | |
| content_html = md.convert(content) | |
| except ImportError: | |
| # Fallback if markdown is not available | |
| content_html = content.replace('\n\n', '</p><p>').replace('\n', '<br>') | |
| if content_html and not content_html.startswith('<p>'): | |
| content_html = f'<p>{content_html}</p>' | |
| else: | |
| content_html = "<p>No content available.</p>" | |
| # Generate image placeholder or actual image | |
| image_html = "" | |
| if image_description: | |
| # Check if we have actual image data | |
| images = lesson.get("images", []) | |
| if images and len(images) > 0: | |
| # Display actual generated images | |
| image_html = "<div class='lesson-images'>" | |
| for img in images: | |
| if isinstance(img, dict) and img.get("url"): | |
| img_url = img.get("url", "") | |
| img_caption = img.get("description", image_description) | |
| image_html += f""" | |
| <div class='lesson-image'> | |
| <img src='{img_url}' alt='{img_caption}' loading='lazy' style='max-width: 100%; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); border: 2px solid #4a4a7a;'> | |
| <p class='image-caption'>{img_caption}</p> | |
| </div> | |
| """ | |
| image_html += "</div>" | |
| else: | |
| # Fallback to placeholder | |
| image_html = f""" | |
| <div class='lesson-image'> | |
| <div class='image-placeholder'> | |
| <div class='image-icon'>πΌοΈ</div> | |
| <div class='image-description'>{image_description}</div> | |
| <div class='image-note'>(Image generation in progress...)</div> | |
| </div> | |
| </div> | |
| """ | |
| html += f""" | |
| <div class='lesson-card'> | |
| <h3>π {title}</h3> | |
| {f"<div class='duration-info'>β±οΈ Duration: {duration} minutes</div>" if duration else ""} | |
| {f"<h4>π― Learning Objectives:</h4><ul>{''.join([f'<li>{obj}</li>' for obj in objectives])}</ul>" if objectives else ""} | |
| {image_html} | |
| <div class='lesson-content'> | |
| {content_html} | |
| </div> | |
| {f"<h4>π‘ Key Takeaways:</h4><ul>{''.join([f'<li>{takeaway}</li>' for takeaway in key_takeaways])}</ul>" if key_takeaways else ""} | |
| </div> | |
| """ | |
| html += "</div>" | |
| return html | |
| def format_flashcards(flashcards: list) -> str: | |
| """Format flashcards from JSON data into interactive HTML with CSS-only flip""" | |
| if not flashcards: | |
| return "<div class='info'>π No flashcards generated yet.</div>" | |
| # Add the CSS for flashcard flip functionality | |
| css = """ | |
| <style> | |
| .flashcards-container { | |
| padding: 1rem; | |
| background: #1a1a2e; | |
| border-radius: 12px; | |
| margin: 1rem 0; | |
| max-height: none !important; | |
| overflow: visible !important; | |
| } | |
| .flashcard-wrapper { | |
| perspective: 1000px; | |
| margin: 1rem 0; | |
| height: 200px; | |
| } | |
| .flip-checkbox { | |
| display: none; | |
| } | |
| .flashcard { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| transform-style: preserve-3d; | |
| transition: transform 0.6s; | |
| display: block; | |
| } | |
| .flip-checkbox:checked + .flashcard { | |
| transform: rotateY(180deg); | |
| } | |
| .flashcard-inner { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| transform-style: preserve-3d; | |
| } | |
| .flashcard-front, .flashcard-back { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| backface-visibility: hidden; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.3); | |
| } | |
| .flashcard-front { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .flashcard-back { | |
| background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); | |
| color: white; | |
| transform: rotateY(180deg); | |
| } | |
| .flashcard-category { | |
| position: absolute; | |
| top: 10px; | |
| right: 15px; | |
| background: rgba(255,255,255,0.2); | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| font-weight: bold; | |
| } | |
| .flashcard-content { | |
| font-size: 1.2rem; | |
| font-weight: 500; | |
| line-height: 1.4; | |
| margin: 1rem 0; | |
| color: white; | |
| } | |
| .flashcard-hint { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 0.8rem; | |
| opacity: 0.8; | |
| font-style: italic; | |
| } | |
| .flashcard:hover { | |
| box-shadow: 0 6px 12px rgba(0,0,0,0.4); | |
| } | |
| </style> | |
| """ | |
| html = css + "<div class='flashcards-container'>" | |
| html += "<p style='color: #e0e7ff; text-align: center; margin-bottom: 1rem;'><strong>π Click on any flashcard to flip it and see the answer!</strong></p>" | |
| for i, card in enumerate(flashcards): | |
| question = card.get("question", "") | |
| answer = card.get("answer", "") | |
| category = card.get("category", "General") | |
| # Use CSS-only flip with checkbox hack | |
| html += f""" | |
| <div class='flashcard-wrapper'> | |
| <input type='checkbox' id='flip-{i}' class='flip-checkbox'> | |
| <label for='flip-{i}' class='flashcard'> | |
| <div class='flashcard-inner'> | |
| <div class='flashcard-front'> | |
| <div class='flashcard-category'>{category}</div> | |
| <div class='flashcard-content'>{question}</div> | |
| <div class='flashcard-hint'>Click to flip</div> | |
| </div> | |
| <div class='flashcard-back'> | |
| <div class='flashcard-category'>{category}</div> | |
| <div class='flashcard-content'>{answer}</div> | |
| <div class='flashcard-hint'>Click to flip back</div> | |
| </div> | |
| </div> | |
| </label> | |
| </div> | |
| """ | |
| html += "</div>" | |
| return html | |
| def format_quiz(quiz: dict) -> str: | |
| """Format quiz from JSON data into interactive HTML with working JavaScript.""" | |
| if not quiz or not quiz.get("questions"): | |
| return "<div class='info'>π No quiz generated yet.</div>" | |
| title = quiz.get("title", "Course Quiz") | |
| instructions = quiz.get("instructions", "Choose the best answer for each question.") | |
| questions = quiz.get("questions", []) | |
| if not questions: | |
| return "<div class='info'>π No quiz questions available.</div>" | |
| # Generate unique quiz ID | |
| quiz_id = f"quiz_{abs(hash(str(questions)))%10000}" | |
| # CSS and JavaScript for quiz functionality | |
| quiz_html = f""" | |
| <style> | |
| .quiz-container {{ | |
| background: #1a1a2e; | |
| border-radius: 12px; | |
| padding: 2rem; | |
| color: #e0e7ff; | |
| max-height: none !important; | |
| overflow: visible !important; | |
| }} | |
| .quiz-container h3 {{ | |
| color: #667eea; | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| }} | |
| .quiz-question {{ | |
| background: #2d2d54; | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| margin: 1.5rem 0; | |
| border-left: 4px solid #667eea; | |
| }} | |
| .quiz-question h4 {{ | |
| color: #e0e7ff; | |
| margin-bottom: 1rem; | |
| font-size: 1.1rem; | |
| }} | |
| .quiz-options {{ | |
| margin: 1rem 0; | |
| }} | |
| .quiz-option-label {{ | |
| display: flex; | |
| align-items: center; | |
| padding: 0.75rem 1rem; | |
| background: #3a3a6b; | |
| border: 2px solid #4a4a7a; | |
| border-radius: 8px; | |
| margin: 0.5rem 0; | |
| cursor: pointer; | |
| color: #e0e7ff; | |
| transition: all 0.2s; | |
| }} | |
| .quiz-option-label:hover {{ | |
| background: #4a4a7a; | |
| border-color: #667eea; | |
| }} | |
| .quiz-radio {{ | |
| display: none; | |
| }} | |
| .quiz-radio:checked + .quiz-option-label {{ | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| }} | |
| .radio-custom {{ | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid #667eea; | |
| border-radius: 50%; | |
| margin-right: 0.75rem; | |
| position: relative; | |
| }} | |
| .quiz-radio:checked + .quiz-option-label .radio-custom::after {{ | |
| content: ''; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: white; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| }} | |
| .quiz-feedback {{ | |
| margin-top: 1rem; | |
| padding: 1rem; | |
| border-radius: 6px; | |
| font-weight: 500; | |
| display: none; | |
| }} | |
| .feedback-correct {{ | |
| background: #d4edda; | |
| color: #155724; | |
| border: 1px solid #c3e6cb; | |
| }} | |
| .feedback-incorrect {{ | |
| background: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #f5c6cb; | |
| }} | |
| .feedback-unanswered {{ | |
| background: #fff3cd; | |
| color: #856404; | |
| border: 1px solid #ffeaa7; | |
| }} | |
| .quiz-results {{ | |
| margin-top: 2rem; | |
| padding: 1.5rem; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| color: white; | |
| border-radius: 8px; | |
| text-align: center; | |
| font-size: 1.1rem; | |
| display: none; | |
| }} | |
| .quiz-score {{ | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| margin-bottom: 0.5rem; | |
| }} | |
| </style> | |
| <div class="quiz-container" id="{quiz_id}"> | |
| <h3>π {title}</h3> | |
| <p style="text-align:center; color:#b8c5d6; margin-bottom: 2rem;"><em>{instructions}</em></p> | |
| <form id="quiz-form-{quiz_id}"> | |
| """ | |
| # Display each question | |
| for idx, q in enumerate(questions): | |
| question_text = q.get("question", "") | |
| options = q.get("options", []) | |
| correct_answer = q.get("correct_answer", "A") | |
| explanation = q.get("explanation", "") | |
| quiz_html += f""" | |
| <div class="quiz-question" data-correct="{correct_answer}" data-explanation="{explanation}"> | |
| <h4>Q{idx+1}: {question_text}</h4> | |
| <div class="quiz-options"> | |
| """ | |
| # Display options | |
| for j, option in enumerate(options): | |
| option_letter = option[0] if option and len(option) > 0 else chr(65 + j) | |
| option_text = option[3:] if option.startswith(f"{option_letter}. ") else option | |
| quiz_html += f""" | |
| <div> | |
| <input type="radio" id="q{idx}_o{j}_{quiz_id}" name="q{idx}" value="{option_letter}" class="quiz-radio"> | |
| <label for="q{idx}_o{j}_{quiz_id}" class="quiz-option-label"> | |
| <span class="radio-custom"></span> | |
| <strong>{option_letter}.</strong> {option_text} | |
| </label> | |
| </div> | |
| """ | |
| quiz_html += f""" | |
| </div> | |
| <div class="quiz-feedback" id="feedback-{idx}-{quiz_id}"></div> | |
| </div> | |
| """ | |
| # Close form and add results container | |
| quiz_html += f""" | |
| </form> | |
| </div> | |
| """ | |
| return quiz_html | |
| def create_coursecrafter_interface() -> gr.Blocks: | |
| """Create the main Course Creator Gradio interface""" | |
| with gr.Blocks( | |
| title="Course Creator AI - Intelligent Course Generator", | |
| css=get_custom_css(), | |
| theme=gr.themes.Soft() | |
| ) as interface: | |
| # Header | |
| gr.HTML(""" | |
| <div class="header-container"> | |
| <h1>π Course Creator AI</h1> | |
| <p>Generate comprehensive mini-courses with AI-powered content, flashcards, and quizzes</p> | |
| </div> | |
| """) | |
| # LLM Provider Configuration | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.HTML("<h3>π€ LLM Provider Configuration</h3>") | |
| with gr.Row(): | |
| llm_provider = gr.Dropdown( | |
| label="LLM Provider", | |
| choices=["openai", "anthropic", "google", "openai_compatible"], | |
| value="google", | |
| info="Choose your preferred LLM provider" | |
| ) | |
| api_key_input = gr.Textbox( | |
| label="API Key", | |
| placeholder="Enter your API key here...", | |
| type="password", | |
| info="Your API key for the selected provider (optional for OpenAI-compatible)" | |
| ) | |
| # OpenAI-Compatible endpoint configuration (initially hidden) | |
| with gr.Row(visible=False) as openai_compatible_row: | |
| endpoint_url_input = gr.Textbox( | |
| label="Endpoint URL", | |
| placeholder="https://your-endpoint.com/v1", | |
| info="Base URL for OpenAI-compatible API" | |
| ) | |
| model_name_input = gr.Textbox( | |
| label="Model Name", | |
| placeholder="your-model-name", | |
| info="Model name to use with the endpoint" | |
| ) | |
| # Main interface | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Course generation form | |
| topic_input = gr.Textbox( | |
| label="Course Topic", | |
| placeholder="e.g., Introduction to Python Programming", | |
| lines=1 | |
| ) | |
| difficulty_input = gr.Dropdown( | |
| label="Difficulty Level", | |
| choices=["beginner", "intermediate", "advanced"], | |
| value="beginner" | |
| ) | |
| lesson_count = gr.Slider( | |
| label="Number of Lessons", | |
| minimum=1, | |
| maximum=10, | |
| value=5, | |
| step=1 | |
| ) | |
| generate_btn = gr.Button( | |
| "π Generate Course", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| # Chat interface for course refinement | |
| gr.HTML("<hr><h3>π¬ Course Assistant</h3>") | |
| # Chat window with proper styling | |
| with gr.Column(): | |
| chat_display = gr.HTML( | |
| value=""" | |
| <div class='chat-window'> | |
| <div class='chat-messages' id='chat-messages'> | |
| <div class='chat-message assistant-message'> | |
| <div class='message-avatar'>π€</div> | |
| <div class='message-content'> | |
| <div class='message-text'>Hi! I'm your Course Assistant. Generate a course first, then ask me questions about the lessons, concepts, or content!</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """, | |
| elem_id="chat-display" | |
| ) | |
| with gr.Row(): | |
| chat_input = gr.Textbox( | |
| placeholder="Ask me to modify the course...", | |
| lines=1, | |
| scale=4, | |
| container=False | |
| ) | |
| chat_btn = gr.Button("Send", variant="secondary", scale=1) | |
| with gr.Column(scale=2): | |
| # Course preview tabs with enhanced components | |
| course_preview = CoursePreview() | |
| with gr.Tabs(): | |
| with gr.Tab("π Lessons"): | |
| lessons_output = gr.HTML( | |
| value=""" | |
| <div class='lessons-container' style='padding: 2rem; text-align: center; background: #1a1a2e; border-radius: 12px; color: #e0e7ff;'> | |
| <h3 style='color: #667eea; margin-bottom: 1rem;'>π Ready to Generate Your Course!</h3> | |
| <p style='color: #b8c5d6; font-size: 1.1rem; margin-bottom: 1.5rem;'>Enter a topic and click "Generate Course" to create comprehensive lessons with AI-powered content.</p> | |
| <div style='background: #2d2d54; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #667eea;'> | |
| <p style='color: #e0e7ff; margin: 0;'>π‘ <strong>Tip:</strong> Try topics like "Introduction to Python Programming", "Digital Marketing Basics", or "Climate Change Science"</p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Tab("π Flashcards"): | |
| flashcards_output = gr.HTML( | |
| value=""" | |
| <div class='flashcards-container' style='padding: 2rem; text-align: center; background: #1a1a2e; border-radius: 12px; color: #e0e7ff;'> | |
| <h3 style='color: #667eea; margin-bottom: 1rem;'>π Interactive Flashcards</h3> | |
| <p style='color: #b8c5d6;'>Flashcards will appear here after course generation. They'll help reinforce key concepts with spaced repetition learning!</p> | |
| </div> | |
| """ | |
| ) | |
| with gr.Tab("π Quizzes"): | |
| # Quiz functionality with HTML content and state management | |
| quiz_state = gr.State({}) # Store quiz data | |
| quizzes_output = gr.HTML( | |
| value=""" | |
| <div class='quiz-container' style='padding: 2rem; text-align: center; background: #1a1a2e; border-radius: 12px; color: #e0e7ff;'> | |
| <h3 style='color: #667eea; margin-bottom: 1rem;'>π Knowledge Assessment</h3> | |
| <p style='color: #b8c5d6;'>Interactive quizzes will appear here to test your understanding of the course material!</p> | |
| </div> | |
| """ | |
| ) | |
| quiz_results = gr.HTML(visible=False) | |
| quiz_submit_btn = gr.Button("Submit Quiz", variant="primary", visible=False) | |
| with gr.Tab("π¨ Images"): | |
| images_section = course_preview._create_images_section() | |
| image_gallery = images_section["image_gallery"] | |
| image_details = images_section["image_details"] | |
| # Store generated course content for chat context | |
| course_context = {"content": "", "topic": "", "agent": None} | |
| # Provider change handler to show/hide OpenAI-compatible fields | |
| def on_provider_change(provider): | |
| if provider == "openai_compatible": | |
| return gr.update(visible=True) | |
| else: | |
| return gr.update(visible=False) | |
| # Event handlers | |
| async def generate_course_wrapper(topic: str, difficulty: str, lessons: int, provider: str, api_key: str, endpoint_url: str, model_name: str, progress=gr.Progress()): | |
| """Wrapper for course generation with progress tracking""" | |
| if not topic.strip(): | |
| return ( | |
| "<div class='error'>β Please enter a topic for your course.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| if not api_key.strip() and provider != "openai_compatible": | |
| return ( | |
| "<div class='error'>β Please enter your API key for the selected LLM provider.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| if provider == "openai_compatible" and not endpoint_url.strip(): | |
| return ( | |
| "<div class='error'>β Please enter the endpoint URL for OpenAI-compatible provider.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| if provider == "openai_compatible" and not model_name.strip(): | |
| return ( | |
| "<div class='error'>β Please enter the model name for OpenAI-compatible provider.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| try: | |
| # Initialize progress | |
| progress(0, desc="π Initializing Course Generator...") | |
| # Set the API key and configuration for the selected provider | |
| import os | |
| if provider == "openai": | |
| os.environ["OPENAI_API_KEY"] = api_key | |
| elif provider == "anthropic": | |
| os.environ["ANTHROPIC_API_KEY"] = api_key | |
| elif provider == "google": | |
| os.environ["GOOGLE_API_KEY"] = api_key | |
| elif provider == "openai_compatible": | |
| if api_key.strip(): | |
| os.environ["OPENAI_COMPATIBLE_API_KEY"] = api_key | |
| os.environ["OPENAI_COMPATIBLE_BASE_URL"] = endpoint_url | |
| os.environ["OPENAI_COMPATIBLE_MODEL"] = model_name | |
| # IMPORTANT: Create a fresh agent instance to pick up the new environment variables | |
| # This ensures the LlmClient reinitializes with the updated API keys | |
| agent = SimpleCourseAgent() | |
| # Use the new dynamic configuration method to update provider settings | |
| config_kwargs = {} | |
| if provider == "openai_compatible": | |
| config_kwargs["base_url"] = endpoint_url | |
| config_kwargs["model"] = model_name | |
| # Update provider configuration dynamically | |
| config_success = agent.update_provider_config(provider, api_key, **config_kwargs) | |
| if not config_success: | |
| return ( | |
| f"<div class='error'>β Failed to configure provider '{provider}'. Please check your API key and settings.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| course_context["agent"] = agent | |
| course_context["topic"] = topic | |
| # Verify the provider is available with the new configuration | |
| available_providers = agent.get_available_providers() | |
| if provider not in available_providers: | |
| return ( | |
| f"<div class='error'>β Provider '{provider}' is not available after configuration. Please check your API key and configuration.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| progress(0.1, desc="βοΈ Setting up generation options...") | |
| # Create generation options | |
| options = GenerationOptions( | |
| difficulty=DifficultyLevel(difficulty), | |
| lesson_count=lessons, | |
| include_images=True, | |
| include_flashcards=True, | |
| include_quizzes=True | |
| ) | |
| progress(0.15, desc="π Checking available providers...") | |
| # Get available providers | |
| available_providers = agent.get_available_providers() | |
| if not available_providers: | |
| return ( | |
| "<div class='error'>β No LLM providers available. Please check your API keys.</div>", | |
| "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| progress(0.2, desc="π Starting course generation...") | |
| # Use the default provider from config (no need to override) | |
| # The agent will automatically use the configured default provider | |
| # Start course generation | |
| lessons_html = "" | |
| flashcards_html = "" | |
| quizzes_html = "" | |
| # Stream the generation process | |
| course_data = None | |
| current_progress = 0.2 | |
| # Add a simple counter for fallback progress | |
| chunk_count = 0 | |
| max_expected_chunks = 10 # Rough estimate | |
| async for chunk in agent.generate_course(topic, options): | |
| chunk_count += 1 | |
| print(f"π Progress Debug: Received chunk type='{chunk.type}', content='{chunk.content}'") | |
| # Update progress based on chunk content | |
| if chunk.type == "progress": | |
| # Check if the progress message matches our known steps (handle emojis) | |
| step_found = False | |
| progress_message = chunk.content.lower() | |
| print(f"π Checking progress message: '{progress_message}'") | |
| if "research completed" in progress_message: | |
| current_progress = 0.3 | |
| step_found = True | |
| print(f"β Matched: Research completed -> {current_progress}") | |
| progress(current_progress, desc="π Research completed, planning course structure...") | |
| elif "course structure planned" in progress_message: | |
| current_progress = 0.4 | |
| step_found = True | |
| print(f"β Matched: Course structure planned -> {current_progress}") | |
| progress(current_progress, desc="π Course structure planned, generating content...") | |
| elif "lessons created" in progress_message: | |
| current_progress = 0.6 | |
| step_found = True | |
| print(f"β Matched: Lessons created -> {current_progress}") | |
| progress(current_progress, desc="βοΈ Lessons created, generating flashcards...") | |
| elif "flashcards created" in progress_message: | |
| current_progress = 0.75 | |
| step_found = True | |
| print(f"β Matched: Flashcards created -> {current_progress}") | |
| progress(current_progress, desc="π Flashcards created, creating quiz...") | |
| elif "quiz created" in progress_message: | |
| current_progress = 0.8 | |
| step_found = True | |
| print(f"β Matched: Quiz created -> {current_progress}") | |
| progress(current_progress, desc="β Quiz created, generating images...") | |
| elif "images generated" in progress_message: | |
| current_progress = 0.9 | |
| step_found = True | |
| print(f"β Matched: Images generated -> {current_progress}") | |
| progress(current_progress, desc="π¨ Images generated, finalizing course...") | |
| elif "finalizing course" in progress_message: | |
| current_progress = 0.95 | |
| step_found = True | |
| print(f"β Matched: Finalizing course -> {current_progress}") | |
| progress(current_progress, desc="π¦ Assembling final course data...") | |
| if not step_found: | |
| # Fallback: increment progress based on chunk count | |
| fallback_progress = min(0.2 + (chunk_count / max_expected_chunks) * 0.6, 0.85) | |
| current_progress = max(current_progress, fallback_progress) | |
| print(f"β οΈ No match found, using fallback: {fallback_progress}") | |
| progress(current_progress, desc=f"οΏ½οΏ½ {chunk.content}") | |
| elif chunk.type == "course_complete": | |
| current_progress = 0.95 | |
| progress(current_progress, desc="π¦ Finalizing course data...") | |
| # Parse the complete course data | |
| try: | |
| course_data = json.loads(chunk.content) | |
| except: | |
| course_data = None | |
| progress(0.97, desc="π¨ Processing course content...") | |
| # If we got course data, format it nicely | |
| if course_data: | |
| course_context["content"] = course_data | |
| # Format lessons | |
| lessons_html = format_lessons(course_data.get("lessons", [])) | |
| # Format flashcards | |
| flashcards_html = format_flashcards(course_data.get("flashcards", [])) | |
| # Format quiz | |
| quiz_data = course_data.get("quiz", {}) | |
| quizzes_html = format_quiz(quiz_data) | |
| # Show quiz button if quiz exists - be more permissive to ensure it shows | |
| quiz_btn_visible = bool(quiz_data and (quiz_data.get("questions") or len(str(quiz_data)) > 50)) | |
| print(f"π― Quiz button visibility: {quiz_btn_visible} (quiz_data: {bool(quiz_data)}, questions: {bool(quiz_data.get('questions') if quiz_data else False)})") | |
| # Force quiz button to be visible if we have any quiz content | |
| if quiz_data and not quiz_btn_visible: | |
| print("β οΈ Forcing quiz button to be visible due to quiz data presence") | |
| quiz_btn_visible = True | |
| progress(0.98, desc="πΌοΈ Processing images for gallery...") | |
| # Prepare image gallery data - fix the format for Gradio Gallery | |
| images = [] | |
| image_details_list = [] | |
| # Process images from lessons | |
| for lesson in course_data.get("lessons", []): | |
| lesson_images = lesson.get("images", []) | |
| for i, img in enumerate(lesson_images): | |
| try: | |
| if isinstance(img, dict): | |
| # Handle different image data formats | |
| image_url = img.get("url") or img.get("data_url") | |
| if image_url: | |
| alt_text = img.get("caption", img.get("description", "Educational image")) | |
| # Handle base64 data URLs by converting to temp files | |
| if image_url.startswith('data:image/'): | |
| import base64 | |
| import tempfile | |
| import os | |
| # Extract base64 data | |
| header, data = image_url.split(',', 1) | |
| image_data = base64.b64decode(data) | |
| # Determine file extension from header | |
| if 'jpeg' in header or 'jpg' in header: | |
| ext = '.jpg' | |
| elif 'png' in header: | |
| ext = '.png' | |
| elif 'gif' in header: | |
| ext = '.gif' | |
| elif 'webp' in header: | |
| ext = '.webp' | |
| else: | |
| ext = '.jpg' # Default | |
| # Create temp file | |
| temp_fd, temp_path = tempfile.mkstemp(suffix=ext, prefix=f'course_img_{i}_') | |
| try: | |
| with os.fdopen(temp_fd, 'wb') as f: | |
| f.write(image_data) | |
| images.append(temp_path) | |
| image_details_list.append({ | |
| "url": temp_path, | |
| "caption": alt_text, | |
| "lesson": lesson.get("title", "Unknown lesson") | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Failed to save temp image {i}: {e}") | |
| os.close(temp_fd) # Close if write failed | |
| continue | |
| elif image_url.startswith('http'): | |
| # Regular URL - Gradio can handle these directly | |
| images.append(image_url) | |
| image_details_list.append({ | |
| "url": image_url, | |
| "caption": alt_text, | |
| "lesson": lesson.get("title", "Unknown lesson") | |
| }) | |
| else: | |
| # Assume it's a file path | |
| if len(image_url) <= 260: # Windows path limit | |
| images.append(image_url) | |
| image_details_list.append({ | |
| "url": image_url, | |
| "caption": alt_text, | |
| "lesson": lesson.get("title", "Unknown lesson") | |
| }) | |
| else: | |
| print(f"β οΈ Skipping image {i}: path too long ({len(image_url)} chars)") | |
| elif isinstance(img, str): | |
| # Handle case where image is just a URL string | |
| if img.startswith('data:image/'): | |
| # Handle base64 data URLs | |
| import base64 | |
| import tempfile | |
| import os | |
| try: | |
| header, data = img.split(',', 1) | |
| image_data = base64.b64decode(data) | |
| # Determine file extension from header | |
| if 'jpeg' in header or 'jpg' in header: | |
| ext = '.jpg' | |
| elif 'png' in header: | |
| ext = '.png' | |
| elif 'gif' in header: | |
| ext = '.gif' | |
| elif 'webp' in header: | |
| ext = '.webp' | |
| else: | |
| ext = '.jpg' # Default | |
| # Create temp file | |
| temp_fd, temp_path = tempfile.mkstemp(suffix=ext, prefix=f'course_img_{i}_') | |
| try: | |
| with os.fdopen(temp_fd, 'wb') as f: | |
| f.write(image_data) | |
| images.append(temp_path) | |
| image_details_list.append({ | |
| "url": temp_path, | |
| "caption": "Educational image", | |
| "lesson": lesson.get("title", "Unknown lesson") | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Failed to save temp image {i}: {e}") | |
| os.close(temp_fd) # Close if write failed | |
| continue | |
| except Exception as e: | |
| print(f"β οΈ Error processing base64 image {i}: {e}") | |
| continue | |
| else: | |
| # Regular URL or file path | |
| images.append(img) | |
| image_details_list.append({ | |
| "url": img, | |
| "caption": "Educational image", | |
| "lesson": lesson.get("title", "Unknown lesson") | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Error processing image {i}: {e}") | |
| continue | |
| # Also check for standalone images in course data | |
| standalone_images = course_data.get("images", []) | |
| for i, img in enumerate(standalone_images): | |
| try: | |
| if isinstance(img, dict): | |
| image_url = img.get("url") or img.get("data_url") | |
| if image_url: | |
| alt_text = img.get("caption", img.get("description", "Course image")) | |
| # Handle base64 data URLs | |
| if image_url.startswith('data:image/'): | |
| import base64 | |
| import tempfile | |
| import os | |
| try: | |
| header, data = image_url.split(',', 1) | |
| image_data = base64.b64decode(data) | |
| # Determine file extension from header | |
| if 'jpeg' in header or 'jpg' in header: | |
| ext = '.jpg' | |
| elif 'png' in header: | |
| ext = '.png' | |
| elif 'gif' in header: | |
| ext = '.gif' | |
| elif 'webp' in header: | |
| ext = '.webp' | |
| else: | |
| ext = '.jpg' # Default | |
| # Create temp file | |
| temp_fd, temp_path = tempfile.mkstemp(suffix=ext, prefix=f'standalone_img_{i}_') | |
| try: | |
| with os.fdopen(temp_fd, 'wb') as f: | |
| f.write(image_data) | |
| images.append(temp_path) | |
| image_details_list.append({ | |
| "url": temp_path, | |
| "caption": alt_text, | |
| "lesson": "Course Overview" | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Failed to save temp standalone image {i}: {e}") | |
| os.close(temp_fd) # Close if write failed | |
| continue | |
| except Exception as e: | |
| print(f"β οΈ Error processing base64 standalone image {i}: {e}") | |
| continue | |
| else: | |
| images.append(image_url) | |
| image_details_list.append({ | |
| "url": image_url, | |
| "caption": alt_text, | |
| "lesson": "Course Overview" | |
| }) | |
| elif isinstance(img, str): | |
| if img.startswith('data:image/'): | |
| # Handle base64 data URLs | |
| import base64 | |
| import tempfile | |
| import os | |
| try: | |
| header, data = img.split(',', 1) | |
| image_data = base64.b64decode(data) | |
| # Determine file extension from header | |
| if 'jpeg' in header or 'jpg' in header: | |
| ext = '.jpg' | |
| elif 'png' in header: | |
| ext = '.png' | |
| elif 'gif' in header: | |
| ext = '.gif' | |
| elif 'webp' in header: | |
| ext = '.webp' | |
| else: | |
| ext = '.jpg' # Default | |
| # Create temp file | |
| temp_fd, temp_path = tempfile.mkstemp(suffix=ext, prefix=f'standalone_img_{i}_') | |
| try: | |
| with os.fdopen(temp_fd, 'wb') as f: | |
| f.write(image_data) | |
| images.append(temp_path) | |
| image_details_list.append({ | |
| "url": temp_path, | |
| "caption": "Course image", | |
| "lesson": "Course Overview" | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Failed to save temp standalone image {i}: {e}") | |
| os.close(temp_fd) # Close if write failed | |
| continue | |
| except Exception as e: | |
| print(f"β οΈ Error processing base64 standalone image {i}: {e}") | |
| continue | |
| else: | |
| images.append(img) | |
| image_details_list.append({ | |
| "url": img, | |
| "caption": "Course image", | |
| "lesson": "Course Overview" | |
| }) | |
| except Exception as e: | |
| print(f"β οΈ Error processing standalone image {i}: {e}") | |
| continue | |
| print(f"πΈ Prepared {len(images)} images for gallery display") | |
| # Create image details HTML for display | |
| if image_details_list: | |
| image_details_html = "<div class='image-details-container'>" | |
| image_details_html += "<h4>πΌοΈ Image Gallery</h4>" | |
| image_details_html += f"<p>Total images: {len(image_details_list)}</p>" | |
| image_details_html += "<ul>" | |
| for i, img_detail in enumerate(image_details_list, 1): | |
| image_details_html += f"<li><strong>Image {i}:</strong> {img_detail['caption']} (from {img_detail['lesson']})</li>" | |
| image_details_html += "</ul></div>" | |
| else: | |
| image_details_html = "<div class='image-details'>No images available</div>" | |
| progress(1.0, desc="β Course generation complete!") | |
| return ( | |
| lessons_html, flashcards_html, quizzes_html, | |
| gr.update(visible=quiz_btn_visible), images, image_details_html | |
| ) | |
| else: | |
| quiz_btn_visible = False | |
| progress(1.0, desc="β οΈ Course generation completed with issues") | |
| return ( | |
| "", "", "", | |
| gr.update(visible=quiz_btn_visible), [], "<div class='image-details'>No images available</div>" | |
| ) | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"Error in course generation: {error_details}") | |
| return ( | |
| "", "", "", | |
| gr.update(visible=False), [], "<div class='image-details'>Error loading images</div>" | |
| ) | |
| def handle_quiz_submit(): | |
| """Handle quiz submission using client-side processing""" | |
| # This function will be replaced by client-side JavaScript | |
| return gr.update() | |
| async def handle_chat(message: str, current_chat: str): | |
| """Handle chat messages for answering questions about the course content""" | |
| if not message.strip(): | |
| return current_chat, "" | |
| if not course_context["content"] or not course_context["agent"]: | |
| assistant_response = "Please generate a course first before asking questions about it." | |
| else: | |
| try: | |
| # Get the agent and course content | |
| agent = course_context["agent"] | |
| course_data = course_context["content"] | |
| topic = course_context["topic"] | |
| # Create context from the course content | |
| course_context_text = f"Course Topic: {topic}\n\n" | |
| # Add lessons content | |
| lessons = course_data.get("lessons", []) | |
| for i, lesson in enumerate(lessons, 1): | |
| course_context_text += f"Lesson {i}: {lesson.get('title', '')}\n" | |
| course_context_text += f"Content: {lesson.get('content', '')[:1000]}...\n" | |
| if lesson.get('key_takeaways'): | |
| course_context_text += f"Key Takeaways: {', '.join(lesson.get('key_takeaways', []))}\n" | |
| course_context_text += "\n" | |
| # Add flashcards context | |
| flashcards = course_data.get("flashcards", []) | |
| if flashcards: | |
| course_context_text += "Flashcards:\n" | |
| for card in flashcards[:5]: # Limit to first 5 | |
| course_context_text += f"Q: {card.get('question', '')} A: {card.get('answer', '')}\n" | |
| course_context_text += "\n" | |
| # Create a focused prompt for answering questions | |
| prompt = f"""You are a helpful course assistant. Answer the user's question about the course content below. | |
| Course Content: | |
| {course_context_text} | |
| User Question: {message} | |
| Instructions: | |
| - Answer based ONLY on the course content provided above | |
| - Be helpful, clear, and educational | |
| - If the question is about something not covered in the course, say so politely | |
| - Keep responses concise but informative | |
| - Use a friendly, teaching tone | |
| Answer:""" | |
| # Use the default provider (same as course generation) | |
| provider = agent.default_provider | |
| available_providers = agent.get_available_providers() | |
| if provider not in available_providers: | |
| # Fallback to first available if default isn't available | |
| provider = available_providers[0] if available_providers else None | |
| if provider: | |
| # Use the agent's LLM to get a response | |
| from ..agents.simple_course_agent import Message | |
| messages = [ | |
| Message(role="system", content="You are a helpful course assistant that answers questions about course content."), | |
| Message(role="user", content=prompt) | |
| ] | |
| print(f"π€ Chat using LLM provider: {provider}") | |
| assistant_response = await agent._get_llm_response(provider, messages) | |
| # Clean up the response | |
| assistant_response = assistant_response.strip() | |
| if assistant_response.startswith("Answer:"): | |
| assistant_response = assistant_response[7:].strip() | |
| else: | |
| assistant_response = "Sorry, no LLM providers are available to answer your question." | |
| except Exception as e: | |
| print(f"Error in chat: {e}") | |
| assistant_response = "Sorry, I encountered an error while trying to answer your question. Please try again." | |
| # Extract existing messages from current chat HTML | |
| existing_messages = "" | |
| if current_chat and "chat-message" in current_chat: | |
| # Keep existing messages | |
| start = current_chat.find('<div class="chat-messages"') | |
| if start != -1: | |
| end = current_chat.find('</div>', start) | |
| if end != -1: | |
| existing_content = current_chat[start:end] | |
| # Extract just the message divs | |
| import re | |
| messages_match = re.findall(r'<div class="chat-message.*?</div>\s*</div>', existing_content, re.DOTALL) | |
| existing_messages = ''.join(messages_match) | |
| # Create new chat HTML with existing messages plus new ones | |
| new_chat = f""" | |
| <div class='chat-window'> | |
| <div class='chat-messages' id='chat-messages'> | |
| {existing_messages} | |
| <div class='chat-message user-message'> | |
| <div class='message-avatar'>π€</div> | |
| <div class='message-content'> | |
| <div class='message-text'>{message}</div> | |
| </div> | |
| </div> | |
| <div class='chat-message assistant-message'> | |
| <div class='message-avatar'>π€</div> | |
| <div class='message-content'> | |
| <div class='message-text'>{assistant_response}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return new_chat, "" | |
| # Connect provider change event | |
| llm_provider.change( | |
| fn=on_provider_change, | |
| inputs=[llm_provider], | |
| outputs=[openai_compatible_row] | |
| ) | |
| generate_btn.click( | |
| fn=generate_course_wrapper, | |
| inputs=[topic_input, difficulty_input, lesson_count, llm_provider, api_key_input, endpoint_url_input, model_name_input], | |
| outputs=[ | |
| lessons_output, flashcards_output, quizzes_output, quiz_submit_btn, image_gallery, image_details | |
| ] | |
| ) | |
| chat_btn.click( | |
| fn=handle_chat, | |
| inputs=[chat_input, chat_display], | |
| outputs=[chat_display, chat_input] | |
| ) | |
| # Use a much simpler approach with direct JavaScript execution | |
| quiz_submit_btn.click( | |
| fn=None, # No Python function needed | |
| js=""" | |
| function() { | |
| // Find all quiz questions and process them | |
| const questions = document.querySelectorAll('.quiz-question'); | |
| if (questions.length === 0) { | |
| alert('No quiz questions found!'); | |
| return; | |
| } | |
| let score = 0; | |
| let total = questions.length; | |
| let hasAnswers = false; | |
| questions.forEach((question, idx) => { | |
| const radios = question.querySelectorAll('input[type="radio"]'); | |
| const correctAnswer = question.dataset.correct; | |
| const explanation = question.dataset.explanation || ''; | |
| let selectedRadio = null; | |
| radios.forEach(radio => { | |
| if (radio.checked) { | |
| selectedRadio = radio; | |
| hasAnswers = true; | |
| } | |
| }); | |
| // Create or find feedback element | |
| let feedback = question.querySelector('.quiz-feedback'); | |
| if (!feedback) { | |
| feedback = document.createElement('div'); | |
| feedback.className = 'quiz-feedback'; | |
| question.appendChild(feedback); | |
| } | |
| if (selectedRadio) { | |
| const userAnswer = selectedRadio.value; | |
| if (userAnswer === correctAnswer) { | |
| score++; | |
| feedback.innerHTML = `<div style="background: #d4edda; color: #155724; padding: 1rem; border-radius: 6px; margin-top: 1rem;">β <strong>Correct!</strong> ${explanation}</div>`; | |
| } else { | |
| feedback.innerHTML = `<div style="background: #f8d7da; color: #721c24; padding: 1rem; border-radius: 6px; margin-top: 1rem;">β <strong>Incorrect.</strong> The correct answer is <strong>${correctAnswer}</strong>. ${explanation}</div>`; | |
| } | |
| } else { | |
| feedback.innerHTML = `<div style="background: #fff3cd; color: #856404; padding: 1rem; border-radius: 6px; margin-top: 1rem;">β οΈ <strong>No answer selected.</strong> The correct answer is <strong>${correctAnswer}</strong>. ${explanation}</div>`; | |
| } | |
| feedback.style.display = 'block'; | |
| }); | |
| if (hasAnswers) { | |
| const percentage = Math.round((score / total) * 100); | |
| // Create or find results container | |
| let resultsContainer = document.querySelector('.quiz-results'); | |
| if (!resultsContainer) { | |
| resultsContainer = document.createElement('div'); | |
| resultsContainer.className = 'quiz-results'; | |
| resultsContainer.style.cssText = 'margin-top: 2rem; padding: 1.5rem; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border-radius: 8px; text-align: center; font-size: 1.1rem;'; | |
| document.querySelector('.quiz-container').appendChild(resultsContainer); | |
| } | |
| let message = ''; | |
| if (percentage >= 80) { | |
| message = 'π Excellent work!'; | |
| } else if (percentage >= 60) { | |
| message = 'π Good job!'; | |
| } else { | |
| message = 'π Keep studying!'; | |
| } | |
| resultsContainer.innerHTML = ` | |
| <div style="font-size: 1.5rem; font-weight: bold; margin-bottom: 0.5rem;">π Final Score: ${score}/${total} (${percentage}%)</div> | |
| <p>${message}</p> | |
| `; | |
| resultsContainer.style.display = 'block'; | |
| resultsContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } else { | |
| alert('Please answer at least one question before submitting!'); | |
| } | |
| } | |
| """ | |
| ) | |
| return interface | |
| def launch_app(share: bool = False, debug: bool = False) -> None: | |
| """Launch the Course Creator application""" | |
| interface = create_coursecrafter_interface() | |
| interface.launch( | |
| share=share, | |
| debug=debug, | |
| server_name="0.0.0.0", | |
| server_port=7862 | |
| ) | |
| if __name__ == "__main__": | |
| launch_app(debug=True) |