capradeepgujaran commited on
Commit
14d9366
1 Parent(s): d48c34b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -146
app.py CHANGED
@@ -21,10 +21,6 @@ class QuizFeedback:
21
  selected: Optional[str]
22
  correct_answer: str
23
 
24
- class QuizGenerationError(Exception):
25
- """Exception raised for errors in quiz generation"""
26
- pass
27
-
28
  class QuizGenerator:
29
  def __init__(self, api_key: str):
30
  self.client = Groq(api_key=api_key)
@@ -117,26 +113,34 @@ class QuizGenerator:
117
  )
118
 
119
  class FontManager:
 
 
120
  @staticmethod
121
  def install_fonts():
 
122
  try:
123
- subprocess.run(["apt-get", "update", "-y"], check=True)
124
- subprocess.run(
125
- [
126
- "apt-get", "install", "-y",
127
- "fonts-liberation",
128
- "fontconfig",
129
- "fonts-dejavu-core"
130
- ],
131
- check=True
132
- )
 
 
133
  subprocess.run(["fc-cache", "-f"], check=True)
134
  print("Fonts installed successfully")
 
 
135
  except Exception as e:
136
- print(f"Warning: Font installation error: {e}")
137
 
138
  @staticmethod
139
  def get_font_paths() -> Dict[str, str]:
 
140
  standard_paths = [
141
  "/usr/share/fonts",
142
  "/usr/local/share/fonts",
@@ -149,6 +153,7 @@ class FontManager:
149
  'bold': None
150
  }
151
 
 
152
  fonts_to_try = {
153
  'regular': [
154
  'LiberationSans-Regular.ttf',
@@ -163,12 +168,14 @@ class FontManager:
163
  }
164
 
165
  def find_font(font_name: str) -> Optional[str]:
 
166
  for base_path in standard_paths:
167
  for root, _, files in os.walk(os.path.expanduser(base_path)):
168
  if font_name in files:
169
  return os.path.join(root, font_name)
170
  return None
171
 
 
172
  for style in ['regular', 'bold']:
173
  for font_name in fonts_to_try[style]:
174
  font_path = find_font(font_name)
@@ -176,19 +183,20 @@ class FontManager:
176
  font_paths[style] = font_path
177
  break
178
 
 
179
  if not all(font_paths.values()):
180
  try:
181
  for style in ['regular', 'bold']:
182
  if not font_paths[style]:
183
  result = subprocess.run(
184
- ['fc-match', '-f', '%{file}', f'sans-serif:style={style}'],
185
  capture_output=True,
186
  text=True
187
  )
188
  if result.returncode == 0 and result.stdout.strip():
189
  font_paths[style] = result.stdout.strip()
190
  except Exception as e:
191
- print(f"Warning: Font matching error: {e}")
192
 
193
  return font_paths
194
 
@@ -203,9 +211,30 @@ class CertificateGenerator:
203
  self.border_color = '#4682B4'
204
  self.background_color = '#F0F8FF'
205
 
 
206
  FontManager.install_fonts()
207
  self.font_paths = FontManager.get_font_paths()
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  def generate(
210
  self,
211
  score: float,
@@ -214,6 +243,9 @@ class CertificateGenerator:
214
  company_logo: Optional[str] = None,
215
  participant_photo: Optional[str] = None
216
  ) -> str:
 
 
 
217
  try:
218
  certificate = self._create_base_certificate()
219
  draw = ImageDraw.Draw(certificate)
@@ -228,42 +260,42 @@ class CertificateGenerator:
228
  print(f"Error generating certificate: {e}")
229
  return None
230
 
 
 
 
231
  def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
232
- fonts = {}
233
  try:
234
- if self.font_paths['regular'] and self.font_paths['bold']:
235
- fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 60)
236
- fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 40)
237
- fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 30)
238
- else:
239
- raise ValueError("No suitable fonts found")
240
  except Exception as e:
