LovnishVerma commited on
Commit
e1da95c
·
verified ·
1 Parent(s): c37eb7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +567 -838
app.py CHANGED
@@ -1,6 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- CCC Plus Quiz App Enhanced Production-ready Gradio app
 
4
  """
5
 
6
  import argparse
@@ -10,7 +11,7 @@ import tempfile
10
  import json
11
  from datetime import datetime
12
  from pathlib import Path
13
- from typing import Optional, Tuple, List, Dict
14
 
15
  import gradio as gr
16
  import pandas as pd
@@ -18,948 +19,677 @@ from reportlab.lib import colors
18
  from reportlab.lib.pagesizes import A4
19
  from reportlab.lib.units import mm
20
  from reportlab.pdfgen import canvas
21
- from reportlab.platypus import Image, Table, TableStyle
 
 
22
 
23
- # -----------------------
24
- # Configuration & Logging
25
- # -----------------------
26
  logging.basicConfig(
27
  level=logging.INFO,
28
- format="%(asctime)s [%(levelname)s] %(message)s",
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
- # Configuration constants
33
  DEFAULT_CSV_PATH = "quiz.csv"
34
  REQUIRED_COLUMNS = {"question", "option1", "option2", "option3", "option4", "answer"}
35
- MAX_QUESTION_LENGTH = 300
36
- MAX_OPTION_LENGTH = 80
37
- PDF_MARGIN = 20 * mm
38
 
39
- # -----------------------
40
- # Enhanced Utility helpers
41
- # -----------------------
42
- def extract_file_path(uploaded) -> Optional[str]:
43
- """
44
- Robustly extract a usable file-system path from Gradio's uploaded-file object.
45
- """
46
- if uploaded is None:
47
- return None
48
-
49
- # If already a filesystem path string
50
- if isinstance(uploaded, str):
51
- return uploaded if os.path.exists(uploaded) else None
52
-
53
- # If Gradio returns a dict (common)
54
- if isinstance(uploaded, dict):
55
- for key in ("tmp_path", "tempfile", "file", "name", "tmpfile", "path"):
56
- p = uploaded.get(key)
57
- if isinstance(p, str) and os.path.exists(p):
58
- return p
59
- # sometimes 'name' is only filename but 'tempfile' is full path
60
- if "name" in uploaded and os.path.exists(uploaded["name"]):
61
- return uploaded["name"]
62
- return None
63
-
64
- # If file-like object (has .name pointing to path)
65
- if hasattr(uploaded, "name") and isinstance(uploaded.name, str) and os.path.exists(uploaded.name):
66
- return uploaded.name
67
-
68
- # last-ditch: try repr
69
- try:
70
- s = str(uploaded)
71
- if os.path.exists(s):
72
- return s
73
- except Exception:
74
- pass
75
-
76
- return None
77
-
78
-
79
- def safe_read_csv(path: str) -> pd.DataFrame:
80
- """
81
- Read CSV with enhanced error handling and encoding detection.
82
- """
83
- if path is None:
84
- raise FileNotFoundError("No CSV path provided.")
85
-
86
- if not os.path.exists(path):
87
- raise FileNotFoundError(f"File not found: {path}")
88
-
89
- # Try multiple encodings
90
- encodings = ['utf-8', 'latin1', 'cp1252', 'iso-8859-1']
91
-
92
- for encoding in encodings:
93
  try:
94
- df = pd.read_csv(path, encoding=encoding)
95
- logger.info(f"Successfully read CSV with {encoding} encoding")
96
- return df
97
- except UnicodeDecodeError:
98
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  except Exception as e:
100
- logger.warning(f"Error reading CSV with {encoding}: {e}")
101
- continue
102
-
103
- raise ValueError("Could not read CSV file with any supported encoding")
104
-
105
-
106
- def validate_quiz_data(quiz: List[Dict]) -> List[str]:
107
- """
108
- Validate quiz data and return list of warnings/errors.
109
- """
110
- warnings = []
111
 
112
- if not quiz:
113
- warnings.append("No questions found in the quiz data")
114
- return warnings
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- for i, q in enumerate(quiz, 1):
117
- # Check for empty questions
118
- if not q.get("question", "").strip():
119
- warnings.append(f"Question {i}: Empty question text")
120
 
121
- # Check for duplicate options
122
- options = [opt.strip().lower() for opt in q.get("options", []) if opt.strip()]
123
- if len(options) != len(set(options)):
124
- warnings.append(f"Question {i}: Duplicate answer options detected")
 
 
 
 
 
 
 
 
 
125
 
126
- # Check if answer matches any option
127
- answer = q.get("answer", "").strip()
128
- if answer not in q.get("options", []):
129
- warnings.append(f"Question {i}: Answer '{answer}' not found in options")
130
 
131
- # Check for very short options
132
- short_options = [opt for opt in q.get("options", []) if len(opt.strip()) < 2]
133
- if short_options:
134
- warnings.append(f"Question {i}: Very short answer options detected")
135
-
136
- return warnings
137
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- # -----------------------
140
- # Enhanced Quiz CSV loader
141
- # -----------------------
142
- def load_quiz_from_path(path: str) -> Tuple[List[Dict], List[str]]:
143
- """
144
- Load quiz from CSV and return quiz data and validation warnings.
145
- """
146
- df = safe_read_csv(path)
147
-
148
- # Normalize column names (case-insensitive)
149
- df.columns = df.columns.str.strip().str.lower()
150
- cols = set(df.columns)
151
-
152
- # Check for required columns
153
- missing = REQUIRED_COLUMNS - cols
154
- if missing:
155
- raise ValueError(f"CSV missing required columns (case-insensitive): {missing}")
156
 
157
- # Process quiz data
158
- quiz = []
159
- for idx, row in df.iterrows():
 
 
160
  try:
161
- question = str(row["question"]).strip()
162
- options = [
163
- str(row["option1"]).strip(),
164
- str(row["option2"]).strip(),
165
- str(row["option3"]).strip(),
166
- str(row["option4"]).strip(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  ]
168
- answer = str(row["answer"]).strip()
169
 
170
- # Skip empty questions
171
- if not question or question.lower() in ['nan', 'none', '']:
172
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- quiz.append({
175
- "question": question,
176
- "options": options,
177
- "answer": answer
178
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  except Exception as e:
181
- logger.warning(f"Error processing row {idx + 1}: {e}")
182
- continue
183
-
184
- # Validate quiz data
185
- warnings = validate_quiz_data(quiz)
186
-
187
- return quiz, warnings
188
-
189
-
190
- # -----------------------
191
- # Enhanced PDF generation
192
- # -----------------------
193
- def generate_report_pdf(
194
- user_name: str,
195
- quiz_title: str,
196
- history: List[Dict],
197
- score: int,
198
- logo_path: Optional[str] = None,
199
- include_statistics: bool = True
200
- ) -> str:
201
- """
202
- Create an enhanced PDF report with statistics and better formatting.
203
- """
204
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
205
- tmp = tempfile.NamedTemporaryFile(
206
- prefix=f"CCCPlus_Report_{ts}_",
207
- suffix=".pdf",
208
- delete=False
209
- )
210
- tmp.close()
211
- filename = tmp.name
212
-
213
- c = canvas.Canvas(filename, pagesize=A4)
214
- width, height = A4
215
- y = height - PDF_MARGIN
216
-
217
- # Draw logo if available
218
- if logo_path and os.path.exists(logo_path):
219
- try:
220
- img_w = 30 * mm
221
- img = Image(logo_path, img_w, img_w)
222
- img.drawOn(c, PDF_MARGIN, y - img_w)
223
- x_title = PDF_MARGIN + img_w + 8
224
- except Exception as e:
225
- logger.warning(f"Failed to draw logo on PDF: {e}")
226
- x_title = PDF_MARGIN
227
- else:
228
- x_title = PDF_MARGIN
229
 
230
- # Header
231
- c.setFont("Helvetica-Bold", 18)
232
- c.drawString(x_title, y - 8, quiz_title)
233
-
234
- c.setFont("Helvetica", 10)
235
- c.drawString(x_title, y - 28, f"Candidate: {user_name}")
236
- c.drawString(x_title, y - 42, f"Date: {datetime.now().strftime('%d %b %Y %H:%M:%S')}")
237
- y -= 60
238
 
239
- # Score and Statistics
240
- c.setFont("Helvetica-Bold", 14)
241
- total_questions = len(history)
242
- percentage = (score / total_questions * 100) if total_questions > 0 else 0
 
 
 
 
243
 
244
- c.drawString(PDF_MARGIN, y, f"Final Score: {score} / {total_questions} ({percentage:.1f}%)")
245
- y -= 22
246
-
247
- if include_statistics and history:
248
- # Add performance statistics
249
- c.setFont("Helvetica", 10)
250
- correct_count = sum(1 for h in history if h.get("correct", False))
251
- incorrect_count = total_questions - correct_count
252
-
253
- c.drawString(PDF_MARGIN, y, f"Correct Answers: {correct_count}")
254
- c.drawString(PDF_MARGIN + 100, y, f"Incorrect Answers: {incorrect_count}")
255
- y -= 16
256
-
257
- y -= 10
258
-
259
- # Results table
260
- table_data = [["#", "Question", "Your Answer", "Correct Answer", "Result"]]
261
-
262
- for i, h in enumerate(history, start=1):
263
- result = "✓ Correct" if h.get("correct") else "✗ Incorrect"
264
-
265
- # Truncate long text to prevent layout issues
266
- question_text = (h.get("question") or "")[:MAX_QUESTION_LENGTH]
267
- selected_text = (h.get("selected") or "")[:MAX_OPTION_LENGTH]
268
- answer_text = (h.get("answer") or "")[:MAX_OPTION_LENGTH]
269
-
270
- table_data.append([
271
- str(i),
272
- question_text,
273
- selected_text,
274
- answer_text,
275
- result
276
- ])
277
-
278
- # Create and style table
279
- col_widths = [12 * mm, 90 * mm, 40 * mm, 40 * mm, 25 * mm]
280
- table = Table(table_data, colWidths=col_widths)
281
-
282
- table_style = [
283
- ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
284
- ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#003366")),
285
- ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
286
- ("VALIGN", (0, 0), (-1, -1), "TOP"),
287
- ("FONTSIZE", (0, 0), (-1, -1), 8),
288
- ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
289
- ]
290
-
291
- # Add alternating row colors
292
- for i in range(1, len(table_data)):
293
- if i % 2 == 0:
294
- table_style.append(("BACKGROUND", (0, i), (-1, i), colors.HexColor("#f5f5f5")))
295
 
296
- table.setStyle(TableStyle(table_style))
297
-
298
- # Layout table with page breaks if needed
299
- w, h_table = table.wrapOn(c, width - 2 * PDF_MARGIN, y - PDF_MARGIN)
300
-
301
- if y - h_table < PDF_MARGIN + 30:
302
- c.showPage()
303
- y = height - PDF_MARGIN
304
 
305
- table.drawOn(c, PDF_MARGIN, y - h_table)
306
- y = y - h_table - 30
307
-
308
- # Footer
309
- c.setFont("Helvetica", 8)
310
- footer_text = f"NIELIT Chandigarh — © {datetime.now().year} — Generated automatically"
311
- c.drawCentredString(width / 2, 12 * mm, footer_text)
312
-
313
- c.save()
314
- logger.info(f"Generated enhanced PDF report: {filename}")
315
- return filename
316
-
317
-
318
- # -----------------------
319
- # Enhanced Quiz Logic
320
- # -----------------------
321
- def prepare_quiz(csv_file_text: str, csv_upload):
322
- """
323
- Load questions from CSV with enhanced validation and feedback.
324
- """
325
- uploaded_path = extract_file_path(csv_upload)
326
- path = uploaded_path or (csv_file_text if csv_file_text and os.path.exists(csv_file_text) else None)
327
 
328
- if not path:
329
- msg = "⚠️ No questions loaded. Upload a CSV file or ensure quiz.csv exists in the app folder."
330
- return "", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), msg, {}
331
-
332
- try:
333
- quiz, warnings = load_quiz_from_path(path)
334
- except Exception as e:
335
- logger.exception("Failed to load quiz CSV")
336
- error_msg = f"❌ Error loading CSV: {str(e)}"
337
- return "", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), error_msg, {}
338
-
339
- if not quiz:
340
- msg = "❌ No valid questions found in CSV file."
341
- return "", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), msg, {}
342
 
343
- # Prepare state
344
- state = {
345
- "quiz": quiz,
346
- "index": 0,
347
- "score": 0,
348
- "history": [],
349
- "started": False,
350
- "total_questions": len(quiz)
351
- }
352
-
353
- first_question = quiz[0]
354
-
355
- # Create status message with warnings
356
- msg_parts = [f"✅ Loaded {len(quiz)} questions successfully."]
357
 
358
- if warnings:
359
- msg_parts.append(f"\n⚠️ {len(warnings)} validation warning(s):")
360
- for warning in warnings[:3]: # Show first 3 warnings
361
- msg_parts.append(f" • {warning}")
362
- if len(warnings) > 3:
363
- msg_parts.append(f" • ... and {len(warnings) - 3} more warnings")
364
 
365
- msg_parts.append("\n🚀 Click 'Start Quiz' to begin!")
366
- status_msg = "\n".join(msg_parts)
 
 
367
 
368
- return (
369
- first_question["question"],
370
- gr.update(choices=first_question["options"], value=None),
371
- gr.update(visible=True),
372
- gr.update(visible=False),
373
- status_msg,
374
- state
375
- )
376
-
377
-
378
- def get_enhanced_progress_html(state) -> str:
379
- """
380
- Enhanced progress display with better styling and information.
381
- """
382
- if not state or "quiz" not in state or not state["quiz"]:
383
- return "<div style='text-align: center; color: #666;'>No quiz loaded</div>"
384
-
385
- total = len(state["quiz"])
386
- current = min(state.get("index", 0), total)
387
- score = state.get("score", 0)
388
-
389
- # Calculate percentage
390
- progress_pct = int((current / total) * 100) if total > 0 else 0
391
- score_pct = int((score / total) * 100) if total > 0 else 0
392
-
393
- # Determine progress color based on score percentage
394
- if score_pct >= 80:
395
- score_color = "#28a745" # green
396
- elif score_pct >= 60:
397
- score_color = "#ffc107" # yellow
398
- else:
399
- score_color = "#dc3545" # red
400
-
401
- html = f"""
402
- <div style='background: linear-gradient(135deg, #f8f9fa, #e9ecef);
403
- padding: 16px; border-radius: 12px; border: 1px solid #dee2e6;'>
404
- <div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;'>
405
- <span style='font-weight: 600; color: #495057;'>Question {current} of {total}</span>
406
- <span style='font-weight: 600; color: {score_color};'>Score: {score}/{total}</span>
407
  </div>
