mikaelJ46 commited on
Commit
dc89a69
Β·
verified Β·
1 Parent(s): 6037e25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +549 -78
app.py CHANGED
@@ -12,6 +12,7 @@ import time
12
  import re
13
  from PIL import Image
14
  import io
 
15
 
16
  # ---------- 1. Configure ALL AI Systems ----------
17
  # Gemini (Primary)
@@ -200,33 +201,6 @@ Return ONLY valid JSON:
200
  except:
201
  return {"subject": "Unknown", "year": "Unknown", "series": "Unknown", "variant": "Unknown", "paper_number": "Unknown", "syllabus_code": "Unknown"}
202
 
203
- def extract_questions_from_text(text, paper_id, paper_title, subject, paper_details):
204
- if not text or len(text) < 100:
205
- return []
206
-
207
- prompt = f"""Extract ALL questions from this IGCSE {subject} paper.
208
-
209
- Paper Text: {text[:8000]}
210
-
211
- Return JSON array:
212
- [{{"question_number": "1(a)", "question_text": "...", "marks": 2, "topic": "...", "requires_insert": false, "question_type": "..."}}]"""
213
-
214
- try:
215
- response, _ = ask_ai(prompt, temperature=0.2)
216
- clean_txt = response.replace("```json", "").replace("```", "").strip()
217
- questions = json.loads(clean_txt)
218
-
219
- for q in questions:
220
- q['paper_id'] = paper_id
221
- q['paper_title'] = paper_title
222
- q['subject'] = subject
223
- q['year'] = paper_details.get('year', 'Unknown')
224
- q['series'] = paper_details.get('series', 'Unknown')
225
-
226
- return questions
227
- except:
228
- return []
229
-
230
  def process_insert_file(insert_file):
231
  if insert_file is None:
232
  return None, None
@@ -243,7 +217,363 @@ def process_insert_file(insert_file):
243
  except:
244
  return None, None
245
 
246
- # ---------- 6. AI Tutor ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  def ai_tutor_chat(message, history, subject, topic):
248
  if not message.strip():
249
  return history
@@ -283,7 +613,7 @@ Guide students to deep understanding through questioning and clear explanations.
283
  def clear_chat():
284
  return []
285
 
286
- # ---------- 7. Concept Explainer ----------
287
  def explain_concept(subject, concept):
288
  if not concept:
289
  return "Enter a concept to explain!"
@@ -308,7 +638,7 @@ Focus on understanding, not memorization."""
308
 
309
  return response
310
 
311
- # ---------- 8. Problem Solver ----------
312
  def solve_problem(subject, problem, show_steps):
313
  if not problem.strip():
314
  return "Enter a problem!"
@@ -334,7 +664,7 @@ Use clear formatting and explain reasoning."""
334
 
335
  return response
336
 
337
- # ---------- 9. Scenario Analyzer ----------
338
  def analyze_scenario(subject, scenario_description, question):
339
  if not scenario_description.strip():
340
  return "Describe the scenario!"