241
  print(f"Font loading error: {e}. Using default font.")
242
  default = ImageFont.load_default()
243
- fonts = {
244
- 'title': default,
245
- 'text': default,
246
- 'subtitle': default
247
- }
248
- return fonts
249
-
250
- def _create_base_certificate(self) -> Image.Image:
251
- return Image.new('RGB', self.certificate_size, self.background_color)
252
 
253
  def _add_borders(self, draw: ImageDraw.Draw):
 
254
  draw.rectangle([20, 20, 1180, 780], outline=self.border_color, width=3)
255
  draw.rectangle([40, 40, 1160, 760], outline=self.border_color, width=1)
 
 
256
  self._add_decorative_corners(draw)
257
 
258
  def _add_decorative_corners(self, draw: ImageDraw.Draw):
259
  corner_size = 20
260
  corners = [
 
261
  [(20, 40), (20 + corner_size, 40)],
262
  [(40, 20), (40, 20 + corner_size)],
 
263
  [(1180 - corner_size, 40), (1180, 40)],
264
  [(1160, 20), (1160, 20 + corner_size)],
 
265
  [(20, 760), (20 + corner_size, 760)],
266
  [(40, 780 - corner_size), (40, 780)],
 
267
  [(1180 - corner_size, 760), (1180, 760)],
268
  [(1160, 780 - corner_size), (1160, 780)]
269
  ]
@@ -279,12 +311,15 @@ class CertificateGenerator:
279
  course_name: str,
280
  score: float
281
  ):
 
282
  draw.text((600, 100), "CertifyMe AI", font=fonts['title'], fill=self.border_color, anchor="mm")
283
  draw.text((600, 160), "Certificate of Achievement", font=fonts['subtitle'], fill=self.border_color, anchor="mm")
284
 
 
285
  name = str(name).strip() or "Participant"
286
  course_name = str(course_name).strip() or "Assessment"
