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 gemma2-9b-it""" | |
| 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="gemma2-9b-it", | |
| temperature=0.1, | |
| max_tokens=6000 | |
| ) | |
| # 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() |