Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| CCC Plus Quiz App - Completely Rewritten for Better UX | |
| A modern, intuitive quiz application with improved interface and functionality | |
| """ | |
| import argparse | |
| import logging | |
| import os | |
| import tempfile | |
| import json | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Optional, Tuple, List, Dict, Any | |
| import gradio as gr | |
| import pandas as pd | |
| from reportlab.lib import colors | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.units import mm | |
| from reportlab.pdfgen import canvas | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(message)s" | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Configuration | |
| DEFAULT_CSV_PATH = "quiz.csv" | |
| REQUIRED_COLUMNS = {"question", "option1", "option2", "option3", "option4", "answer"} | |
| class QuizApp: | |
| """Main Quiz Application Class""" | |
| def __init__(self): | |
| self.reset_state() | |
| def reset_state(self): | |
| """Reset all quiz state variables""" | |
| self.quiz_data = [] | |
| self.current_index = 0 | |
| self.score = 0 | |
| self.user_answers = [] | |
| self.quiz_started = False | |
| self.quiz_completed = False | |
| self.start_time = None | |
| self.end_time = None | |
| self.user_name = "" | |
| def load_csv(self, file_path: str) -> Tuple[bool, str]: | |
| """Load and validate CSV file""" | |
| try: | |
| if not file_path or not os.path.exists(file_path): | |
| return False, "β File not found. Please check the file path." | |
| # Try different encodings | |
| df = None | |
| for encoding in ['utf-8', 'latin1', 'cp1252', 'iso-8859-1']: | |
| try: | |
| df = pd.read_csv(file_path, encoding=encoding) | |
| break | |
| except UnicodeDecodeError: | |
| continue | |
| if df is None: | |
| return False, "β Could not read the CSV file. Please check the file encoding." | |
| # Normalize column names | |
| df.columns = df.columns.str.strip().str.lower() | |
| # Check required columns | |
| missing_cols = REQUIRED_COLUMNS - set(df.columns) | |
| if missing_cols: | |
| return False, f"β Missing required columns: {', '.join(missing_cols)}" | |
| # Process questions | |
| self.quiz_data = [] | |
| for idx, row in df.iterrows(): | |
| try: | |
| question = str(row['question']).strip() | |
| if not question or question.lower() in ['nan', 'none', '']: | |
| continue | |
| options = [ | |
| str(row['option1']).strip(), | |
| str(row['option2']).strip(), | |
| str(row['option3']).strip(), | |
| str(row['option4']).strip() | |
| ] | |
| answer = str(row['answer']).strip() | |
| # Validate answer exists in options | |
| if answer not in options: | |
| logger.warning(f"Question {idx+1}: Answer '{answer}' not found in options") | |
| continue | |
| self.quiz_data.append({ | |
| 'question': question, | |
| 'options': options, | |
| 'answer': answer | |
| }) | |
| except Exception as e: | |
| logger.warning(f"Error processing row {idx+1}: {e}") | |
| continue | |
| if not self.quiz_data: | |
| return False, "β No valid questions found in the CSV file." | |
| return True, f"β Successfully loaded {len(self.quiz_data)} questions!" | |
| except Exception as e: | |
| logger.exception("Error loading CSV") | |
| return False, f"β Error loading CSV: {str(e)}" | |
| def start_quiz(self, name: str) -> Tuple[bool, str]: | |
| """Start the quiz""" | |
| if not self.quiz_data: | |
| return False, "β No quiz loaded. Please load a CSV file first." | |
| self.user_name = name.strip() if name else "CCC PLUS QUIZ REPORT" | |
| self.current_index = 0 | |
| self.score = 0 | |
| self.user_answers = [] | |
| self.quiz_started = True | |
| self.quiz_completed = False | |
| self.start_time = datetime.now() | |
| return True, "π Quiz started! Answer each question and click Next to proceed." | |
| def get_current_question(self) -> Dict[str, Any]: | |
| """Get current question data""" | |
| if not self.quiz_started or not self.quiz_data or self.current_index >= len(self.quiz_data): | |
| return {} | |
| question_data = self.quiz_data[self.current_index] | |
| return { | |
| 'question_num': self.current_index + 1, | |
| 'total_questions': len(self.quiz_data), | |
| 'question': question_data['question'], | |
| 'options': question_data['options'], | |
| 'progress': ((self.current_index) / len(self.quiz_data)) * 100 | |
| } | |
| def submit_answer(self, selected_answer: str) -> Tuple[bool, str, bool]: | |
| """Submit answer and move to next question""" | |
| if not self.quiz_started or not self.quiz_data: | |
| return False, "β Quiz not started.", False | |
| if self.current_index >= len(self.quiz_data): | |
| return False, "β Quiz already completed.", True | |
| current_q = self.quiz_data[self.current_index] | |
| is_correct = selected_answer == current_q['answer'] | |
| # Store the answer | |
| self.user_answers.append({ | |
| 'question': current_q['question'], | |
| 'selected': selected_answer, | |
| 'correct_answer': current_q['answer'], | |
| 'is_correct': is_correct | |
| }) | |
| if is_correct: | |
| self.score += 1 | |
| # Move to next question | |
| self.current_index += 1 | |
| # Check if quiz is completed | |
| if self.current_index >= len(self.quiz_data): | |
| self.quiz_completed = True | |
| self.end_time = datetime.now() | |
| duration = self.end_time - self.start_time | |
| duration_str = f"{duration.seconds // 60}m {duration.seconds % 60}s" | |
| percentage = (self.score / len(self.quiz_data)) * 100 | |
| return True, f"""π **Quiz Completed!** | |
| **Final Score:** {self.score} out of {len(self.quiz_data)} ({percentage:.1f}%) | |
| **Time Taken:** {duration_str} | |
| **Performance:** {self.get_performance_level(percentage)} | |
| Click "Generate Report" to download your detailed results!""", True | |
| # Quiz continues | |
| feedback = f"β Correct!" if is_correct else f"β Incorrect. The correct answer was: **{current_q['answer']}**" | |
| return True, feedback, False | |
| def get_performance_level(self, percentage: float) -> str: | |
| """Get performance level based on percentage""" | |
| if percentage >= 90: | |
| return "π Excellent" | |
| elif percentage >= 80: | |
| return "π Good" | |
| elif percentage >= 60: | |
| return "π Average" | |
| else: | |
| return "π Needs Improvement" | |
| def generate_pdf_report(self, photo_path: Optional[str] = None) -> Optional[str]: | |
| """Generate PDF report with candidate photo""" | |
| if not self.user_answers: | |
| return None | |
| try: | |
| # Create temporary file | |
| temp_file = tempfile.NamedTemporaryFile( | |
| prefix="CCC_Quiz_Report_", | |
| suffix=".pdf", | |
| delete=False | |
| ) | |
| temp_file.close() | |
| # Create PDF | |
| doc = SimpleDocTemplate( | |
| temp_file.name, | |
| pagesize=A4, | |
| leftMargin=25*mm, | |
| rightMargin=25*mm, | |
| topMargin=25*mm, | |
| bottomMargin=25*mm | |
| ) | |
| styles = getSampleStyleSheet() | |
| story = [] | |
| # Custom styles | |
| title_style = ParagraphStyle( | |
| 'CustomTitle', | |
| parent=styles['Heading1'], | |
| fontSize=18, | |
| alignment=TA_CENTER, | |
| textColor=colors.HexColor('#003366'), | |
| spaceAfter=12 | |
| ) | |
| subtitle_style = ParagraphStyle( | |
| 'CustomSubtitle', | |
| parent=styles['Normal'], | |
| fontSize=12, | |
| alignment=TA_CENTER, | |
| textColor=colors.HexColor('#666666'), | |
| spaceAfter=20 | |
| ) | |
| # Header with candidate photo | |
| try: | |
| from reportlab.platypus import Image | |
| header_data = [] | |
| if photo_path and os.path.exists(photo_path): | |
| try: | |
| # Create header table with photo | |
| photo_img = Image(photo_path, width=60, height=60) | |
| header_info = [ | |
| "CCC Plus - Computer Concepts Plus", | |
| "Quiz Assessment Report", | |
| f"Candidate: {self.user_name}", | |
| datetime.now().strftime('%d %B %Y at %H:%M:%S') | |
| ] | |
| header_table = Table([ | |
| [photo_img, Paragraph("<br/>".join(header_info), styles['Normal'])] | |
| ], colWidths=[80, 400]) | |
| header_table.setStyle(TableStyle([ | |
| ('ALIGN', (0, 0), (0, 0), 'CENTER'), | |
| ('VALIGN', (0, 0), (-1, -1), 'TOP'), | |
| ('FONTSIZE', (1, 0), (1, 0), 12), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 12), | |
| ])) | |
| story.append(header_table) | |
| except Exception as e: | |
| logger.warning(f"Could not add photo to PDF: {e}") | |
| # Fallback to text header | |
| story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style)) | |
| story.append(Paragraph("Quiz Assessment Report", subtitle_style)) | |
| else: | |
| # Text-only header | |
| story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style)) | |
| story.append(Paragraph("Quiz Assessment Report", subtitle_style)) | |
| except ImportError: | |
| # If reportlab Image is not available, use text header | |
| story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style)) | |
| story.append(Paragraph("Quiz Assessment Report", subtitle_style)) | |
| story.append(Spacer(1, 20)) | |
| # Candidate info | |
| info_data = [ | |
| ['Candidate:', self.user_name], | |
| ['Date:', datetime.now().strftime('%d %B %Y at %H:%M:%S')], | |
| ['Questions Attempted:', str(len(self.user_answers))], | |
| ['Correct Answers:', str(self.score)], | |
| ['Score Percentage:', f"{(self.score/len(self.user_answers)*100):.1f}%"], | |
| ['Performance Level:', self.get_performance_level((self.score/len(self.user_answers)*100))] | |
| ] | |
| info_table = Table(info_data, colWidths=[120, 200]) | |
| info_table.setStyle(TableStyle([ | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, -1), 10), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), | |
| ])) | |
| story.append(info_table) | |
| story.append(Spacer(1, 20)) | |
| # Results table with proper text wrapping | |
| story.append(Paragraph("Detailed Results", styles['Heading2'])) | |
| story.append(Spacer(1, 12)) | |
| table_data = [['#', 'Question', 'Your Answer', 'Correct Answer', 'Result']] | |
| # Custom paragraph style for table cells | |
| cell_style = ParagraphStyle( | |
| 'CellStyle', | |
| parent=styles['Normal'], | |
| fontSize=8, | |
| leading=10, | |
| wordWrap='LTR', | |
| leftIndent=2, | |
| rightIndent=2 | |
| ) | |
| for i, answer in enumerate(self.user_answers, 1): | |
| result_icon = 'β' if answer['is_correct'] else 'β' | |
| # Wrap text in Paragraph objects for proper text wrapping | |
| question_para = Paragraph(answer['question'], cell_style) | |
| selected_para = Paragraph(answer['selected'], cell_style) | |
| correct_para = Paragraph(answer['correct_answer'], cell_style) | |
| table_data.append([ | |
| str(i), | |
| question_para, | |
| selected_para, | |
| correct_para, | |
| result_icon | |
| ]) | |
| # Adjust column widths for better fit | |
| results_table = Table(table_data, colWidths=[15*mm, 90*mm, 35*mm, 35*mm, 15*mm]) | |
| results_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#003366')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('ALIGN', (0, 0), (0, -1), 'CENTER'), # Center align the # column | |
| ('ALIGN', (-1, 0), (-1, -1), 'CENTER'), # Center align the Result column | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, 0), 9), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')]), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')), | |
| ('VALIGN', (0, 0), (-1, -1), 'TOP'), | |
| ('BOTTOMPADDING', (0, 0), (-1, -1), 6), | |
| ('TOPPADDING', (0, 0), (-1, -1), 6), | |
| ('LEFTPADDING', (0, 0), (-1, -1), 4), | |
| ('RIGHTPADDING', (0, 0), (-1, -1), 4), | |
| ])) | |
| story.append(results_table) | |
| story.append(Spacer(1, 30)) | |
| # Footer | |
| footer_style = ParagraphStyle( | |
| 'Footer', | |
| parent=styles['Normal'], | |
| fontSize=8, | |
| alignment=TA_CENTER, | |
| textColor=colors.HexColor('#666666') | |
| ) | |
| story.append(Paragraph( | |
| f"NIELIT Chandigarh β Β© {datetime.now().year} β Generated automatically", | |
| footer_style | |
| )) | |
| doc.build(story) | |
| logger.info(f"Generated PDF report: {temp_file.name}") | |
| return temp_file.name | |
| except Exception as e: | |
| logger.exception("Error generating PDF report") | |
| return None | |
| # Global quiz app instance | |
| quiz_app = QuizApp() | |
| def handle_load_quiz(csv_path: str, uploaded_file) -> Tuple[str, Any, Any, Any]: | |
| """Handle quiz loading""" | |
| # Extract file path | |
| file_path = None | |
| if uploaded_file is not None: | |
| file_path = uploaded_file.name if hasattr(uploaded_file, 'name') else str(uploaded_file) | |
| elif csv_path and csv_path.strip(): | |
| file_path = csv_path.strip() | |
| if not file_path: | |
| return "β Please provide a CSV file path or upload a file.", gr.update(), gr.update(), gr.update(visible=False) | |
| # Reset quiz state | |
| quiz_app.reset_state() | |
| # Load quiz | |
| success, message = quiz_app.load_csv(file_path) | |
| if success: | |
| return message, gr.update(visible=True), gr.update(visible=True), gr.update(visible=False) | |
| else: | |
| return message, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) | |
| def handle_start_quiz(name: str) -> Tuple[str, Any, Any, Any, Any, str, Any]: | |
| """Handle quiz start""" | |
| success, message = quiz_app.start_quiz(name) | |
| if not success: | |
| return message, gr.update(), gr.update(), gr.update(), gr.update(), "", gr.update() | |
| # Get first question | |
| q_data = quiz_app.get_current_question() | |
| if not q_data: | |
| return "β No questions available.", gr.update(), gr.update(), gr.update(), gr.update(), "", gr.update() | |
| question_html = f""" | |
| <div style="background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 20px; border-radius: 12px; margin-bottom: 20px;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> | |
| <span style="font-weight: 600; color: #495057;">Question {q_data['question_num']} of {q_data['total_questions']}</span> | |
| <span style="font-weight: 600; color: #007bff;">Score: {quiz_app.score}/{q_data['question_num']-1 if q_data['question_num'] > 1 else 0}</span> | |
| </div> | |
| <div style="background: #e9ecef; border-radius: 10px; overflow: hidden; height: 8px; margin-bottom: 15px;"> | |
| <div style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: {q_data['progress']:.1f}%;"></div> | |
| </div> | |
| <h3 style="color: #212529; margin: 0;">{q_data['question']}</h3> | |
| </div> | |
| """ | |
| return ( | |
| message, | |
| gr.update(visible=True), | |
| gr.update(visible=True), | |
| gr.update(value=question_html, visible=True), | |
| gr.update(choices=q_data['options'], value=None, visible=True), | |
| "", | |
| gr.update(visible=True) | |
| ) | |
| def handle_next_question(selected_answer: str) -> Tuple[str, Any, Any, str, Any]: | |
| """Handle answer submission and move to next question""" | |
| if not selected_answer: | |
| return "β οΈ Please select an answer before proceeding.", gr.update(), gr.update(), "", gr.update() | |
| success, feedback, is_completed = quiz_app.submit_answer(selected_answer) | |
| if not success: | |
| return feedback, gr.update(), gr.update(), feedback, gr.update() | |
| if is_completed: | |
| # Quiz completed | |
| return ( | |
| feedback, | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| feedback, | |
| gr.update(visible=True) | |
| ) | |
| # Get next question | |
| q_data = quiz_app.get_current_question() | |
| if not q_data: | |
| return "β Error getting next question.", gr.update(), gr.update(), feedback, gr.update() | |
| question_html = f""" | |
| <div style="background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 20px; border-radius: 12px; margin-bottom: 20px;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> | |
| <span style="font-weight: 600; color: #495057;">Question {q_data['question_num']} of {q_data['total_questions']}</span> | |
| <span style="font-weight: 600; color: #007bff;">Score: {quiz_app.score}/{q_data['question_num']-1}</span> | |
| </div> | |
| <div style="background: #e9ecef; border-radius: 10px; overflow: hidden; height: 8px; margin-bottom: 15px;"> | |
| <div style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: {q_data['progress']:.1f}%;"></div> | |
| </div> | |
| <h3 style="color: #212529; margin: 0;">{q_data['question']}</h3> | |
| </div> | |
| """ | |
| return ( | |
| feedback, | |
| gr.update(value=question_html, visible=True), | |
| gr.update(choices=q_data['options'], value=None, visible=True), | |
| feedback, | |
| gr.update() | |
| ) | |
| def handle_generate_report(photo_file) -> Tuple[str, Any]: | |
| """Handle report generation with candidate photo""" | |
| if not quiz_app.user_answers: | |
| return "β No quiz results available. Please complete a quiz first.", gr.update(visible=False) | |
| # Generate markdown report | |
| total_questions = len(quiz_app.user_answers) | |
| correct_answers = quiz_app.score | |
| percentage = (correct_answers / total_questions) * 100 | |
| report_lines = [ | |
| f"# π CCC Plus Quiz Report", | |
| f"", | |
| f"**Candidate:** {quiz_app.user_name}", | |
| f"**Date:** {datetime.now().strftime('%d %B %Y at %H:%M:%S')}", | |
| f"**Questions:** {total_questions}", | |
| f"**Correct Answers:** {correct_answers}", | |
| f"**Score:** {percentage:.1f}%", | |
| f"**Performance:** {quiz_app.get_performance_level(percentage)}", | |
| f"", | |
| f"## π Detailed Results", | |
| f"", | |
| f"| # | Question | Your Answer | Correct Answer | Result |", | |
| f"|---|----------|-------------|----------------|--------|" | |
| ] | |
| for i, answer in enumerate(quiz_app.user_answers, 1): | |
| status = "β " if answer['is_correct'] else "β" | |
| question_short = answer['question'][:60] + "..." if len(answer['question']) > 60 else answer['question'] | |
| report_lines.append( | |
| f"| {i} | {question_short} | {answer['selected']} | {answer['correct_answer']} | {status} |" | |
| ) | |
| report_md = "\n".join(report_lines) | |
| # Extract photo path | |
| photo_path = None | |
| if photo_file is not None: | |
| photo_path = photo_file.name if hasattr(photo_file, 'name') else str(photo_file) | |
| # Generate PDF | |
| pdf_path = quiz_app.generate_pdf_report(photo_path) | |
| if pdf_path: | |
| return report_md, gr.update(value=pdf_path, visible=True) | |
| else: | |
| return report_md + "\n\nβ οΈ **PDF generation failed.**", gr.update(visible=False) | |
| def handle_reset() -> Tuple[str, Any, Any, Any, Any, str, Any, str, Any]: | |
| """Handle complete reset""" | |
| quiz_app.reset_state() | |
| return ( | |
| "π Application reset successfully. Load a new quiz to begin.", | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False, value=""), | |
| gr.update(visible=False, choices=[], value=None), | |
| "", | |
| gr.update(visible=False), | |
| "Complete a quiz to see your report here.", | |
| gr.update(visible=False) | |
| ) | |
| def create_app(): | |
| """Create the Gradio interface""" | |
| # Custom CSS for better styling | |
| custom_css = """ | |
| .gradio-container { | |
| max-width: 1200px !important; | |
| margin: 0 auto; | |
| } | |
| .quiz-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 30px; | |
| border-radius: 15px; | |
| color: white; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| } | |
| .control-panel { | |
| background: #f8f9fa; | |
| border-radius: 15px; | |
| padding: 25px; | |
| border: 1px solid #e9ecef; | |
| } | |
| .quiz-area { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 25px; | |
| border: 1px solid #e9ecef; | |
| min-height: 400px; | |
| } | |
| .report-section { | |
| background: #f8f9fa; | |
| border-radius: 15px; | |
| padding: 25px; | |
| border: 1px solid #e9ecef; | |
| margin-top: 20px; | |
| } | |
| /* Logo */ | |
| #logo-img img { | |
| max-height: 70px; | |
| width: auto; | |
| border-radius: 8px; | |
| } | |
| """ | |
| with gr.Blocks(title="CCC Plus Quiz App", css=custom_css, theme=gr.themes.Soft()) as app: | |
| with gr.Row(elem_classes="quiz-header"): | |
| with gr.Column(scale=1): | |
| # Check if logo exists before trying to load it | |
| logo_path = "nielit_logo.png" if os.path.exists("nielit_logo.png") else None | |
| logo_img = gr.Image( | |
| value=logo_path, | |
| elem_id="logo-img", | |
| interactive=False, | |
| show_label=False | |
| ) | |
| with gr.Column(scale=6): | |
| gr.HTML(""" | |
| <div class="quiz-header-text"> | |
| <h1>π₯οΈ CCC Plus Quiz App</h1> | |
| <p>Computer Concepts Plus - Interactive Quiz Platform</p> | |
| <p>NIELIT Chandigarh β’ National Institute of Electronics & Information Technology</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # Left Panel - Controls | |
| with gr.Column(scale=1, elem_classes="control-panel"): | |
| gr.Markdown("### π Load Quiz") | |
| csv_path = gr.Textbox( | |
| label="CSV File Path", | |
| placeholder="Enter path to your quiz CSV file...", | |
| value=DEFAULT_CSV_PATH | |
| ) | |
| csv_upload = gr.File( | |
| label="Or Upload CSV File", | |
| file_types=[".csv"], | |
| file_count="single" | |
| ) | |
| load_btn = gr.Button("π Load Quiz", variant="primary", size="lg") | |
| load_status = gr.Markdown("π No quiz loaded. Please load a CSV file to begin.") | |
| gr.Markdown("---") | |
| gr.Markdown("### π€ Candidate Info") | |
| user_name = gr.Textbox( | |
| label="Your Full Name", | |
| placeholder="Enter your name for the certificate...", | |
| visible=False | |
| ) | |
| start_btn = gr.Button("π Start Quiz", variant="primary", size="lg", visible=False) | |
| gr.Markdown("---") | |
| gr.Markdown("### π Actions") | |
| report_btn = gr.Button("π Generate Report", visible=False) | |
| reset_btn = gr.Button("π Reset All", variant="secondary") | |
| # Candidate photo upload | |
| gr.Markdown("---") | |
| gr.Markdown("### πΈ Candidate Photo") | |
| gr.Markdown("*Upload a photo to include in the PDF report (optional)*") | |
| photo_upload = gr.File( | |
| label="Upload Candidate Photo", | |
| file_types=[".png", ".jpg", ".jpeg"] | |
| ) | |
| # Right Panel - Quiz Area | |
| with gr.Column(scale=2, elem_classes="quiz-area"): | |
| feedback_msg = gr.Markdown("Welcome! Load a quiz file and enter your name to begin.", visible=True) | |
| question_display = gr.HTML(visible=False) | |
| answer_options = gr.Radio( | |
| label="Select your answer:", | |
| choices=[], | |
| visible=False | |
| ) | |
| next_btn = gr.Button("β‘οΈ Submit Answer & Next", variant="primary", size="lg", visible=False) | |
| # Report Section | |
| with gr.Row(): | |
| with gr.Column(elem_classes="report-section"): | |
| gr.Markdown("### π Quiz Report") | |
| report_display = gr.Markdown("Complete a quiz to see your report here.") | |
| report_download = gr.File(label="π₯ Download PDF Report", visible=False) | |
| # Event Handlers | |
| load_btn.click( | |
| fn=handle_load_quiz, | |
| inputs=[csv_path, csv_upload], | |
| outputs=[load_status, user_name, start_btn, report_btn] | |
| ) | |
| start_btn.click( | |
| fn=handle_start_quiz, | |
| inputs=[user_name], | |
| outputs=[feedback_msg, start_btn, next_btn, question_display, answer_options, user_name, report_btn] | |
| ) | |
| next_btn.click( | |
| fn=handle_next_question, | |
| inputs=[answer_options], | |
| outputs=[feedback_msg, question_display, answer_options, feedback_msg, report_btn] | |
| ) | |
| report_btn.click( | |
| fn=handle_generate_report, | |
| inputs=[photo_upload], | |
| outputs=[report_display, report_download] | |
| ) | |
| reset_btn.click( | |
| fn=handle_reset, | |
| inputs=[], | |
| outputs=[ | |
| load_status, user_name, start_btn, question_display, | |
| answer_options, feedback_msg, report_btn, report_display, report_download | |
| ] | |
| ) | |
| # Footer | |
| gr.HTML(f""" | |
| <div style="text-align: center; margin-top: 40px; padding: 20px; | |
| background: #f8f9fa; border-radius: 10px; color: #6c757d;"> | |
| <p><strong>NIELIT Chandigarh</strong> β’ Β© {datetime.now().year} β’ Enhanced Quiz Application</p> | |
| <p style="font-size: 0.9em;">Built with β€οΈ using Gradio β’ Version 2.0</p> | |
| </div> | |
| """) | |
| return app | |
| def main(): | |
| """Main entry point""" | |
| parser = argparse.ArgumentParser(description="CCC Plus Quiz App") | |
| parser.add_argument("--host", default="127.0.0.1", help="Host to bind to") | |
| parser.add_argument("--port", type=int, default=7860, help="Port to serve on") | |
| parser.add_argument("--share", action="store_true", help="Create public share link") | |
| parser.add_argument("--debug", action="store_true", help="Enable debug logging") | |
| args = parser.parse_args() | |
| if args.debug: | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| app = create_app() | |
| logger.info(f"Starting CCC Plus Quiz App on {args.host}:{args.port}") | |
| try: | |
| app.launch( | |
| server_name=args.host, | |
| server_port=args.port, | |
| share=args.share, | |
| show_error=True | |
| ) | |
| except KeyboardInterrupt: | |
| logger.info("Application stopped by user") | |
| except Exception as e: | |
| logger.exception(f"Failed to start application: {e}") | |
| return 1 | |
| return 0 | |
| if __name__ == "__main__": | |
| # For development - create public share | |
| app = create_app() | |
| app.launch(share=True, show_error=True) | |
| # For production use: | |
| # exit(main()) |