287
 
 
288
  content = [
289
  (300, "This is to certify that", 'black'),
290
  (380, name, self.border_color),
@@ -320,6 +355,7 @@ class CertificateGenerator:
320
  temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
321
  certificate.save(temp_file.name, 'PNG', quality=95)
322
  return temp_file.name
 
323
 
324
  class QuizApp:
325
  def __init__(self, api_key: str):
@@ -490,64 +526,20 @@ class QuizApp:
490
  <p>You need 80% or higher to pass and receive a certificate.</p>
491
  </div>
492
  """
493
- def on_generate_questions(text: str, num_questions: int) -> List:
494
- """Generate quiz questions and setup initial state"""
495
- success, questions = quiz_app.generate_questions(text, num_questions)
496
-
497
- # Handle failure case
498
- if not success or not questions:
499
- return [
500
- "", # question_display
501
- gr.update(choices=[], visible=False), # current_options
502
- "", # question_counter
503
- gr.update(visible=False), # question_box
504
- [], # current_questions
505
- 0, # current_question_idx
506
- [None] * 5, # answer_state
507
- gr.Tabs(selected=2), # tabs - navigate to assessment tab
508
- gr.update(visible=False), # results_group
509
- gr.update(visible=False) # view_cert_btn
510
- ]
511
-
512
- # Setup initial state with first question
513
- initial_answers = [None] * len(questions)
514
- question = questions[0]
515
- question_html = f"""
516
- # Question 1 of {len(questions)}
517
-
518
- {question.question}
519
- """
520
-
521
- return [
522
- question_html, # question_display
523
- gr.update(
524
- choices=question.options,
525
- value=None,
526
- visible=True,
527
- label="Select your answer:"
528
- ), # current_options
529
- f"Question 1 of {len(questions)}", # question_counter
530
- gr.update(visible=True), # question_box
531
- questions, # current_questions
532
- 0, # current_question_idx
533
- initial_answers, # answer_state
534
- gr.Tabs(selected=2), # tabs - navigate to assessment tab
535
- gr.update(visible=False), # results_group
536
- gr.update(visible=False) # view_cert_btn
537
- ]
538
  def create_quiz_interface():
539
  if not os.getenv("GROQ_API_KEY"):
540
  raise EnvironmentError("Please set your GROQ_API_KEY environment variable")
541
 
542
- global quiz_app
543
  quiz_app = QuizApp(os.getenv("GROQ_API_KEY"))
544
 
545
  with gr.Blocks(title="CertifyMe AI", theme=gr.themes.Soft()) as demo:
546
  # State management
547
  current_questions = gr.State([])
548
  current_question_idx = gr.State(0)
549
- answer_state = gr.State([None] * 5)
550
 
 
551
  gr.Markdown("""
552
  # 🎓 CertifyMe AI
553
  ### Transform Your Knowledge into Recognized Achievements
@@ -555,7 +547,7 @@ def create_quiz_interface():
555
 
556
  with gr.Tabs() as tabs:
557
  # Profile Setup Tab
558
- with gr.Tab(label="📋 Step 1: Profile Setup", id=1) as setup_tab:
559
  with gr.Row():
560
  name = gr.Textbox(label="Full Name", placeholder="Enter your full name")
561
  email = gr.Textbox(label="Email", placeholder="Enter your email")
@@ -581,54 +573,40 @@ def create_quiz_interface():
581
  generate_btn = gr.Button("Generate Assessment", variant="primary", size="lg")
582
 
583
  # Assessment Tab
584
- with gr.Tab(label="📝 Step 2: Take Assessment", id=2) as assessment_tab:
585
  with gr.Column(visible=True) as question_box:
 
586
  with gr.Group():
587
- question_display = gr.Markdown("")
 
 
 
588
  current_options = gr.Radio(
589
  choices=[],
590
  label="Select your answer:",
591
  visible=False
592
  )
593
 
 
594
  with gr.Row():
595
  prev_btn = gr.Button("← Previous", variant="secondary", size="sm")
596
  question_counter = gr.Markdown("Question 1 of 3")
597
  next_btn = gr.Button("Next →", variant="secondary", size="sm")
598
 
599
- gr.Markdown("---")
600
 
601
- with gr.Row():
602
- submit_btn = gr.Button(
603
- "Submit Assessment",
604
- variant="primary",
605
- size="lg"
606
- )
607
- reset_btn = gr.Button(
608
- "Reset Quiz",
609
- variant="secondary",
610
- size="lg"
611
- )
612
 
613
  # Results section
614
  with gr.Group(visible=False) as results_group:
615
  feedback_box = gr.Markdown("")
616
- with gr.Row():
617
- view_cert_btn = gr.Button(
618
- "View Certificate",
619
- variant="primary",
620
- size="lg",
621
- visible=False
622
- )
623
- back_to_assessment = gr.Button(
624
- "Back to Assessment",
625
- variant="secondary",
626
- size="lg",
627
- visible=True
628
- )
629
 
630
  # Certification Tab
631
- with gr.Tab(label="🎓 Step 3: Get Certified", id=3) as cert_tab:
632
  score_display = gr.Number(label="Your Score")
633
  result_message = gr.Markdown("")
634
  course_name = gr.Textbox(
@@ -637,6 +615,141 @@ def create_quiz_interface():
637
  )
638
  certificate_display = gr.Image(label="Your Certificate")
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  # Event handlers
641
  generate_btn.click(
642
  fn=on_generate_questions,
@@ -650,14 +763,33 @@ def create_quiz_interface():
650
  current_question_idx,
651
  answer_state,
652
  tabs,
653
- results_group,
654
- view_cert_btn
655
  ]
656
- ).then(
657
- fn=goto_take_assessment,
658
- outputs=tabs
659
  )
