Spaces:
Sleeping
Sleeping
import gradio as gr | |
from groq import Groq | |
import os | |
from PIL import Image, ImageDraw, ImageFont, ImageFilter | |
from datetime import datetime | |
import json | |
import tempfile | |
from typing import List, Dict, Tuple, Optional | |
from dataclasses import dataclass | |
import subprocess | |
import re | |
import random | |
class Question: | |
question: str | |
options: List[str] | |
correct_answer: int | |
class QuizFeedback: | |
is_correct: bool | |
selected: Optional[str] | |
correct_answer: str | |
class QuizGenerator: | |
def __init__(self, api_key: str): | |
self.client = Groq(api_key=api_key) | |
def generate_questions(self, text: str, num_questions: int) -> List[Question]: | |
"""Generate quiz questions using llama-3.2-3b-preview model""" | |
prompt = self._create_prompt(text, num_questions) | |
try: | |
# API call with simplified parameters | |
response = self.client.chat.completions.create( | |
messages=[ | |
{ | |
"role": "system", | |
"content": "You are a quiz generator. Generate multiple choice questions that are clear and focused." | |
}, | |
{ | |
"role": "user", | |
"content": prompt | |
} | |
], | |
model="llama-3.2-3b-preview", | |
temperature=0.1, | |
max_tokens=1024 | |
) | |
# Extract content safely | |
content = response.choices[0].message.content | |
if not content: | |
raise ValueError("Empty response content") | |
# Parse and validate questions | |
questions = self._parse_response(content) | |
validated = self._validate_questions(questions, num_questions) | |
if not validated: | |
raise ValueError("No valid questions generated") | |
return validated | |
except Exception as e: | |
print(f"Error in generate_questions: {str(e)}") | |
if 'response' in locals(): | |
print("Response content:", content if 'content' in locals() else None) | |
raise QuizGenerationError(f"Failed to generate questions: {str(e)}") | |
def _create_prompt(self, text: str, num_questions: int) -> str: | |
"""Create a simple, clear prompt optimized for llama-3.2-3b-preview""" | |
return f"""Create {num_questions} multiple choice questions about this text. Return only the JSON array in this exact format: | |
[ | |
{{ | |
"question": "Write the question here?", | |
"options": [ | |
"First option", | |
"Second option", | |
"Third option", | |
"Fourth option" | |
], | |
"correct_answer": 0 | |
}} | |
] | |
Rules: | |
1. Return only the JSON array | |
2. Each question must have exactly 4 options | |
3. correct_answer must be 0, 1, 2, or 3 | |
4. No explanations or additional text | |
Text to use: | |
{text.strip()}""" | |
def _parse_response(self, response_text: str) -> List[Dict]: | |
"""Parse response with improved error handling""" | |
try: | |
# Clean up the response text | |
cleaned = response_text.strip() | |
# Remove any markdown formatting | |
cleaned = cleaned.replace('```json', '').replace('```', '').strip() | |
# Find the JSON array | |
start = cleaned.find('[') | |
end = cleaned.rfind(']') + 1 | |
if start == -1 or end == 0: | |
raise ValueError("No JSON array found in response") | |
json_str = cleaned[start:end] | |
# Remove any trailing commas before closing brackets | |
json_str = re.sub(r',(\s*})', r'\1', json_str) | |
json_str = re.sub(r',(\s*])', r'\1', json_str) | |
# Try to parse the cleaned JSON | |
try: | |
return json.loads(json_str) | |
except json.JSONDecodeError: | |
# If that fails, try using ast.literal_eval as a fallback | |
import ast | |
return ast.literal_eval(json_str) | |
except Exception as e: | |
print(f"Parse error details: {str(e)}") | |
print(f"Attempted to parse: {response_text}") | |
# Last resort: try to fix the JSON manually | |
try: | |
# Remove any trailing commas and fix newlines | |
fixed = re.sub(r',(\s*[}\]])', r'\1', response_text) | |
fixed = fixed.replace('}\n{', '},{') | |
fixed = fixed.strip() | |
if not fixed.startswith('['): | |
fixed = '[' + fixed | |
if not fixed.endswith(']'): | |
fixed = fixed + ']' | |
return json.loads(fixed) | |
except: | |
raise ValueError(f"Failed to parse response: {str(e)}") | |
def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]: | |
"""Validate questions with strict checking""" | |
validated = [] | |
for q in questions[:num_questions]: | |
try: | |
# Skip invalid questions | |
if not isinstance(q, dict): | |
continue | |
# Check required fields | |
if not all(key in q for key in ['question', 'options', 'correct_answer']): | |
continue | |
# Validate options | |
if not isinstance(q['options'], list) or len(q['options']) != 4: | |
continue | |
# Validate correct_answer | |
try: | |
correct_idx = int(q['correct_answer']) | |
if not 0 <= correct_idx <= 3: | |
continue | |
except (ValueError, TypeError): | |
continue | |
# Create validated Question object | |
validated.append(Question( | |
question=str(q['question']).strip(), | |
options=[str(opt).strip() for opt in q['options']], | |
correct_answer=correct_idx | |
)) | |
except Exception as e: | |
print(f"Validation error: {str(e)}") | |
continue | |
return validated | |
def _is_valid_json(self, json_str: str) -> bool: | |
"""Check if a string is valid JSON""" | |
try: | |
json.loads(json_str) | |
return True | |
except: | |
return False | |
class FontManager: | |
"""Manages font installation and loading for the certificate generator""" | |
def install_fonts(): | |
"""Install required fonts if they're not already present""" | |
try: | |
# Install fonts package | |
subprocess.run([ | |
"apt-get", "update", "-y" | |
], check=True) | |
subprocess.run([ | |
"apt-get", "install", "-y", | |
"fonts-liberation", # Liberation Sans fonts | |
"fontconfig", # Font configuration | |
"fonts-dejavu-core" # DejaVu fonts as fallback | |
], check=True) | |
# Clear font cache | |
subprocess.run(["fc-cache", "-f"], check=True) | |
print("Fonts installed successfully") | |
except subprocess.CalledProcessError as e: | |
print(f"Warning: Could not install fonts: {e}") | |
except Exception as e: | |
print(f"Warning: Unexpected error installing fonts: {e}") | |
def get_font_paths() -> Dict[str, str]: | |
"""Get the paths to the required fonts with multiple fallbacks""" | |
standard_paths = [ | |
"/usr/share/fonts", | |
"/usr/local/share/fonts", | |
"/usr/share/fonts/truetype", | |
"~/.fonts" | |
] | |
font_paths = { | |
'regular': None, | |
'bold': None | |
} | |
# Common font filenames to try | |
fonts_to_try = { | |
'regular': [ | |
'LiberationSans-Regular.ttf', | |
'DejaVuSans.ttf', | |
'FreeSans.ttf' | |
], | |
'bold': [ | |
'LiberationSans-Bold.ttf', | |
'DejaVuSans-Bold.ttf', | |
'FreeSans-Bold.ttf' | |
] | |
} | |
def find_font(font_name: str) -> Optional[str]: | |
"""Search for a font file in standard locations""" | |
for base_path in standard_paths: | |
for root, _, files in os.walk(os.path.expanduser(base_path)): | |
if font_name in files: | |
return os.path.join(root, font_name) | |
return None | |
# Try to find each font | |
for style in ['regular', 'bold']: | |
for font_name in fonts_to_try[style]: | |
font_path = find_font(font_name) | |
if font_path: | |
font_paths[style] = font_path | |
break | |
# If no fonts found, try using fc-match as fallback | |
if not all(font_paths.values()): | |
try: | |
for style in ['regular', 'bold']: | |
if not font_paths[style]: | |
result = subprocess.run( | |
['fc-match', '-f', '%{file}', 'sans-serif:style=' + style], | |
capture_output=True, | |
text=True | |
) | |
if result.returncode == 0 and result.stdout.strip(): | |
font_paths[style] = result.stdout.strip() | |
except Exception as e: | |
print(f"Warning: Could not use fc-match to find fonts: {e}") | |
return font_paths | |
class QuizGenerationError(Exception): | |
"""Exception raised for errors in quiz generation""" | |
pass | |
class CertificateGenerator: | |
def __init__(self): | |
self.certificate_size = (1200, 800) | |
self.background_color = '#FFFFFF' | |
self.border_color = '#1C1D1F' | |
# Install fonts if needed | |
FontManager.install_fonts() | |
self.font_paths = FontManager.get_font_paths() | |
def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: | |
"""Load fonts with fallbacks""" | |
fonts = {} | |
try: | |
if self.font_paths['regular'] and self.font_paths['bold']: | |
fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 36) | |
fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 14) | |
fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 20) | |
fonts['name'] = ImageFont.truetype(self.font_paths['bold'], 32) | |
else: | |
raise ValueError("No suitable fonts found") | |
except Exception as e: | |
print(f"Font loading error: {e}. Using default font.") | |
default = ImageFont.load_default() | |
fonts = { | |
'title': default, | |
'subtitle': default, | |
'text': default, | |
'name': default | |
} | |
return fonts | |
def _add_professional_border(self, draw: ImageDraw.Draw): | |
"""Add professional border with improved corners""" | |
padding = 40 | |
border_width = 2 | |
corner_radius = 10 | |
# Draw rounded rectangle border | |
x0, y0 = padding, padding | |
x1, y1 = self.certificate_size[0] - padding, self.certificate_size[1] - padding | |
# Draw corners | |
draw.arc((x0, y0, x0 + corner_radius * 2, y0 + corner_radius * 2), 180, 270, '#1C1D1F', border_width) | |
draw.arc((x1 - corner_radius * 2, y0, x1, y0 + corner_radius * 2), 270, 0, '#1C1D1F', border_width) | |
draw.arc((x0, y1 - corner_radius * 2, x0 + corner_radius * 2, y1), 90, 180, '#1C1D1F', border_width) | |
draw.arc((x1 - corner_radius * 2, y1 - corner_radius * 2, x1, y1), 0, 90, '#1C1D1F', border_width) | |
# Draw lines | |
draw.line((x0 + corner_radius, y0, x1 - corner_radius, y0), '#1C1D1F', border_width) # Top | |
draw.line((x0 + corner_radius, y1, x1 - corner_radius, y1), '#1C1D1F', border_width) # Bottom | |
draw.line((x0, y0 + corner_radius, x0, y1 - corner_radius), '#1C1D1F', border_width) # Left | |
draw.line((x1, y0 + corner_radius, x1, y1 - corner_radius), '#1C1D1F', border_width) # Right | |
def _add_content( | |
self, | |
draw: ImageDraw.Draw, | |
fonts: Dict[str, ImageFont.FreeTypeFont], | |
name: str, | |
course_name: str, | |
score: float, | |
y_offset: int = 140 | |
): | |
"""Add content with adjusted vertical positioning""" | |
# Add "CERTIFICATE OF COMPLETION" text | |
draw.text((60, y_offset), "CERTIFICATE OF COMPLETION", font=fonts['subtitle'], fill='#666666') | |
# Add course name (large and bold) | |
course_name = course_name.strip() or "Assessment" | |
draw.text((60, y_offset + 60), course_name, font=fonts['title'], fill='#1C1D1F') | |
# Add instructor info | |
draw.text((60, y_offset + 160), "Instructor", font=fonts['subtitle'], fill='#666666') | |
draw.text((60, y_offset + 190), "CertifyMe AI", font=fonts['text'], fill='#1C1D1F') | |
# Add participant name (large) | |
name = name.strip() or "Participant" | |
draw.text((60, y_offset + 280), name, font=fonts['name'], fill='#1C1D1F') | |
# Add date and score info | |
date_str = datetime.now().strftime("%b. %d, %Y") | |
# Date section | |
draw.text((60, y_offset + 360), "Date", font=fonts['subtitle'], fill='#666666') | |
draw.text((60, y_offset + 390), date_str, font=fonts['text'], fill='#1C1D1F') | |
# Score section | |
draw.text((300, y_offset + 360), "Score", font=fonts['subtitle'], fill='#666666') | |
draw.text((300, y_offset + 390), f"{float(score):.1f}%", font=fonts['text'], fill='#1C1D1F') | |
# Footer section | |
certificate_id = f"Certificate no: {datetime.now().strftime('%Y%m%d')}-{abs(hash(name)) % 10000:04d}" | |
ref_number = f"Reference Number: {abs(hash(name + date_str)) % 10000:04d}" | |
draw.text((60, 720), certificate_id, font=fonts['subtitle'], fill='#666666') | |
draw.text((1140, 720), ref_number, font=fonts['subtitle'], fill='#666666', anchor="ra") | |
def _add_logo(self, certificate: Image.Image, logo_path: str): | |
try: | |
logo = Image.open(logo_path) | |
# Resize logo to appropriate size | |
logo.thumbnail((150, 80)) | |
# Position in top-left corner with padding | |
certificate.paste(logo, (60, 50), mask=logo if 'A' in logo.getbands() else None) | |
except Exception as e: | |
print(f"Error adding logo: {e}") | |
def _add_photo(self, certificate: Image.Image, photo_path: str): | |
"""Add a clear circular profile photo in the top-right corner with adjusted position""" | |
try: | |
if not photo_path or not os.path.exists(photo_path): | |
print(f"Photo path does not exist: {photo_path}") | |
return | |
# Open and process photo | |
photo = Image.open(photo_path) | |
# Define size for circular photo | |
size = (120, 120) | |
# Convert to RGB if not already | |
if photo.mode not in ('RGB', 'RGBA'): | |
photo = photo.convert('RGB') | |
# Create high-quality circular mask | |
mask = Image.new('L', size, 0) | |
draw = ImageDraw.Draw(mask) | |
draw.ellipse((0, 0, size[0], size[1]), fill=255) | |
# Resize photo maintaining aspect ratio | |
aspect = photo.width / photo.height | |
if aspect > 1: | |
new_height = size[1] | |
new_width = int(new_height * aspect) | |
else: | |
new_width = size[0] | |
new_height = int(new_width / aspect) | |
photo = photo.resize((new_width, max(new_height, 1)), Image.Resampling.LANCZOS) | |
# Center crop | |
if aspect > 1: | |
left = (new_width - size[0]) // 2 | |
photo = photo.crop((left, 0, left + size[0], size[1])) | |
else: | |
top = (new_height - size[1]) // 2 | |
photo = photo.crop((0, top, size[0], top + size[1])) | |
# Create circular photo | |
output = Image.new('RGBA', size, (0, 0, 0, 0)) | |
output.paste(photo, (0, 0)) | |
output.putalpha(mask) | |
# Adjusted position - moved down from top | |
photo_x = certificate.width - size[0] - 60 # 60px from right | |
photo_y = 50 # Increased from 40 to 50px from top | |
# Add white background circle | |
bg = Image.new('RGBA', size, (255, 255, 255, 255)) | |
certificate.paste(bg, (photo_x, photo_y), mask=mask) | |
# Paste the photo | |
certificate.paste(output, (photo_x, photo_y), mask=output) | |
print(f"Successfully added photo at position ({photo_x}, {photo_y})") | |
except Exception as e: | |
print(f"Error adding photo: {str(e)}") | |
import traceback | |
traceback.print_exc() | |
def generate( | |
self, | |
score: float, | |
name: str, | |
course_name: str, | |
company_logo: Optional[str] = None, | |
participant_photo: Optional[str] = None | |
) -> str: | |
"""Generate certificate with improved photo handling""" | |
try: | |
# Create base certificate | |
certificate = Image.new('RGB', self.certificate_size, self.background_color) | |
draw = ImageDraw.Draw(certificate) | |
# Add border | |
self._add_professional_border(draw) | |
# Load fonts | |
fonts = self._load_fonts() | |
# Add company logo if provided | |
if company_logo and os.path.exists(company_logo): | |
self._add_logo(certificate, company_logo) | |
# Add participant photo if provided | |
if participant_photo: | |
print(f"Processing photo: {participant_photo}") # Debug info | |
self._add_photo(certificate, participant_photo) | |
# Add content | |
self._add_content(draw, fonts, str(name), str(course_name), float(score)) | |
# Save certificate | |
return self._save_certificate(certificate) | |
except Exception as e: | |
print(f"Error generating certificate: {str(e)}") | |
import traceback | |
traceback.print_exc() | |
return None | |
def _create_base_certificate(self) -> Image.Image: | |
"""Create base certificate with improved background""" | |
# Create base image | |
certificate = Image.new('RGB', self.certificate_size, self.background_color) | |
# Add subtle gradient background (optional) | |
draw = ImageDraw.Draw(certificate) | |
# Add very subtle grain texture for professional look (optional) | |
width, height = certificate.size | |
for x in range(0, width, 4): | |
for y in range(0, height, 4): | |
if random.random() > 0.5: | |
draw.point((x, y), fill=(250, 250, 250)) | |
return certificate | |
def _save_certificate(self, certificate: Image.Image) -> str: | |
"""Save certificate with improved error handling""" | |
try: | |
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png') | |
certificate.save(temp_file.name, 'PNG', quality=95) | |
print(f"Certificate saved to: {temp_file.name}") # Debug info | |
return temp_file.name | |
except Exception as e: | |
print(f"Error saving certificate: {str(e)}") | |
return None | |
class QuizApp: | |
def __init__(self, api_key: str): | |
self.quiz_generator = QuizGenerator(api_key) | |
self.certificate_generator = CertificateGenerator() | |
self.current_questions: List[Question] = [] | |
def generate_questions(self, text: str, num_questions: int) -> Tuple[bool, List[Question]]: | |
""" | |
Generate quiz questions using the QuizGenerator | |
Returns (success, questions) tuple | |
""" | |
try: | |
questions = self.quiz_generator.generate_questions(text, num_questions) | |
self.current_questions = questions | |
return True, questions | |
except Exception as e: | |
print(f"Error generating questions: {e}") | |
return False, [] | |
def calculate_score(self, answers: List[Optional[str]]) -> Tuple[float, bool, List[QuizFeedback]]: | |
""" | |
Calculate the quiz score and generate feedback | |
Returns (score, passed, feedback) tuple | |
""" | |
if not answers or not self.current_questions: | |
return 0, False, [] | |
feedback = [] | |
correct = 0 | |
for question, answer in zip(self.current_questions, answers): | |
if answer is None: | |
feedback.append(QuizFeedback(False, None, question.options[question.correct_answer])) | |
continue | |
try: | |
selected_index = question.options.index(answer) | |
is_correct = selected_index == question.correct_answer | |
if is_correct: | |
correct += 1 | |
feedback.append(QuizFeedback( | |
is_correct, | |
answer, | |
question.options[question.correct_answer] | |
)) | |
except ValueError: | |
feedback.append(QuizFeedback(False, answer, question.options[question.correct_answer])) | |
score = (correct / len(self.current_questions)) * 100 | |
return score, score >= 80, feedback | |
def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]: | |
""" | |
Event handler for generating new questions | |
""" | |
if not text.strip(): | |
return ( | |
gr.update(value=""), | |
gr.update(value="⚠️ Please enter some text content to generate questions."), | |
*[gr.update(visible=False, choices=[]) for _ in range(5)], | |
[], | |
gr.update(selected=1) | |
) | |
success, questions = self.generate_questions(text, num_questions) | |
if not success or not questions: | |
return ( | |
gr.update(value=""), | |
gr.update(value="❌ Failed to generate questions. Please try again."), | |
*[gr.update(visible=False, choices=[]) for _ in range(5)], | |
[], | |
gr.update(selected=1) | |
) | |
# Create question display | |
questions_html = "# 📝 Assessment Questions\n\n" | |
questions_html += "> Please select one answer for each question.\n\n" | |
# Update radio buttons | |
updates = [] | |
for i, q in enumerate(questions): | |
questions_html += f"### Question {i+1}\n{q.question}\n\n" | |
updates.append(gr.update( | |
visible=True, | |
choices=q.options, | |
value=None, | |
label=f"Select your answer:" | |
)) | |
# Hide unused radio buttons | |
for i in range(len(questions), 5): | |
updates.append(gr.update(visible=False, choices=[])) | |
return ( | |
gr.update(value=questions_html), | |
gr.update(value=""), | |
*updates, | |
questions, | |
gr.update(selected=1) | |
) | |
def submit_quiz(self, q1: Optional[str], q2: Optional[str], q3: Optional[str], | |
q4: Optional[str], q5: Optional[str], questions: List[Question] | |
) -> Tuple[gr.update, List[gr.update], float, str, gr.update]: | |
""" | |
Event handler for quiz submission | |
""" | |
answers = [q1, q2, q3, q4, q5][:len(questions)] | |
if not all(a is not None for a in answers): | |
return ( | |
gr.update(value="⚠️ Please answer all questions before submitting."), | |
*[gr.update() for _ in range(5)], | |
0, | |
"", | |
gr.update(selected=1) | |
) | |
score, passed, feedback = self.calculate_score(answers) | |
# Create feedback HTML | |
feedback_html = "# Assessment Results\n\n" | |
for i, (q, f) in enumerate(zip(self.current_questions, feedback)): | |
color = "green" if f.is_correct else "red" | |
symbol = "✅" if f.is_correct else "❌" | |
feedback_html += f""" | |
### Question {i+1} | |
{q.question} | |
<div style="color: {color}; padding: 10px; margin: 5px 0; border-left: 3px solid {color};"> | |
{symbol} Your answer: {f.selected} | |
{'' if f.is_correct else f'<br>Correct answer: {f.correct_answer}'} | |
</div> | |
""" | |
# Add result message | |
if passed: | |
feedback_html += self._create_success_message(score) | |
result_msg = f"🎉 Congratulations! You passed with {score:.1f}%" | |
else: | |
feedback_html += self._create_failure_message(score) | |
result_msg = f"Score: {score:.1f}%. You need 80% to pass." | |
return ( | |
gr.update(value=feedback_html), | |
*[gr.update(visible=False) for _ in range(5)], | |
score, | |
result_msg, | |
gr.update(selected=2) | |
) | |
def _create_success_message(self, score: float) -> str: | |
return f""" | |
<div style="background-color: #e6ffe6; padding: 20px; margin-top: 20px; border-radius: 10px;"> | |
<h3 style="color: #008000;">🎉 Congratulations!</h3> | |
<p>You passed the assessment with a score of {score:.1f}%</p> | |
<p>Your certificate has been generated.</p> | |
</div> | |
""" | |
def _create_failure_message(self, score: float) -> str: | |
return f""" | |
<div style="background-color: #ffe6e6; padding: 20px; margin-top: 20px; border-radius: 10px;"> | |
<h3 style="color: #cc0000;">Please Try Again</h3> | |
<p>Your score: {score:.1f}%</p> | |
<p>You need 80% or higher to pass and receive a certificate.</p> | |
</div> | |
""" | |
def create_quiz_interface(): | |
if not os.getenv("GROQ_API_KEY"): | |
raise EnvironmentError("Please set your GROQ_API_KEY environment variable") | |
quiz_app = QuizApp(os.getenv("GROQ_API_KEY")) | |
with gr.Blocks(title="CertifyMe AI", theme=gr.themes.Soft()) as demo: | |
# State management | |
current_questions = gr.State([]) | |
current_question_idx = gr.State(0) | |
answer_state = gr.State([None] * 5) | |
# Header | |
gr.Markdown(""" | |
# 🎓 CertifyMe AI | |
### Transform Your Knowledge into Recognized Achievements | |
""") | |
with gr.Tabs() as tabs: | |
# Profile Setup Tab | |
with gr.Tab(id=1, label="📋 Step 1: Profile Setup"): | |
with gr.Row(): | |
name = gr.Textbox(label="Full Name", placeholder="Enter your full name") | |
email = gr.Textbox(label="Email", placeholder="Enter your email") | |
text_input = gr.Textbox( | |
label="Learning Content", | |
placeholder="Enter the text content you want to be assessed on", | |
lines=10 | |
) | |
num_questions = gr.Slider( | |
minimum=1, | |
maximum=20, | |
value=10, | |
step=1, | |
label="Number of Questions" | |
) | |
with gr.Row(): | |
company_logo = gr.Image(label="Company Logo (Optional)", type="filepath") | |
participant_photo = gr.Image(label="Your Photo (Optional)", type="filepath") | |
generate_btn = gr.Button("Generate Assessment", variant="primary", size="lg") | |
# Assessment Tab | |
with gr.Tab(id=2, label="📝 Step 2: Take Assessment"): | |
with gr.Column() as main_container: | |
# Questions Section | |
with gr.Column(visible=True) as question_box: | |
question_display = gr.Markdown("") | |
current_options = gr.Radio( | |
choices=[], | |
label="Select your answer:", | |
visible=False | |
) | |
with gr.Row(): | |
prev_btn = gr.Button("← Previous", variant="secondary", size="sm") | |
question_counter = gr.Markdown("Question 1") | |
next_btn = gr.Button("Next →", variant="secondary", size="sm") | |
gr.Markdown("---") # Separator | |
submit_btn = gr.Button( | |
"Submit Assessment", | |
variant="primary", | |
size="lg" | |
) | |
# Results Section in Assessment Tab | |
with gr.Column(visible=False) as results_group: | |
result_message = gr.Markdown( | |
label="Result", | |
show_label=True | |
) | |
feedback_box = gr.Markdown( | |
label="Detailed Feedback", | |
show_label=True | |
) | |
gr.Markdown("---") # Separator | |
with gr.Row(equal_height=True): | |
reset_btn = gr.Button( | |
"Reset Quiz", | |
variant="secondary", | |
size="lg", | |
visible=False | |
) | |
view_cert_btn = gr.Button( | |
"View Certificate", | |
variant="primary", | |
size="lg", | |
visible=False | |
) | |
# Certification Tab (Hidden by default) | |
with gr.Tab(id=3, label="🎓 Step 3: Get Certified", visible=False) as cert_tab: | |
score_display = gr.Number(label="Your Score", visible=False) | |
course_name = gr.Textbox( | |
label="Certification Title", | |
value="Professional Assessment Certification", | |
interactive=False # Make it non-editable | |
) | |
certificate_display = gr.Image(label="Your Certificate") | |
# Update view_cert_btn click handler to show certification tab | |
def show_certificate_tab(): | |
return [ | |
gr.update(visible=True), # Make cert_tab visible | |
gr.update(selected=3) # Switch to cert_tab | |
] | |
# Helper Functions | |
def on_generate_questions(text, num_questions): | |
if not text.strip(): | |
return [ | |
"", | |
gr.update(visible=False), | |
gr.update(choices=[], visible=False), | |
"", | |
[], | |
0, | |
[None] * 5, | |
gr.update(selected=1), | |
gr.update(visible=False), | |
gr.update(visible=False) | |
] | |
success, questions = quiz_app.generate_questions(text, num_questions) | |
if not success or not questions: | |
return [ | |
"", | |
gr.update(visible=False), | |
gr.update(choices=[], visible=False), | |
"", | |
[], | |
0, | |
[None] * 5, | |
gr.update(selected=1), | |
gr.update(visible=False), | |
gr.update(visible=False) | |
] | |
question = questions[0] | |
question_md = f"""### Question 1 | |
{question.question}""" | |
return [ | |
question_md, | |
gr.update(visible=True), | |
gr.update( | |
choices=question.options, | |
value=None, | |
visible=True, | |
label="Select your answer:" | |
), | |
f"Question 1 of {len(questions)}", | |
questions, | |
0, | |
[None] * len(questions), | |
gr.update(selected=2), | |
gr.update(visible=False), | |
gr.update(visible=False) | |
] | |
def navigate(direction, current_idx, questions, answers, current_answer): | |
if not questions: | |
return [0, answers, "", gr.update(choices=[], visible=False), "", gr.update(visible=False)] | |
new_answers = list(answers) | |
if current_answer is not None and 0 <= current_idx < len(new_answers): | |
new_answers[current_idx] = current_answer | |
new_idx = max(0, min(len(questions) - 1, current_idx + direction)) | |
question = questions[new_idx] | |
question_md = f"""### Question {new_idx + 1} | |
{question.question}""" | |
return [ | |
new_idx, | |
new_answers, | |
question_md, | |
gr.update( | |
choices=question.options, | |
value=new_answers[new_idx] if new_idx < len(new_answers) else None, | |
visible=True, | |
label="Select your answer:" | |
), | |
f"Question {new_idx + 1} of {len(questions)}", | |
gr.update(visible=True) | |
] | |
def update_answer_state(answer, idx, current_answers): | |
new_answers = list(current_answers) | |
if 0 <= idx < len(new_answers): | |
new_answers[idx] = answer | |
return new_answers | |
def reset_quiz(text, num_questions): | |
"""Handle quiz reset""" | |
return on_generate_questions(text, num_questions) | |
def view_certificate(): | |
"""Navigate to certificate tab""" | |
return gr.update(selected=3) | |
def handle_prev(current_idx, questions, answers, current_answer): | |
return navigate(-1, current_idx, questions, answers, current_answer) | |
def handle_next(current_idx, questions, answers, current_answer): | |
return navigate(1, current_idx, questions, answers, current_answer) | |
def on_submit(questions, answers, current_idx, current_answer): | |
"""Handle quiz submission with proper Markdown rendering and emojis""" | |
final_answers = list(answers) | |
if 0 <= current_idx < len(final_answers): | |
final_answers[current_idx] = current_answer | |
if not all(a is not None for a in final_answers[:len(questions)]): | |
return [ | |
"⚠️ Please answer all questions before submitting.", | |
gr.update(visible=True), | |
0, | |
"", | |
gr.update(visible=True), | |
gr.update(visible=True), | |
gr.update(visible=False), | |
gr.update(visible=False) | |
] | |
score, passed, feedback = quiz_app.calculate_score(final_answers[:len(questions)]) | |
# Create feedback content using proper Markdown with emojis | |
feedback_content = f"""# Assessment Results | |
**Score: {score:.1f}%** | |
""" | |
for i, (q, f) in enumerate(zip(questions, feedback)): | |
icon = "✅" if f.is_correct else "❌" | |
color = "green" if f.is_correct else "red" | |
# Using markdown syntax with color formatting | |
feedback_content += f"""### Question {i+1} | |
{q.question} | |
{icon} **Your answer:** {f.selected or 'No answer'} | |
{'' if f.is_correct else f'**Correct answer:** {f.correct_answer}'} | |
""" | |
# Add summary box | |
if passed: | |
feedback_content += f""" | |
--- | |
## 🎉 Congratulations! | |
You passed with a score of {score:.1f}%! | |
""" | |
else: | |
feedback_content += f""" | |
--- | |
## Need Improvement | |
You scored {score:.1f}%. You need 80% or higher to pass. | |
Please try again. | |
""" | |
return [ | |
feedback_content, # feedback_box | |
gr.update(visible=True), # results_group | |
score, # score_display | |
f"Score: {score:.1f}%", # result_message | |
gr.update(visible=False), # question_box | |
gr.update(visible=not passed), # reset_btn | |
gr.update(visible=passed), # view_cert_btn | |
gr.update(selected=2) # tabs | |
] | |
# Event Handlers | |
generate_btn.click( | |
fn=on_generate_questions, | |
inputs=[text_input, num_questions], | |
outputs=[ | |
question_display, | |
question_box, | |
current_options, | |
question_counter, | |
current_questions, | |
current_question_idx, | |
answer_state, | |
tabs, | |
results_group, | |
view_cert_btn | |
] | |
) | |
prev_btn.click( | |
fn=handle_prev, | |
inputs=[current_question_idx, current_questions, answer_state, current_options], | |
outputs=[current_question_idx, answer_state, question_display, current_options, question_counter, question_box] | |
) | |
next_btn.click( | |
fn=handle_next, | |
inputs=[current_question_idx, current_questions, answer_state, current_options], | |
outputs=[current_question_idx, answer_state, question_display, current_options, question_counter, question_box] | |
) | |
submit_btn.click( | |
fn=on_submit, | |
inputs=[current_questions, answer_state, current_question_idx, current_options], | |
outputs=[ | |
feedback_box, | |
results_group, | |
score_display, | |
result_message, # Now properly defined | |
question_box, | |
reset_btn, | |
view_cert_btn, | |
tabs | |
] | |
) | |
reset_btn.click( | |
fn=on_generate_questions, | |
inputs=[text_input, num_questions], | |
outputs=[ | |
question_display, | |
question_box, | |
current_options, | |
question_counter, | |
current_questions, | |
current_question_idx, | |
answer_state, | |
tabs, | |
results_group, | |
view_cert_btn | |
] | |
) | |
view_cert_btn.click( | |
fn=show_certificate_tab, | |
outputs=[cert_tab, tabs] | |
) | |
current_options.change( | |
fn=update_answer_state, | |
inputs=[current_options, current_question_idx, answer_state], | |
outputs=answer_state | |
) | |
score_display.change( | |
fn=lambda s, n, c, l, p: quiz_app.certificate_generator.generate(s, n, c, l, p) or gr.update(value=None), | |
inputs=[score_display, name, course_name, company_logo, participant_photo], | |
outputs=certificate_display | |
) | |
return demo | |
if __name__ == "__main__": | |
demo = create_quiz_interface() | |
demo.launch() |