|
import logging |
|
import os |
|
import subprocess |
|
import sys |
|
import tempfile |
|
from typing import List, Tuple, Optional |
|
import gradio as gr |
|
from components.state import SessionState, LearningUnit, ExplanationResponse, get_unit_status_emoji |
|
from agents.models import CodeExample |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
logger.setLevel(logging.INFO) |
|
|
|
def create_new_session_copy(session: SessionState) -> SessionState: |
|
"""Creates a deep copy of the session state to ensure immutability for Gradio.""" |
|
return session.model_copy() |
|
|
|
def run_code_snippet(code: str) -> str: |
|
"""Executes a Python code snippet and returns its output.""" |
|
try: |
|
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py', encoding='utf-8') as tmp_file: |
|
tmp_file.write(code) |
|
tmp_file_path = tmp_file.name |
|
|
|
|
|
process = subprocess.run( |
|
[sys.executable, tmp_file_path], |
|
capture_output=True, |
|
text=True, |
|
check=False, |
|
encoding='utf-8' |
|
) |
|
|
|
|
|
os.remove(tmp_file_path) |
|
|
|
if process.returncode == 0: |
|
return process.stdout.strip() |
|
else: |
|
return f"Error:\n{process.stderr.strip()}" |
|
except Exception as e: |
|
return f"Execution failed: {e}" |
|
|
|
def update_progress_display(session: SessionState) -> Tuple[gr.Markdown, gr.Markdown, gr.Markdown, gr.HTML, gr.Dataframe]: |
|
"""Updates the progress display components based on the current session state.""" |
|
summary = session.get_progress_summary() |
|
total_units = summary["total_units"] |
|
completed_units = summary["completed_units"] |
|
in_progress_units = summary["in_progress_units"] |
|
|
|
average_score = session.get_average_quiz_score() |
|
|
|
|
|
completed_stats_card = gr.Markdown(f""" |
|
<div style="background: rgba(51, 65, 85, 0.6); padding: 20px; border-radius: 12px; text-align: center;"> |
|
<h3 style="color: #10b981; margin-top: 0; font-size: 1.5em;">β
Completed</h3> |
|
<p style="color: white; font-size: 2.5em; font-weight: 700; margin: 5px 0;">{completed_units}</p> |
|
<p style="color: #94a3b8; margin-bottom: 0;">Units mastered</p> |
|
</div> |
|
""") |
|
|
|
in_progress_stats_card = gr.Markdown(f""" |
|
<div style="background: rgba(51, 65, 85, 0.6); padding: 20px; border-radius: 12px; text-align: center;"> |
|
<h3 style="color: #3b82f6; margin-top: 0; font-size: 1.5em;">π In Progress</h3> |
|
<p style="color: white; font-size: 2.5em; font-weight: 700; margin: 5px 0;">{in_progress_units}</p> |
|
<p style="color: #94a3b8; margin-bottom: 0;">Units learning</p> |
|
</div> |
|
""") |
|
|
|
average_score_stats_card = gr.Markdown(f""" |
|
<div style="background: rgba(51, 65, 85, 0.6); padding: 20px; border-radius: 12px; text-align: center;"> |
|
<h3 style="color: #f59e0b; margin-top: 0; font-size: 1.5em;">π― Average Score</h3> |
|
<p style="color: white; font-size: 2.5em; font-weight: 700; margin: 5px 0;">{average_score:.0f}%</p> |
|
<p style="color: #94a3b8; margin-bottom: 0;">Quiz performance</p> |
|
</div> |
|
""") |
|
|
|
|
|
data = [] |
|
for i, unit in enumerate(session.units): |
|
status_emoji = get_unit_status_emoji(unit) |
|
quiz_score_display = "N/A" |
|
unit_total_questions = 0 |
|
unit_answered_questions = 0 |
|
|
|
if unit.quiz_data: |
|
|
|
unit_correct_questions = 0 |
|
|
|
if unit.quiz_data.mcqs: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.mcqs if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.mcqs) |
|
unit_answered_questions += sum(1 for q in unit.quiz_data.mcqs if q.user_answer is not None) |
|
|
|
if unit.quiz_data.true_false: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.true_false if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.true_false) |
|
unit_answered_questions += sum(1 for q in unit.quiz_data.true_false if q.user_answer is not None) |
|
|
|
if unit.quiz_data.fill_in_the_blank: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.fill_in_the_blank if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.fill_in_the_blank) |
|
unit_answered_questions += sum(1 for q in unit.quiz_data.fill_in_the_blank if q.user_answer is not None) |
|
|
|
if unit.quiz_data.open_ended: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.open_ended if q.score is not None and q.score >= 5) |
|
unit_total_questions += len(unit.quiz_data.open_ended) |
|
unit_answered_questions += sum(1 for q in unit.quiz_data.open_ended if q.user_answer is not None) |
|
|
|
if unit_total_questions > 0: |
|
quiz_score_display = f"{int((unit_correct_questions / unit_total_questions) * 100)}%" |
|
|
|
progress_percentage = 0 |
|
if unit.status == "completed": |
|
progress_percentage = 100 |
|
elif unit.status == "in_progress": |
|
if unit_total_questions > 0: |
|
progress_percentage = int((unit_answered_questions / unit_total_questions) * 100) |
|
else: |
|
|
|
progress_percentage = 0 |
|
|
|
data.append([ |
|
f"{i+1}. {unit.title}", |
|
f"{status_emoji} {unit.status.replace('_', ' ').title()}", |
|
quiz_score_display, |
|
progress_percentage |
|
]) |
|
|
|
|
|
overall_progress_percentage = 0 |
|
if total_units > 0: |
|
overall_progress_percentage = int((completed_units / total_units) * 100) |
|
|
|
overall_progress_html = gr.HTML(f""" |
|
<div style="background: rgba(51, 65, 85, 0.6); padding: 20px; border-radius: 12px; margin: 10px 0;"> |
|
<h3 style="color: #10b981; margin-top: 0;">Total Course Progress: {overall_progress_percentage}%</h3> |
|
<div style="background: rgba(30, 41, 59, 0.8); border-radius: 8px; height: 20px; overflow: hidden;"> |
|
<div style="background: linear-gradient(135deg, #10b981, #059669); height: 100%; width: {overall_progress_percentage}%; transition: width 0.5s ease;"></div> |
|
</div> |
|
<p style="color: #94a3b8; margin-bottom: 0;">Keep going! You're making great progress.</p> |
|
</div> |
|
""") |
|
|
|
return ( |
|
completed_stats_card, |
|
in_progress_stats_card, |
|
average_score_stats_card, |
|
overall_progress_html, |
|
gr.Dataframe(value=data, |
|
headers=["Learning Unit", "Status", "Quiz Score", "Progress"], |
|
datatype=["str", "str", "str", "number"], |
|
interactive=False) |
|
) |
|
|
|
def format_unit_info_markdown(unit: LearningUnit, content_preview_length: int = 300) -> str: |
|
"""Formats the current unit's information into a Markdown string.""" |
|
content_preview = unit.content_raw[:content_preview_length] + "..." if len(unit.content_raw) > content_preview_length else unit.content_raw |
|
return f""" |
|
### Current Unit: {unit.title} |
|
**Status:** {get_unit_status_emoji(unit)} {unit.status.replace('_', ' ').title()} \n |
|
**Summary:** {unit.summary} |
|
""" |
|
|
|
def format_units_display_markdown(units: List[LearningUnit]) -> str: |
|
"""Formats a list of learning units into a Markdown string for display.""" |
|
if not units: |
|
return "No units generated yet." |
|
|
|
markdown_output = "### Generated Learning Units:\n\n" |
|
for i, unit in enumerate(units): |
|
status_emoji = get_unit_status_emoji(unit) |
|
markdown_output += f"- {status_emoji} **{i+1}. {unit.title}**\n" |
|
markdown_output += f" *Summary*: {unit.summary}\n" |
|
if unit.explanation: |
|
markdown_output += f" *Explanation Generated*: Yes\n" |
|
if unit.quiz_data: |
|
markdown_output += f" *Quiz Generated*: Yes\n" |
|
|
|
unit_correct_questions = 0 |
|
unit_total_questions = 0 |
|
if unit.quiz_data.mcqs: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.mcqs if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.mcqs) |
|
if unit.quiz_data.true_false: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.true_false if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.true_false) |
|
if unit.quiz_data.fill_in_the_blank: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.fill_in_the_blank if q.is_correct) |
|
unit_total_questions += len(unit.quiz_data.fill_in_the_blank) |
|
if unit.quiz_data.open_ended: |
|
unit_correct_questions += sum(1 for q in unit.quiz_data.open_ended if q.score is not None and q.score >= 5) |
|
unit_total_questions += len(unit.quiz_data.open_ended) |
|
|
|
if unit_total_questions > 0: |
|
markdown_output += f" *Quiz Score*: {int((unit_correct_questions / unit_total_questions) * 100)}%\n" |
|
markdown_output += "\n" |
|
return markdown_output |
|
|
|
def format_unit_dropdown_choices(units: List[LearningUnit]) -> Tuple[List[str], Optional[str]]: |
|
"""Formats a list of learning units for dropdown choices and returns a default value.""" |
|
if not units: |
|
return ["No units available"], None |
|
choices = [f"{i+1}. {unit.title}" for i, unit in enumerate(units)] |
|
default_value = choices[0] if choices else None |
|
return choices, default_value |
|
|
|
def format_mcq_feedback(is_correct: bool, correct_answer: str, explanation: str) -> str: |
|
"""Formats the feedback for an MCQ question.""" |
|
feedback_class = "correct-feedback" if is_correct else "incorrect-feedback" |
|
status = "Correct!" if is_correct else "Incorrect." |
|
return f""" |
|
<div class="{feedback_class}"> |
|
<p><strong>{status}</strong></p> |
|
<p>The correct answer was: <strong>{correct_answer}</strong></p> |
|
<p>Explanation: {explanation}</p> |
|
</div> |
|
""" |
|
|
|
def process_explanation_for_rendering(explanation_data: ExplanationResponse) -> Tuple[str, List[CodeExample]]: |
|
""" |
|
Processes the explanation data to prepare it for Gradio Markdown rendering, |
|
inserting placeholders for code blocks. |
|
""" |
|
processed_markdown = explanation_data.markdown |
|
code_examples_for_ui = [] |
|
|
|
|
|
|
|
for i, visual_aid in enumerate(explanation_data.visual_aids): |
|
if visual_aid.type == "image" and visual_aid.path: |
|
|
|
processed_markdown = processed_markdown.replace( |
|
f"[FIGURE: {i}]", |
|
f"" |
|
) |
|
|
|
|
|
for i, code_example in enumerate(explanation_data.code_examples): |
|
|
|
processed_markdown = processed_markdown.replace( |
|
f"[CODE: {i}]", |
|
f"[CODE_INSERTION_POINT_{i}]" |
|
) |
|
code_examples_for_ui.append(code_example) |
|
|
|
return processed_markdown, code_examples_for_ui |
|
|