660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  prev_btn.click(
662
  fn=handle_prev,
663
  inputs=[
@@ -694,14 +826,33 @@ def create_quiz_interface():
694
  ]
695
  )
696
 
 
 
 
 
 
 
 
697
  current_options.change(
698
  fn=update_answer_state,
699
  inputs=[current_options, current_question_idx, answer_state],
700
  outputs=answer_state
701
  )
702
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  submit_btn.click(
704
- fn=on_submit,
705
  inputs=[
706
  current_questions,
707
  answer_state,
@@ -714,40 +865,13 @@ def create_quiz_interface():
714
  score_display,
715
  result_message,
716
  question_box,
717
- tabs,
718
- view_cert_btn
719
- ]
720
- )
721
-
722
- reset_btn.click(
723
- fn=reset_quiz,
724
- inputs=[text_input, num_questions],
725
- outputs=[
726
- question_display,
727
- current_options,
728
- question_counter,
729
- question_box,
730
- current_questions,
731
- current_question_idx,
732
- answer_state,
733
- tabs,
734
- results_group,
735
- view_cert_btn
736
  ]
737
  )
738
 
739
- view_cert_btn.click(
740
- fn=goto_certificate,
741
- outputs=tabs
742
- )
743
-
744
- back_to_assessment.click(
745
- fn=goto_take_assessment,
746
- outputs=tabs
747
- )
748
-
749
  score_display.change(
750
- fn=lambda s, n, c, l, p: quiz_app.certificate_generator.generate(s, n, c, l, p) or gr.update(value=None),
751
  inputs=[score_display, name, course_name, company_logo, participant_photo],
752
  outputs=certificate_display
753
  )
@@ -756,4 +880,4 @@ def create_quiz_interface():
756
 
757
  if __name__ == "__main__":
758
  demo = create_quiz_interface()
759
- demo.launch()
 
21
  selected: Optional[str]
22
  correct_answer: str
23
 
 
 
 
 
24
  class QuizGenerator:
25
  def __init__(self, api_key: str):
26
  self.client = Groq(api_key=api_key)
 
113
  )
114
 
115
  class FontManager:
116
+ """Manages font installation and loading for the certificate generator"""
117
+
118
  @staticmethod
119
  def install_fonts():
120
+ """Install required fonts if they're not already present"""
121
  try:
122
+ # Install fonts package
123
+ subprocess.run([
124
+ "apt-get", "update", "-y"
125
+ ], check=True)
126
+ subprocess.run([
127
+ "apt-get", "install", "-y",
128
+ "fonts-liberation", # Liberation Sans fonts
129
+ "fontconfig", # Font configuration
130
+ "fonts-dejavu-core" # DejaVu fonts as fallback
131
+ ], check=True)
132
+
133
+ # Clear font cache
134
  subprocess.run(["fc-cache", "-f"], check=True)
135
  print("Fonts installed successfully")
136
+ except subprocess.CalledProcessError as e:
137
+ print(f"Warning: Could not install fonts: {e}")
138
  except Exception as e:
139
+ print(f"Warning: Unexpected error installing fonts: {e}")
140
 
141
  @staticmethod
142
  def get_font_paths() -> Dict[str, str]:
143
+ """Get the paths to the required fonts with multiple fallbacks"""
144
  standard_paths = [
145
  "/usr/share/fonts",
146
  "/usr/local/share/fonts",
 
153
  'bold': None
154
  }
155
 
