mayurm6 commited on
Commit
596ebca
·
verified ·
1 Parent(s): 62bf672

Upload 3 files

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