@@ -364,7 +694,7 @@ Think critically and justify reasoning."""
364
 
365
  return response
366
 
367
- # ---------- 10. Practice Questions ----------
368
  def generate_question(subject, topic, difficulty):
369
  if not topic:
370
  return "Select a topic!", "", ""
@@ -444,30 +774,10 @@ Return JSON:
444
  except:
445
  return response
446
 
447
- # ---------- 11. Past Papers Browser ----------
448
  def search_questions_by_topic(subject, topic):
449
- if not questions_index:
450
- return " No questions yet. Admin needs to upload papers!"
451
-
452
- matching = [q for q in questions_index
453
- if q['subject'] == subject and
454
- (topic.lower() in q['topic'].lower() or topic.lower() in q['question_text'].lower())]
455
-
456
- if not matching:
457
- return f" No questions found for {topic} in {subject}."
458
-
459
- result = f"### Found {len(matching)} question(s) on '{topic}'\n\n"
460
-
461
- for i, q in enumerate(matching, 1):
462
- insert_note = " **[Requires Insert]**" if q.get('requires_insert') else ""
463
- result += f"""**Question {i}** - {q['year']} {q['series']} Paper {q.get('paper_number', '?')}
464
- **{q['question_number']}** [{q['marks']} marks]{insert_note}
465
- {q['question_text']}
466
-
467
- {'─'*80}
468
- """
469
-
470
- return result
471
 
472
  def view_papers_student(subject):
473
  filtered = [p for p in papers_storage if p["subject"] == subject]
@@ -493,15 +803,18 @@ def view_papers_student(subject):
493
 
494
  return result
495
 
496
- # ---------- 12. Admin Functions ----------
497
  def verify_admin_password(password):
498
  if password == ADMIN_PASSWORD:
499
  return gr.update(visible=True), gr.update(visible=False), " Access granted!"
500
  return gr.update(visible=False), gr.update(visible=True), " Incorrect password!"
501
 
502
- def upload_paper(title, subject, content, pdf_file, insert_file):
 
 
 
503
  if not all([title, subject, content]):
504
- return " Fill all fields!", get_papers_list(), " Waiting..."
505
 
506
  paper_id = len(papers_storage) + 1
507
 
@@ -512,7 +825,7 @@ def upload_paper(title, subject, content, pdf_file, insert_file):
512
  if pdf_text and not pdf_text.startswith("Error"):
513
  paper_details = identify_paper_details(pdf_text, pdf_file.name)
514
  pdf_content_storage[paper_id] = pdf_text
515
- content += f"\n[ PDF: {len(pdf_text)} chars]"
516
 
517
  insert_data = None
518
  insert_type = None
@@ -520,7 +833,7 @@ def upload_paper(title, subject, content, pdf_file, insert_file):
520
  insert_data, insert_type = process_insert_file(insert_file)
521
  if insert_data:
522
  insert_storage[paper_id] = (insert_data, insert_type)
523
- content += f"\n[ Insert: {insert_type}]"
524
 
525
  papers_storage.append({
526
  "id": paper_id,
@@ -533,14 +846,30 @@ def upload_paper(title, subject, content, pdf_file, insert_file):
533
  "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
534
  })
535
 
536
- status_msg = " Paper uploaded!"
537
  if pdf_text and not pdf_text.startswith("Error"):
538
- status_msg += "\n Extracting questions..."
539
- questions = extract_questions_from_text(pdf_text, paper_id, title, subject, paper_details)
 
 
540
  questions_index.extend(questions)
541
- status_msg += f"\n Extracted {len(questions)} questions!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
543
- return status_msg, get_papers_list(), f" Papers: {len(papers_storage)} | Questions: {len(questions_index)}"
544
 
545
  def get_papers_list():
546
  if not papers_storage:
@@ -556,7 +885,7 @@ def get_papers_list():
556
 
557
  return "\n".join(result)
558
 
559
- # ---------- 13. Gradio UI ----------
560
  with gr.Blocks(theme=gr.themes.Soft(), title="IGCSE ICT & PE Platform") as app:
561
  gr.Markdown("""
562
  # IGCSE Learning Platform
@@ -620,24 +949,166 @@ _Deep Understanding Through AI-Powered Learning_
620
 
621
  gr.Button(" Analyze", variant="primary").click(analyze_scenario, [scen_subj, scen_desc, scen_q], scen_output)
622
 
623
- with gr.Tab(" Past Papers"):
624
- gr.Markdown("### Search Questions by Topic")
 
625
 
626
  with gr.Row():
627
- pp_subject = gr.Radio(["ICT", "Physical Education"], label="Subject", value="ICT")
628
- pp_topic = gr.Dropdown(ict_topics, label="Topic")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
 
630
- pp_subject.change(update_topics, pp_subject, pp_topic)
 
 
631
 
632
- search_btn = gr.Button(" Search", variant="primary")
633
- questions_output = gr.Markdown(value="Select a topic and click Search")
 
 
634
 