156
+ # Common font filenames to try
157
  fonts_to_try = {
158
  'regular': [
159
  'LiberationSans-Regular.ttf',
 
168
  }
169
 
170
  def find_font(font_name: str) -> Optional[str]:
171
+ """Search for a font file in standard locations"""
172
  for base_path in standard_paths:
173
  for root, _, files in os.walk(os.path.expanduser(base_path)):
174
  if font_name in files:
175
  return os.path.join(root, font_name)
176
  return None
177
 
178
+ # Try to find each font
179
  for style in ['regular', 'bold']:
180
  for font_name in fonts_to_try[style]:
181
  font_path = find_font(font_name)
 
183
  font_paths[style] = font_path
184
  break
185
 
186
+ # If no fonts found, try using fc-match as fallback
187
  if not all(font_paths.values()):
188
  try:
189
  for style in ['regular', 'bold']:
190
  if not font_paths[style]:
191
  result = subprocess.run(
192
+ ['fc-match', '-f', '%{file}', 'sans-serif:style=' + style],
193
  capture_output=True,
194
  text=True
195
  )
196
  if result.returncode == 0 and result.stdout.strip():
197
  font_paths[style] = result.stdout.strip()
198
  except Exception as e:
199
+ print(f"Warning: Could not use fc-match to find fonts: {e}")
200
 
201
  return font_paths
202
 
 
211
  self.border_color = '#4682B4'
212
  self.background_color = '#F0F8FF'
213
 
214
+ # Install fonts if needed
215
  FontManager.install_fonts()
216
  self.font_paths = FontManager.get_font_paths()
217
 
218
+ def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
219
+ """Load fonts with fallbacks"""
220
+ fonts = {}
221
+ try:
222
+ if self.font_paths['regular'] and self.font_paths['bold']:
223
+ fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 60)
224
+ fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 40)
225
+ fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 30)
226
+ else:
227
+ raise ValueError("No suitable fonts found")
228
+ except Exception as e:
229
+ print(f"Font loading error: {e}. Using default font.")
230
+ default = ImageFont.load_default()
231
+ fonts = {
232
+ 'title': default,
233
+ 'text': default,
234
+ 'subtitle': default
235
+ }
236
+ return fonts
237
+
238
  def generate(
239
  self,
240
  score: float,
 
243
  company_logo: Optional[str] = None,
244
  participant_photo: Optional[str] = None
245
  ) -> str:
246
+ """
247
+ Generate a certificate with custom styling and optional logo/photo
248
+ """
249
  try:
250
  certificate = self._create_base_certificate()
251
  draw = ImageDraw.Draw(certificate)
 
260
  print(f"Error generating certificate: {e}")
261
  return None
262
 
263
+ def _create_base_certificate(self) -> Image.Image:
264
+ return Image.new('RGB', self.certificate_size, self.background_color)
265
+
266
  def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
 
267
  try:
268
+ return {
269
+ 'title': ImageFont.truetype("arial.ttf", 60),
270
+ 'text': ImageFont.truetype("arial.ttf", 40),
271
+ 'subtitle': ImageFont.truetype("arial.ttf", 30)
272
+ }
 
273
  except Exception as e:
274
  print(f"Font loading error: {e}. Using default font.")
275
  default = ImageFont.load_default()
276
+ return {'title': default, 'text': default, 'subtitle': default}
 
 
 
 
 
 
 
 
277
 
278
  def _add_borders(self, draw: ImageDraw.Draw):
279
+ # Main borders
280
  draw.rectangle([20, 20, 1180, 780], outline=self.border_color, width=3)
281
  draw.rectangle([40, 40, 1160, 760], outline=self.border_color, width=1)
282
+
283
+ # Decorative corners
284
  self._add_decorative_corners(draw)
285
 
286
  def _add_decorative_corners(self, draw: ImageDraw.Draw):
287
  corner_size = 20
288
  corners = [
289
+ # Top-left
290
  [(20, 40), (20 + corner_size, 40)],
291
  [(40, 20), (40, 20 + corner_size)],
292
+ # Top-right
293
  [(1180 - corner_size, 40), (1180, 40)],
294
  [(1160, 20), (1160, 20 + corner_size)],
295
+ # Bottom-left
296
  [(20, 760), (20 + corner_size, 760)],
297
  [(40, 780 - corner_size), (40, 780)],
298
+ # Bottom-right
299
  [(1180 - corner_size, 760), (1180, 760)],
300
  [(1160, 780 - corner_size), (1160, 780)]
301
  ]
 
