AptlyDigital commited on
Commit
dae87b9
·
verified ·
1 Parent(s): 8828ecf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -692
app.py CHANGED
@@ -1,5 +1,4 @@
1
- # app.py - AI Study Tutor for SEA Exam Preparation
2
- # Enhanced with PDF upload and RAG capabilities
3
 
4
  import os
5
  import json
@@ -10,27 +9,22 @@ from pathlib import Path
10
 
11
  import gradio as gr
12
  from groq import Groq
13
- import PyPDF2 # For PDF text extraction
14
 
15
  # -----------------------------
16
  # Configuration
17
  # -----------------------------
18
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "").strip()
19
- client = Groq(api_key=GROQ_API_KEY)
20
 
21
  # SEA-specific configurations
22
- SEA_SUBJECTS = [
23
- "Mathematics",
24
- "English Language Arts"
25
- ]
26
 
27
  SEA_MATH_TOPICS = [
28
  "Number Theory (Fractions, Decimals, Percentages)",
29
  "Measurement (Perimeter, Area, Volume)",
30
- "Geometry",
31
- "Algebra Basics",
32
- "Word Problems",
33
- "Data Interpretation"
34
  ]
35
 
36
  SEA_ENGLISH_TOPICS = [
@@ -42,99 +36,91 @@ SEA_ENGLISH_TOPICS = [
42
  "Listening Comprehension (simulated)"
43
  ]
44
 
45
- LANG_OPTIONS = ["English"] # Primary language for SEA exam
46
  LEVEL_OPTIONS = ["Beginner", "Intermediate", "Advanced"]
47
-
48
- # Storage for uploaded documents
49
  UPLOADED_DOCS_FILE = "sea_exam_documents.json"
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  # -----------------------------
52
  # Document Processing Functions
53
  # -----------------------------
54
  def extract_text_from_pdf(file_bytes: bytes, filename: str) -> str:
55
- """Extract text from uploaded PDF files with SEA-specific formatting."""
56
  try:
57
- # Create temporary file
58
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_file:
59
  tmp_file.write(file_bytes)
60
  tmp_file_path = tmp_file.name
61
 
62
- # Extract text using PyPDF2
63
  full_text = ""
64
  with open(tmp_file_path, 'rb') as pdf_file:
65
  pdf_reader = PyPDF2.PdfReader(pdf_file)
66
-
67
  for page_num in range(len(pdf_reader.pages)):
68
  page = pdf_reader.pages[page_num]
69
  page_text = page.extract_text()
70
-
71
- # Add page marker for reference
72
- full_text += f"\n--- SEA Paper Page {page_num+1} ---\n"
73
- full_text += page_text + "\n"
74
 
75
- # Clean up temp file
76
  os.unlink(tmp_file_path)
77
-
78
- # Post-process: Detect question patterns
79
- processed_text = enhance_sea_text_extraction(full_text, filename)
80
-
81
- return processed_text
82
 
83
  except Exception as e:
84
  return f"ERROR processing {filename}: {str(e)}"
85
 
86
- def enhance_sea_text_extraction(text: str, filename: str) -> str:
87
- """Enhance extracted text with SEA-specific pattern recognition."""
88
- enhancements = []
89
-
90
- # Detect common SEA question patterns
91
- question_patterns = [
92
- r"Question\s+\d+[:\.]\s*(.*?)(?=\nQuestion\s+\d+|$)",
93
- r"\d+\.\s+(.*?)(?=\n\d+\.|\Z)",
94
- r"Section\s+[A-Z][:\.]\s*(.*?)(?=\nSection\s+[A-Z]|\Z)"
95
- ]
96
-
97
- for pattern in question_patterns:
98
- matches = re.findall(pattern, text, re.DOTALL | re.IGNORECASE)
99
- if matches:
100
- enhancements.append(f"Detected {len(matches)} SEA-style questions")
101
- break
102
-
103
- # Add metadata based on filename
104
- year_match = re.search(r'(20\d{2}|19\d{2})', filename)
105
- subject_match = re.search(r'(math|english|mathematics|language)', filename, re.IGNORECASE)
106
-
107
- metadata = f"\n[FILE METADATA]\nFilename: {filename}\n"
108
- if year_match:
109
- metadata += f"Year: {year_match.group(1)}\n"
110
- if subject_match:
111
- metadata += f"Subject: {subject_match.group(1).title()}\n"
112
-
113
- return metadata + "\n" + text + "\n" + "\n".join(enhancements)
114
-
115
  def process_uploaded_documents(files) -> str:
116
- """Process all uploaded SEA exam documents and build searchable index."""
117
  if not files:
118
- return "⚠️ No files uploaded. Please upload SEA exam PDFs or text files."
119
 
120
  all_documents = []
121
- processing_summary = []
122
 
123
  for file_info in files:
124
- # Gradio provides (temp_path, original_filename) for each file
125
  if isinstance(file_info, tuple) and len(file_info) >= 2:
126
  file_path, filename = file_info[0], file_info[1]
127
  else:
128
- # Fallback for different Gradio versions
129
  file_path = file_info
130
  filename = os.path.basename(str(file_info))
131
 
132
  try:
133
- # Read file content
134
  with open(file_path, 'rb') as f:
135
  file_bytes = f.read()
136
 
137
- # Extract text based on file type
138
  if filename.lower().endswith('.pdf'):
139
  text_content = extract_text_from_pdf(file_bytes, filename)
140
  file_type = "PDF"
@@ -142,86 +128,29 @@ def process_uploaded_documents(files) -> str:
142
  text_content = file_bytes.decode('utf-8', errors='replace')
143
  file_type = "Text"
144
  else:
145
- text_content = f"Unsupported file type: {filename}"
146
- file_type = "Unknown"
147
 
148
- # Create structured document entry
149
  doc_entry = {
150
  "filename": filename,
151
- "content": text_content[:10000] if len(text_content) > 10000 else text_content, # Limit size
152
  "type": file_type,
153
- "subject": detect_subject_from_content(text_content),
154
- "size_chars": len(text_content),
155
  "upload_time": gr.utils.datetime.datetime.now().isoformat()
156
  }
157
-
158
  all_documents.append(doc_entry)
159
- processing_summary.append(f"✅ {filename} ({file_type}, {len(text_content)} chars)")
160
 
161
  except Exception as e:
162
- error_msg = f"Failed to process {filename}: {str(e)}"
163
- processing_summary.append(error_msg)
164
- print(error_msg)
165
 
166
- # Save documents to JSON file for persistence
167
  try:
168
  with open(UPLOADED_DOCS_FILE, 'w', encoding='utf-8') as f:
169
  json.dump(all_documents, f, ensure_ascii=False, indent=2)
170
 
171
- # Create a quick-search index
172
- create_search_index(all_documents)
173
-
174
- summary = f"📚 **Processing Complete**\n\n"
175
- summary += f"**Processed {len(all_documents)} files:**\n"
176
- summary += "\n".join(processing_summary)
177
- summary += f"\n\n📁 Documents saved to: `{UPLOADED_DOCS_FILE}`"
178
- summary += f"\n🔍 Index created for RAG queries."
179
-
180
- return summary
181
-
182
  except Exception as e:
183
  return f"❌ Error saving documents: {str(e)}"
184
 
185
- def detect_subject_from_content(text: str) -> str:
186
- """Auto-detect subject from document content."""
187
- text_lower = text.lower()
188
-
189
- math_keywords = ['fraction', 'decimal', 'percentage', 'geometry', 'algebra', 'equation', 'calculate', 'sum']
190
- english_keywords = ['comprehension', 'grammar', 'vocabulary', 'essay', 'reading', 'writing', 'passage']
191
-
192
- math_count = sum(1 for keyword in math_keywords if keyword in text_lower)
193
- english_count = sum(1 for keyword in english_keywords if keyword in text_lower)
194
-
195
- if math_count > english_count:
196
- return "Mathematics"
197
- elif english_count > math_count:
198
- return "English Language Arts"
199
- else:
200
- return "General SEA"
201
-
202
- def create_search_index(documents: List[Dict]):
203
- """Create a simplified search index for quick lookups."""
204
- index_entries = []
205
-
206
- for doc in documents:
207
- # Extract first few lines as preview
208
- preview_lines = doc['content'].split('\n')[:10]
209
- preview = ' '.join([line.strip() for line in preview_lines if line.strip()])
210
-
211
- index_entry = {
212
- "filename": doc['filename'],
213
- "subject": doc['subject'],
214
- "preview": preview[:200] + "..." if len(preview) > 200 else preview,
215
- "size": doc['size_chars']
216
- }
217
- index_entries.append(index_entry)
218
-
219
- # Save index
220
- with open("sea_document_index.json", 'w', encoding='utf-8') as f:
221
- json.dump(index_entries, f, ensure_ascii=False, indent=2)
222
-
223
  def get_relevant_context(subject: str, topic: str, max_context: int = 1500) -> str:
224
- """Retrieve relevant context from uploaded SEA papers."""
225
  try:
226
  if not os.path.exists(UPLOADED_DOCS_FILE):
227
  return ""
@@ -230,220 +159,106 @@ def get_relevant_context(subject: str, topic: str, max_context: int = 1500) -> s
230
  documents = json.load(f)
231
 
232
  relevant_parts = []
233
- topic_lower = topic.lower()
234
- subject_lower = subject.lower()
235
-
236
  for doc in documents:
237
- doc_content = doc.get('content', '').lower()
238
- doc_subject = doc.get('subject', '').lower()
239
-
240
- # Check relevance
241
- relevance_score = 0
242
- if topic_lower in doc_content:
243
- relevance_score += 3
244
- if subject_lower in doc_subject or subject_lower in doc_content:
245
- relevance_score += 2
246
-
247
- if relevance_score > 0:
248
- # Extract most relevant snippet
249
- content = doc['content']
250
-
251
- # Try to find topic mention
252
- if topic_lower in content.lower():
253
- idx = content.lower().find(topic_lower)
254
- start = max(0, idx - 200)
255
- end = min(len(content), idx + 500)
256
- snippet = content[start:end]
257
- else:
258
- # Take beginning of document
259
- snippet = content[:500] + "..."
260
-
261
- relevant_parts.append(f"\n--- From: {doc['filename']} (Subject: {doc['subject']}) ---\n{snippet}\n")
262
 
263
- # Combine and limit total size
264
  combined = "\n".join(relevant_parts)
265
  if len(combined) > max_context:
266
- combined = combined[:max_context] + "\n...[context truncated]..."
267
 
268
  return combined if combined else ""
269
-
270
- except Exception as e:
271
- print(f"Context retrieval error: {e}")
272
  return ""
273
 
274
  # -----------------------------
275
  # Enhanced Generation with RAG
276
  # -----------------------------
277
  def generate_with_context(prompt: str, subject: str, topic: str, language: str, level: str) -> str:
278
- """Enhanced generator using uploaded SEA papers as context."""
279
- # Retrieve relevant context from uploaded documents
280
  context = get_relevant_context(subject, topic)
281
 
282
- context_header = ""
283
  if context:
284
- context_header = f"""
285
- IMPORTANT CONTEXT FROM UPLOADED SEA EXAM PAPERS:
286
  {context}
287
 
288
- BASED ON THE ABOVE SEA EXAM CONTEXT, please respond to the following request:
 
 
 
 
 
 
 
289
  """
290
  else:
291
- context_header = """
292
- NOTE: No SEA exam papers uploaded yet. For more accurate SEA-aligned content, upload past papers using the document upload section.
293
- """
294
-
295
- # Build enhanced prompt
296
- enhanced_prompt = f"""
297
- SEA EXAM TUTOR MODE
298
- {context_header}
299
- ---
300
- REQUEST DETAILS:
301
  Subject: {subject}
302
  Topic: {topic}
303
- Language: {language}
304
- Student Level: {level}
305
 
306
  TASK: {prompt}
307
 
308
- SPECIFIC SEA REQUIREMENTS:
309
- 1. Align with Trinidad & Tobago SEA exam standards
310
- 2. Use appropriate difficulty for {level} level
311
- 3. Format similar to actual SEA past papers
312
- 4. Include clear, step-by-step explanations where applicable
313
- 5. Focus on conceptual understanding rather than rote memorization
314
  """
315
 
316
  return generate_with_groq(enhanced_prompt)
317
 
318
  # -----------------------------
319
- # Original Helper Functions (Updated to use RAG)
320
  # -----------------------------
321
- def generate_with_groq(prompt: str) -> str:
322
- """Call Groq API with error handling."""
323
- if not GROQ_API_KEY:
324
- return "❌ Missing GROQ_API_KEY. Please set it as a secret/environment variable."
325
-
326
- try:
327
- response = client.chat.completions.create(
328
- model="llama-3.1-8b-instant",
329
- messages=[{"role": "user", "content": prompt}],
330
- temperature=0.7,
331
- max_tokens=800,
332
- )
333
- return response.choices[0].message.content
334
- except Exception as e:
335
- return f"❌ API error: {e}"
336
-
337
  def build_system_context(subject: str, topic: str, language: str, level: str) -> str:
338
- return (
339
- f"Subject: {subject}\n"
340
- f"Topic: {topic}\n"
341
- f"Language: {language}\n"
342
- f"Student Level: {level}\n"
343
- f"Exam: Trinidad & Tobago Secondary Entrance Assessment (SEA)\n"
344
- )
345
 
346
  def prompt_explanation(subject: str, topic: str, language: str, level: str) -> str:
347
  ctx = build_system_context(subject, topic, language, level)
348
- return (
349
- f"{ctx}\n"
350
- "Task: Write a clear, friendly, step-by-step explanation of the topic suitable for SEA exam preparation. "
351
- "Use examples similar to those found in SEA past papers. "
352
- "Include common mistakes students make and how to avoid them. "
353
- "Reply in English only."
354
- )
355
-
356
- def prompt_resources(subject: str, topic: str, language: str, level: str) -> str:
357
- ctx = build_system_context(subject, topic, language, level)
358
- return (
359
- f"{ctx}\n"
360
- "Task: Recommend SEA-specific learning resources. "
361
- "Include official resources, practice papers, and study strategies. "
362
- "Return as a markdown list with resource type, description, and why it's useful for SEA. "
363
- "Reply in English only."
364
- )
365
-
366
- def prompt_roadmap(subject: str, topic: str, language: str, level: str) -> str:
367
- ctx = build_system_context(subject, topic, language, level)
368
- return (
369
- f"{ctx}\n"
370
- "Task: Create a 4-week study roadmap for this SEA topic. "
371
- "Include weekly goals, practice activities, and checkpoints. "
372
- "Add test-taking strategies specific to SEA exam format. "
373
- "Reply in English only."
374
- )
375
 
376
  def prompt_quiz(subject: str, topic: str, language: str, level: str) -> str:
377
- ctx = build_system_context(subject, topic, language, level)
378
- return (
379
- f"{ctx}\n"
380
- "Task: Create SEA-style multiple choice questions with 4 options each. "
381
- "Return STRICT JSON only with this schema:\n"
382
- "{\n"
383
- ' "questions": [\n'
384
- ' {\n'
385
- ' "question": "string",\n'
386
- ' "options": ["A", "B", "C", "D"],\n'
387
- ' "answer_index": 0,\n'
388
- ' "explanation": "string"\n'
389
- " }\n"
390
- " ]\n"
391
- "}\n"
392
- "Requirements:\n"
393
- "- Exactly 3-5 questions\n"
394
- "- Options A-D only\n"
395
- "- answer_index is 0-3\n"
396
- "- Include explanation for answer\n"
397
- "- Questions must be SEA exam appropriate\n"
398
- )
399
-
400
- def prompt_past_paper_question(subject: str, topic: str) -> str:
401
- """Generate a new question in SEA exam format."""
402
- return (
403
- f"Subject: {subject}\n"
404
- f"Topic: {topic}\n"
405
- "Task: Create a NEW practice question in the exact format of Trinidad & Tobago SEA exam. "
406
- "Include:\n"
407
- "1. The question text\n"
408
- "2. Multiple choice options (A-D) or structured answer format\n"
409
- "3. Correct answer\n"
410
- "4. Step-by-step solution\n"
411
- "5. Marks allocation\n"
412
- "6. Common errors to avoid\n"
413
- "Make it original but consistent with SEA standards."
414
- )
415
 
416
  # -----------------------------
417
- # Gradio Callbacks (Updated)
418
  # -----------------------------
419
  def on_generate_explanation(subject, topic, language, level):
420
- base_prompt = prompt_explanation(subject, topic, language, level)
421
- return generate_with_context(base_prompt, subject, topic, language, level)
422
-
423
- def on_generate_resources(subject, topic, language, level):
424
- base_prompt = prompt_resources(subject, topic, language, level)
425
- return generate_with_context(base_prompt, subject, topic, language, level)
426
-
427
- def on_generate_roadmap(subject, topic, language, level):
428
- base_prompt = prompt_roadmap(subject, topic, language, level)
429
- return generate_with_context(base_prompt, subject, topic, language, level)
430
 
431
  def on_generate_quiz(subject, topic, language, level):
432
- base_prompt = prompt_quiz(subject, topic, language, level)
433
- raw_json = generate_with_context(base_prompt, subject, topic, language, level)
434
 
435
- # Parse and normalize quiz
436
- quiz = normalize_quiz(parse_quiz_json(raw_json))
 
 
 
 
 
 
 
437
 
438
- # Update UI components
439
  vis = [False] * 5
440
- labels = [("Question", ["Option 1", "Option 2", "Option 3", "Option 4"])] * 5
441
 
442
- for i, q in enumerate(quiz[:5]): # Max 5 questions
443
  vis[i] = True
444
- labels[i] = (f"Q{i+1}. {q['question']}", q["options"])
445
-
446
- status = f"✅ Generated {len(quiz)} SEA-style questions." if quiz else "⚠️ No valid questions generated."
447
 
448
  return (
449
  quiz,
@@ -452,420 +267,77 @@ def on_generate_quiz(subject, topic, language, level):
452
  gr.update(visible=vis[2], label=labels[2][0], choices=labels[2][1], value=None),
453
  gr.update(visible=vis[3], label=labels[3][0], choices=labels[3][1], value=None),
454
  gr.update(visible=vis[4], label=labels[4][0], choices=labels[4][1], value=None),
455
- status
456
  )
457
 
458
- def on_generate_past_paper_question(subject, topic):
459
- prompt = prompt_past_paper_question(subject, topic)
460
- return generate_with_context(prompt, subject, topic, "English", "Intermediate")
461
-
462
- # -----------------------------
463
- # Original Quiz Functions (Keep as is)
464
- # -----------------------------
465
- def parse_quiz_json(text: str) -> Dict[str, Any]:
466
- """Extract and parse JSON quiz from model output."""
467
- try:
468
- parsed = json.loads(text)
469
- if "questions" in parsed:
470
- return parsed
471
- except Exception:
472
- pass
473
-
474
- match = re.search(r"\{(?:[^{}]|(?R))*\}", text, re.DOTALL)
475
- if match:
476
- try:
477
- parsed = json.loads(match.group(0))
478
- if "questions" in parsed:
479
- return parsed
480
- except Exception:
481
- pass
482
-
483
- return {"questions": []}
484
-
485
- def normalize_quiz(quiz: Dict[str, Any]) -> List[Dict[str, Any]]:
486
- """Ensure each question has required fields."""
487
- cleaned = []
488
- for q in quiz.get("questions", []):
489
- question = q.get("question")
490
- options = q.get("options", [])
491
- answer_index = q.get("answer_index")
492
-
493
- if (
494
- isinstance(question, str)
495
- and isinstance(options, list)
496
- and 2 <= len(options) <= 5
497
- and isinstance(answer_index, int)
498
- and 0 <= answer_index < len(options)
499
- ):
500
- cleaned.append({
501
- "question": question.strip(),
502
- "options": [str(o).strip() for o in options],
503
- "answer_index": answer_index,
504
- "explanation": q.get("explanation", "No explanation provided.")
505
- })
506
-
507
- return cleaned[:5]
508
-
509
- def evaluate_answers(
510
- user_choices: List[int], quiz_data: List[Dict[str, Any]]
511
- ) -> Tuple[str, str]:
512
- """Compute score and feedback."""
513
- if not quiz_data:
514
- return "No quiz available.", "Generate a quiz first."
515
-
516
- correct = 0
517
- details = []
518
-
519
- for i, q in enumerate(quiz_data):
520
- user_idx = user_choices[i] if i < len(user_choices) else None
521
- ans_idx = q["answer_index"]
522
- is_correct = (user_idx == ans_idx)
523
-
524
- if is_correct:
525
- correct += 1
526
-
527
- chosen = (
528
- f"{q['options'][user_idx]}"
529
- if isinstance(user_idx, int) and 0 <= user_idx < len(q["options"])
530
- else "No answer"
531
- )
532
-
533
- details.append(
534
- f"**Q{i+1}:** {'✅ Correct' if is_correct else '❌ Incorrect'}\n"
535
- f"Your answer: {chosen}\n"
536
- f"Correct answer: {q['options'][ans_idx]}\n"
537
- f"Explanation: {q.get('explanation', 'No explanation')}\n"
538
- )
539
-
540
- total = len(quiz_data)
541
- score_text = f"## 📊 Score: {correct} / {total}"
542
-
543
- if correct == total:
544
- feedback = "**Excellent!** You've mastered these SEA-style questions."
545
- elif correct >= total * 0.7:
546
- feedback = "**Good work!** Review the explanations for any mistakes."
547
- else:
548
- feedback = "**Keep practicing!** Review the topic and try again."
549
-
550
- feedback += "\n\n### Question Details:\n" + "\n".join(details)
551
- return score_text, feedback
552
-
553
- def on_display_results(quiz_state, a1, a2, a3, a4, a5):
554
- quiz = quiz_state or []
555
-
556
- # Map selected options to indices
557
- selections = []
558
- chosen_texts = [a1, a2, a3, a4, a5]
559
-
560
- for i, q in enumerate(quiz):
561
- chosen = chosen_texts[i] if i < len(chosen_texts) else None
562
- if chosen is None:
563
- selections.append(None)
564
- continue
565
-
566
- try:
567
- idx = q["options"].index(chosen)
568
- selections.append(idx)
569
- except ValueError:
570
- selections.append(None)
571
-
572
- return evaluate_answers(selections, quiz)
573
-
574
  # -----------------------------
575
- # Enhanced Gradio UI
576
  # -----------------------------
577
  CSS = """
578
- :root {
579
- --sea-blue: #1a5f7a;
580
- --sea-light-blue: #57cc99;
581
- --card-bg: #f8f9fa;
582
- --border: #dee2e6;
583
- }
584
- .gradio-container {max-width: 1200px !important; font-family: 'Segoe UI', sans-serif;}
585
- #title h1 {color: var(--sea-blue); margin-bottom: 6px; border-bottom: 3px solid var(--sea-light-blue); padding-bottom: 10px;}
586
- #subtitle {color: #495057; margin-top: 0; font-style: italic;}
587
- .card {
588
- background: var(--card-bg);
589
- border: 1px solid var(--border);
590
- border-radius: 12px;
591
- padding: 18px;
592
- box-shadow: 0 4px 12px rgba(26, 95, 122, 0.08);
593
- margin-bottom: 20px;
594
- }
595
- .btn-primary button {
596
- background: linear-gradient(135deg, var(--sea-blue), #2a9d8f) !important;
597
- border: none !important;
598
- color: white !important;
599
- font-weight: 600 !important;
600
- border-radius: 8px !important;
601
- padding: 10px 24px !important;
602
- }
603
- .btn-primary button:hover {
604
- background: linear-gradient(135deg, #2a9d8f, var(--sea-blue)) !important;
605
- transform: translateY(-2px);
606
- transition: all 0.3s ease;
607
- }
608
- .section-title {
609
- font-weight: 700;
610
- color: var(--sea-blue);
611
- margin-bottom: 12px;
612
- font-size: 18px;
613
- display: flex;
614
- align-items: center;
615
- gap: 8px;
616
- }
617
- .section-title::before {
618
- content: "📘";
619
- }
620
- .upload-section {
621
- border: 2px dashed var(--sea-light-blue) !important;
622
- background: rgba(87, 204, 153, 0.05) !important;
623
- }
624
- .sea-badge {
625
- background: var(--sea-light-blue);
626
- color: white;
627
- padding: 2px 8px;
628
- border-radius: 12px;
629
- font-size: 12px;
630
- font-weight: 600;
631
- margin-left: 8px;
632
- }
633
  """
634
 
635
- with gr.Blocks(css=CSS, theme=gr.themes.Soft(primary_hue="blue")) as demo:
636
- gr.Markdown(
637
- """
638
- <div id='title'>
639
- <h1>🇹🇹 AI SEA Exam Tutor - Trinidad & Tobago</h1>
640
- <p id='subtitle'>Secondary Entrance Assessment Preparation Assistant</p>
641
- </div>
642
- """
643
- )
644
-
 
 
 
 
 
 
 
 
 
645
  with gr.Row():
646
  with gr.Column(scale=1):
647
  with gr.Group(elem_classes="card"):
648
- gr.Markdown("### 📝 SEA Study Parameters")
649
-
650
- subject = gr.Dropdown(
651
- choices=SEA_SUBJECTS,
652
- value="Mathematics",
653
- label="SEA Subject",
654
- info="Select subject area"
655
- )
656
-
657
- # Dynamic topic based on subject
658
- def update_topics(subject):
659
- if subject == "Mathematics":
660
- return gr.Dropdown(choices=SEA_MATH_TOPICS, value=SEA_MATH_TOPICS[0])
661
- else:
662
- return gr.Dropdown(choices=SEA_ENGLISH_TOPICS, value=SEA_ENGLISH_TOPICS[0])
663
-
664
- topic = gr.Dropdown(
665
- choices=SEA_MATH_TOPICS,
666
- value=SEA_MATH_TOPICS[0],
667
- label="Topic Area"
668
- )
669
-
670
- subject.change(update_topics, inputs=[subject], outputs=[topic])
671
-
672
- language = gr.Dropdown(
673
- choices=LANG_OPTIONS,
674
- value="English",
675
- label="Language",
676
- interactive=False # SEA is primarily English
677
- )
678
-
679
- level = gr.Radio(
680
- choices=LEVEL_OPTIONS,
681
- value="Intermediate",
682
- label="Student Level"
683
- )
684
 
685
  with gr.Column(scale=2):
686
- # Document Upload Section
687
- with gr.Group(elem_classes="card upload-section"):
688
- gr.Markdown("### 📤 Upload SEA Exam Papers")
689
- gr.Markdown("Upload past papers, answer sheets, or study materials. The AI will use these to generate accurate SEA-style content.")
690
-
691
  uploaded_files = gr.Files(
692
- label="Upload Files (PDF, TXT)",
693
  file_types=[".pdf", ".txt"],
694
- file_count="multiple",
695
- interactive=True
696
- )
697
-
698
- with gr.Row():
699
- process_btn = gr.Button(
700
- "Process Uploaded Documents",
701
- variant="primary",
702
- scale=2
703
- )
704
- clear_btn = gr.Button("Clear Files", variant="secondary", scale=1)
705
-
706
- upload_status = gr.Markdown(
707
- "**Status:** No documents uploaded yet. Upload SEA papers for enhanced accuracy.",
708
- elem_classes="status-text"
709
- )
710
-
711
- # Processing events
712
- process_btn.click(
713
- fn=process_uploaded_documents,
714
- inputs=[uploaded_files],
715
- outputs=[upload_status]
716
- )
717
-
718
- clear_btn.click(
719
- fn=lambda: (None, "✅ Files cleared. Upload new documents."),
720
- inputs=[],
721
- outputs=[uploaded_files, upload_status]
722
  )
 
 
 
723
 
724
- # Main Features in Tabs
725
- with gr.Tabs():
726
- with gr.TabItem("📚 Explanation & Resources"):
727
- with gr.Column():
728
- with gr.Group(elem_classes="card"):
729
- gr.Markdown("<div class='section-title'>Generate SEA-Aligned Explanation</div>")
730
- btn_explain = gr.Button("Generate Explanation", variant="primary")
731
- explanation = gr.Markdown(
732
- label="SEA-Focused Explanation",
733
- value="Click 'Generate Explanation' for a topic-specific guide.",
734
- elem_classes="output-area"
735
- )
736
-
737
- with gr.Group(elem_classes="card"):
738
- gr.Markdown("<div class='section-title'>Generate Study Resources</div>")
739
- btn_resources = gr.Button("Generate Resources", variant="primary")
740
- resources = gr.Markdown(
741
- label="Recommended Resources",
742
- value="Resources will appear here.",
743
- elem_classes="output-area"
744
- )
745
-
746
- with gr.TabItem("🗺️ Study Roadmap"):
747
- with gr.Column():
748
- with gr.Group(elem_classes="card"):
749
- gr.Markdown("<div class='section-title'>Generate 4-Week Study Roadmap</div>")
750
- btn_roadmap = gr.Button("Generate Roadmap", variant="primary")
751
- roadmap = gr.Markdown(
752
- label="Study Roadmap",
753
- value="Your personalized roadmap will appear here.",
754
- elem_classes="output-area"
755
- )
756
-
757
- with gr.TabItem("📝 Quiz & Assessment"):
758
- with gr.Column():
759
- with gr.Group(elem_classes="card"):
760
- gr.Markdown("<div class='section-title'>Generate SEA-Style Quiz</div>")
761
-
762
- with gr.Row():
763
- btn_quiz = gr.Button("Generate New Quiz", variant="primary", scale=2)
764
- btn_past_paper = gr.Button("Generate Past Paper Question", variant="secondary", scale=1)
765
-
766
- quiz_info = gr.Markdown("Click 'Generate New Quiz' to create SEA-style questions.")
767
-
768
- # Past paper question output
769
- past_paper_output = gr.Markdown(visible=False)
770
-
771
- # Quiz state and questions
772
- quiz_state = gr.State([])
773
-
774
- # Question containers (up to 5)
775
- with gr.Column(visible=False) as quiz_container:
776
- q1 = gr.Radio(label="Question 1", choices=[], visible=False, interactive=True)
777
- q2 = gr.Radio(label="Question 2", choices=[], visible=False, interactive=True)
778
- q3 = gr.Radio(label="Question 3", choices=[], visible=False, interactive=True)
779
- q4 = gr.Radio(label="Question 4", choices=[], visible=False, interactive=True)
780
- q5 = gr.Radio(label="Question 5", choices=[], visible=False, interactive=True)
781
-
782
- with gr.Group(elem_classes="card"):
783
- gr.Markdown("<div class='section-title'>Evaluate Your Answers</div>")
784
- btn_results = gr.Button("Check Answers", variant="primary")
785
-
786
- with gr.Row():
787
- with gr.Column(scale=1):
788
- score = gr.Markdown("**Score:** Not assessed yet.")
789
- with gr.Column(scale=3):
790
- feedback = gr.Markdown("**Feedback:** Submit quiz answers for evaluation.")
791
-
792
- with gr.TabItem("ℹ️ System Info"):
793
  with gr.Group(elem_classes="card"):
794
- gr.Markdown("### System Information")
795
- gr.Markdown(f"""
796
- **Current Configuration:**
797
- - Model: Llama 3.1 8B Instant (via Groq)
798
- - RAG Enabled: {'Yes' if os.path.exists(UPLOADED_DOCS_FILE) else 'No'}
799
- - Documents Loaded: {len(json.load(open(UPLOADED_DOCS_FILE))) if os.path.exists(UPLOADED_DOCS_FILE) else 0}
800
- - Subjects Configured: {len(SEA_SUBJECTS)}
801
-
802
- **How to use:**
803
- 1. Upload SEA past papers (PDF format)
804
- 2. Select subject and topic
805
- 3. Generate explanations, resources, or quizzes
806
- 4. The AI will reference uploaded papers for accuracy
807
-
808
- **Note:** All content is generated based on SEA exam standards and any uploaded materials.
809
- """)
810
-
811
- # Event Handlers
812
- btn_explain.click(
813
- fn=on_generate_explanation,
814
- inputs=[subject, topic, language, level],
815
- outputs=[explanation]
816
- )
817
 
818
- btn_resources.click(
819
- fn=on_generate_resources,
820
- inputs=[subject, topic, language, level],
821
- outputs=[resources]
822
- )
823
-
824
- btn_roadmap.click(
825
- fn=on_generate_roadmap,
826
- inputs=[subject, topic, language, level],
827
- outputs=[roadmap]
828
- )
829
-
830
- btn_quiz.click(
831
- fn=on_generate_quiz,
832
- inputs=[subject, topic, language, level],
833
- outputs=[quiz_state, q1, q2, q3, q4, q5, quiz_info]
834
- ).then(
835
- fn=lambda: gr.update(visible=True),
836
- inputs=[],
837
- outputs=[quiz_container]
838
- )
839
-
840
- btn_past_paper.click(
841
- fn=on_generate_past_paper_question,
842
- inputs=[subject, topic],
843
- outputs=[past_paper_output]
844
- ).then(
845
- fn=lambda: gr.update(visible=True),
846
- inputs=[],
847
- outputs=[past_paper_output]
848
- )
849
-
850
- btn_results.click(
851
- fn=on_display_results,
852
- inputs=[quiz_state, q1, q2, q3, q4, q5],
853
- outputs=[score, feedback]
854
- )
855
 
856
- # -----------------------------
857
- # Launch Application
858
- # -----------------------------
859
  if __name__ == "__main__":
860
- # Create necessary directories
861
- os.makedirs("uploads", exist_ok=True)
862
- os.makedirs("data", exist_ok=True)
863
-
864
- # Launch with file upload support
865
- demo.launch(
866
- server_name="0.0.0.0",
867
- server_port=7860,
868
- share=False,
869
- max_file_size="20mb", # Limit file size for safety
870
- show_error=True
871
- )
 
1
+ # app.py - AI SEA Exam Tutor with UI API Key Entry
 
2
 
3
  import os
4
  import json
 
9
 
10
  import gradio as gr
11
  from groq import Groq
12
+ import PyPDF2
13
 
14
  # -----------------------------
15
  # Configuration
16
  # -----------------------------
17
+ # API key is now handled via UI input
18
+ api_key_state = {"value": ""}
19
 
20
  # SEA-specific configurations
21
+ SEA_SUBJECTS = ["Mathematics", "English Language Arts"]
 
 
 
22
 
23
  SEA_MATH_TOPICS = [
24
  "Number Theory (Fractions, Decimals, Percentages)",
25
  "Measurement (Perimeter, Area, Volume)",
26
+ "Geometry", "Algebra Basics",
27
+ "Word Problems", "Data Interpretation"
 
 
28
  ]
29
 
30
  SEA_ENGLISH_TOPICS = [
 
36
  "Listening Comprehension (simulated)"
37
  ]
38
 
39
+ LANG_OPTIONS = ["English"]
40
  LEVEL_OPTIONS = ["Beginner", "Intermediate", "Advanced"]
 
 
41
  UPLOADED_DOCS_FILE = "sea_exam_documents.json"
42
 
43
+ # -----------------------------
44
+ # API Key Management
45
+ # -----------------------------
46
+ def update_api_key(api_key):
47
+ """Store API key in session state"""
48
+ api_key_state["value"] = api_key.strip()
49
+ if api_key_state["value"]:
50
+ return "✅ API key saved (not visible for security)"
51
+ else:
52
+ return "⚠️ API key cleared"
53
+
54
+ def get_groq_client():
55
+ """Get Groq client using UI-provided API key"""
56
+ api_key = api_key_state["value"]
57
+ if not api_key:
58
+ return None, "❌ No API key provided"
59
+ try:
60
+ client = Groq(api_key=api_key)
61
+ return client, ""
62
+ except Exception as e:
63
+ return None, f"❌ Invalid API key: {str(e)}"
64
+
65
+ def generate_with_groq(prompt: str) -> str:
66
+ """Call Groq API using UI-provided API key"""
67
+ client, error_msg = get_groq_client()
68
+ if error_msg:
69
+ return error_msg
70
+
71
+ try:
72
+ response = client.chat.completions.create(
73
+ model="llama-3.1-8b-instant",
74
+ messages=[{"role": "user", "content": prompt}],
75
+ temperature=0.7,
76
+ max_tokens=800,
77
+ )
78
+ return response.choices[0].message.content
79
+ except Exception as e:
80
+ return f"❌ API error: {e}"
81
+
82
  # -----------------------------
83
  # Document Processing Functions
84
  # -----------------------------
85
  def extract_text_from_pdf(file_bytes: bytes, filename: str) -> str:
86
+ """Extract text from uploaded PDF files"""
87
  try:
 
88
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_file:
89
  tmp_file.write(file_bytes)
90
  tmp_file_path = tmp_file.name
91
 
 
92
  full_text = ""
93
  with open(tmp_file_path, 'rb') as pdf_file:
94
  pdf_reader = PyPDF2.PdfReader(pdf_file)
 
95
  for page_num in range(len(pdf_reader.pages)):
96
  page = pdf_reader.pages[page_num]
97
  page_text = page.extract_text()
98
+ full_text += f"\n--- Page {page_num+1} ---\n{page_text}\n"
 
 
 
99
 
 
100
  os.unlink(tmp_file_path)
101
+ return full_text
 
 
 
 
102
 
103
  except Exception as e:
104
  return f"ERROR processing {filename}: {str(e)}"
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def process_uploaded_documents(files) -> str:
107
+ """Process uploaded SEA exam documents"""
108
  if not files:
109
+ return "⚠️ No files uploaded"
110
 
111
  all_documents = []
 
112
 
113
  for file_info in files:
 
114
  if isinstance(file_info, tuple) and len(file_info) >= 2:
115
  file_path, filename = file_info[0], file_info[1]
116
  else:
 
117
  file_path = file_info
118
  filename = os.path.basename(str(file_info))
119
 
120
  try:
 
121
  with open(file_path, 'rb') as f:
122
  file_bytes = f.read()
123
 
 
124
  if filename.lower().endswith('.pdf'):
125
  text_content = extract_text_from_pdf(file_bytes, filename)
126
  file_type = "PDF"
 
128
  text_content = file_bytes.decode('utf-8', errors='replace')
129
  file_type = "Text"
130
  else:
131
+ continue
 
132
 
 
133
  doc_entry = {
134
  "filename": filename,
135
+ "content": text_content[:10000] if len(text_content) > 10000 else text_content,
136
  "type": file_type,
 
 
137
  "upload_time": gr.utils.datetime.datetime.now().isoformat()
138
  }
 
139
  all_documents.append(doc_entry)
 
140
 
141
  except Exception as e:
142
+ print(f"Failed to process {filename}: {str(e)}")
 
 
143
 
 
144
  try:
145
  with open(UPLOADED_DOCS_FILE, 'w', encoding='utf-8') as f:
146
  json.dump(all_documents, f, ensure_ascii=False, indent=2)
147
 
148
+ return f"✅ Processed {len(all_documents)} files. Ready for RAG queries."
 
 
 
 
 
 
 
 
 
 
149
  except Exception as e:
150
  return f"❌ Error saving documents: {str(e)}"
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  def get_relevant_context(subject: str, topic: str, max_context: int = 1500) -> str:
153
+ """Retrieve relevant context from uploaded papers"""
154
  try:
155
  if not os.path.exists(UPLOADED_DOCS_FILE):
156
  return ""
 
159
  documents = json.load(f)
160
 
161
  relevant_parts = []
 
 
 
162
  for doc in documents:
163
+ content = doc.get('content', '').lower()
164
+ if topic.lower() in content or subject.lower() in content:
165
+ relevant_parts.append(f"\n--- From: {doc['filename']} ---\n{doc['content'][:500]}...\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
 
167
  combined = "\n".join(relevant_parts)
168
  if len(combined) > max_context:
169
+ combined = combined[:max_context] + "\n...[truncated]..."
170
 
171
  return combined if combined else ""
172
+ except:
 
 
173
  return ""
174
 
175
  # -----------------------------
176
  # Enhanced Generation with RAG
177
  # -----------------------------
178
  def generate_with_context(prompt: str, subject: str, topic: str, language: str, level: str) -> str:
179
+ """Enhanced generator using uploaded papers as context"""
 
180
  context = get_relevant_context(subject, topic)
181
 
 
182
  if context:
183
+ enhanced_prompt = f"""
184
+ SEA EXAM CONTEXT FROM UPLOADED PAPERS:
185
  {context}
186
 
187
+ REQUEST:
188
+ Subject: {subject}
189
+ Topic: {topic}
190
+ Level: {level}
191
+
192
+ TASK: {prompt}
193
+
194
+ Create content aligned with Trinidad & Tobago SEA exam standards.
195
  """
196
  else:
197
+ enhanced_prompt = f"""
 
 
 
 
 
 
 
 
 
198
  Subject: {subject}
199
  Topic: {topic}
200
+ Level: {level}
 
201
 
202
  TASK: {prompt}
203
 
204
+ Create SEA-aligned content. (No papers uploaded yet)
 
 
 
 
 
205
  """
206
 
207
  return generate_with_groq(enhanced_prompt)
208
 
209
  # -----------------------------
210
+ # Helper Functions
211
  # -----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  def build_system_context(subject: str, topic: str, language: str, level: str) -> str:
213
+ return f"SEA Exam - {subject}: {topic} ({level})"
 
 
 
 
 
 
214
 
215
  def prompt_explanation(subject: str, topic: str, language: str, level: str) -> str:
216
  ctx = build_system_context(subject, topic, language, level)
217
+ return f"{ctx}\nWrite a step-by-step SEA exam explanation with examples."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  def prompt_quiz(subject: str, topic: str, language: str, level: str) -> str:
220
+ return f"""
221
+ Subject: {subject}, Topic: {topic}, Level: {level}
222
+ Create 3-5 SEA-style multiple choice questions. Return JSON:
223
+ {{
224
+ "questions": [
225
+ {{
226
+ "question": "string",
227
+ "options": ["A", "B", "C", "D"],
228
+ "answer_index": 0
229
+ }}
230
+ ]
231
+ }}
232
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  # -----------------------------
235
+ # Gradio Callbacks
236
  # -----------------------------
237
  def on_generate_explanation(subject, topic, language, level):
238
+ prompt = prompt_explanation(subject, topic, language, level)
239
+ return generate_with_context(prompt, subject, topic, language, level)
 
 
 
 
 
 
 
 
240
 
241
  def on_generate_quiz(subject, topic, language, level):
242
+ prompt = prompt_quiz(subject, topic, language, level)
243
+ raw = generate_with_context(prompt, subject, topic, language, level)
244
 
245
+ # Parse JSON
246
+ quiz = []
247
+ try:
248
+ match = re.search(r'\{.*\}', raw, re.DOTALL)
249
+ if match:
250
+ parsed = json.loads(match.group())
251
+ quiz = parsed.get("questions", [])
252
+ except:
253
+ pass
254
 
255
+ # Update UI
256
  vis = [False] * 5
257
+ labels = [("Q", ["A", "B", "C", "D"])] * 5
258
 
259
+ for i, q in enumerate(quiz[:5]):
260
  vis[i] = True
261
+ labels[i] = (f"Q{i+1}. {q.get('question', '')}", q.get('options', []))
 
 
262
 
263
  return (
264
  quiz,
 
267
  gr.update(visible=vis[2], label=labels[2][0], choices=labels[2][1], value=None),
268
  gr.update(visible=vis[3], label=labels[3][0], choices=labels[3][1], value=None),
269
  gr.update(visible=vis[4], label=labels[4][0], choices=labels[4][1], value=None),
270
+ f"Generated {len(quiz)} questions" if quiz else "No questions generated"
271
  )
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  # -----------------------------
274
+ # Gradio UI
275
  # -----------------------------
276
  CSS = """
277
+ .card {background: #f8f9fa; border-radius: 10px; padding: 15px; margin-bottom: 15px;}
278
+ .btn-primary button {background: #2563eb; color: white; border: none; border-radius: 6px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  """
280
 
281
+ with gr.Blocks(css=CSS, theme=gr.themes.Soft()) as demo:
282
+ gr.Markdown("# 🇹🇹 AI SEA Exam Tutor")
283
+
284
+ # API Key Section
285
+ with gr.Group(elem_classes="card"):
286
+ gr.Markdown("### 🔑 API Key Configuration")
287
+ with gr.Row():
288
+ api_key_input = gr.Textbox(
289
+ label="Groq API Key",
290
+ type="password",
291
+ placeholder="gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
292
+ lines=1,
293
+ scale=3
294
+ )
295
+ api_key_btn = gr.Button("Save Key", variant="primary", scale=1)
296
+ api_key_status = gr.Markdown("Enter API key and click Save")
297
+ api_key_btn.click(update_api_key, [api_key_input], [api_key_status])
298
+
299
+ # Main Inputs
300
  with gr.Row():
301
  with gr.Column(scale=1):
302
  with gr.Group(elem_classes="card"):
303
+ gr.Markdown("### Study Parameters")
304
+ subject = gr.Dropdown(SEA_SUBJECTS, value="Mathematics", label="Subject")
305
+ topic = gr.Dropdown(SEA_MATH_TOPICS, value=SEA_MATH_TOPICS[0], label="Topic")
306
+ level = gr.Radio(LEVEL_OPTIONS, value="Intermediate", label="Level")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
  with gr.Column(scale=2):
309
+ with gr.Group(elem_classes="card"):
310
+ gr.Markdown("### 📤 Upload SEA Papers")
 
 
 
311
  uploaded_files = gr.Files(
312
+ label="Upload PDF/TXT files",
313
  file_types=[".pdf", ".txt"],
314
+ file_count="multiple"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  )
316
+ process_btn = gr.Button("Process Documents", variant="primary")
317
+ upload_status = gr.Markdown("Upload files then click Process")
318
+ process_btn.click(process_uploaded_documents, [uploaded_files], [upload_status])
319
 
320
+ # Features
321
+ with gr.Row():
322
+ with gr.Column():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  with gr.Group(elem_classes="card"):
324
+ gr.Markdown("### Explanation")
325
+ btn_explain = gr.Button("Generate Explanation", variant="primary")
326
+ explanation = gr.Markdown("Explanation will appear here")
327
+ btn_explain.click(on_generate_explanation, [subject, topic, "English", level], [explanation])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ with gr.Row():
330
+ with gr.Column():
331
+ with gr.Group(elem_classes="card"):
332
+ gr.Markdown("### Quiz")
333
+ btn_quiz = gr.Button("Generate Quiz", variant="primary")
334
+ quiz_info = gr.Markdown("Click to generate quiz")
335
+ quiz_state = gr.State([])
336
+ q1 = gr.Radio([], visible=False, label="Q1")
337
+ q2 = gr.Radio([], visible=False, label="Q2")
338
+ q3 = gr.Radio([], visible=False, label="Q3")
339
+ btn_quiz.click(on_generate_quiz, [subject, topic, "English", level],
340
+ [quiz_state, q1, q2, q3, quiz_info, quiz_info, quiz_info])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
 
 
 
342
  if __name__ == "__main__":
343
+ demo.launch(server_name="0.0.0.0", server_port=7860)