408
-
409
- <div style='background: #e9ecef; border-radius: 10px; overflow: hidden; height: 20px; margin-bottom: 8px;'>
410
- <div style='background: linear-gradient(90deg, #007bff, #0056b3);
411
- height: 100%; width: {progress_pct}%;
412
- transition: width 0.3s ease;'></div>
413
- </div>
414
-
415
- <div style='display: flex; justify-content: space-between; font-size: 12px; color: #6c757d;'>
416
- <span>Progress: {progress_pct}%</span>
417
- <span>Accuracy: {score_pct}%</span>
418
  </div>
 
419
  </div>
420
  """
421
 
422
- return html
423
-
424
-
425
- # -----------------------
426
- # Existing functions with minor enhancements
427
- # -----------------------
428
- def start_quiz(state):
429
- """Start the quiz with enhanced state management."""
430
- if not state or "quiz" not in state or not state["quiz"]:
431
- return "❌ No quiz loaded. Please load a CSV file first.", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
432
-
433
- # Reset quiz state
434
- state.update({
435
- "index": 0,
436
- "score": 0,
437
- "history": [],
438
- "started": True,
439
- "start_time": datetime.now()
440
- })
441
-
442
- first_question = state["quiz"][0]
443
  return (
444
- first_question["question"],
445
- gr.update(choices=first_question["options"], value=None),
446
- gr.update(visible=True),
447
- gr.update(visible=False),
448
- state
 
 
449
  )
450
 
451
-
452
- def submit_answer(state, selected):
453
- """Submit answer with enhanced feedback."""
454
- if not state or "quiz" not in state:
455
- return "❌ No quiz loaded.", gr.update(visible=False), gr.update(visible=False), state
456
 
457
- if not state.get("started"):
458
- return "❌ Quiz not started. Click 'Start Quiz' first.", gr.update(visible=False), gr.update(visible=False), state
459
-
460
- current_idx = state["index"]
461
- current_question = state["quiz"][current_idx]
462
- selected_answer = selected if selected is not None else ""
463
-
464
- is_correct = (selected_answer == current_question["answer"])
465
-
466
- # Store the result temporarily
467
- state["last_result"] = {
468
- "question": current_question["question"],
469
- "selected": selected_answer,
470
- "answer": current_question["answer"],
471
- "correct": is_correct
472
- }
473
 
474
- # Enhanced feedback
475
- if is_correct:
476
- feedback = f"✅ <span style='color: #28a745; font-weight: 600;'>Correct!</span>"
477
- else:
478
- feedback = f"❌ <span style='color: #dc3545; font-weight: 600;'>Incorrect.</span> The correct answer is: <strong>{current_question['answer']}</strong>"
479
 
480
- return feedback, gr.update(visible=True), gr.update(visible=True), state
481
-
482
-
483
- def next_question(state):
484
- """Move to next question with enhanced state management."""
485
- if not state or "quiz" not in state:
486
- return "❌ No quiz loaded.", gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
487
-
488
- # Process the last result if it exists
489
- if "last_result" in state:
490
- last_result = state.pop("last_result")
491
- state["history"].append(last_result)
492
- if last_result.get("correct"):
493
- state["score"] += 1
494
-
495
- # Move to next question
496
- state["index"] += 1
497
-
498
- # Check if quiz is complete
499
- if state["index"] >= len(state["quiz"]):
500
- state["started"] = False
501
- state["end_time"] = datetime.now()
502
-
503
- # Calculate completion time
504
- if "start_time" in state:
505
- duration = state["end_time"] - state["start_time"]
506
- duration_str = f"Completed in {duration.seconds // 60}m {duration.seconds % 60}s"
507
- else:
508
- duration_str = ""
509
-
510
- completion_msg = f"""
511
- 🎉 <strong>Quiz Completed!</strong><br>
512
- 📊 Final Score: <strong>{state['score']} / {len(state['quiz'])} ({state['score']/len(state['quiz'])*100:.1f}%)</strong><br>
513
- ⏱️ {duration_str}
514
- """
515
-
516
- return completion_msg, gr.update(choices=[]), gr.update(visible=False), gr.update(visible=False), state
517
 
518
- # Show next question
519
- next_q = state["quiz"][state["index"]]
520
  return (
521
- next_q["question"],
522
- gr.update(choices=next_q["options"], value=None),
523
- gr.update(visible=True),
524
- gr.update(visible=False),
525
- state
526
  )
527
 
528
-
529
- def show_report(state, user_name: str, logo_file):
530
- """Generate enhanced report with better formatting."""
531
- if not state or "history" not in state:
532
- return "📝 No quiz results available. Complete a quiz first.", None
533
-
534
- history = list(state["history"])
535
-
536
- # Include the last result if it exists but wasn't processed yet
537
- if "last_result" in state:
538
- history.append(state["last_result"])
539
-
540
- if not history:
541
- return "📝 No quiz results available. Complete a quiz first.", None
542
 
543
  # Generate markdown report
544
- candidate_name = user_name.strip() if user_name else "Anonymous Candidate"
545
- total_questions = len(history)
546
- correct_answers = sum(1 for h in history if h.get("correct", False))
547
- percentage = (correct_answers / total_questions * 100) if total_questions > 0 else 0
548
-
549
- # Determine performance level
550
- if percentage >= 90:
551
- performance = "🏆 Excellent"
552
- elif percentage >= 80:
553
- performance = "👍 Good"
554
- elif percentage >= 60:
555
- performance = "👌 Average"
556
- else:
557
- performance = "📚 Needs Improvement"
558
-
559
- md_lines = [
560
- f"# 📋 Quiz Report Card",
561
- f"**Candidate:** {candidate_name}",
562
  f"**Date:** {datetime.now().strftime('%d %B %Y at %H:%M:%S')}",
563
- f"**Final Score:** **{correct_answers} / {total_questions} ({percentage:.1f}%)**",
564
- f"**Performance Level:** {performance}",
565
- "",
566
- "## 📊 Detailed Results",
567
- "",
568
- "| # | Question | Your Answer | Correct Answer | Result |",
569
- "|---:|---|---|---|:---:|"
 
 
570
  ]
571
 
572
- for i, result in enumerate(history, start=1):
573
- status_icon = "✅" if result.get("correct") else "❌"
574
- question_text = (result.get("question", "") or "").replace("\n", " ")[:120]
575
- if len(question_text) > 117:
576
- question_text += "..."
577
-
578
- md_lines.append(
579
- f"| {i} | {question_text} | {result.get('selected', 'No answer')} | "
580
- f"{result.get('answer', 'Unknown')} | {status_icon} |"
581
  )
582
 
583
- report_md = "\n".join(md_lines)
584
 
585
  # Generate PDF
586
- logo_path = extract_file_path(logo_file) or (
587
- "nielit_logo.png" if os.path.exists("nielit_logo.png") else None
588
- )
589
 
590
- try:
591
- pdf_path = generate_report_pdf(
592
- candidate_name,
593
- "CCC Plus - Computer Concepts Plus",
594
- history,
595
- correct_answers,
596
- logo_path=logo_path,
597
- include_statistics=True
598
- )
599
- return report_md, pdf_path
600
-
601
- except Exception as e:
602
- logger.exception("Failed to generate PDF report")
603
- error_report = report_md + f"\n\n**⚠️ PDF Generation Failed:** {str(e)}"
604
- return error_report, None
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
 
607
- # -----------------------
608
- # Enhanced Gradio Interface
609
- # -----------------------
610
- def build_app():
611
- """Build the enhanced Gradio interface."""
612
- css = """
613
- .header {
614
- display: flex;
615
- align-items: center;
616
- gap: 12px;
617
  }