635
- search_btn.click(search_questions_by_topic, [pp_subject, pp_topic], questions_output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
 
637
- gr.Markdown("---\n### Browse All Papers")
638
- browse_subject = gr.Radio(["ICT", "Physical Education"], label="Subject", value="ICT")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  papers_display = gr.Markdown()
640
- gr.Button(" Show Papers").click(view_papers_student, browse_subject, papers_display)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
  with gr.Tab(" Practice"):
643
  gr.Markdown("### Generate & Practice Questions")
@@ -685,7 +1156,7 @@ _Deep Understanding Through AI-Powered Learning_
685
  gr.Markdown("### Uploaded Papers")
686
  lst = gr.Textbox(lines=26, value=get_papers_list(), interactive=False)
687
 
688
- up.click(upload_paper, [t, s, c, pdf, insert], [st, lst, stats])
689
 
690
  login_btn.click(verify_admin_password, [pwd], [admin_section, login_section, login_status])
691
 
 
12
  import re
13
  from PIL import Image
14
  import io
15
+ from collections import defaultdict
16
 
17
  # ---------- 1. Configure ALL AI Systems ----------
18
  # Gemini (Primary)
 
201
  except:
202
  return {"subject": "Unknown", "year": "Unknown", "series": "Unknown", "variant": "Unknown", "paper_number": "Unknown", "syllabus_code": "Unknown"}
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  def process_insert_file(insert_file):
205
  if insert_file is None:
206
  return None, None
 
217
  except:
218
  return None, None
219
 
220
+ # ---------- 6. Enhanced Question Extraction Functions ----------
221
+ def extract_questions_comprehensive(text, paper_id, paper_title, subject, paper_details):
222
+ """
223
+ Multi-pass question extraction with validation and topic mapping
224
+ """
225
+ if not text or len(text) < 100:
226
+ return []
227
+
228
+ # First pass: Extract questions with AI
229
+ questions = extract_questions_with_ai(text, paper_id, paper_title, subject, paper_details)
230
+
231
+ # Second pass: Validate and enhance questions
232
+ validated_questions = validate_and_enhance_questions(questions, text, subject)
233
+
234
+ # Third pass: Map to specific topics
235
+ questions_with_topics = map_questions_to_topics(validated_questions, subject)
236
+
237
+ return questions_with_topics
238
+
239
+ def extract_questions_with_ai(text, paper_id, paper_title, subject, paper_details):
240
+ """
241
+ Primary AI extraction with detailed prompting
242
+ """
243
+ # Split text into chunks if too long
244
+ max_chunk_size = 6000
245
+ text_chunks = [text[i:i+max_chunk_size] for i in range(0, len(text), max_chunk_size)]
246
+
247
+ all_questions = []
248
+
249
+ for chunk_idx, chunk in enumerate(text_chunks):
250
+ prompt = f"""Extract ALL questions from this IGCSE {subject} exam paper chunk.
251
+
252
+ Paper: {paper_title}
253
+ Text Chunk {chunk_idx + 1}:
254
+ {chunk}
255
+
256
+ For EACH question found, provide detailed JSON with:
257
+ 1. Question number (e.g., "1(a)", "2(b)(i)")
258
+ 2. Complete question text
259
+ 3. Marks allocated
260
+ 4. Main topic being tested
261
+ 5. Whether it requires reference to an insert/diagram
262
+ 6. Question type (knowledge, application, analysis, evaluation)
263
+ 7. Key concepts being tested
264
+ 8. Command words used (e.g., "explain", "describe", "evaluate")
265
+
266
+ Return ONLY a valid JSON array:
267
+ [
268
+ {{
269
+ "question_number": "1(a)",
270
+ "question_text": "Complete question text here...",
271
+ "marks": 2,
272
+ "topic": "Primary topic",
273
+ "subtopics": ["subtopic1", "subtopic2"],
274
+ "requires_insert": false,
275
+ "question_type": "application",
276
+ "key_concepts": ["concept1", "concept2"],
277
+ "command_words": ["explain"],
278
+ "difficulty": "medium"
279
+ }}
280
+ ]
281
+
282
+ IMPORTANT:
283
+ - Include ALL questions, even multi-part ones
284
+ - Preserve question numbering exactly as it appears
285
+ - Mark questions requiring inserts/diagrams as "requires_insert": true
286
+ - Be specific with topics (e.g., "RAM vs ROM" not just "Hardware")"""
287
+
288
+ try:
289
+ response, _ = ask_ai(prompt, temperature=0.1)
290
+ clean_txt = response.replace("```json", "").replace("```", "").strip()
291
+
292
+ # Handle multiple JSON objects or arrays
293
+ if clean_txt.startswith('['):
294
+ questions_chunk = json.loads(clean_txt)
295
+ else:
296
+ # Try to find JSON array in response
297
+ json_match = re.search(r'\[.*\]', clean_txt, re.DOTALL)
298
+ if json_match:
299
+ questions_chunk = json.loads(json_match.group())
300
+ else:
301
+ continue
302
+
303
+ # Add metadata to each question
304
+ for q in questions_chunk:
305
+ q['paper_id'] = paper_id
306
+ q['paper_title'] = paper_title
307
+ q['subject'] = subject
308
+ q['year'] = paper_details.get('year', 'Unknown')
309
+ q['series'] = paper_details.get('series', 'Unknown')
310
+ q['paper_number'] = paper_details.get('paper_number', 'Unknown')
311
+ q['variant'] = paper_details.get('variant', 'Unknown')
312
+ q['chunk_index'] = chunk_idx
313
+
314
+ all_questions.extend(questions_chunk)
315
+
316
+ except Exception as e:
317
+ print(f"⚠ Error extracting from chunk {chunk_idx}: {e}")
318
+ continue
319
+
320
+ return all_questions
321
+
322
+ def validate_and_enhance_questions(questions, full_text, subject):
323
+ """
324
+ Validate extracted questions and fix common issues
325
+ """
326
+ validated = []
327
+
328
+ for q in questions:
329
+ # Ensure required fields exist
330
+ if not q.get('question_text') or not q.get('question_number'):
331
+ continue
332
+
333
+ # Clean question text
334
+ q['question_text'] = q['question_text'].strip()
335
+
336
+ # Ensure marks is an integer
337
+ try:
338
+ q['marks'] = int(q.get('marks', 0))
339
+ except:
340
+ q['marks'] = 0
341
+
342
+ # Set defaults for optional fields
343
+ q['topic'] = q.get('topic', 'General')
344
+ q['subtopics'] = q.get('subtopics', [])
345
+ q['requires_insert'] = q.get('requires_insert', False)
346
+ q['question_type'] = q.get('question_type', 'knowledge')
347
+ q['key_concepts'] = q.get('key_concepts', [])
348
+ q['command_words'] = q.get('command_words', [])
349
+ q['difficulty'] = q.get('difficulty', 'medium')
350
+
351
+ # Auto-detect insert requirement if not specified
352
+ if not q['requires_insert']:
353
+ insert_keywords = ['diagram', 'figure', 'table', 'insert', 'image', 'screenshot', 'shown']
354
+ q['requires_insert'] = any(kw in q['question_text'].lower() for kw in insert_keywords)
355
+
356
+ validated.append(q)
357
+
358
+ return validated
359
+
360
+ def map_questions_to_topics(questions, subject):
361
+ """
362
+ Map questions to specific curriculum topics
363
+ """
364
+ topic_lists = {
365
+ "ICT": ict_topics,
366
+ "Physical Education": pe_topics
367
+ }
368
+
369
+ topics = topic_lists.get(subject, [])
370
+
371
+ for q in questions:
372
+ # If topic is generic, try to find more specific match
373
+ if q['topic'] in ['General', 'Unknown', '']:
374
+ best_match = find_best_topic_match(q['question_text'], topics)
375
+ if best_match:
376
+ q['topic'] = best_match
377
+
378
+ # Add searchable keywords
379
+ q['search_keywords'] = generate_search_keywords(q)
380
+
381
+ return questions
382
+
383
+ def find_best_topic_match(question_text, topic_list):
384
+ """
385
+ Find the most relevant topic for a question
386
+ """
387
+ question_lower = question_text.lower()
388
+
389
+ # Direct keyword matching
390
+ for topic in topic_list:
391
+ topic_keywords = topic.lower().split()
392
+ if any(keyword in question_lower for keyword in topic_keywords if len(keyword) > 3):
393
+ return topic
394
+
395
+ # Use AI for complex matching
396
+ prompt = f"""Which ONE topic from this list best matches the question?
397
+
398
+ Topics: {', '.join(topic_list)}
399
+
400
+ Question: {question_text[:300]}
401
+
402
+ Return ONLY the exact topic name from the list, nothing else."""
403
+
404
+ try:
405
+ response, _ = ask_ai(prompt, temperature=0.1)
406
+ matched_topic = response.strip()
407
+ if matched_topic in topic_list:
408
+ return matched_topic
409
+ except:
410
+ pass
411
+
412
+ return "General"
413
+
414
+ def generate_search_keywords(question):
415
+ """
416
+ Generate searchable keywords from question
417
+ """
418
+ keywords = set()
419
+
420
+ # Add from question text
421
+ text_words = re.findall(r'\b\w{4,}\b', question['question_text'].lower())
422
+ keywords.update(text_words)
423
+
424
+ # Add from metadata
425
+ keywords.add(question['topic'].lower())
426
+ keywords.update([st.lower() for st in question.get('subtopics', [])])
427
+ keywords.update([kc.lower() for kc in question.get('key_concepts', [])])
428
+
429
+ # Remove common words
430
+ stop_words = {'what', 'which', 'when', 'where', 'explain', 'describe', 'give', 'state', 'name'}
431
+ keywords = keywords - stop_words
432
+
433
+ return list(keywords)
434
+
435
+ # ---------- 7. Enhanced Search Function ----------
436
+ def search_questions_advanced(subject, search_term, filters=None):
437
+ """
438
+ Advanced question search with multiple filters
439
+
440
+ filters = {
441
+ 'year': '2023',
442
+ 'series': 'June',
443
+ 'marks_min': 2,
444
+ 'marks_max': 8,
445
+ 'question_type': 'application',
446
+ 'difficulty': 'medium',
447
+ 'requires_insert': False
448
+ }
449
+ """
450
+ if not questions_index:
451
+ return "πŸ“š No questions yet. Admin needs to upload papers!"
452
+
453
+ # Filter by subject
454
+ matching = [q for q in questions_index if q['subject'] == subject]
455
+
456
+ # Filter by search term (topic, keywords, question text)
457
+ if search_term:
458
+ search_lower = search_term.lower()
459
+ matching = [q for q in matching if (
460
+ search_lower in q['topic'].lower() or
461
+ search_lower in q['question_text'].lower() or
462
+ any(search_lower in kw for kw in q.get('search_keywords', []))
463
+ )]
464
+
465
+ # Apply additional filters
466
+ if filters:
467
+ if 'year' in filters and filters['year']:
468
+ matching = [q for q in matching if q.get('year') == filters['year']]
469
+
470
+ if 'series' in filters and filters['series']:
471
+ matching = [q for q in matching if q.get('series') == filters['series']]
472
+
473
+ if 'marks_min' in filters:
474
+ matching = [q for q in matching if q.get('marks', 0) >= filters['marks_min']]
475
+
476
+ if 'marks_max' in filters:
477
+ matching = [q for q in matching if q.get('marks', 0) <= filters['marks_max']]
478
+
479
+ if 'question_type' in filters and filters['question_type']:
480
+ matching = [q for q in matching if q.get('question_type') == filters['question_type']]
481
+
482
+ if 'difficulty' in filters and filters['difficulty']:
483
+ matching = [q for q in matching if q.get('difficulty') == filters['difficulty']]
484
+
485
+ if 'requires_insert' in filters:
486
+ matching = [q for q in matching if q.get('requires_insert') == filters['requires_insert']]
487
+
488
+ if not matching:
489
+ return f"πŸ” No questions found matching your criteria."
490
+
491
+ # Group by paper
492
+ questions_by_paper = defaultdict(list)
493
+ for q in matching:
494
+ questions_by_paper[q['paper_title']].append(q)
495
+
496
+ # Format results
497
+ result = f"### 🎯 Found {len(matching)} question(s) from {len(questions_by_paper)} paper(s)\n\n"
498
+
499
+ for paper_title, questions in questions_by_paper.items():
500
+ result += f"#### πŸ“„ {paper_title}\n\n"
501
+
502
+ for q in questions:
503
+ insert_icon = "πŸ–ΌοΈ " if q.get('requires_insert') else ""
504
+ difficulty_icon = {"easy": "βœ…", "medium": "⚑", "hard": "πŸ”₯"}.get(q.get('difficulty', 'medium'), "⚑")
505
+
506
+ result += f"""**{q['question_number']}** | {insert_icon}{difficulty_icon} {q['marks']} marks | {q.get('question_type', 'knowledge').title()}
507
+ πŸ“Œ Topic: **{q['topic']}**
508
+ {f"πŸ”Έ Subtopics: {', '.join(q.get('subtopics', []))}" if q.get('subtopics') else ""}
509
+
510
+ {q['question_text']}
511
+
512
+ {f"πŸ’‘ Key concepts: {', '.join(q.get('key_concepts', []))}" if q.get('key_concepts') else ""}
513
+ {f"πŸ“ Command words: {', '.join(q.get('command_words', []))}" if q.get('command_words') else ""}
514
+
515
+ {'─'*80}
516
+ """
517
+
518
+ result += "\n"
519
+
520
+ return result
521
+
522
+ # ---------- 8. Question Analytics ----------
523
+ def analyze_question_bank(subject=None):
524
+ """
525
+ Provide analytics on available questions
526
+ """
527
+ questions = questions_index if not subject else [q for q in questions_index if q['subject'] == subject]
528
+
529
+ if not questions:
530
+ return "No questions available for analysis."
531
+
532
+ # Topic distribution
533
+ topic_counts = defaultdict(int)
534
+ for q in questions:
535
+ topic_counts[q['topic']] += 1
536
+
537
+ # Difficulty distribution
538
+ difficulty_counts = defaultdict(int)
539
+ for q in questions:
540
+ difficulty_counts[q.get('difficulty', 'medium')] += 1
541
+
542
+ # Question type distribution
543
+ type_counts = defaultdict(int)
544
+ for q in questions:
545
+ type_counts[q.get('question_type', 'knowledge')] += 1
546
+
547
+ # Year distribution
548
+ year_counts = defaultdict(int)
549
+ for q in questions:
550
+ year_counts[q.get('year', 'Unknown')] += 1
551
+
552
+ result = f"""### πŸ“Š Question Bank Analytics
553
+ **Total Questions:** {len(questions)}
554
+ **Subject:** {subject or 'All'}
555
+
556
+ #### πŸ“š Topics Coverage
557
+ """
558
+
559
+ for topic, count in sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)[:10]:
560
+ result += f"- {topic}: {count} questions\n"
561
+
562
+ result += f"\n#### 🎯 Difficulty Distribution\n"
563
+ for diff, count in difficulty_counts.items():
564
+ result += f"- {diff.title()}: {count} questions\n"
565
+
566
+ result += f"\n#### πŸ“ Question Types\n"
567
+ for qtype, count in type_counts.items():
568
+ result += f"- {qtype.title()}: {count} questions\n"
569
+
570
+ result += f"\n#### πŸ“… Year Coverage\n"
571
+ for year, count in sorted(year_counts.items(), reverse=True):
572
+ result += f"- {year}: {count} questions\n"
573
+
574
+ return result
575
+
576
+ # ---------- 9. AI Tutor ----------
577
  def ai_tutor_chat(message, history, subject, topic):
