AnkitPatil commited on
Commit
0a2a7be
Β·
verified Β·
1 Parent(s): 586b948

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +835 -0
app.py ADDED
@@ -0,0 +1,835 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from groq import Groq
3
+ import os
4
+ from PIL import Image, ImageDraw, ImageFont
5
+ from datetime import datetime
6
+ import json
7
+ import tempfile
8
+ from typing import List, Dict, Tuple, Optional
9
+ from dataclasses import dataclass
10
+ import subprocess
11
+
12
+ @dataclass
13
+ class Question:
14
+ question: str
15
+ options: List[str]
16
+ correct_answer: int
17
+
18
+ @dataclass
19
+ class QuizFeedback:
20
+ is_correct: bool
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)
27
+
28
+ def generate_questions(self, text: str, num_questions: int) -> List[Question]:
29
+ prompt = self._create_prompt(text, num_questions)
30
+
31
+ try:
32
+ response = self.client.chat.completions.create(
33
+ messages=[
34
+ {
35
+ "role": "system",
36
+ "content": "You are a quiz generator. Create clear questions with concise answer options."
37
+ },
38
+ {
39
+ "role": "user",
40
+ "content": prompt
41
+ }
42
+ ],
43
+ model="llama-3.2-3b-preview",
44
+ temperature=0.3,
45
+ max_tokens=2048
46
+ )
47
+
48
+ questions = self._parse_response(response.choices[0].message.content)
49
+ return self._validate_questions(questions, num_questions)
50
+
51
+ except Exception as e:
52
+ raise QuizGenerationError(f"Failed to generate questions: {str(e)}")
53
+
54
+ def _create_prompt(self, text: str, num_questions: int) -> str:
55
+ return f"""Create exactly {num_questions} multiple choice questions based on this text:
56
+ {text}
57
+
58
+ For each question:
59
+ 1. Create a clear, concise question
60
+ 2. Provide exactly 4 options
61
+ 3. Mark the correct answer with the index (0-3)
62
+ 4. Ensure options are concise and clear
63
+
64
+ Return ONLY a JSON array with this EXACT format - no other text:
65
+ [
66
+ {{
67
+ "question": "Question text here?",
68
+ "options": [
69
+ "Brief option 1",
70
+ "Brief option 2",
71
+ "Brief option 3",
72
+ "Brief option 4"
73
+ ],
74
+ "correct_answer": 0
75
+ }}
76
+ ]
77
+ Keep all options concise (10 words or less each).
78
+ """
79
+
80
+ def _parse_response(self, response_text: str) -> List[Dict]:
81
+ response_text = response_text.replace("```json", "").replace("```", "").strip()
82
+ start_idx = response_text.find("[")
83
+
84
+
85
+
86
+ end_idx = response_text.rfind("]")
87
+
88
+ if start_idx == -1 or end_idx == -1:
89
+ raise ValueError("No valid JSON array found in response")
90
+
91
+ response_text = response_text[start_idx:end_idx + 1]
92
+ return json.loads(response_text)
93
+
94
+ def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]:
95
+ validated = []
96
+ for q in questions:
97
+ if not self._is_valid_question(q):
98
+ continue
99
+ validated.append(Question(
100
+ question=q["question"].strip(),
101
+ options=[opt.strip()[:100] for opt in q["options"]],
102
+ correct_answer=int(q["correct_answer"]) % 4
103
+ ))
104
+
105
+ if not validated:
106
+ raise ValueError("No valid questions after validation")
107
+
108
+ return validated[:num_questions]
109
+
110
+ def _is_valid_question(self, question: Dict) -> bool:
111
+ return (
112
+ all(key in question for key in ["question", "options", "correct_answer"]) and
113
+ isinstance(question["options"], list) and
114
+ len(question["options"]) == 4 and
115
+ all(isinstance(opt, str) for opt in question["options"])
116
+ )
117
+
118
+ class FontManager:
119
+ """Manages font installation and loading for the certificate generator"""
120
+
121
+ @staticmethod
122
+ def install_fonts():
123
+ """Install required fonts if they're not already present"""
124
+ try:
125
+ # Install fonts package
126
+ subprocess.run([
127
+ "apt-get", "update", "-y"
128
+ ], check=True)
129
+ subprocess.run([
130
+ "apt-get", "install", "-y",
131
+ "fonts-liberation", # Liberation Sans fonts
132
+ "fontconfig", # Font configuration
133
+ "fonts-dejavu-core" # DejaVu fonts as fallback
134
+ ], check=True)
135
+
136
+ # Clear font cache
137
+ subprocess.run(["fc-cache", "-f"], check=True)
138
+ print("Fonts installed successfully")
139
+ except subprocess.CalledProcessError as e:
140
+ print(f"Warning: Could not install fonts: {e}")
141
+ except Exception as e:
142
+ print(f"Warning: Unexpected error installing fonts: {e}")
143
+
144
+ @staticmethod
145
+ def get_font_paths() -> Dict[str, str]:
146
+ """Get the paths to the required fonts with multiple fallbacks"""
147
+ standard_paths = [
148
+ "/usr/share/fonts",
149
+ "/usr/local/share/fonts",
150
+ "/usr/share/fonts/truetype",
151
+ "~/.fonts"
152
+ ]
153
+
154
+ font_paths = {
155
+ 'regular': None,
156
+ 'bold': None
157
+ }
158
+
159
+ # Common font filenames to try
160
+ fonts_to_try = {
161
+ 'regular': [
162
+ 'LiberationSans-Regular.ttf',
163
+ 'DejaVuSans.ttf',
164
+ 'FreeSans.ttf'
165
+ ],
166
+ 'bold': [
167
+ 'LiberationSans-Bold.ttf',
168
+ 'DejaVuSans-Bold.ttf',
169
+ 'FreeSans-Bold.ttf'
170
+ ]
171
+ }
172
+
173
+ def find_font(font_name: str) -> Optional[str]:
174
+ """Search for a font file in standard locations"""
175
+ for base_path in standard_paths:
176
+ for root, _, files in os.walk(os.path.expanduser(base_path)):
177
+ if font_name in files:
178
+ return os.path.join(root, font_name)
179
+ return None
180
+
181
+ # Try to find each font
182
+ for style in ['regular', 'bold']:
183
+ for font_name in fonts_to_try[style]:
184
+ font_path = find_font(font_name)
185
+ if font_path:
186
+ font_paths[style] = font_path
187
+ break
188
+
189
+ # If no fonts found, try using fc-match as fallback
190
+ if not all(font_paths.values()):
191
+ try:
192
+ for style in ['regular', 'bold']:
193
+ if not font_paths[style]:
194
+ result = subprocess.run(
195
+ ['fc-match', '-f', '%{file}', 'sans-serif:style=' + style],
196
+ capture_output=True,
197
+ text=True
198
+ )
199
+ if result.returncode == 0 and result.stdout.strip():
200
+ font_paths[style] = result.stdout.strip()
201
+ except Exception as e:
202
+ print(f"Warning: Could not use fc-match to find fonts: {e}")
203
+
204
+ return font_paths
205
+
206
+ class QuizGenerationError(Exception):
207
+ """Exception raised for errors in quiz generation"""
208
+ pass
209
+
210
+ class CertificateGenerator:
211
+ def __init__(self):
212
+ self.certificate_size = (1200, 800)
213
+ self.background_color = '#FFFFFF'
214
+ self.border_color = '#1C1D1F'
215
+
216
+ # Install fonts if needed
217
+ FontManager.install_fonts()
218
+ self.font_paths = FontManager.get_font_paths()
219
+
220
+ def _load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
221
+ """Load fonts with fallbacks"""
222
+ fonts = {}
223
+ try:
224
+ if self.font_paths['regular'] and self.font_paths['bold']:
225
+ fonts['title'] = ImageFont.truetype(self.font_paths['bold'], 36)
226
+ fonts['subtitle'] = ImageFont.truetype(self.font_paths['regular'], 14)
227
+ fonts['text'] = ImageFont.truetype(self.font_paths['regular'], 20)
228
+ fonts['name'] = ImageFont.truetype(self.font_paths['bold'], 32)
229
+ else:
230
+ raise ValueError("No suitable fonts found")
231
+ except Exception as e:
232
+ print(f"Font loading error: {e}. Using default font.")
233
+ default = ImageFont.load_default()
234
+ fonts = {
235
+ 'title': default,
236
+ 'subtitle': default,
237
+ 'text': default,
238
+ 'name': default
239
+ }
240
+ return fonts
241
+
242
+ def _add_professional_border(self, draw: ImageDraw.Draw):
243
+ # Single elegant inner border with padding
244
+ padding = 40
245
+ draw.rectangle(
246
+ [(padding, padding),
247
+ (self.certificate_size[0] - padding, self.certificate_size[1] - padding)],
248
+ outline='#1C1D1F',
249
+ width=2
250
+ )
251
+
252
+ def _add_content(
253
+ self,
254
+ draw: ImageDraw.Draw,
255
+ fonts: Dict[str, ImageFont.FreeTypeFont],
256
+ name: str,
257
+ course_name: str,
258
+ score: float
259
+ ):
260
+ # Add "CERTIFICATE OF COMPLETION" text
261
+ draw.text((60, 140), "CERTIFICATE OF COMPLETION", font=fonts['subtitle'], fill='#666666')
262
+
263
+ # Add course name (large and bold)
264
+ course_name = course_name.strip() or "Assessment"
265
+ text_width = draw.textlength(course_name, fonts['title'])
266
+ max_width = self.certificate_size[0] - 120 # Leave margins
267
+ if text_width > max_width:
268
+ words = course_name.split()
269
+ lines = []
270
+ current_line = []
271
+ current_width = 0
272
+ for word in words:
273
+ word_width = draw.textlength(word + " ", fonts['title'])
274
+ if current_width + word_width <= max_width:
275
+ current_line.append(word)
276
+ current_width += word_width
277
+ else:
278
+ lines.append(" ".join(current_line))
279
+ current_line = [word]
280
+ current_width = word_width
281
+ if current_line:
282
+ lines.append(" ".join(current_line))
283
+ course_name = "\n".join(lines)
284
+
285
+ draw.multiline_text((60, 200), course_name, font=fonts['title'], fill='#1C1D1F', spacing=10)
286
+
287
+ # Add instructor info
288
+ draw.text((60, 300), "Instructor", font=fonts['subtitle'], fill='#666666')
289
+ draw.text((60, 330), "CertifyMe AI", font=fonts['text'], fill='#1C1D1F')
290
+
291
+ # Add participant name (large)
292
+ name = name.strip() or "Participant"
293
+ draw.text((60, 420), name, font=fonts['name'], fill='#1C1D1F')
294
+
295
+ # Add date and score info with spacing
296
+ date_str = datetime.now().strftime("%b. %d, %Y")
297
+
298
+ # Date section
299
+ draw.text((60, 500), "Date", font=fonts['subtitle'], fill='#666666')
300
+ draw.text((60, 530), date_str, font=fonts['text'], fill='#1C1D1F')
301
+
302
+ # Score section
303
+ draw.text((300, 500), "Score", font=fonts['subtitle'], fill='#666666')
304
+ draw.text((300, 530), f"{float(score):.1f}%", font=fonts['text'], fill='#1C1D1F')
305
+
306
+ # Footer section with certificate number and reference
307
+ certificate_id = f"Certificate no: {datetime.now().strftime('%Y%m%d')}-{abs(hash(name)) % 10000:04d}"
308
+ ref_number = f"Reference Number: {abs(hash(name + date_str)) % 10000:04d}"
309
+
310
+ # Draw footer text aligned to left and right
311
+ draw.text((60, 720), certificate_id, font=fonts['subtitle'], fill='#666666')
312
+ draw.text((1140, 720), ref_number, font=fonts['subtitle'], fill='#666666', anchor="ra")
313
+
314
+ def _add_logo(self, certificate: Image.Image, logo_path: str):
315
+ try:
316
+ logo = Image.open(logo_path)
317
+ # Resize logo to appropriate size
318
+ logo.thumbnail((150, 80))
319
+ # Position in top-left corner with padding
320
+ certificate.paste(logo, (60, 50), mask=logo if 'A' in logo.getbands() else None)
321
+ except Exception as e:
322
+ print(f"Error adding logo: {e}")
323
+
324
+ def _add_photo(self, certificate: Image.Image, photo_path: str):
325
+ try:
326
+ photo = Image.open(photo_path)
327
+ # Create circular mask
328
+ size = (100, 100)
329
+ mask = Image.new('L', size, 0)
330
+ draw = ImageDraw.Draw(mask)
331
+ draw.ellipse((0, 0, size[0], size[1]), fill=255)
332
+
333
+ # Resize photo maintaining aspect ratio
334
+ photo.thumbnail(size)
335
+
336
+ # Create a circular photo
337
+ output = Image.new('RGBA', size, (0, 0, 0, 0))
338
+ output.paste(photo, (0, 0))
339
+ output.putalpha(mask)
340
+
341
+ # Position in top-right corner with padding
342
+ certificate.paste(output, (1000, 50), mask=output)
343
+ except Exception as e:
344
+ print(f"Error adding photo: {e}")
345
+
346
+ def generate(
347
+ self,
348
+ score: float,
349
+ name: str,
350
+ course_name: str,
351
+ company_logo: Optional[str] = None,
352
+ participant_photo: Optional[str] = None
353
+ ) -> str:
354
+ try:
355
+ certificate = self._create_base_certificate()
356
+ draw = ImageDraw.Draw(certificate)
357
+
358
+ # Add professional border
359
+ self._add_professional_border(draw)
360
+
361
+ fonts = self._load_fonts()
362
+ self._add_content(draw, fonts, str(name), str(course_name), float(score))
363
+
364
+ if company_logo:
365
+ self._add_logo(certificate, company_logo)
366
+
367
+ if participant_photo:
368
+ self._add_photo(certificate, participant_photo)
369
+
370
+ return self._save_certificate(certificate)
371
+ except Exception as e:
372
+ print(f"Error generating certificate: {e}")
373
+ return None
374
+
375
+ def _create_base_certificate(self) -> Image.Image:
376
+ return Image.new('RGB', self.certificate_size, self.background_color)
377
+
378
+ def _save_certificate(self, certificate: Image.Image) -> str:
379
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
380
+ certificate.save(temp_file.name, 'PNG', quality=95)
381
+ return temp_file.name
382
+
383
+
384
+ class QuizApp:
385
+ def __init__(self, api_key: str):
386
+ self.quiz_generator = QuizGenerator(api_key)
387
+ self.certificate_generator = CertificateGenerator()
388
+ self.current_questions: List[Question] = []
389
+
390
+ def generate_questions(self, text: str, num_questions: int) -> Tuple[bool, List[Question]]:
391
+ """
392
+ Generate quiz questions using the QuizGenerator
393
+ Returns (success, questions) tuple
394
+ """
395
+ try:
396
+ questions = self.quiz_generator.generate_questions(text, num_questions)
397
+ self.current_questions = questions
398
+ return True, questions
399
+ except Exception as e:
400
+ print(f"Error generating questions: {e}")
401
+ return False, []
402
+
403
+ def calculate_score(self, answers: List[Optional[str]]) -> Tuple[float, bool, List[QuizFeedback]]:
404
+ """
405
+ Calculate the quiz score and generate feedback
406
+ Returns (score, passed, feedback) tuple
407
+ """
408
+ if not answers or not self.current_questions:
409
+ return 0, False, []
410
+
411
+ feedback = []
412
+ correct = 0
413
+
414
+ for question, answer in zip(self.current_questions, answers):
415
+ if answer is None:
416
+ feedback.append(QuizFeedback(False, None, question.options[question.correct_answer]))
417
+ continue
418
+
419
+ try:
420
+ selected_index = question.options.index(answer)
421
+ is_correct = selected_index == question.correct_answer
422
+ if is_correct:
423
+ correct += 1
424
+ feedback.append(QuizFeedback(
425
+ is_correct,
426
+ answer,
427
+ question.options[question.correct_answer]
428
+ ))
429
+ except ValueError:
430
+ feedback.append(QuizFeedback(False, answer, question.options[question.correct_answer]))
431
+
432
+ score = (correct / len(self.current_questions)) * 100
433
+ return score, score >= 80, feedback
434
+
435
+ def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]:
436
+ """
437
+ Event handler for generating new questions
438
+ """
439
+ if not text.strip():
440
+ return (
441
+ gr.update(value=""),
442
+ gr.update(value="⚠️ Please enter some text content to generate questions."),
443
+ *[gr.update(visible=False, choices=[]) for _ in range(5)],
444
+ [],
445
+ gr.update(selected=1)
446
+ )
447
+
448
+ success, questions = self.generate_questions(text, num_questions)
449
+
450
+ if not success or not questions:
451
+ return (
452
+ gr.update(value=""),
453
+ gr.update(value="❌ Failed to generate questions. Please try again."),
454
+ *[gr.update(visible=False, choices=[]) for _ in range(5)],
455
+ [],
456
+ gr.update(selected=1)
457
+ )
458
+
459
+ # Create question display
460
+ questions_html = "# πŸ“ Assessment Questions\n\n"
461
+ questions_html += "> Please select one answer for each question.\n\n"
462
+
463
+ # Update radio buttons
464
+ updates = []
465
+ for i, q in enumerate(questions):
466
+ questions_html += f"### Question {i+1}\n{q.question}\n\n"
467
+ updates.append(gr.update(
468
+ visible=True,
469
+ choices=q.options,
470
+ value=None,
471
+ label=f"Select your answer:"
472
+ ))
473
+
474
+ # Hide unused radio buttons
475
+ for i in range(len(questions), 5):
476
+ updates.append(gr.update(visible=False, choices=[]))
477
+
478
+ return (
479
+ gr.update(value=questions_html),
480
+ gr.update(value=""),
481
+ *updates,
482
+ questions,
483
+ gr.update(selected=1)
484
+ )
485
+
486
+ def submit_quiz(self, q1: Optional[str], q2: Optional[str], q3: Optional[str],
487
+ q4: Optional[str], q5: Optional[str], questions: List[Question]
488
+ ) -> Tuple[gr.update, List[gr.update], float, str, gr.update]:
489
+ """
490
+ Event handler for quiz submission
491
+ """
492
+ answers = [q1, q2, q3, q4, q5][:len(questions)]
493
+
494
+ if not all(a is not None for a in answers):
495
+ return (
496
+ gr.update(value="⚠️ Please answer all questions before submitting."),
497
+ *[gr.update() for _ in range(5)],
498
+ 0,
499
+ "",
500
+ gr.update(selected=1)
501
+ )
502
+
503
+ score, passed, feedback = self.calculate_score(answers)
504
+
505
+ # Create feedback HTML
506
+ feedback_html = "# Assessment Results\n\n"
507
+ for i, (q, f) in enumerate(zip(self.current_questions, feedback)):
508
+ color = "green" if f.is_correct else "red"
509
+ symbol = "βœ…" if f.is_correct else "❌"
510
+ feedback_html += f"""
511
+ ### Question {i+1}
512
+ {q.question}
513
+
514
+ <div style="color: {color}; padding: 10px; margin: 5px 0; border-left: 3px solid {color};">
515
+ {symbol} Your answer: {f.selected}
516
+ {'' if f.is_correct else f'<br>Correct answer: {f.correct_answer}'}
517
+ </div>
518
+ """
519
+
520
+ # Add result message
521
+ if passed:
522
+ feedback_html += self._create_success_message(score)
523
+ result_msg = f"πŸŽ‰ Congratulations! You passed with {score:.1f}%"
524
+ else:
525
+ feedback_html += self._create_failure_message(score)
526
+ result_msg = f"Score: {score:.1f}%. You need 80% to pass."
527
+
528
+ return (
529
+ gr.update(value=feedback_html),
530
+ *[gr.update(visible=False) for _ in range(5)],
531
+ score,
532
+ result_msg,
533
+ gr.update(selected=2)
534
+ )
535
+
536
+ def _create_success_message(self, score: float) -> str:
537
+ return f"""
538
+ <div style="background-color: #e6ffe6; padding: 20px; margin-top: 20px; border-radius: 10px;">
539
+ <h3 style="color: #008000;">πŸŽ‰ Congratulations!</h3>
540
+ <p>You passed the assessment with a score of {score:.1f}%</p>
541
+ <p>Your certificate has been generated.</p>
542
+ </div>
543
+ """
544
+
545
+ def _create_failure_message(self, score: float) -> str:
546
+ return f"""
547
+ <div style="background-color: #ffe6e6; padding: 20px; margin-top: 20px; border-radius: 10px;">
548
+ <h3 style="color: #cc0000;">Please Try Again</h3>
549
+ <p>Your score: {score:.1f}%</p>
550
+ <p>You need 80% or higher to pass and receive a certificate.</p>
551
+ </div>
552
+ """
553
+
554
+ def create_quiz_interface():
555
+ if not os.getenv("GROQ_API_KEY"):
556
+ raise EnvironmentError("Please set your GROQ_API_KEY environment variable")
557
+
558
+ quiz_app = QuizApp(os.getenv("GROQ_API_KEY"))
559
+
560
+ with gr.Blocks(title="CertifyMe AI", theme=gr.themes.Soft()) as demo:
561
+ # State management
562
+ current_questions = gr.State([])
563
+ current_question_idx = gr.State(0)
564
+ answer_state = gr.State([None] * 5) # Store all answers
565
+
566
+ # Header
567
+ gr.Markdown("""
568
+ # πŸŽ“ CertifyMe AI
569
+ ### Transform Your Knowledge into Recognized Achievements
570
+ """)
571
+
572
+ with gr.Tabs() as tabs:
573
+ # Profile Setup Tab
574
+ with gr.Tab(id=1,label="πŸ“‹ Step 1: Profile Setup"):
575
+ with gr.Row():
576
+ name = gr.Textbox(label="Full Name", placeholder="Enter your full name")
577
+ email = gr.Textbox(label="Email", placeholder="Enter your email")
578
+
579
+ text_input = gr.Textbox(
580
+ label="Learning Content",
581
+ placeholder="Enter the text content you want to be assessed on",
582
+ lines=10
583
+ )
584
+
585
+ num_questions = gr.Slider(
586
+ minimum=1,
587
+ maximum=20,
588
+ value=10,
589
+ step=1,
590
+ label="Number of Questions"
591
+ )
592
+
593
+ with gr.Row():
594
+ company_logo = gr.Image(label="Company Logo (Optional)", type="filepath")
595
+ participant_photo = gr.Image(label="Your Photo (Optional)", type="filepath")
596
+
597
+ generate_btn = gr.Button("Generate Assessment", variant="primary", size="lg")
598
+
599
+ # Assessment Tab
600
+ with gr.Tab(id=2,label="πŸ“ Step 2: Take Assessment") as assessment_tab:
601
+ with gr.Column(visible=True) as question_box:
602
+ # Question section
603
+ with gr.Group():
604
+ # Question display
605
+ question_display = gr.Markdown("", label="Current Question")
606
+
607
+ # Single radio group for current question
608
+ current_options = gr.Radio(
609
+ choices=[],
610
+ label="Select your answer:",
611
+ visible=False
612
+ )
613
+
614
+ # Navigation
615
+ with gr.Row():
616
+ prev_btn = gr.Button("← Previous", variant="secondary", size="sm")
617
+ question_counter = gr.Markdown("Question 1")
618
+ next_btn = gr.Button("Next β†’", variant="secondary", size="sm")
619
+
620
+ gr.Markdown("---") # Separator
621
+
622
+ with gr.Row():
623
+ submit_btn = gr.Button(
624
+ "Submit Assessment",
625
+ variant="primary",
626
+ size="lg"
627
+ )
628
+ reset_btn = gr.Button(
629
+ "Reset Quiz",
630
+ variant="secondary",
631
+ size="lg"
632
+ )
633
+
634
+ # Results section
635
+ with gr.Group(visible=False) as results_group:
636
+ feedback_box = gr.Markdown("")
637
+ with gr.Row():
638
+ view_cert_btn = gr.Button(
639
+ "View Certificate",
640
+ variant="primary",
641
+ size="lg",
642
+ visible=False
643
+ )
644
+ back_to_assessment = gr.Button( # Add this button definition
645
+ "Back to Assessment",
646
+ variant="secondary",
647
+ size="lg",
648
+ visible=True
649
+ )
650
+
651
+ # Certification Tab
652
+ with gr.Tab(id=3,label="πŸŽ“ Step 3: Get Certified"):
653
+ score_display = gr.Number(label="Your Score")
654
+ result_message = gr.Markdown("")
655
+ course_name = gr.Textbox(
656
+ label="Certification Title",
657
+ value="Professional Assessment Certification"
658
+ )
659
+ certificate_display = gr.Image(label="Your Certificate")
660
+
661
+ # Helper Functions
662
+ def on_generate_questions(text, num_questions):
663
+ """Generate quiz questions and setup initial state"""
664
+ success, questions = quiz_app.generate_questions(text, num_questions)
665
+ if not success:
666
+ return [
667
+ "", # question_display
668
+ gr.update(choices=[], visible=False), # current_options
669
+ "", # question_counter
670
+ gr.update(visible=False), # question_box
671
+ [], # current_questions
672
+ 0, # current_question_idx
673
+ [None] * 5, # answer_state
674
+ gr.Tabs(selected=2), # tabs
675
+ gr.update(visible=False), # results_group
676
+ gr.update(visible=False) # view_cert_btn - added this
677
+ ]
678
+
679
+ # Setup initial state with first question
680
+ initial_answers = [None] * len(questions)
681
+ question = questions[0]
682
+ question_html = f"""## Question 1
683
+ {question.question}
684
+
685
+ Please select one answer:"""
686
+
687
+ return [
688
+ question_html, # question_display
689
+ gr.update(
690
+ choices=question.options,
691
+ value=None,
692
+ visible=True,
693
+ label="Select your answer for Question 1:"
694
+ ), # current_options
695
+ f"Question 1 of {len(questions)}", # question_counter
696
+ gr.update(visible=True), # question_box
697
+ questions, # current_questions
698
+ 0, # current_question_idx
699
+ initial_answers, # answer_state
700
+ gr.Tabs(selected=2), # tabs
701
+ gr.update(visible=False), # results_group
702
+ gr.update(visible=False) # view_cert_btn - added this
703
+ ]
704
+ def navigate(direction, current_idx, questions, answers, current_answer):
705
+ """Handle navigation between questions"""
706
+ if not questions:
707
+ return [
708
+ 0,
709
+ answers,
710
+ "",
711
+ gr.update(choices=[], value=None, visible=False),
712
+ "",
713
+ gr.update(visible=False)
714
+ ]
715
+
716
+ new_answers = list(answers)
717
+ if current_answer and 0 <= current_idx < len(new_answers):
718
+ new_answers[current_idx] = current_answer
719
+
720
+ new_idx = max(0, min(len(questions) - 1, current_idx + direction))
721
+ question = questions[new_idx]
722
+
723
+ return [
724
+ new_idx,
725
+ new_answers,
726
+ f"## Question {new_idx + 1}\n{question.question}\n\nPlease select one answer:",
727
+ gr.update(
728
+ choices=question.options,
729
+ value=new_answers[new_idx] if new_idx < len(new_answers) else None,
730
+ visible=True,
731
+ label=f"Select your answer for Question {new_idx + 1}:"
732
+ ),
733
+ f"Question {new_idx + 1} of {len(questions)}",
734
+ gr.update(visible=True)
735
+ ]
736
+ def handle_prev(current_idx, questions, answers, current_answer):
737
+ return navigate(-1, current_idx, questions, answers, current_answer)
738
+
739
+ def handle_next(current_idx, questions, answers, current_answer):
740
+ return navigate(1, current_idx, questions, answers, current_answer)
741
+
742
+ def update_answer_state(answer, idx, current_answers):
743
+ new_answers = list(current_answers)
744
+ if 0 <= idx < len(new_answers):
745
+ new_answers[idx] = answer
746
+ return new_answers
747
+
748
+ def reset_quiz(text, num_questions):
749
+ """Handle quiz reset"""
750
+ return on_generate_questions(text, num_questions)
751
+
752
+ def view_certificate():
753
+ """Navigate to certificate tab"""
754
+ return gr.Tabs(selected=2)
755
+
756
+ def on_submit(questions, answers, current_idx, current_answer):
757
+ final_answers = list(answers)
758
+ if 0 <= current_idx < len(final_answers):
759
+ final_answers[current_idx] = current_answer
760
+
761
+ if not all(a is not None for a in final_answers[:len(questions)]):
762
+ return [
763
+ "⚠️ Please answer all questions before submitting.",
764
+ gr.update(visible=True),
765
+ 0,
766
+ "",
767
+ gr.update(visible=True),
768
+ gr.Tabs(selected=2),
769
+ gr.update(visible=False)
770
+ ]
771
+
772
+ score, passed, feedback = quiz_app.calculate_score(final_answers[:len(questions)])
773
+
774
+ feedback_html = "# Assessment Results\n\n"
775
+ for i, (q, f) in enumerate(zip(questions, feedback)):
776
+ color = "green" if f.is_correct else "red"
777
+ symbol = "βœ…" if f.is_correct else "❌"
778
+
779
+ feedback_html += f"""### Question {i+1}
780
+ {q.question}
781
+ <div style="color: {color}; padding: 10px; margin: 10px 0; border-left: 3px solid {color}; background-color: {'#f8fff8' if f.is_correct else '#fff8f8'};">
782
+ {symbol} Your answer: {f.selected or "No answer"}
783
+ {'' if f.is_correct else f'<br>Correct answer: {f.correct_answer}'}
784
+ </div>\n"""
785
+
786
+ result_msg = "πŸŽ‰ Passed!" if passed else "Please try again"
787
+ if not passed:
788
+ feedback_html += f"""<div style="background-color: #ffe6e6; padding: 20px; margin: 20px 0; border-radius: 10px; border: 1px solid #ffcccc;">
789
+ <h3 style="color: #cc0000; margin-top: 0;">Please Try Again</h3>
790
+ <p style="margin: 10px 0;">Your score: {score:.1f}%</p>
791
+ <p style="margin: 10px 0;">You need 80% or higher to pass and receive a certificate.</p>
792
+ </div>"""
793
+ else:
794
+ feedback_html += f"""<div style="background-color: #e6ffe6; padding: 20px; margin: 20px 0; border-radius: 10px; border: 1px solid #ccffcc;">
795
+ <h3 style="color: #008000; margin-top: 0;">πŸŽ‰ Congratulations!</h3>
796
+ <p style="margin: 10px 0;">You passed with a score of {score:.1f}%</p>
797
+ <p style="margin: 10px 0;">Click "View Certificate" to see your certificate.</p>
798
+ </div>"""
799
+
800
+ return [
801
+ feedback_html,
802
+ gr.update(visible=True),
803
+ score,
804
+ result_msg,
805
+ gr.update(visible=False),
806
+ gr.Tabs(selected=2),
807
+ gr.update(visible=passed)
808
+ ]
809
+ # Event handlers
810
+
811
+ generate_btn.click(fn=on_generate_questions, inputs=[text_input, num_questions], outputs=[question_display, current_options, question_counter, question_box, current_questions, current_question_idx, answer_state, tabs, results_group, view_cert_btn]).then(fn=lambda: gr.Tabs(selected=2), outputs=tabs)
812
+
813
+ 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])
814
+
815
+ 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])
816
+
817
+ current_options.change(fn=update_answer_state, inputs=[current_options, current_question_idx, answer_state], outputs=answer_state)
818
+
819
+ 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, tabs, view_cert_btn])
820
+
821
+ reset_btn.click(fn=reset_quiz, inputs=[text_input, num_questions], outputs=[question_display, current_options, question_counter, question_box, current_questions, current_question_idx, answer_state, tabs, results_group, view_cert_btn])
822
+
823
+ view_cert_btn.click(fn=lambda: gr.Tabs(selected=3), outputs=tabs)
824
+
825
+ back_to_assessment.click(fn=lambda: gr.Tabs(selected=2), outputs=tabs)
826
+
827
+ 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)
828
+
829
+
830
+ return demo
831
+
832
+ if __name__ == "__main__":
833
+ demo = create_quiz_interface()
834
+ demo.launch()
835
+