YazeedBinShihah commited on
Commit
af49acd
·
verified ·
1 Parent(s): 74a883e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -978
app.py CHANGED
@@ -1,978 +1,32 @@
1
- import gradio as gr
2
- import os
3
- import sys
4
- import json
5
- import re
6
-
7
- # Ensure the current directory is in the path
8
- current_dir = os.path.dirname(os.path.abspath(__file__))
9
- sys.path.append(current_dir)
10
-
11
- from smart_tutor_core import crew
12
-
13
- # ----------------------------------------------------------------------
14
- # Helper: Parse Output
15
- # ----------------------------------------------------------------------
16
-
17
-
18
- def parse_agent_output(raw_output: str):
19
- """
20
- Tries to parse JSON from the raw string output.
21
- Returns (data_dict, is_json).
22
- """
23
- data = None
24
- try:
25
- data = json.loads(raw_output)
26
- return data, True
27
- except json.JSONDecodeError:
28
- # Try finding JSON block
29
- match = re.search(r"(\{.*\})", raw_output, re.DOTALL)
30
- if match:
31
- try:
32
- data = json.loads(match.group(1))
33
- return data, True
34
- except:
35
- pass
36
- return raw_output, False
37
-
38
-
39
- def clean_text(text: str) -> str:
40
- """
41
- Aggressively removes markdown formatting to ensure clean text display.
42
- Removes: **bold**, __bold__, *italic*, _italic_, `code`
43
- """
44
- if not text:
45
- return ""
46
- text = str(text)
47
- # Remove bold/italic markers
48
- text = re.sub(r"\*\*|__|`", "", text)
49
- text = re.sub(r"^\s*\*\s+", "", text) # Remove leading list asterisks if any
50
- return text.strip()
51
-
52
-
53
- # ----------------------------------------------------------------------
54
- # Helper: Format Text Output for Display
55
- # ----------------------------------------------------------------------
56
-
57
-
58
- def format_text_output(raw_text):
59
- """
60
- Converts raw agent text (markdown-ish) into
61
- beautifully styled HTML inside a summary-box.
62
- """
63
- if not raw_text:
64
- return ""
65
- text = str(raw_text).strip()
66
-
67
- # Convert markdown headings to HTML
68
- text = re.sub(r"^### (.+)$", r"<h3>\1</h3>", text, flags=re.MULTILINE)
69
- text = re.sub(r"^## (.+)$", r"<h2>\1</h2>", text, flags=re.MULTILINE)
70
- text = re.sub(r"^# (.+)$", r"<h2>\1</h2>", text, flags=re.MULTILINE)
71
-
72
- # Convert **bold** to <strong>
73
- text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
74
-
75
- # Convert bullet lists (- item or * item)
76
- lines = text.split("\n")
77
- result = []
78
- in_list = False
79
-
80
- for line in lines:
81
- stripped = line.strip()
82
- is_bullet = (
83
- stripped.startswith("- ")
84
- or stripped.startswith("* ")
85
- or re.match(r"^\d+\.\s", stripped)
86
- )
87
-
88
- if is_bullet:
89
- if not in_list:
90
- tag = "ul" # Always use bullets as requested
91
- result.append(f"<{tag}>")
92
- in_list = tag
93
- # Remove both -/* and 1. from the start of the line
94
- content = re.sub(r"^[-*]\s+|^\d+\.\s+", "", stripped)
95
- result.append(f"<li>{content}</li>")
96
- else:
97
- if in_list:
98
- result.append(f"</{in_list}>")
99
- in_list = False
100
- if stripped.startswith("<h"):
101
- result.append(stripped)
102
- elif stripped:
103
- result.append(f"<p>{stripped}</p>")
104
-
105
- if in_list:
106
- result.append(f"</{in_list}>")
107
-
108
- html = "\n".join(result)
109
- return f"<div class='summary-box'>{html}</div>"
110
-
111
-
112
- # ----------------------------------------------------------------------
113
- # Logic: Run Agent
114
- # ----------------------------------------------------------------------
115
-
116
-
117
- def run_agent(file, user_text):
118
- if not user_text and not file:
119
- return (
120
- gr.update(
121
- visible=True,
122
- value="<div class='error-box'>⚠️ Please enter a request or upload a file.</div>",
123
- ),
124
- gr.update(visible=False), # Quiz Group
125
- None, # State
126
- )
127
-
128
- full_request = user_text
129
-
130
- # Check if user wants a quiz but didn't upload a file (common error)
131
- if "quiz" in user_text.lower() and not file:
132
- return (
133
- gr.update(
134
- visible=True,
135
- value="<div class='error-box'>⚠️ To generate a quiz, please upload a document first.</div>",
136
- ),
137
- gr.update(visible=False),
138
- None,
139
- )
140
-
141
- if file:
142
- # file is a filepath string because type='filepath'
143
- full_request = f"""USER REQUEST: {user_text}
144
-
145
- IMPORTANT: The file to process is located at this EXACT path:
146
- {file}
147
-
148
- You MUST use this exact path when calling tools (process_file, store_quiz, etc.)."""
149
-
150
- # SYSTEM PROMPT INJECTION to force JSON format from the agent
151
- system_instruction = "\n\n(SYSTEM NOTE: If generating a quiz, you MUST call the store_quiz tool and return its VALID JSON output including 'quiz_id'. Do NOT return just the questions text.)"
152
-
153
- try:
154
- inputs = {"user_request": full_request + system_instruction}
155
- result = crew.kickoff(inputs=inputs)
156
- raw_output = str(result)
157
-
158
- print(f"\n{'='*60}")
159
- print(f"[DEBUG] raw_output (first 500 chars):")
160
- print(raw_output[:500])
161
- print(f"{'='*60}")
162
-
163
- data, is_json = parse_agent_output(raw_output)
164
-
165
- print(f"[DEBUG] is_json={is_json}")
166
- if is_json:
167
- print(
168
- f"[DEBUG] keys={list(data.keys()) if isinstance(data, dict) else 'not a dict'}"
169
- )
170
- if isinstance(data, dict) and "questions" in data:
171
- print(f"[DEBUG] num questions={len(data['questions'])}")
172
-
173
- # Case 1: Quiz Output (Success)
174
- if is_json and "questions" in data:
175
- # We accept it even if quiz_id is missing, but grading might fail.
176
- return (
177
- gr.update(visible=False), # Hide Summary
178
- gr.update(visible=True), # Show Quiz
179
- data, # Store Data
180
- )
181
-
182
- # Case 2: Grade Result (Standard JSON from grade_quiz) - Handled nicely
183
- if is_json and "score" in data:
184
- markdown = format_grade_result(data)
185
- return (
186
- gr.update(visible=True, value=markdown),
187
- gr.update(visible=False),
188
- None,
189
- )
190
-
191
- # Case 3: Normal Text / Summary / Explanation
192
- html_content = format_text_output(raw_output)
193
- return (
194
- gr.update(visible=True, value=html_content),
195
- gr.update(visible=False),
196
- None,
197
- )
198
-
199
- except Exception as e:
200
- error_msg = f"<div class='error-box'>❌ Error: {str(e)}</div>"
201
- return (
202
- gr.update(visible=True, value=error_msg),
203
- gr.update(visible=False),
204
- None,
205
- )
206
-
207
-
208
- # ----------------------------------------------------------------------
209
- # Logic: Quiz Render & Grading
210
- # ----------------------------------------------------------------------
211
-
212
-
213
- def render_quiz(quiz_data):
214
- """
215
- Renders the quiz questions dynamically.
216
- Returns updates for: [Radios x10] + [Feedbacks x10] + [CheckBtn] (Total 21)
217
- """
218
- updates = []
219
-
220
- if not quiz_data:
221
- # Hide everything
222
- return [gr.update(visible=False)] * 21
223
-
224
- questions = quiz_data.get("questions", [])
225
-
226
- # 1. Update Radios (10 slots)
227
- for i in range(10):
228
- if i < len(questions):
229
- q = questions[i]
230
- q_txt = clean_text(q.get("question", "Question text missing"))
231
- question_text = f"{i+1}. {q_txt}"
232
-
233
- # Ensure options are a dict and sorted
234
- raw_options = q.get("options", {})
235
- if not isinstance(raw_options, dict):
236
- # Fallback if options came as a list or string
237
- raw_options = {"A": "Error loading options"}
238
-
239
- # Sort by key A, B, C, D...
240
- # We strictly enforce the "Key. Value" format
241
- choices = []
242
- for key in sorted(raw_options.keys()):
243
- val = clean_text(raw_options[key])
244
- choices.append(f"{key}. {val}")
245
-
246
- updates.append(
247
- gr.update(
248
- visible=True,
249
- label=question_text,
250
- choices=choices,
251
- value=None,
252
- interactive=True,
253
- )
254
- )
255
- else:
256
- updates.append(gr.update(visible=False, choices=[], value=None))
257
-
258
- # 2. Update Feedbacks (10 slots) - Hide them initially
259
- for i in range(10):
260
- updates.append(gr.update(visible=False, value=""))
261
-
262
- # 3. Show Grid/Check Button
263
- updates.append(gr.update(visible=True))
264
-
265
- return updates
266
-
267
-
268
- def grade_quiz_ui(quiz_data, *args):
269
- """
270
- Collects answers, calls agent (or tool), and returns graded results designed for UI.
271
- Input args: [Radio1_Val, Radio2_Val, ..., Radio10_Val] (Length 10)
272
- Output: [Radios x10] + [Feedbacks x10] + [ResultMsg] (Total 21)
273
- """
274
- # args tuple contains the values of the 10 radios
275
- answers_list = args[0:10]
276
-
277
- updates = []
278
-
279
- # Validation
280
- if not quiz_data or "quiz_id" not in quiz_data:
281
- # Fallback if ID is missing
282
- error_updates = [gr.update()] * 10 + [gr.update()] * 10
283
- error_updates.append(
284
- gr.update(
285
- visible=True,
286
- value="<div class='error-box'>⚠️ Error: Quiz ID not found. Cannot grade this quiz.</div>",
287
- )
288
- )
289
- return error_updates
290
-
291
- quiz_id = quiz_data["quiz_id"]
292
-
293
- # Construct answer map {"1": "A", ...}
294
- user_answers = {}
295
- for i, ans in enumerate(answers_list):
296
- if ans:
297
- # ans is like "A. Option Text" -> extract "A"
298
- selected_opt = ans.split(".")[0]
299
- # Use qid from data if available, else i+1
300
- qid = str(i + 1)
301
- # Try to match qid from quiz_data if possible
302
- if i < len(quiz_data.get("questions", [])):
303
- q = quiz_data["questions"][i]
304
- qid = str(q.get("qid", i + 1))
305
-
306
- user_answers[qid] = selected_opt
307
-
308
- # Construct the JSON for the agent
309
- answers_json = json.dumps(user_answers)
310
- grading_request = f"Grade quiz {quiz_id} with answers {answers_json}\n(SYSTEM: Return valid JSON matching GradeQuizResult schema.)"
311
-
312
- try:
313
- inputs = {"user_request": grading_request}
314
- result = crew.kickoff(inputs=inputs)
315
- raw_output = str(result)
316
- data, is_json = parse_agent_output(raw_output)
317
-
318
- if is_json and "score" in data:
319
- return format_grade_result_interactive(data, answers_list)
320
- else:
321
- # Fallback error in result box
322
- error_updates = [gr.update()] * 10 + [gr.update()] * 10
323
- error_updates.append(
324
- gr.update(
325
- visible=True,
326
- value=f"<div class='error-box'>Error parsing grading result: {raw_output}</div>",
327
- )
328
- )
329
- return error_updates
330
-
331
- except Exception as e:
332
- error_updates = [gr.update()] * 10 + [gr.update()] * 10
333
- error_updates.append(
334
- gr.update(
335
- visible=True, value=f"<div class='error-box'>Error: {str(e)}</div>"
336
- )
337
- )
338
- return error_updates
339
-
340
-
341
- def format_grade_result_interactive(data, user_answers_list):
342
- """
343
- Updates the UI with colors and correctness.
344
- Returns 21 updates.
345
- """
346
- details = data.get("details", [])
347
- # Map details by QID or index for safety
348
- details_map = {}
349
- for det in details:
350
- details_map[str(det.get("qid"))] = det
351
-
352
- radio_updates = []
353
- feedback_updates = []
354
-
355
- # Iterate 10 slots
356
- for i in range(10):
357
- # Find corresponding detail
358
- # We assume strict ordering i=0 -> Q1
359
- # But let's try to be smart with QID if possible
360
- qid = (
361
- str(data.get("details", [])[i].get("qid"))
362
- if i < len(data.get("details", []))
363
- else str(i + 1)
364
- )
365
- det = details_map.get(qid)
366
-
367
- if det:
368
- # Clean feedback text
369
- correct_raw = det.get("correct_answer", "?")
370
- correct = clean_text(correct_raw)
371
-
372
- explanation_raw = det.get("explanation", "")
373
- explanation = clean_text(explanation_raw)
374
-
375
- is_correct = det.get("is_correct", False)
376
-
377
- # 1. Lock Radio
378
- radio_updates.append(gr.update(interactive=False))
379
-
380
- # 2. Show Feedback Box
381
- css_class = (
382
- "feedback-box-correct" if is_correct else "feedback-box-incorrect"
383
- )
384
-
385
- # Title
386
- title_text = "Correct Answer!" if is_correct else "Incorrect Answer."
387
- title_icon = "✅" if is_correct else "❌"
388
-
389
- html_content = f"""
390
- <div class='{css_class}'>
391
- <div class='feedback-header'>
392
- <span class='feedback-icon'>{title_icon}</span>
393
- <span class='feedback-title'>{title_text}</span>
394
- </div>
395
- <div class='feedback-body'>
396
- <div class='feedback-correct-answer'><strong>Correct Answer:</strong> {correct}</div>
397
- {'<div class="feedback-explanation"><strong>Explanation:</strong> ' + explanation + '</div>' if explanation else ''}
398
- </div>
399
- </div>
400
- """
401
-
402
- feedback_updates.append(gr.update(visible=True, value=html_content))
403
- else:
404
- # No detail (maybe question didn't exist)
405
- radio_updates.append(gr.update(visible=False))
406
- feedback_updates.append(gr.update(visible=False))
407
-
408
- # 3. Final Score Msg
409
- percentage = data.get("percentage", 0)
410
- emoji = "🏆" if percentage >= 80 else "📊"
411
-
412
- # Create a nice result card
413
- score_html = f"""
414
- <div class='result-card'>
415
- <div class='result-header'>{emoji} Quiz Completed!</div>
416
- <div class='result-score'>Your Score: {data.get('score')} / {data.get('total')}</div>
417
- <div class='result-percentage'>({percentage}%)</div>
418
- </div>
419
- """
420
-
421
- return (
422
- radio_updates + feedback_updates + [gr.update(visible=True, value=score_html)]
423
- )
424
-
425
-
426
- def format_grade_result(data):
427
- """Standard markdown formatter for standalone grade result"""
428
- score = data.get("percentage", 0)
429
- emoji = "🎉" if score > 70 else "📚"
430
- md = f"# {emoji} Score: {data.get('score')}/{data.get('total')}\n\n"
431
- for cx in data.get("details", []):
432
- md += f"- **Q{cx['qid']}**: {cx['is_correct'] and '✅' or '❌'} (Correct: {cx.get('correct_answer')})\n"
433
- return md
434
-
435
-
436
- # ----------------------------------------------------------------------
437
- # CSS Styling
438
- # ----------------------------------------------------------------------
439
-
440
- custom_css = """
441
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
442
-
443
- body {
444
- font-family: 'Poppins', sans-serif !important;
445
- background: #f8fafc; /* Lighter background */
446
- color: #334155;
447
- font-weight: 400; /* Regular weight by default */
448
- }
449
-
450
- .gradio-container {
451
- max-width: 900px !important;
452
- margin: 40px auto !important;
453
- background: #ffffff;
454
- border-radius: 24px;
455
- box-shadow: 0 20px 40px -10px rgba(0,0,0,0.1);
456
- padding: 0 !important;
457
- overflow: hidden;
458
- border: 1px solid rgba(255,255,255,0.8);
459
- }
460
-
461
- /* ================= HEADER ================= */
462
- .header-box {
463
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
464
- color: white;
465
- padding: 60px 40px;
466
- text-align: center;
467
- position: relative;
468
- overflow: hidden;
469
- margin-bottom: 30px;
470
- }
471
-
472
- .header-box::before {
473
- content: '';
474
- position: absolute;
475
- top: -50%;
476
- left: -50%;
477
- width: 200%;
478
- height: 200%;
479
- background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
480
- animation: rotate 20s linear infinite;
481
- }
482
-
483
- .header-box h1 {
484
- color: white !important;
485
- margin: 0;
486
- font-size: 3em !important;
487
- font-weight: 700;
488
- letter-spacing: -1px;
489
- text-shadow: 0 4px 10px rgba(0,0,0,0.2);
490
- position: relative;
491
- z-index: 1;
492
- }
493
-
494
- .header-box p {
495
- color: #e0e7ff !important;
496
- font-size: 1.25em !important;
497
- margin-top: 15px;
498
- font-weight: 300;
499
- position: relative;
500
- z-index: 1;
501
- }
502
-
503
- @keyframes rotate {
504
- from { transform: rotate(0deg); }
505
- to { transform: rotate(360deg); }
506
- }
507
-
508
- /* ================= INPUT PANEL ================= */
509
- .gradio-row {
510
- gap: 30px !important;
511
- padding: 0 40px 40px 40px;
512
- }
513
-
514
- /* Logic to remove padding from internal rows if needed, simplified here */
515
-
516
- /* Buttons */
517
- button.primary {
518
- background: linear-gradient(90deg, #4f46e5 0%, #6366f1 100%) !important;
519
- border: none !important;
520
- color: white !important;
521
- font-weight: 600 !important;
522
- padding: 12px 24px !important;
523
- border-radius: 12px !important;
524
- box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4) !important;
525
- transition: all 0.3s ease !important;
526
- }
527
-
528
- button.primary:hover {
529
- transform: translateY(-2px);
530
- box-shadow: 0 8px 25px rgba(79, 70, 229, 0.5) !important;
531
- }
532
-
533
- button.secondary {
534
- background: #f3f4f6 !important;
535
- color: #4b5563 !important;
536
- border: 1px solid #e5e7eb !important;
537
- border-radius: 12px !important;
538
- }
539
-
540
- button.secondary:hover {
541
- background: #e5e7eb !important;
542
- }
543
-
544
- /* ================= QUIZ CARDS ================= */
545
- .quiz-question {
546
- background: #ffffff;
547
- border-radius: 16px;
548
- padding: 25px;
549
- margin-bottom: 30px !important;
550
- border: 1px solid #e5e7eb;
551
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
552
- transition: transform 0.2s ease, box-shadow 0.2s ease;
553
- }
554
-
555
- .quiz-question:hover {
556
- transform: translateY(-2px);
557
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
558
- }
559
-
560
- .quiz-question span { /* Label/Title */
561
- font-size: 1.15em !important;
562
- font-weight: 600 !important;
563
- color: #111827;
564
- margin-bottom: 20px;
565
- display: block;
566
- line-height: 1.5;
567
- }
568
-
569
- /* Options Wrapper (The Radio Group) */
570
- .quiz-question .wrap {
571
- display: flex !important;
572
- flex-direction: column !important;
573
- gap: 12px !important;
574
- }
575
-
576
- /* Individual Option Label */
577
- .quiz-question .wrap label {
578
- display: flex !important;
579
- align-items: center !important;
580
- background: #f9fafb;
581
- border: 2px solid #e5e7eb !important; /* Thick border */
582
- padding: 15px 20px !important;
583
- border-radius: 12px !important;
584
- cursor: pointer;
585
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
586
- font-size: 1.05em;
587
- color: #4b5563;
588
- }
589
-
590
- .quiz-question .wrap label:hover {
591
- background: #f3f4f6;
592
- border-color: #6366f1 !important;
593
- color: #4f46e5;
594
- }
595
-
596
- .quiz-question .wrap label.selected {
597
- background: #eef2ff !important;
598
- border-color: #4f46e5 !important;
599
- color: #4338ca !important;
600
- font-weight: 600;
601
- box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1);
602
- }
603
-
604
- /* Hide default circle if possible, or style it.
605
- Gradio's radio inputs are tricky to hide fully without breaking accessibility,
606
- but we can style the container enough. */
607
-
608
- /* ================= RESULTS & FEEDBACK ================= */
609
-
610
- /* Success/Error Cards */
611
- .feedback-box-correct, .feedback-box-incorrect {
612
- margin-top: 20px;
613
- padding: 20px;
614
- border-radius: 12px;
615
- animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
616
- position: relative;
617
- overflow: hidden;
618
- }
619
-
620
- .feedback-box-correct {
621
- background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
622
- border: 1px solid #10b981;
623
- color: #065f46;
624
- }
625
-
626
- .feedback-box-incorrect {
627
- background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
628
- border: 1px solid #ef4444;
629
- color: #991b1b;
630
- }
631
-
632
- .feedback-header {
633
- display: flex;
634
- align-items: center;
635
- gap: 12px;
636
- margin-bottom: 12px;
637
- font-size: 1.2em;
638
- font-weight: 700;
639
- }
640
-
641
- .feedback-icon {
642
- font-size: 1.4em;
643
- background: rgba(255,255,255,0.5);
644
- border-radius: 50%;
645
- width: 32px;
646
- height: 32px;
647
- display: flex;
648
- align-items: center;
649
- justify-content: center;
650
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
651
- }
652
-
653
- .feedback-body {
654
- background: rgba(255,255,255,0.4);
655
- padding: 15px;
656
- border-radius: 8px;
657
- font-size: 1em;
658
- line-height: 1.6;
659
- }
660
-
661
- .feedback-correct-answer {
662
- font-weight: 600;
663
- margin-bottom: 8px;
664
- color: #064e3b; /* darker green */
665
- }
666
- .feedback-box-incorrect .feedback-correct-answer {
667
- color: #7f1d1d; /* darker red */
668
- }
669
-
670
- /* Summary / Explanation Box */
671
- .summary-box {
672
- background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
673
- border-radius: 20px;
674
- padding: 35px 40px;
675
- border: 1px solid #e0e7ff;
676
- box-shadow: 0 8px 30px rgba(79, 70, 229, 0.06);
677
- font-size: 1.05em;
678
- line-height: 1.9;
679
- color: #374151;
680
- position: relative;
681
- overflow: hidden;
682
- }
683
-
684
- .summary-box::before {
685
- content: '';
686
- position: absolute;
687
- top: 0;
688
- left: 0;
689
- right: 0;
690
- height: 4px;
691
- background: linear-gradient(90deg, #4f46e5, #7c3aed, #a78bfa);
692
- }
693
-
694
- .summary-box h2 {
695
- font-size: 1.4em;
696
- font-weight: 700;
697
- color: #312e81;
698
- margin: 0 0 18px 0;
699
- padding-bottom: 12px;
700
- border-bottom: 2px solid #e0e7ff;
701
- display: flex;
702
- align-items: center;
703
- gap: 10px;
704
- }
705
-
706
- .summary-box h3 {
707
- font-size: 1.15em;
708
- font-weight: 600;
709
- color: #4338ca;
710
- margin: 20px 0 10px 0;
711
- }
712
-
713
- .summary-box p {
714
- margin: 0 0 14px 0;
715
- text-align: justify;
716
- }
717
-
718
- .summary-box ul, .summary-box ol {
719
- margin: 10px 0 16px 0;
720
- padding-left: 24px;
721
- }
722
-
723
- .summary-box li {
724
- margin-bottom: 8px;
725
- position: relative;
726
- }
727
-
728
- .summary-box strong {
729
- color: #312e81;
730
- font-weight: 600;
731
- }
732
-
733
- .summary-box .summary-footer {
734
- margin-top: 20px;
735
- padding-top: 14px;
736
- border-top: 1px solid #e0e7ff;
737
- font-size: 0.85em;
738
- color: #9ca3af;
739
- text-align: left;
740
- }
741
-
742
- /* Example Buttons */
743
- #examples-container {
744
- margin: 15px 0;
745
- padding: 10px;
746
- background: #f3f4f6;
747
- border-radius: 12px;
748
- }
749
-
750
- .example-btn {
751
- background: #ffffff !important;
752
- border: 1px solid #e5e7eb !important;
753
- color: #6366f1 !important; /* Indigo text */
754
- font-size: 0.85em !important;
755
- padding: 2px 10px !important;
756
- border-radius: 20px !important; /* Pill shape */
757
- transition: all 0.2s ease !important;
758
- font-weight: 500 !important;
759
- box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
760
- }
761
-
762
- .example-btn:hover {
763
- background: #f5f7ff !important;
764
- border-color: #6366f1 !important;
765
- transform: translateY(-1px);
766
- box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.1) !important;
767
- }
768
-
769
- /* Result Card */
770
- .result-card {
771
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
772
- border-radius: 20px;
773
- padding: 40px;
774
- text-align: center;
775
- color: white;
776
- box-shadow: 0 20px 25px -5px rgba(79, 70, 229, 0.3);
777
- margin-top: 40px;
778
- animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
779
- }
780
-
781
- .result-header {
782
- font-size: 2em;
783
- font-weight: 800;
784
- margin-bottom: 15px;
785
- text-shadow: 0 2px 4px rgba(0,0,0,0.1);
786
- }
787
-
788
- .result-score {
789
- font-size: 3.5em;
790
- font-weight: 800;
791
- margin: 10px 0;
792
- background: -webkit-linear-gradient(#ffffff, #e0e7ff);
793
- -webkit-background-clip: text;
794
- -webkit-text-fill-color: transparent;
795
- }
796
-
797
- .result-percentage {
798
- font-size: 1.5em;
799
- opacity: 0.9;
800
- font-weight: 500;
801
- }
802
-
803
- /* Keyframes */
804
- @keyframes popIn {
805
- from { opacity: 0; transform: scale(0.95) translateY(-5px); }
806
- to { opacity: 1; transform: scale(1) translateY(0); }
807
- }
808
-
809
- @keyframes slideUp {
810
- from { opacity: 0; transform: translateY(40px); }
811
- to { opacity: 1; transform: translateY(0); }
812
- }
813
-
814
- /* Hide Gradio Footer */
815
- footer { display: none !important; }
816
- .gradio-container .prose.footer-content { display: none !important; }
817
- """
818
-
819
- # ----------------------------------------------------------------------
820
- # Main App
821
- # ----------------------------------------------------------------------
822
-
823
- with gr.Blocks(css=custom_css, title="SmartTutor AI") as demo:
824
-
825
- # State
826
- quiz_state = gr.State()
827
-
828
- with gr.Column(elem_classes="header-box"):
829
- gr.HTML(
830
- """
831
- <div style='color: white;'>
832
- <h1 style='color: white; font-size: 3em; margin: 0;'>🧠 SmartTutor AI</h1>
833
- <p style='color: #e0e7ff; font-size: 1.25em;'>Your intelligent companion for learning and assessment</p>
834
- </div>
835
- """
836
- )
837
-
838
- with gr.Row():
839
- # Left Panel: Controls
840
- with gr.Column(scale=1, variant="panel"):
841
- file_input = gr.File(
842
- label="📄 Upload Document", file_types=[".pdf", ".txt"], type="filepath"
843
- )
844
- user_input = gr.Textbox(
845
- label="✍️ Request",
846
- placeholder="e.g. 'Summarize this' or 'Create a quiz'",
847
- lines=3,
848
- )
849
-
850
- # Quick Examples
851
- with gr.Column(elem_id="examples-container"):
852
- gr.Markdown("✨ **Quick Actions:**")
853
- with gr.Row():
854
- ex_summarize = gr.Button(
855
- "📝 Summary (3 lines)", size="sm", elem_classes="example-btn"
856
- )
857
- ex_quiz = gr.Button(
858
- "🧪 3 Questions", size="sm", elem_classes="example-btn"
859
- )
860
- with gr.Row():
861
- ex_explain = gr.Button(
862
- "💡 Main Concepts", size="sm", elem_classes="example-btn"
863
- )
864
-
865
- with gr.Row():
866
- submit_btn = gr.Button("🚀 Run", variant="primary")
867
- clear_btn = gr.Button("🧹 Clear")
868
-
869
- # Right Panel: Results
870
- with gr.Column(scale=2):
871
-
872
- # 1. Summary / Text Output
873
- summary_output = gr.HTML(visible=True)
874
-
875
- # 2. Quiz Group (Hidden initially)
876
- with gr.Group(visible=False) as quiz_group:
877
- gr.Markdown("## 📝 Quiz Time")
878
- gr.Markdown("Select the correct answer for each question.")
879
-
880
- # Create 10 Questions + Feedback slots
881
- q_radios = []
882
- q_feedbacks = []
883
-
884
- for i in range(10):
885
- # Radio
886
- r = gr.Radio(
887
- label=f"Question {i+1}",
888
- visible=False,
889
- elem_classes="quiz-question",
890
- )
891
- q_radios.append(r)
892
-
893
- # Feedback (Markdown/HTML)
894
- fb = gr.HTML(visible=False)
895
- q_feedbacks.append(fb)
896
-
897
- check_btn = gr.Button(
898
- "✅ Check Answers", variant="primary", visible=False
899
- )
900
-
901
- # Final Result Message
902
- quiz_result_msg = gr.Markdown(visible=False)
903
-
904
- # ------------------------------------------------------------------
905
- # Events
906
- # ------------------------------------------------------------------
907
-
908
- # 1. Run Agent
909
- # Returns: [Summary, QuizGroup, QuizState]
910
- submit_btn.click(
911
- fn=run_agent,
912
- inputs=[file_input, user_input],
913
- outputs=[summary_output, quiz_group, quiz_state],
914
- ).success(
915
- # On success, update the quiz UI components (21 items)
916
- fn=render_quiz,
917
- inputs=[quiz_state],
918
- outputs=q_radios + q_feedbacks + [check_btn],
919
- )
920
-
921
- # Example Buttons Handling
922
- # These will ONLY fill the text box. User must click 'Run' manually.
923
-
924
- ex_summarize.click(
925
- fn=lambda: "Summarize this document strictly in exactly 3 lines.",
926
- outputs=[user_input],
927
- )
928
-
929
- ex_quiz.click(
930
- fn=lambda: "Generate a quiz with exactly 3 multiple-choice questions.",
931
- outputs=[user_input],
932
- )
933
-
934
- ex_explain.click(
935
- fn=lambda: "Explain the 5 most important core concepts in this document clearly.",
936
- outputs=[user_input],
937
- )
938
-
939
- # 2. Check Answers
940
- # Inputs: State + 10 Radios
941
- # Outputs: 10 Radios (Lock) + 10 Feedbacks (Show) + ResultMsg
942
- check_btn.click(
943
- fn=grade_quiz_ui,
944
- inputs=[quiz_state] + q_radios,
945
- outputs=q_radios + q_feedbacks + [quiz_result_msg],
946
- )
947
-
948
- # 3. Clear
949
- def reset_ui():
950
- # Reset everything to default
951
- updates = [
952
- gr.update(value=None, interactive=True, visible=False)
953
- ] * 10 # Radios
954
- fb_updates = [gr.update(value="", visible=False)] * 10 # Feedbacks
955
- return (
956
- None,
957
- "", # Inputs
958
- gr.update(value="", visible=True), # Summary
959
- gr.update(visible=False), # Quiz Group
960
- None, # State
961
- *updates,
962
- *fb_updates, # Radios + Feedbacks
963
- gr.update(visible=False), # CheckBtn
964
- gr.update(visible=False), # ResultMsg
965
- )
966
-
967
- clear_btn.click(
968
- fn=reset_ui,
969
- inputs=[],
970
- outputs=[file_input, user_input, summary_output, quiz_group, quiz_state]
971
- + q_radios
972
- + q_feedbacks
973
- + [check_btn, quiz_result_msg],
974
- )
975
-
976
- if __name__ == "__main__":
977
- print("Starting SmartTutor AI...")
978
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.12-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Create a non-root user for security and HF compatibility
13
+ RUN useradd -m -u 1000 user
14
+ USER user
15
+ ENV PATH="/home/user/.local/bin:${PATH}"
16
+
17
+ # Copy requirements first for better caching
18
+ COPY --chown=user requirements.txt .
19
+ RUN pip install --no-cache-dir --user -r requirements.txt
20
+
21
+ # Copy the rest of the application
22
+ COPY --chown=user . .
23
+
24
+ # Gradio Environment Variables
25
+ ENV GRADIO_SERVER_NAME="0.0.0.0" \
26
+ GRADIO_SERVER_PORT=7860 \
27
+ PYTHONUNBUFFERED=1
28
+
29
+ EXPOSE 7860
30
+
31
+ # Run the application
32
+ CMD ["python", "app.py"]