|
""" |
|
π¨ 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>" |
|
|
|
|
|
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", "") |
|
|
|
|
|
if content: |
|
try: |
|
|
|
import markdown |
|
md = markdown.Markdown(extensions=['extra', 'codehilite']) |
|
content_html = md.convert(content) |
|
except ImportError: |
|
|
|
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>" |
|
|
|
|
|
image_html = "" |
|
if image_description: |
|
|
|
images = lesson.get("images", []) |
|
if images and len(images) > 0: |
|
|
|
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: |
|
|
|
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>" |
|
|
|
|
|
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") |
|
|
|
|
|
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>" |
|
|
|
|
|
quiz_id = f"quiz_{abs(hash(str(questions)))%10000}" |
|
|
|
|
|
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}"> |
|
""" |
|
|
|
|
|
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"> |
|
""" |
|
|
|
|
|
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> |
|
""" |
|
|
|
|
|
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: |
|
|
|
|
|
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> |
|
""") |
|
|
|
|
|
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)" |
|
) |
|
|
|
|
|
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" |
|
) |
|
|
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
|
|
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" |
|
) |
|
|
|
|
|
gr.HTML("<hr><h3>π¬ Course Assistant</h3>") |
|
|
|
|
|
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 = 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_state = gr.State({}) |
|
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"] |
|
|
|
|
|
course_context = {"content": "", "topic": "", "agent": None} |
|
|
|
|
|
def on_provider_change(provider): |
|
if provider == "openai_compatible": |
|
return gr.update(visible=True) |
|
else: |
|
return gr.update(visible=False) |
|
|
|
|
|
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: |
|
|
|
progress(0, desc="π Initializing Course Generator...") |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
agent = SimpleCourseAgent() |
|
|
|
|
|
config_kwargs = {} |
|
if provider == "openai_compatible": |
|
config_kwargs["base_url"] = endpoint_url |
|
config_kwargs["model"] = model_name |
|
|
|
|
|
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 |
|
|
|
|
|
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...") |
|
|
|
|
|
options = GenerationOptions( |
|
difficulty=DifficultyLevel(difficulty), |
|
lesson_count=lessons, |
|
include_images=True, |
|
include_flashcards=True, |
|
include_quizzes=True |
|
) |
|
|
|
progress(0.15, desc="π Checking 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...") |
|
|
|
|
|
|
|
|
|
|
|
lessons_html = "" |
|
flashcards_html = "" |
|
quizzes_html = "" |
|
|
|
|
|
course_data = None |
|
current_progress = 0.2 |
|
|
|
|
|
chunk_count = 0 |
|
max_expected_chunks = 10 |
|
|
|
async for chunk in agent.generate_course(topic, options): |
|
chunk_count += 1 |
|
print(f"π Progress Debug: Received chunk type='{chunk.type}', content='{chunk.content}'") |
|
|
|
|
|
if chunk.type == "progress": |
|
|
|
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_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...") |
|
|
|
try: |
|
course_data = json.loads(chunk.content) |
|
except: |
|
course_data = None |
|
|
|
progress(0.97, desc="π¨ Processing course content...") |
|
|
|
|
|
if course_data: |
|
course_context["content"] = course_data |
|
|
|
|
|
lessons_html = format_lessons(course_data.get("lessons", [])) |
|
|
|
|
|
flashcards_html = format_flashcards(course_data.get("flashcards", [])) |
|
|
|
|
|
quiz_data = course_data.get("quiz", {}) |
|
quizzes_html = format_quiz(quiz_data) |
|
|
|
|
|
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)})") |
|
|
|
|
|
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...") |
|
|
|
|
|
images = [] |
|
image_details_list = [] |
|
|
|
|
|
for lesson in course_data.get("lessons", []): |
|
lesson_images = lesson.get("images", []) |
|
for i, img in enumerate(lesson_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", "Educational image")) |
|
|
|
|
|
if image_url.startswith('data:image/'): |
|
import base64 |
|
import tempfile |
|
import os |
|
|
|
|
|
header, data = image_url.split(',', 1) |
|
image_data = base64.b64decode(data) |
|
|
|
|
|
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' |
|
|
|
|
|
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) |
|
continue |
|
|
|
elif image_url.startswith('http'): |
|
|
|
images.append(image_url) |
|
image_details_list.append({ |
|
"url": image_url, |
|
"caption": alt_text, |
|
"lesson": lesson.get("title", "Unknown lesson") |
|
}) |
|
else: |
|
|
|
if len(image_url) <= 260: |
|
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): |
|
|
|
if img.startswith('data:image/'): |
|
|
|
import base64 |
|
import tempfile |
|
import os |
|
|
|
try: |
|
header, data = img.split(',', 1) |
|
image_data = base64.b64decode(data) |
|
|
|
|
|
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' |
|
|
|
|
|
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) |
|
continue |
|
except Exception as e: |
|
print(f"β οΈ Error processing base64 image {i}: {e}") |
|
continue |
|
else: |
|
|
|
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 |
|
|
|
|
|
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")) |
|
|
|
|
|
if image_url.startswith('data:image/'): |
|
import base64 |
|
import tempfile |
|
import os |
|
|
|
try: |
|
header, data = image_url.split(',', 1) |
|
image_data = base64.b64decode(data) |
|
|
|
|
|
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' |
|
|
|
|
|
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) |
|
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/'): |
|
|
|
import base64 |
|
import tempfile |
|
import os |
|
|
|
try: |
|
header, data = img.split(',', 1) |
|
image_data = base64.b64decode(data) |
|
|
|
|
|
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' |
|
|
|
|
|
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) |
|
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") |
|
|
|
|
|
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""" |
|
|
|
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: |
|
|
|
agent = course_context["agent"] |
|
course_data = course_context["content"] |
|
topic = course_context["topic"] |
|
|
|
|
|
course_context_text = f"Course Topic: {topic}\n\n" |
|
|
|
|
|
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" |
|
|
|
|
|
flashcards = course_data.get("flashcards", []) |
|
if flashcards: |
|
course_context_text += "Flashcards:\n" |
|
for card in flashcards[:5]: |
|
course_context_text += f"Q: {card.get('question', '')} A: {card.get('answer', '')}\n" |
|
course_context_text += "\n" |
|
|
|
|
|
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:""" |
|
|
|
|
|
provider = agent.default_provider |
|
available_providers = agent.get_available_providers() |
|
if provider not in available_providers: |
|
|
|
provider = available_providers[0] if available_providers else None |
|
|
|
if provider: |
|
|
|
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) |
|
|
|
|
|
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." |
|
|
|
|
|
existing_messages = "" |
|
if current_chat and "chat-message" in current_chat: |
|
|
|
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] |
|
|
|
import re |
|
messages_match = re.findall(r'<div class="chat-message.*?</div>\s*</div>', existing_content, re.DOTALL) |
|
existing_messages = ''.join(messages_match) |
|
|
|
|
|
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, "" |
|
|
|
|
|
|
|
|
|
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] |
|
) |
|
|
|
|
|
quiz_submit_btn.click( |
|
fn=None, |
|
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) |