311
  course_name: str,
312
  score: float
313
  ):
314
+ # Title and headers
315
  draw.text((600, 100), "CertifyMe AI", font=fonts['title'], fill=self.border_color, anchor="mm")
316
  draw.text((600, 160), "Certificate of Achievement", font=fonts['subtitle'], fill=self.border_color, anchor="mm")
317
 
318
+ # Clean inputs
319
  name = str(name).strip() or "Participant"
320
  course_name = str(course_name).strip() or "Assessment"
321
 
322
+ # Main content
323
  content = [
324
  (300, "This is to certify that", 'black'),
325
  (380, name, self.border_color),
 
355
  temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
356
  certificate.save(temp_file.name, 'PNG', quality=95)
357
  return temp_file.name
358
+
359
 
360
  class QuizApp:
361
  def __init__(self, api_key: str):
 
526
  <p>You need 80% or higher to pass and receive a certificate.</p>
527
  </div>
528
  """
529
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  def create_quiz_interface():
531
  if not os.getenv("GROQ_API_KEY"):
532
  raise EnvironmentError("Please set your GROQ_API_KEY environment variable")
533
 
 
534
  quiz_app = QuizApp(os.getenv("GROQ_API_KEY"))
535
 
536
  with gr.Blocks(title="CertifyMe AI", theme=gr.themes.Soft()) as demo:
537
  # State management
538
  current_questions = gr.State([])
539
  current_question_idx = gr.State(0)
540
+ answer_state = gr.State([None] * 5) # Store all answers
541
 
542
+ # Header
543
  gr.Markdown("""
544
  # 🎓 CertifyMe AI
545
  ### Transform Your Knowledge into Recognized Achievements
 
547
 
548
  with gr.Tabs() as tabs:
549
  # Profile Setup Tab
550
+ with gr.Tab("📋 Step 1: Profile Setup"):
551
  with gr.Row():
552
  name = gr.Textbox(label="Full Name", placeholder="Enter your full name")
553
  email = gr.Textbox(label="Email", placeholder="Enter your email")
 
573
  generate_btn = gr.Button("Generate Assessment", variant="primary", size="lg")
574
 
575
  # Assessment Tab
576
+ with gr.Tab("📝 Step 2: Take Assessment") as assessment_tab:
577
  with gr.Column(visible=True) as question_box:
578
+ # Question section
579
  with gr.Group():
580
+ # Question display
581
+ question_display = gr.Markdown("", label="Current Question")
582
+
583
+ # Single radio group for current question
584
  current_options = gr.Radio(
585
  choices=[],
586
  label="Select your answer:",
587
  visible=False
588
  )
589
 
590
+ # Navigation
591
  with gr.Row():
592
  prev_btn = gr.Button("← Previous", variant="secondary", size="sm")
593
  question_counter = gr.Markdown("Question 1 of 3")
594
  next_btn = gr.Button("Next →", variant="secondary", size="sm")
595
 
596
+ gr.Markdown("---") # Separator
597
 
598
+ submit_btn = gr.Button(
599
+ "Submit Assessment",
600
+ variant="primary",
601
+ size="lg"
602
+ )
 
 
 
 
 
 
603
 
604
  # Results section
605
  with gr.Group(visible=False) as results_group:
606
  feedback_box = gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
608
  # Certification Tab
609
+ with gr.Tab("🎓 Step 3: Get Certified"):
610
  score_display = gr.Number(label="Your Score")
611
  result_message = gr.Markdown("")
612
  course_name = gr.Textbox(
 
615
  )
616
  certificate_display = gr.Image(label="Your Certificate")
617
 
618
+
619
+ def on_generate_questions(text, num_questions):
620
+ success, questions = quiz_app.generate_questions(text, num_questions)
621
+ if not success:
622
+ return [
623
+ "", # question_display
624
+ gr.update(choices=[], visible=False), # current_options
625
+ "", # question_counter
626
+ gr.update(visible=False), # question_box
627
+ [], # current_questions
628
+ 0, # current_question_idx
629
+ [None] * 5, # answer_state
630
+ gr.update(selected=1), # tabs
631
+ gr.update(visible=False) # results_group
632
+ ]
633
+
634
+ # Initialize first question
635
+ initial_answers = [None] * len(questions)
636
+ question = questions[0]
637
+
638
+ return [
639
+ f"""## Question 1
640
+ {question.question}
641
+
642
+ Please select one answer:""", # question_display
643
+ gr.update(
644
+ choices=question.options,
645
+ value=None,
646
+ visible=True,
647
+ label="Select your answer for Question 1:"
648
+ ), # current_options
649
+ f"Question 1 of {len(questions)}", # question_counter
650
+ gr.update(visible=True), # question_box
651
+ questions, # current_questions
652
+ 0, # current_question_idx
653
+ initial_answers, # answer_state
654
+ gr.update(selected=1), # tabs
655
+ gr.update(visible=False) # results_group
656
+ ]
657
+
658
+ def navigate(direction, current_idx, questions, answers, current_answer):
659
+ """
660
+ Handle navigation between questions
661
+ """
662
+ if not questions: # No questions available
663
+ return [
664
+ 0, # current_question_idx
665
+ answers, # answer_state
666
+ "", # question_display
667
+ gr.update(choices=[], value=None, visible=False), # current_options
668
+ "", # question_counter
669
+ gr.update(visible=False) # question_box
670
+ ]
671
+
672
+ # Save current answer
673
+ new_answers = list(answers)
674
+ if current_answer and 0 <= current_idx < len(new_answers):
675
+ new_answers[current_idx] = current_answer
676
+
677
+ # Calculate new index
678
+ new_idx = max(0, min(len(questions) - 1, current_idx + direction))
679
+
680
+ # Get current question
681
+ question = questions[new_idx]
682
+
683
+ return [
684
+ new_idx, # current_question_idx
685
+ new_answers, # answer_state
686
+ f"""## Question {new_idx + 1}
687
+ {question.question}
688
+
689
+ Please select one answer:""", # question_display
690
+ gr.update(
691
+ choices=question.options,
692
+ value=new_answers[new_idx] if new_idx < len(new_answers) else None,
693
+ visible=True,
694
+ label=f"Select your answer for Question {new_idx + 1}:"
695
+ ), # current_options
696
+ f"Question {new_idx + 1} of {len(questions)}", # question_counter
697
+ gr.update(visible=True) # question_box
698
+ ]
699
+
700
+ def on_submit(questions, answers, current_idx, current_answer):
701
+ # Update the current answer in the answers list
702
+ final_answers = list(answers)
703
+ if 0 <= current_idx < len(final_answers):
704
+ final_answers[current_idx] = current_answer
705
+
706
+ # Validate all answers are provided
707
+ if not all(a is not None for a in final_answers[:len(questions)]):
708
+ return {
709
+ feedback_box: "⚠️ Please answer all questions before submitting.",
710
+ results_group: gr.update(visible=True),
711
+ score_display: 0,
712
+ result_message: "",
713
+ question_box: gr.update(visible=True),
714
+ tabs: gr.update(selected=1)
715
+ }
716
+
717
+ score, passed, feedback = quiz_app.calculate_score(final_answers[:len(questions)])
718
+
719
+ # Generate feedback HTML
720
+ feedback_html = "# Assessment Results\n\n"
721
+ for i, (q, f) in enumerate(zip(questions, feedback)):
722
+ color = "green" if f.is_correct else "red"
723
+ symbol = "✅" if f.is_correct else "❌"
724
+ feedback_html += f"""
725
+ ### Question {i+1}
726
+ {q.question}
727
+
728
+ <div style="color: {color}; padding: 10px; margin: 5px 0; border-left: 3px solid {color};">
729
+ {symbol} Your answer: {f.selected or "No answer"}
730
+ {'' if f.is_correct else f'<br>Correct answer: {f.correct_answer}'}
731
+ </div>
732
+ """
733
+
734
+ result_msg = "🎉 Passed!" if passed else "Please try again"
735
+ if not passed:
736
+ feedback_html += f"""
737
+ <div style="background-color: #ffe6e6; padding: 20px; margin-top: 20px; border-radius: 10px;">
738
+ <h3 style="color: #cc0000;">Please Try Again</h3>
739
+ <p>Your score: {score:.1f}%</p>
740
+ <p>You need 80% or higher to pass and receive a certificate.</p>
741
+ </div>
742
+ """
743
+
744
+ return {
745
+ feedback_box: feedback_html,
746
+ results_group: gr.update(visible=True),
747
+ score_display: score,
748
+ result_message: result_msg,
749
+ question_box: gr.update(visible=passed),
750
+ tabs: gr.update(selected=2 if passed else 1)
751
+ }
752
+
753
  # Event handlers
754
  generate_btn.click(
755
  fn=on_generate_questions,
 
763
  current_question_idx,
764
  answer_state,
765
  tabs,
766
+ results_group
 
767
  ]
 
 
 
768
  )