618
- .logo-small {
619
- height: 56px;
 
 
 
 
 
 
620
  }
621
- .card {
622
- border-radius: 12px;
623
- padding: 20px;
624
- box-shadow: 0 6px 18px rgba(0,0,0,0.08);
625
- background: white;
626
  border: 1px solid #e9ecef;
627
  }
628
- .muted {
629
- color: #6c757d;
630
- font-size: 14px;
631
- }
632
- .small {
633
- font-size: 13px;
634
- color: #495057;
635
  }
636
- .status-box {
 
637
  background: #f8f9fa;
638
- border: 1px solid #dee2e6;
639
- border-radius: 8px;
640
- padding: 12px;
641
- font-family: monospace;
642
- font-size: 13px;
643
  }
644
  """
645
-
646
  with gr.Blocks(
647
- title="CCC Plus Quiz App — NIELIT Chandigarh",
648
- css=css,
649
  theme=gr.themes.Soft()
650
- ) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
651
 
652
- # Header section
653
  with gr.Row():
654
- with gr.Column(scale=1, min_width=80):
655
- logo_img = gr.Image(
656
- value="nielit_logo.png" if os.path.exists("nielit_logo.png") else None,
657
- visible=True,
658
- interactive=False,
659
- height=80,
660
- show_label=False
 
661
  )
662
-
663
- with gr.Column(scale=6):
664
- gr.Markdown("""
665
- <div class='header'>
666
- <h1 style='margin:0; color: #003366;'>🖥️ CCC Plus — Computer Concepts Plus</h1>
667
- </div>
668
- <div class='muted' style='margin-top: 8px;'>
669
- Interactive quiz application for practice & assessment • Upload your CSV file or use the default quiz.csv
670
- </div>
671
- """)
672
-
673
- gr.Markdown("---")
674
-
675
- # Main interface
676
- with gr.Row():
677
- # Left panel - Controls
678
- with gr.Column(scale=3):
679
- # Quiz loading section
680
- with gr.Group():
681
- gr.Markdown("### 📁 Load Quiz Questions")
682
- csv_file_text = gr.Textbox(
683
- value=DEFAULT_CSV_PATH,
684
- label="Local CSV file path",
685
- placeholder="Enter path to CSV file...",
686
- interactive=True
687
- )
688
- csv_upload = gr.File(
689
- label="Or upload CSV file",
690
- file_count="single",
691
- file_types=[".csv"]
692
- )
693
- load_btn = gr.Button("🔄 Load Quiz", variant="primary")
694
-
695
- load_status = gr.HTML(
696
- value="<div class='status-box'>📄 No quiz loaded. Upload a CSV file to begin.</div>",
697
- label="Status"
698
- )
699
-
700
- # Candidate information
701
- with gr.Group():
702
- gr.Markdown("### 👤 Candidate Information")
703
- user_name = gr.Textbox(
704
- label="Full Name",
705
- placeholder="Enter your full name for the certificate...",
706
- value=""
707
- )
708
-
709
- # Quiz controls
710
- with gr.Group():
711
- gr.Markdown("### 🎯 Quiz Controls")
712
- start_btn = gr.Button("🚀 Start Quiz", variant="primary", size="lg")
713
- restart_btn = gr.Button("🔄 Reset All", variant="secondary")
714
-
715
- # Question controls (hidden initially)
716
- with gr.Group():
717
- gr.Markdown("### ✍️ Answer Controls")
718
- submit_btn = gr.Button("✅ Submit Answer", variant="primary", visible=False)
719
- next_btn = gr.Button("➡️ Next Question", visible=False)
720
- feedback_html = gr.HTML(visible=False)
721
-
722
- # Report section
723
- with gr.Group():
724
- gr.Markdown("### 📊 Generate Report")
725
- show_report_btn = gr.Button("📄 Generate Report")
726
-
727
- # Right panel - Quiz area
728
- with gr.Column(scale=4):
729
- # Progress and question area
730
- with gr.Group(elem_classes="card"):
731
- progress_html = gr.HTML(
732
- value="<div style='text-align: center; color: #666;'>No quiz loaded</div>"
733
- )
734
-
735
- gr.Markdown("### Current Question")
736
- question_display = gr.Markdown(
737
- "**Your question will appear here after loading a quiz and clicking Start**"
738
- )
739
-
740
- options_radio = gr.Radio(
741
- choices=[],
742
- label="Select your answer:",
743
- interactive=True
744
- )
745
-
746
- # Logo upload
747
- with gr.Group():
748
- gr.Markdown("### 🎨 Branding (Optional)")
749
- logo_upload = gr.File(
750
- label="Upload logo for PDF report (PNG/JPG)",
751
- file_count="single",
752
- file_types=[".png", ".jpg", ".jpeg"]
753
- )
754
-
755
- # Report display area
756
- gr.Markdown("---")
757
- with gr.Row():
758
- with gr.Column():
759
- gr.Markdown("### 📋 Quiz Report")
760
- report_preview = gr.Markdown("Complete a quiz to see your report here.")
761
- report_download = gr.File(
762
- label="📥 Download PDF Report",
763
  visible=False
764
  )
765
-
766
- # State management
767
- quiz_state = gr.State({})
768
-
769
- # Event handlers
770
- def handle_load_quiz(csv_path_text, uploaded_file):
771
- q_text, q_choices, show_submit, show_next, status_msg, new_state = prepare_quiz(csv_path_text, uploaded_file)
772
-
773
- # Update progress display
774
- progress = get_enhanced_progress_html(new_state)
775
-
776
- # Format status message for HTML display
777
- status_html = f"<div class='status-box'>{status_msg.replace(chr(10), '<br>')}</div>"
 
 
 
778
 
779
- return (
780
- q_text, q_choices, show_submit, show_next,
781
- status_html, new_state, progress
782
- )
783
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  load_btn.click(
785
  fn=handle_load_quiz,
786
- inputs=[csv_file_text, csv_upload],
787
- outputs=[
788
- question_display, options_radio, submit_btn, next_btn,
789
- load_status, quiz_state, progress_html
790
- ]
791
  )
792
-
793
- def handle_start_quiz(state):
794
- q_text, q_choices, show_submit, show_next, new_state = start_quiz(state)
795
- progress = get_enhanced_progress_html(new_state)
796
- return q_text, q_choices, show_submit, show_next, new_state, progress
797
-
798
  start_btn.click(
799
  fn=handle_start_quiz,
800
- inputs=[quiz_state],
801
- outputs=[
802
- question_display, options_radio, submit_btn, next_btn,
803
- quiz_state, progress_html
804
- ]
805
- )
806
-
807
- def handle_submit_answer(state, selected):
808
- feedback_text, show_feedback, show_next_btn, new_state = submit_answer(state, selected)
809
- progress = get_enhanced_progress_html(new_state)
810
- return feedback_text, show_feedback, show_next_btn, new_state, progress
811
-
812
- submit_btn.click(
813
- fn=handle_submit_answer,
814
- inputs=[quiz_state, options_radio],
815
- outputs=[feedback_html, feedback_html, next_btn, quiz_state, progress_html]
816
  )
817
-
818
- def handle_next_question(state):
819
- q_text, q_choices, show_submit, show_next, new_state = next_question(state)
820
- progress = get_enhanced_progress_html(new_state)
821
-
822
- # Auto-generate report if quiz is complete
823
- report_md = "Complete a quiz to see your report here."
824
- pdf_file = gr.File.update(visible=False)
825
-
826
- if new_state and new_state.get("index", 0) >= len(new_state.get("quiz", [])):
827
- try:
828
- report_md, pdf_path = show_report(new_state, user_name.value or "Anonymous", None)
829
- if pdf_path:
830
- pdf_file = gr.File.update(value=pdf_path, visible=True)
831
- except Exception as e:
832
- logger.exception("Error generating auto-report")
833
-
834
- return (
835
- q_text, q_choices, show_submit, show_next, new_state,
836
- progress, report_md, pdf_file
837
- )
838
-
839
  next_btn.click(
840
  fn=handle_next_question,
841
- inputs=[quiz_state],
842
- outputs=[
843
- question_display, options_radio, submit_btn, next_btn,
844
- quiz_state, progress_html, report_preview, report_download
845
- ]
846
  )
847
-
848
- def handle_generate_report(state, name, logo_file):
849
- report_md, pdf_path = show_report(state, name, logo_file)
850
- pdf_file = gr.File.update(value=pdf_path, visible=True) if pdf_path else gr.File.update(visible=False)
851
- return report_md, pdf_file
852
-
853
- show_report_btn.click(
854
  fn=handle_generate_report,
855
- inputs=[quiz_state, user_name, logo_upload],
856
- outputs=[report_preview, report_download]
857
  )
858
-
859
- def handle_reset():
860
- return (
861
- "**Your question will appear here after loading a quiz and clicking Start**",
862
- gr.update(choices=[]),
863
- gr.update(visible=False),
864
- gr.update(visible=False),
865
- gr.update(visible=False),
866
- "<div class='status-box'>📄 Reset complete. Load a CSV file to begin.</div>",
867
- {},
868
- "<div style='text-align: center; color: #666;'>No quiz loaded</div>",
869
- "Complete a quiz to see your report here.",
870
- gr.File.update(visible=False)
871
- )
872
-
873
- restart_btn.click(
874
  fn=handle_reset,
875
  inputs=[],
876
  outputs=[
877
- question_display, options_radio, submit_btn, next_btn, feedback_html,
878
- load_status, quiz_state, progress_html, report_preview, report_download
879
  ]
880
  )
881
-
882
- # Auto-update progress when state changes
883
- def update_progress_display(state):
884
- return get_enhanced_progress_html(state)
885
-
886
- quiz_state.change(
887
- fn=update_progress_display,
888
- inputs=[quiz_state],
889
- outputs=[progress_html]
890
- )
891
-
892
  # Footer
893
- gr.Markdown(f"""
894
- ---
895
- <div style='text-align: center; color: #6c757d; font-size: 12px; margin-top: 20px;'>
896
- <strong>NIELIT Chandigarh</strong> National Institute of Electronics & Information Technology<br>
897
- © {datetime.now().year} Enhanced Quiz Application • Version 2.0
898
- </div>
899
  """)
 
 
900
 
901
- return demo
902
-
903
-
904
- # -----------------------
905
- # CLI & Application Entry Point
906
- # -----------------------
907
  def main():
908
- """Main entry point with enhanced CLI options."""
909
- parser = argparse.ArgumentParser(
910
- description="CCC Plus Quiz App — Enhanced Gradio Application",
911
- formatter_class=argparse.RawDescriptionHelpFormatter,
912
- epilog="""
913
- Examples:
914
- %(prog)s --host 0.0.0.0 --port 8080 # Run on all interfaces, port 8080
915
- %(prog)s --share # Create public share link
916
- %(prog)s --debug # Enable debug logging
917
- """
918
- )
919
-
920
- parser.add_argument(
921
- "--host",
922
- default="127.0.0.1",
923
- help="Host to bind to (default: 127.0.0.1, use 0.0.0.0 for all interfaces)"
924
- )
925
- parser.add_argument(
926
- "--port",
927
- type=int,
928
- default=7860,
929
- help="Port to serve on (default: 7860)"
930
- )
931
- parser.add_argument(
932
- "--share",
933
- action="store_true",
934
- help="Create a public share link via Gradio"
935
- )
936
- parser.add_argument(
937
- "--debug",
938
- action="store_true",
939
- help="Enable debug logging"
940
- )
941
 
942
  args = parser.parse_args()
943
 
944
- # Configure logging level
945
  if args.debug:
946
  logging.getLogger().setLevel(logging.DEBUG)
947
- logger.debug("Debug logging enabled")
948
 
949
- # Build and launch application
950
- demo = build_app()
951
 
952
  logger.info(f"Starting CCC Plus Quiz App on {args.host}:{args.port}")
953
- if args.share:
954
- logger.info("Public share link will be created")
955
 
956
  try:
957
- demo.launch(
958
  server_name=args.host,
959
  server_port=args.port,
960
  share=args.share,
961
- show_error=True,
962
- quiet=False
963
  )
964
  except KeyboardInterrupt:
965
  logger.info("Application stopped by user")
@@ -969,11 +699,10 @@ Examples:
969
 
970
  return 0
971
 
972
-
973
  if __name__ == "__main__":
974
- # For development/testing - always create public share
975
- demo = build_app()
976
- demo.launch(share=True, show_error=True)
977
 
978
  # For production use:
979
  # exit(main())
 
1
  #!/usr/bin/env python3
2
  """