578
  if not message.strip():
579
  return history
 
613
  def clear_chat():
614
  return []
615
 
616
+ # ---------- 10. Concept Explainer ----------
617
  def explain_concept(subject, concept):
618
  if not concept:
619
  return "Enter a concept to explain!"
 
638
 
639
  return response
640
 
641
+ # ---------- 11. Problem Solver ----------
642
  def solve_problem(subject, problem, show_steps):
643
  if not problem.strip():
644
  return "Enter a problem!"
 
664
 
665
  return response
666
 
667
+ # ---------- 12. Scenario Analyzer ----------
668
  def analyze_scenario(subject, scenario_description, question):
669
  if not scenario_description.strip():
670
  return "Describe the scenario!"
 
694
 
695
  return response
696
 
697
+ # ---------- 13. Practice Questions ----------
698
  def generate_question(subject, topic, difficulty):
699
  if not topic:
700
  return "Select a topic!", "", ""
 
774
  except:
775
  return response
776
 
777
+ # ---------- 14. Past Papers Browser ----------
778
  def search_questions_by_topic(subject, topic):
779
+ """Simple wrapper for backward compatibility"""
780
+ return search_questions_advanced(subject, topic, filters=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
 
782
  def view_papers_student(subject):
783
  filtered = [p for p in papers_storage if p["subject"] == subject]
 
803
 
804
  return result
805
 
806
+ # ---------- 15. Admin Functions ----------
807
  def verify_admin_password(password):
808
  if password == ADMIN_PASSWORD:
809
  return gr.update(visible=True), gr.update(visible=False), " Access granted!"
810
  return gr.update(visible=False), gr.update(visible=True), " Incorrect password!"
811
 
812
+ def upload_paper_enhanced(title, subject, content, pdf_file, insert_file):
813
+ """
814
+ Enhanced paper upload with comprehensive question extraction
815
+ """
816
  if not all([title, subject, content]):
817
+ return "⚠️ Fill all fields!", get_papers_list(), "πŸ“Š Waiting..."
818
 
819
  paper_id = len(papers_storage) + 1
820
 
 
825
  if pdf_text and not pdf_text.startswith("Error"):
826
  paper_details = identify_paper_details(pdf_text, pdf_file.name)
827
  pdf_content_storage[paper_id] = pdf_text
828
+ content += f"\n[πŸ“„ PDF: {len(pdf_text)} chars]"
829
 
830
  insert_data = None
831
  insert_type = None
 
833
  insert_data, insert_type = process_insert_file(insert_file)
834
  if insert_data:
835
  insert_storage[paper_id] = (insert_data, insert_type)
836
+ content += f"\n[πŸ–ΌοΈ Insert: {insert_type}]"
837
 
838
  papers_storage.append({
839
  "id": paper_id,
 
846
  "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
847
  })
848
 
849
+ status_msg = "βœ… Paper uploaded!"
850
  if pdf_text and not pdf_text.startswith("Error"):
851
+ status_msg += "\nπŸ”„ Extracting and analyzing questions..."
852
+
853
+ # Use enhanced extraction
854
+ questions = extract_questions_comprehensive(pdf_text, paper_id, title, subject, paper_details)
855
  questions_index.extend(questions)
856
+
857
+ status_msg += f"\n✨ Extracted {len(questions)} questions!"
858
+ status_msg += f"\nπŸ“Š Questions analyzed and indexed for search"
859
+
860
+ # Show topic breakdown
861
+ topic_counts = defaultdict(int)
862
+ for q in questions:
863
+ topic_counts[q['topic']] += 1
864
+
865
+ if topic_counts:
866
+ status_msg += f"\n\nπŸ“š Topics covered:"
867
+ for topic, count in sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)[:5]:
868
+ status_msg += f"\n β€’ {topic}: {count} question(s)"
869
+
870
+ stats = f"πŸ“Š Papers: {len(papers_storage)} | Questions: {len(questions_index)}"
871
 
