Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from groq import Groq | |
| import os | |
| from PIL import Image, ImageDraw, ImageFont | |
| from datetime import datetime | |
| import json | |
| import tempfile | |
| from typing import List, Dict, Tuple, Optional | |
| from dataclasses import dataclass | |
| import subprocess | |
| 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]: | |
| prompt = self._create_prompt(text, num_questions) | |
| try: | |
| response = self.client.chat.completions.create( | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": "You are a quiz generator. Create clear questions with concise answer options." | |
| }, | |
| { | |
| "role": "user", | |
| "content": prompt | |
| } | |
| ], | |
| model="llama-3.2-3b-preview", | |
| temperature=0.2, | |
| max_tokens=2048 | |
| ) | |
| questions = self._parse_response(response.choices[0].message.content) | |
| return self._validate_questions(questions, num_questions) | |
| except Exception as e: | |
| raise QuizGenerationError(f"Failed to generate questions: {str(e)}") | |
| def _create_prompt(self, text: str, num_questions: int) -> str: | |
| return f"""Create exactly {num_questions} multiple choice questions based on this text: | |
| {text} | |
| For each question: | |
| 1. Create a clear, concise question | |
| 2. Provide exactly 4 options | |
| 3. Mark the correct answer with the index (0-3) | |
| 4. Ensure options are concise and clear | |
| Return ONLY a JSON array with this EXACT format - no other text: | |
| [ | |
| {{ | |
| "question": "Question text here?", | |
| "options": [ | |
| "Brief option 1", | |
| "Brief option 2", | |
| "Brief option 3", | |
| "Brief option 4" | |
| ], | |
| "correct_answer": 0 | |
| }} | |
| ] | |
| Keep all options concise (10 words or less each). | |
| """ | |
| def _parse_response(self, response_text: str) -> List[Dict]: | |
| response_text = response_text.replace("```json", "").replace("```", "").strip() | |
| start_idx = response_text.find("[") | |
| end_idx = response_text.rfind("]") | |
| if start_idx == -1 or end_idx == -1: | |
| raise ValueError("No valid JSON array found in response") | |
| response_text = response_text[start_idx:end_idx + 1] | |
| return json.loads(response_text) | |
| def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]: | |
| validated = [] | |
| for q in questions: | |
| if not self._is_valid_question(q): | |
| continue | |
| validated.append(Question( | |
| question=q["question"].strip(), | |
| options=[opt.strip()[:100] for opt in q["options"]], | |
| correct_answer=int(q["correct_answer"]) % 4 | |
| )) | |
| if not validated: | |
| raise ValueError("No valid questions after validation") | |
| return validated[:num_questions] | |
| def _is_valid_question(self, question: Dict) -> bool: | |
| return ( | |
| all(key in question for key in ["question", "options", "correct_answer"]) and | |
| isinstance(question["options"], list) and | |
| len(question["options"]) == 4 and | |
| all(isinstance(opt, str) for opt in question["options"]) | |
| ) | |
| 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): | |
| # Single elegant inner border with padding | |
| padding = 40 | |
| draw.rectangle( | |
| [(padding, padding), | |
| (self.certificate_size[0] - padding, self.certificate_size[1] - padding)], | |
| outline='#1C1D1F', | |
| width=2 | |
| ) | |
| def _add_content( | |
| self, | |
| draw: ImageDraw.Draw, | |
| fonts: Dict[str, ImageFont.FreeTypeFont], | |
| name: str, | |
| course_name: str, | |
| score: float | |
| ): | |
| # Add "CERTIFICATE OF COMPLETION" text | |
| draw.text((60, 140), "CERTIFICATE OF COMPLETION", font=fonts['subtitle'], fill='#666666') | |
| # Add course name (large and bold) | |
| course_name = course_name.strip() or "Assessment" | |
| text_width = draw.textlength(course_name, fonts['title']) | |
| max_width = self.certificate_size[0] - 120 # Leave margins | |
| if text_width > max_width: | |
| words = course_name.split() | |
| lines = [] | |
| current_line = [] | |
| current_width = 0 | |
| for word in words: | |
| word_width = draw.textlength(word + " ", fonts['title']) | |
| if current_width + word_width <= max_width: | |
| current_line.append(word) | |
| current_width += word_width | |
| else: | |
| lines.append(" ".join(current_line)) | |
| current_line = [word] | |
| current_width = word_width | |
| if current_line: | |
| lines.append(" ".join(current_line)) | |
| course_name = "\n".join(lines) | |
| draw.multiline_text((60, 200), course_name, font=fonts['title'], fill='#1C1D1F', spacing=10) | |
| # Add instructor info | |
| draw.text((60, 300), "Instructor", font=fonts['subtitle'], fill='#666666') | |
| draw.text((60, 330), "CertifyMe AI", font=fonts['text'], fill='#1C1D1F') | |
| # Add participant name (large) | |
| name = name.strip() or "Participant" | |
| draw.text((60, 420), name, font=fonts['name'], fill='#1C1D1F') | |
| # Add date and score info with spacing | |
| date_str = datetime.now().strftime("%b. %d, %Y") | |
| # Date section | |
| draw.text((60, 500), "Date", font=fonts['subtitle'], fill='#666666') | |
| draw.text((60, 530), date_str, font=fonts['text'], fill='#1C1D1F') | |
| # Score section | |
| draw.text((300, 500), "Score", font=fonts['subtitle'], fill='#666666') | |
| draw.text((300, 530), f"{float(score):.1f}%", font=fonts['text'], fill='#1C1D1F') | |
| # Footer section with certificate number and reference | |
| 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 footer text aligned to left and right | |
| 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): | |
| try: | |
| photo = Image.open(photo_path) | |
| # Create circular mask | |
| size = (100, 100) | |
| 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 | |
| photo.thumbnail(size) | |
| # Create a circular photo | |
| output = Image.new('RGBA', size, (0, 0, 0, 0)) | |
| output.paste(photo, (0, 0)) | |
| output.putalpha(mask) | |
| # Position in top-right corner with padding | |
| certificate.paste(output, (1000, 50), mask=output) | |
| except Exception as e: | |
| print(f"Error adding photo: {e}") | |
| def generate( | |
| self, | |
| score: float, | |
| name: str, | |
| course_name: str, | |
| company_logo: Optional[str] = None, | |
| participant_photo: Optional[str] = None | |
| ) -> str: | |
| try: | |
| certificate = self._create_base_certificate() | |
| draw = ImageDraw.Draw(certificate) | |
| # Add professional border | |
| self._add_professional_border(draw) | |
| fonts = self._load_fonts() | |
| self._add_content(draw, fonts, str(name), str(course_name), float(score)) | |
| if company_logo: | |
| self._add_logo(certificate, company_logo) | |
| if participant_photo: | |
| self._add_photo(certificate, participant_photo) | |
| return self._save_certificate(certificate) | |
| except Exception as e: | |
| print(f"Error generating certificate: {e}") | |
| return None | |
| def _create_base_certificate(self) -> Image.Image: | |
| return Image.new('RGB', self.certificate_size, self.background_color) | |
| def _save_certificate(self, certificate: Image.Image) -> str: | |
| temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png') | |
| certificate.save(temp_file.name, 'PNG', quality=95) | |
| return temp_file.name | |
| 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") as assessment_tab: | |
| 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") | |
| submit_btn = gr.Button( | |
| "Submit Assessment", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| # Results Section | |
| with gr.Column(visible=False) as results_group: | |
| feedback_box = gr.Markdown("") | |
| with gr.Row(): | |
| 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 | |
| with gr.Tab(id=3, label="🎓 Step 3: Get Certified"): | |
| with gr.Column(): | |
| score_display = gr.Number(label="Your Score", visible=False) | |
| course_name = gr.Textbox( | |
| label="Certification Title", | |
| value="Professional Assessment Certification" | |
| ) | |
| certificate_display = gr.Image(label="Your Certificate") | |
| # Helper Functions | |
| def on_generate_questions(text, num_questions): | |
| """Generate quiz questions and setup initial state""" | |
| if not text.strip(): | |
| return [ | |
| gr.Markdown("⚠️ Please enter some text content to generate questions."), | |
| gr.update(visible=False), | |
| gr.update(choices=[], visible=False), | |
| "", | |
| [], | |
| 0, | |
| [None] * 5, | |
| gr.Tabs(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.Markdown("❌ Failed to generate questions. Please try again."), | |
| gr.update(visible=False), | |
| gr.update(choices=[], visible=False), | |
| "", | |
| [], | |
| 0, | |
| [None] * 5, | |
| gr.Tabs(selected=1), | |
| gr.update(visible=False), | |
| gr.update(visible=False) | |
| ] | |
| # Setup initial question | |
| question = questions[0] | |
| question_html = f"""### Question 1 | |
| {question.question}""" | |
| return [ | |
| gr.Markdown(question_html), | |
| 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.Tabs(selected=2), | |
| gr.update(visible=False), | |
| gr.update(visible=False) | |
| ] | |
| def navigate(direction, current_idx, questions, answers, current_answer): | |
| """Handle navigation between questions""" | |
| if not questions: | |
| return [0, answers, "", gr.update(choices=[], visible=False), "", gr.update(visible=False)] | |
| # Update current answer in state | |
| new_answers = list(answers) | |
| if current_answer is not None and 0 <= current_idx < len(new_answers): | |
| new_answers[current_idx] = current_answer | |
| # Calculate new index | |
| new_idx = max(0, min(len(questions) - 1, current_idx + direction)) | |
| question = questions[new_idx] | |
| # Format question display | |
| question_html = f"""### Question {new_idx + 1} | |
| {question.question}""" | |
| return [ | |
| new_idx, | |
| new_answers, | |
| gr.Markdown(question_html), | |
| gr.update( | |
| choices=question.options, | |
| value=new_answers[new_idx] if new_idx < len(new_answers) else None, | |
| visible=True, | |
| label=f"Select your answer:" | |
| ), | |
| f"Question {new_idx + 1} of {len(questions)}", | |
| gr.update(visible=True) | |
| ] | |
| 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 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.Tabs(selected=2) | |
| def on_submit(questions, answers, current_idx, current_answer): | |
| """Handle quiz submission with proper HTML rendering""" | |
| 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 [ | |
| gr.Markdown("⚠️ 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), | |
| gr.update(visible=False) | |
| ] | |
| score, passed, feedback = quiz_app.calculate_score(final_answers[:len(questions)]) | |
| # Create feedback content using proper HTML rendering | |
| feedback_content = f"""# Assessment Results | |
| {' '.join(['✅' if f.is_correct else '❌' for f in feedback])} Score: {score:.1f}% | |
| """ | |
| for i, (q, f) in enumerate(zip(questions, feedback)): | |
| feedback_content += f"""### Question {i+1} | |
| {q.question} | |
| {'✅' if f.is_correct else '❌'} Your answer: **{f.selected or 'No answer'}** | |
| {' ' if f.is_correct else f' \nCorrect answer: **{f.correct_answer}**'} | |
| """ | |
| # Add summary box | |
| if passed: | |
| feedback_content += """ | |
| --- | |
| ### 🎉 Congratulations! | |
| You passed the assessment! Click the "View Certificate" button below to get your certificate. | |
| """ | |
| else: | |
| feedback_content += """ | |
| --- | |
| ### Try Again | |
| You need 80% or higher to pass. Click "Reset Quiz" to try again. | |
| """ | |
| return [ | |
| gr.Markdown(feedback_content), # feedback_box | |
| gr.update(visible=True), # results_group | |
| score, # score_display | |
| "🎉 Passed!" if passed else "Please try again", # result_message | |
| gr.update(visible=False), # question_box | |
| gr.update(visible=not passed), # reset_btn | |
| gr.update(visible=passed), # view_cert_btn | |
| gr.update(visible=True), # back_to_assessment | |
| gr.update(visible=False) # profile_tab | |
| ] | |
| # 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=lambda *args: navigate(-1, *args), | |
| 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=lambda *args: navigate(1, *args), | |
| 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, | |
| question_box, | |
| reset_btn, | |
| view_cert_btn, | |
| back_to_assessment, | |
| 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=lambda: gr.Tabs(selected=3), | |
| outputs=tabs | |
| ) | |
| 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() | |