3
+ CCC Plus Quiz App - Completely Rewritten for Better UX
4
+ A modern, intuitive quiz application with improved interface and functionality
5
  """
6
 
7
  import argparse
 
11
  import json
12
  from datetime import datetime
13
  from pathlib import Path
14
+ from typing import Optional, Tuple, List, Dict, Any
15
 
16
  import gradio as gr
17
  import pandas as pd
 
19
  from reportlab.lib.pagesizes import A4
20
  from reportlab.lib.units import mm
21
  from reportlab.pdfgen import canvas
22
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
23
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
24
+ from reportlab.lib.enums import TA_CENTER, TA_LEFT
25
 
26
+ # Configure logging
 
 
27
  logging.basicConfig(
28
  level=logging.INFO,
29
+ format="%(asctime)s [%(levelname)s] %(message)s"
30
  )
31
  logger = logging.getLogger(__name__)
32
 
33
+ # Configuration
34
  DEFAULT_CSV_PATH = "quiz.csv"
35
  REQUIRED_COLUMNS = {"question", "option1", "option2", "option3", "option4", "answer"}
 
 
 
36
 
37
+ class QuizApp:
38
+ """Main Quiz Application Class"""
39
+
40
+ def __init__(self):
41
+ self.reset_state()
42
+
43
+ def reset_state(self):
44
+ """Reset all quiz state variables"""
45
+ self.quiz_data = []
46
+ self.current_index = 0
47
+ self.score = 0
48
+ self.user_answers = []
49
+ self.quiz_started = False
50
+ self.quiz_completed = False
51
+ self.start_time = None
52
+ self.end_time = None
53
+ self.user_name = ""
54
+
55
+ def load_csv(self, file_path: str) -> Tuple[bool, str]:
56
+ """Load and validate CSV file"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  try:
58
+ if not file_path or not os.path.exists(file_path):
59
+ return False, " File not found. Please check the file path."
60
+
61
+ # Try different encodings
62
+ df = None
63
+ for encoding in ['utf-8', 'latin1', 'cp1252', 'iso-8859-1']:
64
+ try:
65
+ df = pd.read_csv(file_path, encoding=encoding)
66
+ break
67
+ except UnicodeDecodeError:
68
+ continue
69
+
70
+ if df is None:
71
+ return False, "❌ Could not read the CSV file. Please check the file encoding."
72
+
73
+ # Normalize column names
74
+ df.columns = df.columns.str.strip().str.lower()
75
+
76
+ # Check required columns
77
+ missing_cols = REQUIRED_COLUMNS - set(df.columns)
78
+ if missing_cols:
79
+ return False, f"❌ Missing required columns: {', '.join(missing_cols)}"
80
+
81
+ # Process questions
82
+ self.quiz_data = []
83
+ for idx, row in df.iterrows():
84
+ try:
85
+ question = str(row['question']).strip()
86
+ if not question or question.lower() in ['nan', 'none', '']:
87
+ continue
88
+
89
+ options = [
90
+ str(row['option1']).strip(),
91
+ str(row['option2']).strip(),
92
+ str(row['option3']).strip(),
93
+ str(row['option4']).strip()
94
+ ]
95
+
96
+ answer = str(row['answer']).strip()
97
+
98
+ # Validate answer exists in options
99
+ if answer not in options:
100
+ logger.warning(f"Question {idx+1}: Answer '{answer}' not found in options")
101
+ continue
102
+
103
+ self.quiz_data.append({
104
+ 'question': question,
105
+ 'options': options,
106
+ 'answer': answer
107
+ })
108
+
109
+ except Exception as e:
110
+ logger.warning(f"Error processing row {idx+1}: {e}")
111
+ continue
112
+
113
+ if not self.quiz_data:
114
+ return False, "❌ No valid questions found in the CSV file."
115
+
116
+ return True, f"✅ Successfully loaded {len(self.quiz_data)} questions!"
117
+
118
  except Exception as e:
119
+ logger.exception("Error loading CSV")
120
+ return False, f"❌ Error loading CSV: {str(e)}"
 
 
 
 
 
 
 
 
 
121
 
122
+ def start_quiz(self, name: str) -> Tuple[bool, str]:
123
+ """Start the quiz"""
124
+ if not self.quiz_data:
125
+ return False, "❌ No quiz loaded. Please load a CSV file first."
126
+
127
+ self.user_name = name.strip() if name else "Anonymous"
128
+ self.current_index = 0
129
+ self.score = 0
130
+ self.user_answers = []
131
+ self.quiz_started = True
132
+ self.quiz_completed = False
133
+ self.start_time = datetime.now()
134
+
135
+ return True, "🚀 Quiz started! Answer each question and click Next to proceed."
136
 
137
+ def get_current_question(self) -> Dict[str, Any]:
138
+ """Get current question data"""
139
+ if not self.quiz_started or not self.quiz_data or self.current_index >= len(self.quiz_data):
140
+ return {}
141
 
142
+ question_data = self.quiz_data[self.current_index]
143
+ return {
144
+ 'question_num': self.current_index + 1,
145
+ 'total_questions': len(self.quiz_data),
146
+ 'question': question_data['question'],
147
+ 'options': question_data['options'],
148
+ 'progress': ((self.current_index) / len(self.quiz_data)) * 100
149
+ }
150
+
151
+ def submit_answer(self, selected_answer: str) -> Tuple[bool, str, bool]:
152
+ """Submit answer and move to next question"""
153
+ if not self.quiz_started or not self.quiz_data:
154
+ return False, "❌ Quiz not started.", False
155
 