769
 
770
+ # Navigation event handlers
771
+ def handle_prev(current_idx, questions, answers, current_answer):
772
+ result = navigate(-1, current_idx, questions, answers, current_answer)
773
+ return [
774
+ result[current_question_idx],
775
+ result[answer_state],
776
+ result[question_display],
777
+ result[current_options],
778
+ result[question_counter],
779
+ result[question_box]
780
+ ]
781
+
782
+ def handle_next(current_idx, questions, answers, current_answer):
783
+ result = navigate(1, current_idx, questions, answers, current_answer)
784
+ return [
785
+ result[current_question_idx],
786
+ result[answer_state],
787
+ result[question_display],
788
+ result[current_options],
789
+ result[question_counter],
790
+ result[question_box]
791
+ ]
792
+
793
  prev_btn.click(
794
  fn=handle_prev,
795
  inputs=[
 
826
  ]
827
  )
828
 
829
+ # Update answer state when radio button changes
830
+ def update_answer_state(answer, idx, current_answers):
831
+ new_answers = list(current_answers)
832
+ if 0 <= idx < len(new_answers):
833
+ new_answers[idx] = answer
834
+ return new_answers
835
+
836
  current_options.change(
837
  fn=update_answer_state,
838
  inputs=[current_options, current_question_idx, answer_state],
839
  outputs=answer_state
840
  )
841
 
842
+ # Submission handler
843
+ def on_submit_wrapper(questions, answers, current_idx, current_answer):
844
+ result = on_submit(questions, answers, current_idx, current_answer)
845
+ return [
846
+ result[feedback_box],
847
+ result[results_group],
848
+ result[score_display],
849
+ result[result_message],
850
+ result[question_box],
851
+ result[tabs]
852
+ ]
853
+
854
  submit_btn.click(
855
+ fn=on_submit_wrapper,
856
  inputs=[
857
  current_questions,
858
  answer_state,
 
865
  score_display,
866
  result_message,
867
  question_box,
868
+ tabs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  ]
870
  )
871
 
872
+ # Certificate generation
 
 
 
 
 
 
 
 
 
873
  score_display.change(
874
+ fn=quiz_app.certificate_generator.generate,
875
  inputs=[score_display, name, course_name, company_logo, participant_photo],
876
  outputs=certificate_display
877
  )
 
880
 
881
  if __name__ == "__main__":
882
  demo = create_quiz_interface()
883
+ demo.launch()