872
+ return status_msg, get_papers_list(), stats
873
 
874
  def get_papers_list():
875
  if not papers_storage:
 
885
 
886
  return "\n".join(result)
887
 
888
+ # ---------- 16. Gradio UI ----------
889
  with gr.Blocks(theme=gr.themes.Soft(), title="IGCSE ICT & PE Platform") as app:
890
  gr.Markdown("""
891
  # IGCSE Learning Platform
 
949
 
950
  gr.Button(" Analyze", variant="primary").click(analyze_scenario, [scen_subj, scen_desc, scen_q], scen_output)
951
 
952
+ with gr.Tab("πŸ“š Past Papers"):
953
+ gr.Markdown("### πŸ” Advanced Question Search")
954
+ gr.Markdown("*Search through extracted questions by topic, year, difficulty, and more*")
955
 
956
  with gr.Row():
957
+ with gr.Column(scale=2):
958
+ pp_subject = gr.Radio(
959
+ ["ICT", "Physical Education"],
960
+ label="Subject",
961
+ value="ICT"
962
+ )
963
+
964
+ pp_search = gr.Textbox(
965
+ label="πŸ”Ž Search Term",
966
+ placeholder="Enter topic, keyword, or concept (e.g., 'RAM', 'aerobic system')",
967
+ info="Search in topics, question text, and key concepts"
968
+ )
969
+
970
+ with gr.Column(scale=3):
971
+ with gr.Row():
972
+ pp_year = gr.Dropdown(
973
+ choices=["All"] + [str(y) for y in range(2015, 2026)],
974
+ label="πŸ“… Year",
975
+ value="All"
976
+ )
977
+ pp_series = gr.Dropdown(
978
+ choices=["All", "June", "November", "March"],
979
+ label="πŸ“† Series",
980
+ value="All"
981
+ )
982
+
983
+ with gr.Row():
984
+ pp_difficulty = gr.Dropdown(
985
+ choices=["All", "easy", "medium", "hard"],
986
+ label="🎯 Difficulty",
987
+ value="All"
988
+ )
989
+ pp_type = gr.Dropdown(
990
+ choices=["All", "knowledge", "application", "analysis", "evaluation"],
991
+ label="πŸ“ Question Type",
992
+ value="All"
993
+ )
994
+
995
+ with gr.Row():
996
+ pp_marks_min = gr.Number(
997
+ label="Min Marks",
998
+ value=0,
999
+ precision=0
1000
+ )
1001
+ pp_marks_max = gr.Number(
1002
+ label="Max Marks",
1003
+ value=20,
1004
+ precision=0
1005
+ )
1006
+ pp_insert = gr.Checkbox(
1007
+ label="πŸ–ΌοΈ Requires Insert Only",
1008
+ value=False
1009
+ )
1010
 
1011
+ with gr.Row():
1012
+ search_btn = gr.Button("πŸ” Search Questions", variant="primary", size="lg")
1013
+ analytics_btn = gr.Button("πŸ“Š Show Analytics", variant="secondary")
1014
 
1015
+ questions_output = gr.Markdown(
1016
+ value="πŸ‘† Enter search criteria and click Search",
1017
+ label="Search Results"
1018
+ )
1019
 
1020
+ # Search function wrapper
1021
+ def perform_search(subject, search_term, year, series, difficulty, qtype, marks_min, marks_max, requires_insert):
1022
+ filters = {}
1023
+
1024
+ if year != "All":
1025
+ filters['year'] = year
1026
+ if series != "All":
1027
+ filters['series'] = series
1028
+ if difficulty != "All":
1029
+ filters['difficulty'] = difficulty
1030
+ if qtype != "All":
1031
+ filters['question_type'] = qtype
1032
+ if marks_min > 0:
1033
+ filters['marks_min'] = int(marks_min)
1034
+ if marks_max < 20:
1035
+ filters['marks_max'] = int(marks_max)
1036
+ if requires_insert:
1037
+ filters['requires_insert'] = True
1038
+
1039
+ return search_questions_advanced(subject, search_term, filters)
1040
+
1041
+ search_btn.click(
1042
+ perform_search,
1043
+ [pp_subject, pp_search, pp_year, pp_series, pp_difficulty, pp_type, pp_marks_min, pp_marks_max, pp_insert],
1044
+ questions_output
1045
+ )
1046
 
1047
+ analytics_btn.click(
1048
+ analyze_question_bank,
1049
+ pp_subject,
1050
+ questions_output
1051
+ )
1052
+
1053
+ gr.Markdown("---")
1054
+ gr.Markdown("### πŸ“– Quick Topic Search")
1055
+ gr.Markdown("*Click a topic to see all related questions*")
1056
+
1057
+ with gr.Row():
1058
+ quick_subject = gr.Radio(
1059
+ ["ICT", "Physical Education"],
1060
+ label="Subject",
1061
+ value="ICT"
1062
+ )
1063
+ quick_topic = gr.Dropdown(
1064
+ ict_topics,
1065
+ label="Topic"
1066
+ )
1067
+
1068
+ quick_subject.change(
1069
+ lambda s: gr.Dropdown(choices=ict_topics if s == "ICT" else pe_topics, value=None),
1070
+ quick_subject,
1071
+ quick_topic
1072
+ )
1073
+
1074
+ quick_search_btn = gr.Button("⚑ Quick Search", variant="primary")
1075
+ quick_output = gr.Markdown()
1076
+
1077
+ def quick_topic_search(subject, topic):
1078
+ if not topic:
1079
+ return "⚠️ Please select a topic"
1080
+ return search_questions_advanced(subject, topic, None)
1081
+
1082
+ quick_search_btn.click(
1083
+ quick_topic_search,
1084
+ [quick_subject, quick_topic],
1085
+ quick_output
1086
+ )
1087
+
1088
+ gr.Markdown("---")
1089
+ gr.Markdown("### πŸ“š Browse All Papers")
1090
+
1091
+ browse_subject = gr.Radio(
1092
+ ["ICT", "Physical Education"],
1093
+ label="Subject",
1094
+ value="ICT"
1095
+ )
1096
  papers_display = gr.Markdown()
1097
+
1098
+ gr.Button("πŸ“„ Show All Papers").click(
1099
+ view_papers_student,
1100
+ browse_subject,
1101
+ papers_display
1102
+ )
1103
+
1104
+ gr.Markdown("""
1105
+ ---
1106
+ **πŸ’‘ Search Tips:**
1107
+ - Use specific keywords for better results (e.g., "binary conversion" instead of "numbers")
1108
+ - Filter by difficulty to find practice questions at your level
1109
+ - Use "Requires Insert" filter to find questions with diagrams
1110
+ - Check analytics to see topic coverage in your question bank
1111
+ """)
1112
 
1113
  with gr.Tab(" Practice"):
1114
  gr.Markdown("### Generate & Practice Questions")
 
1156
  gr.Markdown("### Uploaded Papers")
1157
  lst = gr.Textbox(lines=26, value=get_papers_list(), interactive=False)
1158
 
1159
+ up.click(upload_paper_enhanced, [t, s, c, pdf, insert], [st, lst, stats])
1160
 
1161
  login_btn.click(verify_admin_password, [pwd], [admin_section, login_section, login_status])
1162