YazeedBinShihah commited on
Commit
ee8ca3d
Β·
verified Β·
1 Parent(s): 050d369

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +977 -24
app.py CHANGED
@@ -1,32 +1,985 @@
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"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(visible=True)] * 10 + [gr.update(visible=False)] * 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(visible=True)] * 10 + [
323
+ gr.update(visible=False)
324
+ ] * 10
325
+ error_updates.append(
326
+ gr.update(
327
+ visible=True,
328
+ value=f"<div class='error-box'>Error parsing grading result: {raw_output}</div>",
329
+ )
330
+ )
331
+ return error_updates
332
+
333
+ except Exception as e:
334
+ error_updates = [gr.update(visible=True)] * 10 + [gr.update(visible=False)] * 10
335
+ error_updates.append(
336
+ gr.update(
337
+ visible=True, value=f"<div class='error-box'>Error: {str(e)}</div>"
338
+ )
339
+ )
340
+ return error_updates
341
+
342
+
343
+ def format_grade_result_interactive(data, user_answers_list):
344
+ """
345
+ Updates the UI with colors and correctness.
346
+ Returns 21 updates.
347
+ """
348
+ details = data.get("details", [])
349
+ # Map details by QID or index for safety
350
+ details_map = {}
351
+ for det in details:
352
+ details_map[str(det.get("qid"))] = det
353
+
354
+ radio_updates = []
355
+ feedback_updates = []
356
+
357
+ # Iterate 10 slots
358
+ for i in range(10):
359
+ # Find corresponding detail
360
+ # We assume strict ordering i=0 -> Q1
361
+ # But let's try to be smart with QID if possible
362
+ qid = (
363
+ str(data.get("details", [])[i].get("qid"))
364
+ if i < len(data.get("details", []))
365
+ else str(i + 1)
366
+ )
367
+ det = details_map.get(qid)
368
+
369
+ if det:
370
+ # Clean feedback text
371
+ correct_raw = det.get("correct_answer", "?")
372
+ correct = clean_text(correct_raw)
373
+
374
+ explanation_raw = det.get("explanation", "")
375
+ explanation = clean_text(explanation_raw)
376
+
377
+ is_correct = det.get("is_correct", False)
378
+
379
+ # 1. Lock Radio
380
+ radio_updates.append(gr.update(interactive=False))
381
+
382
+ # 2. Show Feedback Box
383
+ css_class = (
384
+ "feedback-box-correct" if is_correct else "feedback-box-incorrect"
385
+ )
386
+
387
+ # Title
388
+ title_text = "Correct Answer!" if is_correct else "Incorrect Answer."
389
+ title_icon = "βœ…" if is_correct else "❌"
390
+
391
+ html_content = f"""
392
+ <div class='{css_class}'>
393
+ <div class='feedback-header'>
394
+ <span class='feedback-icon'>{title_icon}</span>
395
+ <span class='feedback-title'>{title_text}</span>
396
+ </div>
397
+ <div class='feedback-body'>
398
+ <div class='feedback-correct-answer'><strong>Correct Answer:</strong> {correct}</div>
399
+ {'<div class="feedback-explanation"><strong>Explanation:</strong> ' + explanation + '</div>' if explanation else ''}
400
+ </div>
401
+ </div>
402
+ """
403
+
404
+ feedback_updates.append(gr.update(visible=True, value=html_content))
405
+ else:
406
+ # No detail (maybe question didn't exist)
407
+ radio_updates.append(gr.update(visible=False))
408
+ feedback_updates.append(gr.update(visible=False))
409
+
410
+ # 3. Final Score Msg
411
+ percentage = data.get("percentage", 0)
412
+ emoji = "πŸ†" if percentage >= 80 else "πŸ“Š"
413
+
414
+ # Create a nice result card
415
+ score_html = f"""
416
+ <div class='result-card'>
417
+ <div class='result-header'>{emoji} Quiz Completed!</div>
418
+ <div class='result-score'>Your Score: {data.get('score')} / {data.get('total')}</div>
419
+ <div class='result-percentage'>({percentage}%)</div>
420
+ </div>
421
+ """
422
+
423
+ return (
424
+ radio_updates + feedback_updates + [gr.update(visible=True, value=score_html)]
425
+ )
426
+
427
+
428
+ def format_grade_result(data):
429
+ """Standard markdown formatter for standalone grade result"""
430
+ score = data.get("percentage", 0)
431
+ emoji = "πŸŽ‰" if score > 70 else "πŸ“š"
432
+ md = f"# {emoji} Score: {data.get('score')}/{data.get('total')}\n\n"
433
+ for cx in data.get("details", []):
434
+ md += f"- **Q{cx['qid']}**: {cx['is_correct'] and 'βœ…' or '❌'} (Correct: {cx.get('correct_answer')})\n"
435
+ return md
436
+
437
+
438
+ # ----------------------------------------------------------------------
439
+ # CSS Styling
440
+ # ----------------------------------------------------------------------
441
+
442
+ custom_css = """
443
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
444
+
445
+ body {
446
+ font-family: 'Poppins', sans-serif !important;
447
+ background: #f8fafc; /* Lighter background */
448
+ color: #334155;
449
+ font-weight: 400; /* Regular weight by default */
450
+ }
451
+
452
+ .gradio-container {
453
+ max-width: 900px !important;
454
+ margin: 40px auto !important;
455
+ background: #ffffff;
456
+ border-radius: 24px;
457
+ box-shadow: 0 20px 40px -10px rgba(0,0,0,0.1);
458
+ padding: 0 !important;
459
+ overflow: hidden;
460
+ border: 1px solid rgba(255,255,255,0.8);
461
+ }
462
+
463
+ /* ================= HEADER ================= */
464
+ .header-box {
465
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
466
+ color: white;
467
+ padding: 60px 40px;
468
+ text-align: center;
469
+ position: relative;
470
+ overflow: hidden;
471
+ margin-bottom: 30px;
472
+ }
473
+
474
+ .header-box::before {
475
+ content: '';
476
+ position: absolute;
477
+ top: -50%;
478
+ left: -50%;
479
+ width: 200%;
480
+ height: 200%;
481
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
482
+ animation: rotate 20s linear infinite;
483
+ }
484
+
485
+ .header-box h1 {
486
+ color: white !important;
487
+ margin: 0;
488
+ font-size: 3em !important;
489
+ font-weight: 700;
490
+ letter-spacing: -1px;
491
+ text-shadow: 0 4px 10px rgba(0,0,0,0.2);
492
+ position: relative;
493
+ z-index: 1;
494
+ }
495
+
496
+ .header-box p {
497
+ color: #e0e7ff !important;
498
+ font-size: 1.25em !important;
499
+ margin-top: 15px;
500
+ font-weight: 300;
501
+ position: relative;
502
+ z-index: 1;
503
+ }
504
+
505
+ @keyframes rotate {
506
+ from { transform: rotate(0deg); }
507
+ to { transform: rotate(360deg); }
508
+ }
509
+
510
+ /* ================= INPUT PANEL ================= */
511
+ .gradio-row {
512
+ gap: 30px !important;
513
+ padding: 0 40px 40px 40px;
514
+ }
515
+
516
+ /* Logic to remove padding from internal rows if needed, simplified here */
517
+
518
+ /* Buttons */
519
+ button.primary {
520
+ background: linear-gradient(90deg, #4f46e5 0%, #6366f1 100%) !important;
521
+ border: none !important;
522
+ color: white !important;
523
+ font-weight: 600 !important;
524
+ padding: 12px 24px !important;
525
+ border-radius: 12px !important;
526
+ box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4) !important;
527
+ transition: all 0.3s ease !important;
528
+ }
529
+
530
+ button.primary:hover {
531
+ transform: translateY(-2px);
532
+ box-shadow: 0 8px 25px rgba(79, 70, 229, 0.5) !important;
533
+ }
534
+
535
+ button.secondary {
536
+ background: #f3f4f6 !important;
537
+ color: #4b5563 !important;
538
+ border: 1px solid #e5e7eb !important;
539
+ border-radius: 12px !important;
540
+ }
541
+
542
+ button.secondary:hover {
543
+ background: #e5e7eb !important;
544
+ }
545
+
546
+ /* ================= QUIZ CARDS ================= */
547
+ .quiz-question {
548
+ background: #ffffff;
549
+ border-radius: 16px;
550
+ padding: 25px;
551
+ margin-bottom: 30px !important;
552
+ border: 1px solid #e5e7eb;
553
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
554
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
555
+ }
556
+
557
+ .quiz-question:hover {
558
+ transform: translateY(-2px);
559
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
560
+ }
561
+
562
+ .quiz-question span { /* Label/Title */
563
+ font-size: 1.15em !important;
564
+ font-weight: 600 !important;
565
+ color: #111827;
566
+ margin-bottom: 20px;
567
+ display: block;
568
+ line-height: 1.5;
569
+ }
570
+
571
+ /* Options Wrapper (The Radio Group) */
572
+ .quiz-question .wrap {
573
+ display: flex !important;
574
+ flex-direction: column !important;
575
+ gap: 12px !important;
576
+ }
577
+
578
+ /* Individual Option Label */
579
+ .quiz-question .wrap label {
580
+ display: flex !important;
581
+ align-items: center !important;
582
+ background: #f9fafb;
583
+ border: 2px solid #e5e7eb !important; /* Thick border */
584
+ padding: 15px 20px !important;
585
+ border-radius: 12px !important;
586
+ cursor: pointer;
587
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
588
+ font-size: 1.05em;
589
+ color: #4b5563;
590
+ }
591
+
592
+ .quiz-question .wrap label:hover {
593
+ background: #f3f4f6;
594
+ border-color: #6366f1 !important;
595
+ color: #4f46e5;
596
+ }
597
+
598
+ .quiz-question .wrap label.selected {
599
+ background: #eef2ff !important;
600
+ border-color: #4f46e5 !important;
601
+ color: #4338ca !important;
602
+ font-weight: 600;
603
+ box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1);
604
+ }
605
+
606
+ /* Hide default circle if possible, or style it.
607
+ Gradio's radio inputs are tricky to hide fully without breaking accessibility,
608
+ but we can style the container enough. */
609
+
610
+ /* ================= RESULTS & FEEDBACK ================= */
611
+
612
+ /* Success/Error Cards */
613
+ .feedback-box-correct, .feedback-box-incorrect {
614
+ margin-top: 20px;
615
+ padding: 20px;
616
+ border-radius: 12px;
617
+ animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
618
+ position: relative;
619
+ overflow: hidden;
620
+ }
621
+
622
+ .feedback-box-correct {
623
+ background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
624
+ border: 1px solid #10b981;
625
+ color: #065f46;
626
+ }
627
+
628
+ .feedback-box-incorrect {
629
+ background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
630
+ border: 1px solid #ef4444;
631
+ color: #991b1b;
632
+ }
633
+
634
+ .feedback-header {
635
+ display: flex;
636
+ align-items: center;
637
+ gap: 12px;
638
+ margin-bottom: 12px;
639
+ font-size: 1.2em;
640
+ font-weight: 700;
641
+ }
642
+
643
+ .feedback-icon {
644
+ font-size: 1.4em;
645
+ background: rgba(255,255,255,0.5);
646
+ border-radius: 50%;
647
+ width: 32px;
648
+ height: 32px;
649
+ display: flex;
650
+ align-items: center;
651
+ justify-content: center;
652
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
653
+ }
654
+
655
+ .feedback-body {
656
+ background: rgba(255,255,255,0.4);
657
+ padding: 15px;
658
+ border-radius: 8px;
659
+ font-size: 1em;
660
+ line-height: 1.6;
661
+ }
662
+
663
+ .feedback-correct-answer {
664
+ font-weight: 600;
665
+ margin-bottom: 8px;
666
+ color: #064e3b; /* darker green */
667
+ }
668
+ .feedback-box-incorrect .feedback-correct-answer {
669
+ color: #7f1d1d; /* darker red */
670
+ }
671
+
672
+ /* Summary / Explanation Box */
673
+ .summary-box {
674
+ background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
675
+ border-radius: 20px;
676
+ padding: 35px 40px;
677
+ border: 1px solid #e0e7ff;
678
+ box-shadow: 0 8px 30px rgba(79, 70, 229, 0.06);
679
+ font-size: 1.05em;
680
+ line-height: 1.9;
681
+ color: #374151;
682
+ position: relative;
683
+ overflow: hidden;
684
+ }
685
+
686
+ .summary-box::before {
687
+ content: '';
688
+ position: absolute;
689
+ top: 0;
690
+ left: 0;
691
+ right: 0;
692
+ height: 4px;
693
+ background: linear-gradient(90deg, #4f46e5, #7c3aed, #a78bfa);
694
+ }
695
+
696
+ .summary-box h2 {
697
+ font-size: 1.4em;
698
+ font-weight: 700;
699
+ color: #312e81;
700
+ margin: 0 0 18px 0;
701
+ padding-bottom: 12px;
702
+ border-bottom: 2px solid #e0e7ff;
703
+ display: flex;
704
+ align-items: center;
705
+ gap: 10px;
706
+ }
707
+
708
+ .summary-box h3 {
709
+ font-size: 1.15em;
710
+ font-weight: 600;
711
+ color: #4338ca;
712
+ margin: 20px 0 10px 0;
713
+ }
714
+
715
+ .summary-box p {
716
+ margin: 0 0 14px 0;
717
+ text-align: justify;
718
+ }
719
+
720
+ .summary-box ul, .summary-box ol {
721
+ margin: 10px 0 16px 0;
722
+ padding-left: 24px;
723
+ }
724
+
725
+ .summary-box li {
726
+ margin-bottom: 8px;
727
+ position: relative;
728
+ }
729
+
730
+ .summary-box strong {
731
+ color: #312e81;
732
+ font-weight: 600;
733
+ }
734
+
735
+ .summary-box .summary-footer {
736
+ margin-top: 20px;
737
+ padding-top: 14px;
738
+ border-top: 1px solid #e0e7ff;
739
+ font-size: 0.85em;
740
+ color: #9ca3af;
741
+ text-align: left;
742
+ }
743
+
744
+ /* Example Buttons */
745
+ #examples-container {
746
+ margin: 15px 0;
747
+ padding: 10px;
748
+ background: #f3f4f6;
749
+ border-radius: 12px;
750
+ }
751
+
752
+ .example-btn {
753
+ background: #ffffff !important;
754
+ border: 1px solid #e5e7eb !important;
755
+ color: #6366f1 !important; /* Indigo text */
756
+ font-size: 0.85em !important;
757
+ padding: 2px 10px !important;
758
+ border-radius: 20px !important; /* Pill shape */
759
+ transition: all 0.2s ease !important;
760
+ font-weight: 500 !important;
761
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
762
+ }
763
+
764
+ .example-btn:hover {
765
+ background: #f5f7ff !important;
766
+ border-color: #6366f1 !important;
767
+ transform: translateY(-1px);
768
+ box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.1) !important;
769
+ }
770
+
771
+ /* Result Card */
772
+ .result-card {
773
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
774
+ border-radius: 20px;
775
+ padding: 40px;
776
+ text-align: center;
777
+ color: white;
778
+ box-shadow: 0 20px 25px -5px rgba(79, 70, 229, 0.3);
779
+ margin-top: 40px;
780
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
781
+ }
782
+
783
+ .result-header {
784
+ font-size: 2em;
785
+ font-weight: 800;
786
+ margin-bottom: 15px;
787
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1);
788
+ }
789
+
790
+ .result-score {
791
+ font-size: 3.5em;
792
+ font-weight: 800;
793
+ margin: 10px 0;
794
+ background: -webkit-linear-gradient(#ffffff, #e0e7ff);
795
+ -webkit-background-clip: text;
796
+ -webkit-text-fill-color: transparent;
797
+ }
798
+
799
+ .result-percentage {
800
+ font-size: 1.5em;
801
+ opacity: 0.9;
802
+ font-weight: 500;
803
+ }
804
+
805
+ /* Keyframes */
806
+ @keyframes popIn {
807
+ from { opacity: 0; transform: scale(0.95) translateY(-5px); }
808
+ to { opacity: 1; transform: scale(1) translateY(0); }
809
+ }
810
+
811
+ @keyframes slideUp {
812
+ from { opacity: 0; transform: translateY(40px); }
813
+ to { opacity: 1; transform: translateY(0); }
814
+ }
815
+
816
+ /* Hide Gradio Footer */
817
+ footer { display: none !important; }
818
+ .gradio-container .prose.footer-content { display: none !important; }
819
+ """
820
+
821
+ # ----------------------------------------------------------------------
822
+ # Main App
823
+ # ----------------------------------------------------------------------
824
+
825
+ with gr.Blocks(css=custom_css, title="SmartTutor AI") as demo:
826
+
827
+ # State
828
+ quiz_state = gr.State()
829
+
830
+ with gr.Column(elem_classes="header-box"):
831
+ gr.HTML(
832
+ """
833
+ <div style='color: white;'>
834
+ <h1 style='color: white; font-size: 3em; margin: 0;'>🧠 SmartTutor AI</h1>
835
+ <p style='color: #e0e7ff; font-size: 1.25em;'>Your intelligent companion for learning and assessment</p>
836
+ </div>
837
+ """
838
+ )
839
+
840
+ with gr.Row():
841
+ # Left Panel: Controls
842
+ with gr.Column(scale=1, variant="panel"):
843
+ file_input = gr.File(
844
+ label="πŸ“„ Upload Document", file_types=[".pdf", ".txt"], type="filepath"
845
+ )
846
+ user_input = gr.Textbox(
847
+ label="✍️ Request",
848
+ placeholder="e.g. 'Summarize this' or 'Create a quiz'",
849
+ lines=3,
850
+ )
851
+
852
+ # Quick Examples
853
+ with gr.Column(elem_id="examples-container"):
854
+ gr.Markdown("✨ **Quick Actions:**")
855
+ with gr.Row():
856
+ ex_summarize = gr.Button(
857
+ "πŸ“ Summary (3 lines)", size="sm", elem_classes="example-btn"
858
+ )
859
+ ex_quiz = gr.Button(
860
+ "πŸ§ͺ 3 Questions", size="sm", elem_classes="example-btn"
861
+ )
862
+ with gr.Row():
863
+ ex_explain = gr.Button(
864
+ "πŸ’‘ Main Concepts", size="sm", elem_classes="example-btn"
865
+ )
866
+
867
+ with gr.Row():
868
+ submit_btn = gr.Button("πŸš€ Run", variant="primary")
869
+ clear_btn = gr.Button("🧹 Clear")
870
+
871
+ # Right Panel: Results
872
+ with gr.Column(scale=2):
873
+
874
+ # 1. Summary / Text Output
875
+ summary_output = gr.HTML(visible=True)
876
+
877
+ # 2. Quiz Group (Hidden initially)
878
+ with gr.Group(visible=False) as quiz_group:
879
+ gr.Markdown("## πŸ“ Quiz Time")
880
+ gr.Markdown("Select the correct answer for each question.")
881
+
882
+ # Create 10 Questions + Feedback slots
883
+ q_radios = []
884
+ q_feedbacks = []
885
+
886
+ for i in range(10):
887
+ # Radio
888
+ r = gr.Radio(
889
+ label=f"Question {i+1}",
890
+ visible=False,
891
+ elem_classes="quiz-question",
892
+ )
893
+ q_radios.append(r)
894
+
895
+ # Feedback (Markdown/HTML)
896
+ fb = gr.HTML(visible=False)
897
+ q_feedbacks.append(fb)
898
+
899
+ check_btn = gr.Button(
900
+ "βœ… Check Answers", variant="primary", visible=False
901
+ )
902
+
903
+ # Final Result Message
904
+ quiz_result_msg = gr.Markdown(visible=False)
905
+
906
+ # ------------------------------------------------------------------
907
+ # Events
908
+ # ------------------------------------------------------------------
909
+
910
+ # 1. Run Agent
911
+ # Returns: [Summary, QuizGroup, QuizState]
912
+ submit_btn.click(
913
+ fn=run_agent,
914
+ inputs=[file_input, user_input],
915
+ outputs=[summary_output, quiz_group, quiz_state],
916
+ ).success(
917
+ # On success, update the quiz UI components (21 items)
918
+ fn=render_quiz,
919
+ inputs=[quiz_state],
920
+ outputs=q_radios + q_feedbacks + [check_btn],
921
+ )
922
+
923
+ # Example Buttons Handling
924
+ # These will ONLY fill the text box. User must click 'Run' manually.
925
+
926
+ ex_summarize.click(
927
+ fn=lambda: "Summarize this document strictly in exactly 3 lines.",
928
+ outputs=[user_input],
929
+ )
930
+
931
+ ex_quiz.click(
932
+ fn=lambda: "Generate a quiz with exactly 3 multiple-choice questions.",
933
+ outputs=[user_input],
934
+ )
935
+
936
+ ex_explain.click(
937
+ fn=lambda: "Explain the 5 most important core concepts in this document clearly.",
938
+ outputs=[user_input],
939
+ )
940
+
941
+ # 2. Check Answers
942
+ # Inputs: State + 10 Radios
943
+ # Outputs: 10 Radios (Lock) + 10 Feedbacks (Show) + ResultMsg
944
+ check_btn.click(
945
+ fn=grade_quiz_ui,
946
+ inputs=[quiz_state] + q_radios,
947
+ outputs=q_radios + q_feedbacks + [quiz_result_msg],
948
+ )
949
+
950
+ # 3. Clear
951
+ def reset_ui():
952
+ # Reset everything to default
953
+ updates = [
954
+ gr.update(value=None, interactive=True, visible=False)
955
+ ] * 10 # Radios
956
+ fb_updates = [gr.update(value="", visible=False)] * 10 # Feedbacks
957
+ return (
958
+ None,
959
+ "", # Inputs
960
+ gr.update(value="", visible=True), # Summary
961
+ gr.update(visible=False), # Quiz Group
962
+ None, # State
963
+ *updates,
964
+ *fb_updates, # Radios + Feedbacks
965
+ gr.update(visible=False), # CheckBtn
966
+ gr.update(visible=False), # ResultMsg
967
+ )
968
+
969
+ clear_btn.click(
970
+ fn=reset_ui,
971
+ inputs=[],
972
+ outputs=[file_input, user_input, summary_output, quiz_group, quiz_state]
973
+ + q_radios
974
+ + q_feedbacks
975
+ + [check_btn, quiz_result_msg],
976
+ )
977
+
978
+ if __name__ == "__main__":
979
+ print("Starting SmartTutor AI on Hugging Face...")
980
+ demo.queue().launch(
981
+ server_name="0.0.0.0",
982
+ server_port=7860,
983
+ share=False,
984
+ show_api=False, # This helps avoid the Schema generation error
985
+ )