156
+ if self.current_index >= len(self.quiz_data):
157
+ return False, "❌ Quiz already completed.", True
 
 
158
 
159
+ current_q = self.quiz_data[self.current_index]
160
+ is_correct = selected_answer == current_q['answer']
161
+
162
+ # Store the answer
163
+ self.user_answers.append({
164
+ 'question': current_q['question'],
165
+ 'selected': selected_answer,
166
+ 'correct_answer': current_q['answer'],
167
+ 'is_correct': is_correct
168
+ })
169
+
170
+ if is_correct:
171
+ self.score += 1
172
+
173
+ # Move to next question
174
+ self.current_index += 1
175
+
176
+ # Check if quiz is completed
177
+ if self.current_index >= len(self.quiz_data):
178
+ self.quiz_completed = True
179
+ self.end_time = datetime.now()
180
+ duration = self.end_time - self.start_time
181
+ duration_str = f"{duration.seconds // 60}m {duration.seconds % 60}s"
182
+
183
+ percentage = (self.score / len(self.quiz_data)) * 100
184
+
185
+ return True, f"""🎉 **Quiz Completed!**
186
+
187
+ **Final Score:** {self.score} out of {len(self.quiz_data)} ({percentage:.1f}%)
188
+ **Time Taken:** {duration_str}
189
+ **Performance:** {self.get_performance_level(percentage)}
190
 
191
+ Click "Generate Report" to download your detailed results!""", True
192
+
193
+ # Quiz continues
194
+ feedback = f"✅ Correct!" if is_correct else f"❌ Incorrect. The correct answer was: **{current_q['answer']}**"
195
+ return True, feedback, False
196
+
197
+ def get_performance_level(self, percentage: float) -> str:
198
+ """Get performance level based on percentage"""
199
+ if percentage >= 90:
200
+ return "🏆 Excellent"
201
+ elif percentage >= 80:
202
+ return "👍 Good"
203
+ elif percentage >= 60:
204
+ return "👌 Average"
205
+ else:
206
+ return "📚 Needs Improvement"
 
207
 
208
+ def generate_pdf_report(self, logo_path: Optional[str] = None) -> Optional[str]:
209
+ """Generate PDF report"""
210
+ if not self.user_answers:
211
+ return None
212
+
213
  try:
214
+ # Create temporary file
215
+ temp_file = tempfile.NamedTemporaryFile(
216
+ prefix="CCC_Quiz_Report_",
217
+ suffix=".pdf",
218
+ delete=False
219
+ )
220
+ temp_file.close()
221
+
222
+ # Create PDF
223
+ doc = SimpleDocTemplate(temp_file.name, pagesize=A4)
224
+ styles = getSampleStyleSheet()
225
+ story = []
226
+
227
+ # Custom styles
228
+ title_style = ParagraphStyle(
229
+ 'CustomTitle',
230
+ parent=styles['Heading1'],
231
+ fontSize=18,
232
+ alignment=TA_CENTER,
233
+ textColor=colors.HexColor('#003366'),
234
+ spaceAfter=12
235
+ )
236
+
237
+ subtitle_style = ParagraphStyle(
238
+ 'CustomSubtitle',
239
+ parent=styles['Normal'],
240
+ fontSize=12,
241
+ alignment=TA_CENTER,
242
+ textColor=colors.HexColor('#666666'),
243
+ spaceAfter=20
244
+ )
245
+
246
+ # Header
247
+ story.append(Paragraph("CCC Plus - Computer Concepts Plus", title_style))
248
+ story.append(Paragraph("Quiz Assessment Report", subtitle_style))
249
+ story.append(Spacer(1, 12))
250
+
251
+ # Candidate info
252
+ info_data = [
253
+ ['Candidate:', self.user_name],
254
+ ['Date:', datetime.now().strftime('%d %B %Y at %H:%M:%S')],
255
+ ['Questions Attempted:', str(len(self.user_answers))],
256
+ ['Correct Answers:', str(self.score)],
257
+ ['Score Percentage:', f"{(self.score/len(self.user_answers)*100):.1f}%"],
258
+ ['Performance Level:', self.get_performance_level((self.score/len(self.user_answers)*100))]
259
  ]
 
260
 
261
+ info_table = Table(info_data, colWidths=[120, 200])
262
+ info_table.setStyle(TableStyle([
263
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
264
+ ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
265
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
266
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 6),
267
+ ]))
268
+
269
+ story.append(info_table)
270
+ story.append(Spacer(1, 20))
271
+
272
+ # Results table
273
+ story.append(Paragraph("Detailed Results", styles['Heading2']))
274
+ story.append(Spacer(1, 12))
275
+
276
+ table_data = [['#', 'Question', 'Your Answer', 'Correct Answer', 'Result']]
277
+
278
+ for i, answer in enumerate(self.user_answers, 1):
279
+ result_icon = '✓' if answer['is_correct'] else '✗'
280
+ question_text = answer['question'][:100] + ('...' if len(answer['question']) > 100 else '')
281
 
282
+ table_data.append([
283
+ str(i),
284
+ question_text,
285
+ answer['selected'],
286
+ answer['correct_answer'],
287
+ result_icon
288
+ ])
289
+
290
+ results_table = Table(table_data, colWidths=[20, 200, 80, 80, 30])
291
+ results_table.setStyle(TableStyle([
292
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#003366')),
293
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
294
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
295
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
296
+ ('FONTSIZE', (0, 0), (-1, -1), 8),
297
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')]),
298
+ ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6')),
299
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
300
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
301
+ ('TOPPADDING', (0, 0), (-1, -1), 4),
302
+ ]))
303
+
304
+ story.append(results_table)
305
+ story.append(Spacer(1, 20))
306
+
307
+ # Footer
308
+ footer_style = ParagraphStyle(
309
+ 'Footer',
310
+ parent=styles['Normal'],
311
+ fontSize=8,
312
+ alignment=TA_CENTER,
313
+ textColor=colors.HexColor('#666666')
314
+ )
315
+
316
+ story.append(Paragraph(
317
+ f"NIELIT Chandigarh — © {datetime.now().year} — Generated automatically",
318
+ footer_style
319
+ ))
320
+
321
+ doc.build(story)
322
+
323
+ logger.info(f"Generated PDF report: {temp_file.name}")
324
+ return temp_file.name
325
 
326
  except Exception as e:
327
+ logger.exception("Error generating PDF report")
328
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
+ # Global quiz app instance
331
+ quiz_app = QuizApp()
 
 
 
 
 
 
332
 
333
+ def handle_load_quiz(csv_path: str, uploaded_file) -> Tuple[str, Any, Any, Any]:
334
+ """Handle quiz loading"""
335
+ # Extract file path
336
+ file_path = None
337
+ if uploaded_file is not None:
338
+ file_path = uploaded_file.name if hasattr(uploaded_file, 'name') else str(uploaded_file)
339
+ elif csv_path and csv_path.strip():
340
+ file_path = csv_path.strip()
341
 
342
+ if not file_path:
343
+ return "❌ Please provide a CSV file path or upload a file.", gr.update(), gr.update(), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ # Reset quiz state
346
+ quiz_app.reset_state()
 
 
 
 
 
 
347
 
348
+ # Load quiz
349
+ success, message = quiz_app.load_csv(file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
+ if success:
352
+ return message, gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
353
+ else:
354
+ return message, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
355
 
356
+ def handle_start_quiz(name: str) -> Tuple[str, Any, Any, Any, Any, str, Any]:
357
+ """Handle quiz start"""
358
+ success, message = quiz_app.start_quiz(name)
 
 
 
 
 
 
 
 
 
 
 
359
 
360
+ if not success:
361
+ return message, gr.update(), gr.update(), gr.update(), gr.update(), "", gr.update()
 
 
 
 
362
 
363
+ # Get first question
364
+ q_data = quiz_app.get_current_question()
365
+ if not q_data:
366
+ return "❌ No questions available.", gr.update(), gr.update(), gr.update(), gr.update(), "", gr.update()
367
 
368
+ question_html = f"""
369
+ <div style="background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 20px; border-radius: 12px; margin-bottom: 20px;">
370
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
371
+ <span style="font-weight: 600; color: #495057;">Question {q_data['question_num']} of {q_data['total_questions']}</span>
372
+ <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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  </div>
374
+ <div style="background: #e9ecef; border-radius: 10px; overflow: hidden; height: 8px; margin-bottom: 15px;">
375
+ <div style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: {q_data['progress']:.1f}%;"></div>
 
 
 
 
 
 
 
 
376
  </div>
377
+ <h3 style="color: #212529; margin: 0;">{q_data['question']}</h3>
378
  </div>
379
  """
380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  return (
382
+ message,
383
+ gr.update(visible=True),
384
+ gr.update(visible=True),
385
+ gr.update(value=question_html, visible=True),
386
+ gr.update(choices=q_data['options'], value=None, visible=True),
387
+ "",
388
+ gr.update(visible=True)
389
  )
390
 
391
+ def handle_next_question(selected_answer: str) -> Tuple[str, Any, Any, str, Any]:
392
+ """Handle answer submission and move to next question"""
393
+ if not selected_answer:
394
+ return "⚠️ Please select an answer before proceeding.", gr.update(), gr.update(), "", gr.update()
 
395
 
396
+ success, feedback, is_completed = quiz_app.submit_answer(selected_answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
+ if not success:
399
+ return feedback, gr.update(), gr.update(), feedback, gr.update()
 
 
 
400
 
401
+ if is_completed:
402
+ # Quiz completed
403
+ return (
404
+ feedback,
405
+ gr.update(visible=False),
406
+ gr.update(visible=False),
407
+ feedback,
408
+ gr.update(visible=True)
409
+ )
410
+
411
+ # Get next question
412
+ q_data = quiz_app.get_current_question()
413
+ if not q_data:
414
+ return "❌ Error getting next question.", gr.update(), gr.update(), feedback, gr.update()
415
+
416
+ question_html = f"""
417
+ <div style="background: linear-gradient(135deg, #f8f9fa, #e9ecef); padding: 20px; border-radius: 12px; margin-bottom: 20px;">
418
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
419
+ <span style="font-weight: 600; color: #495057;">Question {q_data['question_num']} of {q_data['total_questions']}</span>
420
+ <span style="font-weight: 600; color: #007bff;">Score: {quiz_app.score}/{q_data['question_num']-1}</span>
421
+ </div>
422
+ <div style="background: #e9ecef; border-radius: 10px; overflow: hidden; height: 8px; margin-bottom: 15px;">
423
+ <div style="background: linear-gradient(90deg, #007bff, #0056b3); height: 100%; width: {q_data['progress']:.1f}%;"></div>
424
+ </div>
425
+ <h3 style="color: #212529; margin: 0;">{q_data['question']}</h3>
426
+ </div>
427
+ """
 
 
 
 
 
 
 
 
 
 
428
 
 
 
429
  return (
430
+ feedback,
431
+ gr.update(value=question_html, visible=True),
432
+ gr.update(choices=q_data['options'], value=None, visible=True),
433
+ feedback,
434
+ gr.update()
435
  )
436
 
437
+ def handle_generate_report(logo_file) -> Tuple[str, Any]:
438
+ """Handle report generation"""
439
+ if not quiz_app.user_answers:
440
+ return "❌ No quiz results available. Please complete a quiz first.", gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
441
 
442
  # Generate markdown report
443
+ total_questions = len(quiz_app.user_answers)
444
+ correct_answers = quiz_app.score
445
+ percentage = (correct_answers / total_questions) * 100
446
+
447
+ report_lines = [
448
+ f"# 📋 CCC Plus Quiz Report",
449
+ f"",
450
+ f"**Candidate:** {quiz_app.user_name}",
 
 
 
 
 
 
 
 
 
 
451
  f"**Date:** {datetime.now().strftime('%d %B %Y at %H:%M:%S')}",
452
+ f"**Questions:** {total_questions}",
453
+ f"**Correct Answers:** {correct_answers}",
454
+ f"**Score:** {percentage:.1f}%",
455
+ f"**Performance:** {quiz_app.get_performance_level(percentage)}",
456
+ f"",
457
+ f"## 📊 Detailed Results",
458
+ f"",
459
+ f"| # | Question | Your Answer | Correct Answer | Result |",
460
+ f"|---|----------|-------------|----------------|--------|"
461
  ]
462
 
463
+ for i, answer in enumerate(quiz_app.user_answers, 1):
464
+ status = "✅" if answer['is_correct'] else "❌"
465
+ question_short = answer['question'][:60] + "..." if len(answer['question']) > 60 else answer['question']
466
+ report_lines.append(
467
+ f"| {i} | {question_short} | {answer['selected']} | {answer['correct_answer']} | {status} |"
 
 
 
 
468
  )
469
 
470
+ report_md = "\n".join(report_lines)
471
 
472
  # Generate PDF
473
+ pdf_path = quiz_app.generate_pdf_report()
 
 
474
 
475
+ if pdf_path:
476
+ return report_md, gr.update(value=pdf_path, visible=True)
477
+ else:
478
+ return report_md + "\n\n⚠️ **PDF generation failed.**", gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
479
 
480
+ def handle_reset() -> Tuple[str, Any, Any, Any, Any, str, Any, str, Any]:
481
+ """Handle complete reset"""
482
+ quiz_app.reset_state()
483
+
484
+ return (
485
+ "🔄 Application reset successfully. Load a new quiz to begin.",
486
+ gr.update(visible=False),
487
+ gr.update(visible=False),
488
+ gr.update(visible=False, value=""),
489
+ gr.update(visible=False, choices=[], value=None),
490
+ "",
491
+ gr.update(visible=False),
492
+ "Complete a quiz to see your report here.",
493
+ gr.update(visible=False)
494
+ )
495
 
496
+ def create_app():
497
+ """Create the Gradio interface"""
498
+
499
+ # Custom CSS for better styling
500
+ custom_css = """
501
+ .gradio-container {
502
+ max-width: 1200px !important;
503
+ margin: 0 auto;
 
 
504
  }
505
+
506
+ .quiz-header {
507
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
508
+ padding: 30px;
509
+ border-radius: 15px;
510
+ color: white;
511
+ margin-bottom: 30px;
512
+ text-align: center;
513
  }
514
+
515
+ .control-panel {
516
+ background: #f8f9fa;
517
+ border-radius: 15px;
518
+ padding: 25px;
519
  border: 1px solid #e9ecef;
520
  }
521
+
522
+ .quiz-area {
523
+ background: white;
524
+ border-radius: 15px;
525
+ padding: 25px;
526
+ border: 1px solid #e9ecef;
527
+ min-height: 400px;
528
  }
529
+
530
+ .report-section {
531
  background: #f8f9fa;
532
+ border-radius: 15px;
533
+ padding: 25px;
534
+ border: 1px solid #e9ecef;
535
+ margin-top: 20px;
 
536
  }
537
  """
538
+
539
  with gr.Blocks(
540
+ title="CCC Plus Quiz App",
541
+ css=custom_css,
542
  theme=gr.themes.Soft()
543
+ ) as app:
544
+
545
+ # Header
546
+ gr.HTML("""
547
+ <div class="quiz-header">
548
+ <h1 style="margin: 0; font-size: 2.5em;">🖥️ CCC Plus Quiz App</h1>
549
+ <p style="margin: 10px 0 0 0; font-size: 1.2em; opacity: 0.9;">
550
+ Computer Concepts Plus - Interactive Quiz Platform
551
+ </p>
552
+ <p style="margin: 5px 0 0 0; opacity: 0.8;">
553
+ NIELIT Chandigarh • National Institute of Electronics & Information Technology
554
+ </p>
555
+ </div>
556
+ """)
557
 
 
558
  with gr.Row():
559
+ # Left Panel - Controls
560
+ with gr.Column(scale=1, elem_classes="control-panel"):
561
+ gr.Markdown("### 📁 Load Quiz")
562
+
563
+ csv_path = gr.Textbox(
564
+ label="CSV File Path",
565
+ placeholder="Enter path to your quiz CSV file...",
566
+ value=DEFAULT_CSV_PATH
567
  )
568
+
569
+ csv_upload = gr.File(
570
+ label="Or Upload CSV File",
571
+ file_types=[".csv"],
572
+ file_count="single"
573
+ )
574
+
575
+ load_btn = gr.Button("📂 Load Quiz", variant="primary", size="lg")
576
+ load_status = gr.Markdown("📝 No quiz loaded. Please load a CSV file to begin.")
577
+
578
+ gr.Markdown("---")
579
+ gr.Markdown("### 👤 Candidate Info")
580
+
581
+ user_name = gr.Textbox(
582
+ label="Your Full Name",
583
+ placeholder="Enter your name for the certificate...",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  visible=False
585
  )
586
+
587
+ start_btn = gr.Button("🚀 Start Quiz", variant="primary", size="lg", visible=False)
588
+
589
+ gr.Markdown("---")
590
+ gr.Markdown("### 📊 Actions")
591
+
592
+ report_btn = gr.Button("📋 Generate Report", visible=False)
593
+ reset_btn = gr.Button("🔄 Reset All", variant="secondary")
594
+
595
+ # Logo upload
596
+ gr.Markdown("---")
597
+ gr.Markdown("### 🎨 Branding (Optional)")
598
+ logo_upload = gr.File(
599
+ label="Upload Logo for PDF",
600
+ file_types=[".png", ".jpg", ".jpeg"]
601
+ )
602
 
603
+ # Right Panel - Quiz Area
604
+ with gr.Column(scale=2, elem_classes="quiz-area"):
605
+
606
+ feedback_msg = gr.Markdown("Welcome! Load a quiz file and enter your name to begin.", visible=True)
607
+
608
+ question_display = gr.HTML(visible=False)
609
+
610
+ answer_options = gr.Radio(
611
+ label="Select your answer:",
612
+ choices=[],
613
+ visible=False
614
+ )
615
+
616
+ next_btn = gr.Button("➡️ Submit Answer & Next", variant="primary", size="lg", visible=False)
617
+
618
+ # Report Section
619
+ with gr.Row():
620
+ with gr.Column(elem_classes="report-section"):
621
+ gr.Markdown("### 📋 Quiz Report")
622
+ report_display = gr.Markdown("Complete a quiz to see your report here.")
623
+ report_download = gr.File(label="📥 Download PDF Report", visible=False)
624
+
625
+ # Event Handlers
626
  load_btn.click(
627
  fn=handle_load_quiz,
628
+ inputs=[csv_path, csv_upload],
629
+ outputs=[load_status, user_name, start_btn, report_btn]
 
 
 
630
  )
631
+
 
 
 
 
 
632
  start_btn.click(
633
  fn=handle_start_quiz,
634
+ inputs=[user_name],
635
+ outputs=[feedback_msg, start_btn, next_btn, question_display, answer_options, user_name, report_btn]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  )
637
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  next_btn.click(
639
  fn=handle_next_question,
640
+ inputs=[answer_options],
641
+ outputs=[feedback_msg, question_display, answer_options, feedback_msg, report_btn]
 
 
 
642
  )
643
+
644
+ report_btn.click(
 
 
 
 
 
645
  fn=handle_generate_report,
646
+ inputs=[logo_upload],
647
+ outputs=[report_display, report_download]
648
  )
649
+
650
+ reset_btn.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  fn=handle_reset,
652
  inputs=[],
653
  outputs=[
654
+ load_status, user_name, start_btn, question_display,
655
+ answer_options, feedback_msg, report_btn, report_display, report_download
656
  ]
657
  )
658
+
 
 
 
 
 
 
 
 
 
 
659
  # Footer
660
+ gr.HTML(f"""
661
+ <div style="text-align: center; margin-top: 40px; padding: 20px;
662
+ background: #f8f9fa; border-radius: 10px; color: #6c757d;">
663
+ <p><strong>NIELIT Chandigarh</strong> © {datetime.now().year} Enhanced Quiz Application</p>
664
+ <p style="font-size: 0.9em;">Built with ❤️ using Gradio • Version 2.0</p>
665
+ </div>
666
  """)
667
+
668
+ return app
669
 
 
 
 
 
 
 
670
  def main():
671
+ """Main entry point"""
672
+ parser = argparse.ArgumentParser(description="CCC Plus Quiz App")
673
+ parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
674
+ parser.add_argument("--port", type=int, default=7860, help="Port to serve on")
675
+ parser.add_argument("--share", action="store_true", help="Create public share link")
676
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
678
  args = parser.parse_args()
679
 
 
680
  if args.debug:
681
  logging.getLogger().setLevel(logging.DEBUG)
 
682
 
683
+ app = create_app()
 
684
 
685
  logger.info(f"Starting CCC Plus Quiz App on {args.host}:{args.port}")
 
 
686
 
687
  try:
688
+ app.launch(
689
  server_name=args.host,
690
  server_port=args.port,
691
  share=args.share,
692
+ show_error=True
 
693
  )
694
  except KeyboardInterrupt:
695
  logger.info("Application stopped by user")
 
699
 
700
  return 0
701
 
 
702
  if __name__ == "__main__":
703
+ # For development - create public share
704
+ app = create_app()
705
+ app.launch(share=True, show_error=True)
706
 
707
  # For production use:
708
  # exit(main())