Leen172 commited on
Commit
69094d6
·
verified ·
1 Parent(s): e28f0a3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +148 -208
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # -*- coding: utf-8 -*-
2
- # app.py — واجهة واحدة: توليد أسئلة ➜ اختبار تفاعلي بنفس الثيم
3
 
4
  import os, json, uuid, random, unicodedata
5
  from dataclasses import dataclass
@@ -187,84 +187,56 @@ def to_records(items:List[MCQ], source:str, method:str, n:int)->List[dict]:
187
  })
188
  return recs
189
 
190
- # ---------- منطق الاختبار ----------
191
  def correct_letter(rec):
192
  for o in rec["options"]:
193
  if o["is_correct"]: return o["id"]
194
  return ""
195
 
196
  def init_state(records):
197
- return {"records": records, "idx":0, "answers":{}, "revealed":set(), "finished":False}
198
-
199
- def render(rec, user=None, revealed=False):
200
- q_md = f"### السؤال\n{rec['question']}"
201
- ch_list = [f"{o['id']}) {o['text']}" for o in rec["options"]]
202
- exp = rec["explanation"] if revealed else ""
203
- fb = ""
204
- if user and revealed:
205
- fb = "✅ إجابة صحيحة" if user == correct_letter(rec) else f"❌ إجابة خاطئة — الصحيح: {correct_letter(rec)}"
206
- elif user:
207
- fb = f"تم اختيار: {user}"
208
- # مهم: تحديث الـRadio بشكل صريح
209
- ch_update = gr.update(choices=ch_list, value=None, interactive=True)
210
- return q_md, ch_update, exp, fb
211
-
212
- def show(state):
213
- if not state:
214
- return "", gr.update(choices=[], value=None), "", "", ""
215
- rec = state["records"][state["idx"]]
216
- q, ch_update, exp, fb = render(
217
- rec,
218
- state["answers"].get(rec["id"]),
219
- rec["id"] in state["revealed"]
220
- )
221
- pos = f"{state['idx']+1} / {len(state['records'])}"
222
- return q, ch_update, exp, fb, pos
223
-
224
-
225
 
226
- def choose(state, label):
227
- if not state or not label: return state, ""
228
- rec = state["records"][state["idx"]]
229
- letter = label.split(")")[0].strip()
230
- state["answers"][rec["id"]] = letter
231
- if rec["id"] in state["revealed"]:
232
- fb = "✅ إجابة صحيحة" if letter==correct_letter(rec) else f"❌ إجابة خاطئة — الصحيح: {correct_letter(rec)}"
233
- else:
234
- fb = f"تم اختيار: {letter}"
235
- return state, fb
236
-
237
- def prev_(s):
238
- if s: s["idx"]=max(0, s["idx"]-1);
239
- return s
240
- def next_(s):
241
- if s: s["idx"]=min(len(s["records"])-1, s["idx"]+1);
242
- return s
243
- def reveal(s):
244
- if not s: return s, ""
245
- rec = s["records"][s["idx"]]
246
- s["revealed"].add(rec["id"])
247
- u = s["answers"].get(rec["id"])
248
- fb = "✅ إجابة صحيحة" if u==correct_letter(rec) else (f"❌ إجابة خاطئة — الصحيح: {correct_letter(rec)}" if u else f"الصحيح: {correct_letter(rec)}")
249
- return s, fb
250
-
251
- def finish(s):
252
- if not s: return s, ""
253
- c=w=sk=0
254
- for r in s["records"]:
255
- u=s["answers"].get(r["id"])
256
- cor=correct_letter(r)
257
- if u is None: sk+=1
258
- elif u==cor: c+=1
259
- else: w+=1
260
- s["finished"]=True
261
- return s, f"النتيجة: {c}/{len(s['records'])} (صحيح: {c}، خطأ: {w}، متروك: {sk})"
262
 
263
  # ---------- معالجة الإدخال (نص أو ملف) ----------
264
  def build_quiz(text_area, file_path, n, model_id, zoom):
265
  text_area = (text_area or "").strip()
266
  if not text_area and not file_path:
267
- return None, gr.update(visible=True), gr.update(visible=False), "🛈 أدخل نصًا أو ارفع ملفًا أولًا."
268
  if text_area:
269
  src_name = "pasted_text.txt"
270
  raw, method = text_area, "user text"
@@ -275,83 +247,96 @@ def build_quiz(text_area, file_path, n, model_id, zoom):
275
  items = make_mcqs(cleaned, n=int(n))
276
  records = to_records(items, source=src_name, method=method, n=n)
277
  state = init_state(records)
278
- # إظهار قسم الاختبار وإخفاء قسم الإدخال
279
- return state, gr.update(visible=False), gr.update(visible=True), f"تم توليد {len(records)} سؤالًا."
280
-
281
- # ---------- الثيم (CSS مطابق للصورة تقريبًا) ----------
282
- CSS = """
283
- /* خلفية داكنة ولمسة دافئة */
284
- body {
285
- direction: rtl;
286
- font-family: system-ui,'Cairo','IBM Plex Arabic',sans-serif;
287
- background:#0f0f0f;
288
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
- /* عرض مناسب */
291
- .gradio-container { max-width: 980px; margin: 0 auto; }
292
 
293
- /* كارت رئيسي داكن ونص فاتح واضح */
294
- .card {
295
- background:#1b1b1b;
296
- border-radius:20px;
297
- padding:24px;
298
- box-shadow:0 25px 45px rgba(0,0,0,.5);
299
- color:#f5efe6;
300
  }
301
- .card * { color:#f5efe6 !important; }
302
- .card h1,.card h2,.card h3 { margin-top:0; }
303
-
304
- /* نص السؤال أكبر وتباعد مريح */
305
- .card p { font-size:1.12rem; line-height:1.9; word-wrap:break-word; overflow-wrap:anywhere; }
306
 
307
- /* زر أساسي بلون برتقالي دافئ */
308
- .button-primary > button {
309
- background: linear-gradient(180deg,#ff9a55,#ff7d2d);
310
- border:none; color:#1b1b1b; font-weight:700;
311
- }
312
- .button-primary > button:hover { filter:brightness(0.95); }
313
 
314
- /* صندوق الرفع */
315
- .upload-like {
316
- border:2px dashed #ff9a5555; background:#161616;
317
- border-radius:16px; padding:14px; color:#ddd;
318
  }
319
-
320
- /* سلايدر وأزرار داكنة متناسقة */
321
- input[type="range"]::-webkit-slider-thumb { background:#ff7d2d; }
322
- .gr-button { background:#2a2a2a; color:#eaeaea; border:none; }
323
- .gr-button:hover { filter:brightness(1.08); }
324
-
325
- /* خيارات الراديو */
326
- .radio .wrap label { text-align:right; justify-content:flex-end; }
327
- .radio .wrap label span { font-size:1.05rem; }
328
-
329
- /* شريط التقدم */
330
- .progress { text-align:left; opacity:.85; color:#cfcfcf; }
331
-
332
- /* عنوان أعلى الصفحة */
333
- .top-title { color:#e9ded6; }
 
 
 
334
  """
335
 
336
-
337
-
338
  # ---------- واجهة Gradio ----------
339
  with gr.Blocks(title="Question Generator", css=CSS) as demo:
340
- gr.Markdown("<h2 class='top-title' style='text-align:center;margin-top:8px;'>Question Generator</h2>")
341
 
342
- # ===== القسم A: الإدخال (نص/ملف) =====
343
- input_group = gr.Group(visible=True)
344
- with input_group:
345
  with gr.Row():
346
- with gr.Column(scale=2):
347
- gr.Markdown("<h3>أدخل نصًا أو ارفع ملفًا</h3>")
348
- text_area = gr.Textbox(lines=10, placeholder="ألصق هنا مقطع نصي...", label=None)
349
- num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
350
  with gr.Column(scale=1):
351
- file_comp = gr.File(
352
- label="اختر ملفًا", file_count="single",
353
- file_types=[".pdf",".txt"], type="filepath", elem_classes=["upload-like"]
354
- )
355
  with gr.Accordion("خيارات متقدمة (لـ PDF المصوّر)", open=False):
356
  trocr_model = gr.Dropdown(
357
  choices=[
@@ -363,93 +348,48 @@ with gr.Blocks(title="Question Generator", css=CSS) as demo:
363
  value=DEFAULT_TROCR_MODEL, label="نموذج TrOCR"
364
  )
365
  trocr_zoom = gr.Slider(2.0, 3.5, value=DEFAULT_TROCR_ZOOM, step=0.1, label="Zoom OCR")
 
 
 
366
  btn_build = gr.Button("توليد الأسئلة", elem_classes=["button-primary"])
367
- toast = gr.Markdown("", elem_classes=["soft"])
368
 
369
- # حالة عامة للاختبار
370
  state = gr.State(None)
 
371
 
372
- # ===== القسم B: الاختبار =====
373
- quiz_group = gr.Group(visible=False)
374
- with quiz_group:
375
- with gr.Row():
376
- progress = gr.Label("", elem_classes=["progress"])
377
- with gr.Row():
378
- with gr.Column():
379
- # مكوّنات السؤال
380
- q_md = gr.Markdown("", elem_classes=["card"])
381
- choices = gr.Radio(choices=[], label="اختر الإجابة", interactive=True, elem_classes=["radio"])
382
- feedback = gr.Markdown("")
383
- exp_md = gr.Markdown("")
384
- with gr.Row():
385
- btn_prev = gr.Button("السابق")
386
- btn_next = gr.Button("التالي")
387
- btn_reveal = gr.Button("إظهار الإجابة")
388
- btn_finish = gr.Button("إنهاء الاختبار", elem_classes=["button-primary"])
389
- btn_reset = gr.Button("العودة للواجهة", variant="secondary")
390
-
391
- # ===== الربط المنطقي =====
392
- # بناء الاختبار من الإدخال
393
  btn_build.click(
394
  build_quiz,
395
  inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
396
- outputs=[state, input_group, quiz_group, toast]
397
- ).then(
398
- fn=show, inputs=[state],
399
- outputs=[q_md, choices, exp_md, feedback, progress]
400
  )
401
 
402
- # تفاعلات الاختبار
403
- choices.change(lambda s, c: choose(s, c), inputs=[state, choices], outputs=[state, feedback])
404
- btn_prev.click(prev_, inputs=[state], outputs=[state]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
405
- btn_next.click(next_, inputs=[state], outputs=[state]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
406
- btn_reveal.click(reveal, inputs=[state], outputs=[state, feedback]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
407
- btn_finish.click(finish, inputs=[state], outputs=[state, feedback])
408
-
409
- # رجوع للواجهة الأولى
410
- btn_reset.click(
411
- lambda: (None, gr.update(visible=True), gr.update(visible=False), "", "", "", "", ""),
412
- outputs=[state, input_group, quiz_group, feedback, q_md, choices, exp_md, progress]
 
 
 
 
 
 
 
413
  )
414
 
415
- if __name__ == "__main__":
416
- demo.queue().launch()
417
-
418
- # القسم B: الاختبار
419
- quiz_group = gr.Group(visible=False)
420
- with quiz_group:
421
- with gr.Row():
422
- progress = gr.Label("", elem_classes=["progress"])
423
- with gr.Row():
424
- with gr.Column():
425
- # ---------- مكونات عرض السؤال ----------
426
- q_md = gr.Markdown("", elem_classes=["card"])
427
- choices = gr.Radio(choices=[], label="اختر الإجابة", interactive=True, elem_classes=["radio"])
428
- feedback = gr.Markdown("")
429
- exp_md = gr.Markdown("")
430
-
431
- with gr.Row():
432
- btn_prev = gr.Button("السابق")
433
- btn_next = gr.Button("التالي")
434
- btn_reveal = gr.Button("إظهار الإجابة")
435
- btn_finish = gr.Button("إنهاء الاختبار", elem_classes=["button-primary"])
436
- btn_reset = gr.Button("العودة للواجهة", variant="secondary")
437
-
438
- # بناء الاختبار من الإدخال
439
- btn_build.click(
440
- build_quiz,
441
- inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
442
- outputs=[state, input_group, quiz_group, toast]
443
- ).then(fn=show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
444
-
445
- # تفاعلات الاختبار
446
- choices.change(lambda s,c: choose(s,c), inputs=[state, choices], outputs=[state, feedback])
447
- btn_prev.click(prev_, inputs=[state], outputs=[state]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
448
- btn_next.click(next_, inputs=[state], outputs=[state]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
449
- btn_reveal.click(reveal, inputs=[state], outputs=[state, feedback]).then(show, inputs=[state], outputs=[q_md, choices, exp_md, feedback, progress])
450
- btn_finish.click(finish, inputs=[state], outputs=[state, feedback])
451
- btn_reset.click(lambda: (None, gr.update(visible=True), gr.update(visible=False), "", "", "", "", ""),
452
- outputs=[state, input_group, quiz_group, feedback, q_md, choices, exp_md, progress])
453
-
454
  if __name__ == "__main__":
455
  demo.queue().launch()
 
1
  # -*- coding: utf-8 -*-
2
+ # app.py — واجهة داكنة حديثة: توليد أسئلة ➜ عرض كل الأسئلة دفعة واحدة ➜ Submit للنتيجة
3
 
4
  import os, json, uuid, random, unicodedata
5
  from dataclasses import dataclass
 
187
  })
188
  return recs
189
 
190
+ # ---------- منطق الحالة ----------
191
  def correct_letter(rec):
192
  for o in rec["options"]:
193
  if o["is_correct"]: return o["id"]
194
  return ""
195
 
196
  def init_state(records):
197
+ return {"records": records, "finished": False}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ # ---------- HTML للواجهة/الاختبار ----------
200
+ def render_quiz_html(records: List[dict]) -> str:
201
+ # يبني HTML فيه كل الأسئلة دفعة واحدة (راديو لكل سؤال)
202
+ parts = []
203
+ for i, rec in enumerate(records, start=1):
204
+ qid = rec["id"]
205
+ qtxt = rec["question"]
206
+ opts = rec["options"]
207
+ opts_html = []
208
+ for o in opts:
209
+ lid = o["id"]
210
+ txt = o["text"]
211
+ # name = q_<id>, value = letter
212
+ opts_html.append(f"""
213
+ <label class="opt">
214
+ <input type="radio" name="q_{qid}" value="{lid}" />
215
+ <span class="opt-letter">{lid}</span>
216
+ <span class="opt-text">{txt}</span>
217
+ </label>
218
+ """)
219
+ card = f"""
220
+ <div class="q-card" data-qid="{qid}">
221
+ <div class="q-title">السؤال {i}:</div>
222
+ <div class="q-text">{qtxt}</div>
223
+ <div class="opts">{''.join(opts_html)}</div>
224
+ </div>
225
+ """
226
+ parts.append(card)
227
+
228
+ html = f"""
229
+ <div class="quiz-wrap">
230
+ {''.join(parts)}
231
+ </div>
232
+ """
233
+ return html
 
234
 
235
  # ---------- معالجة الإدخال (نص أو ملف) ----------
236
  def build_quiz(text_area, file_path, n, model_id, zoom):
237
  text_area = (text_area or "").strip()
238
  if not text_area and not file_path:
239
+ return None, "", "🛈 أدخل نصًا أو ارفع ملفًا أولًا."
240
  if text_area:
241
  src_name = "pasted_text.txt"
242
  raw, method = text_area, "user text"
 
247
  items = make_mcqs(cleaned, n=int(n))
248
  records = to_records(items, source=src_name, method=method, n=n)
249
  state = init_state(records)
250
+ html = render_quiz_html(records)
251
+ return state, html, f"تم توليد {len(records)} سؤالًا."
252
+
253
+ # ---------- تصحيح الإجابات ----------
254
+ def grade(state, answers_json):
255
+ try:
256
+ user_map = json.loads(answers_json or "{}")
257
+ except Exception:
258
+ user_map = {}
259
+ recs = state["records"] if state else []
260
+ total = len(recs)
261
+ correct = 0
262
+ wrong_details = []
263
+ for rec in recs:
264
+ qid = rec["id"]
265
+ chosen = (user_map.get(qid) or "").strip()
266
+ cor = correct_letter(rec)
267
+ if not chosen:
268
+ wrong_details.append((rec, chosen, cor, "لم يتم اختيار إجابة"))
269
+ elif chosen == cor:
270
+ correct += 1
271
+ else:
272
+ wrong_details.append((rec, chosen, cor, ""))
273
+
274
+ score_md = f"### نتيجتك: **{correct} / {total}**"
275
+ if wrong_details:
276
+ md = ["### الإجابات الخاطئة:"]
277
+ for rec, chosen, cor, note in wrong_details:
278
+ opts = {o["id"]: o["text"] for o in rec["options"]}
279
+ md.append(
280
+ f"- **السؤال:** {rec['question']}\n"
281
+ f" - إجابتك: **{chosen or '—'}** — {opts.get(chosen,'')}\n"
282
+ f" - الصحيحة: **{cor}** — {opts.get(cor,'')}\n"
283
+ f" - الشرح: {rec['explanation']}\n"
284
+ + (f" - ملاحظة: {note}\n" if note else "")
285
+ )
286
+ mistakes_md = "\n".join(md)
287
+ else:
288
+ mistakes_md = "### ممتاز! جميع الإجابات صحيحة ✅"
289
 
290
+ return score_md, mistakes_md
 
291
 
292
+ # ---------- الثيم (CSS داكن حديث) ----------
293
+ CSS = """
294
+ :root{
295
+ --bg:#0f0f0f; --panel:#1a1a1a; --card:#1b1b1b; --muted:#9aa0a6;
296
+ --text:#f5efe6; --accent:#ff7d2d; --accent2:#ff9a55; --border:#2a2a2a;
 
 
297
  }
298
+ body{direction:rtl; font-family:system-ui,'Cairo','IBM Plex Arabic',sans-serif; background:var(--bg);}
299
+ .gradio-container{max-width:1024px;margin:0 auto;padding:8px 8px 40px;}
300
+ .top-title{color:#e9ded6;margin:8px 0 16px 0}
 
 
301
 
302
+ .panel{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px;box-shadow:0 20px 45px rgba(0,0,0,.4)}
303
+ .small{opacity:.85;color:#ddd}
 
 
 
 
304
 
305
+ .button-primary > button{
306
+ background:linear-gradient(180deg,var(--accent2),var(--accent));
307
+ border:none;color:#161616;font-weight:700;
 
308
  }
309
+ .button-primary > button:hover{filter:brightness(.95)}
310
+ .upload-like{border:2px dashed #ff9a5555;background:#141414;border-radius:12px;padding:10px;color:#ddd}
311
+
312
+ /* حقل النص أصغر */
313
+ textarea{min-height:140px}
314
+
315
+ /* بطاقة السؤال */
316
+ .q-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px;margin:14px 0}
317
+ .q-title{color:#f0e6dc;font-weight:700;margin-bottom:8px}
318
+ .q-text{color:var(--text);font-size:1.08rem;line-height:1.8;margin-bottom:10px}
319
+ .opts{display:flex;flex-direction:column;gap:8px}
320
+ .opt{display:flex;gap:10px;align-items:center;background:#191919;border:1px solid #282828;border-radius:12px;padding:10px}
321
+ .opt input{accent-color:var(--accent)}
322
+ .opt-letter{display:inline-flex;width:28px;height:28px;border-radius:8px;background:#222;align-items:center;justify-content:center;font-weight:800;color:#f1f1f1}
323
+ .opt-text{color:#eaeaea}
324
+
325
+ /* قسم النتيجة */
326
+ .result-card{background:#121212;border:1px solid #2a2a2a;border-radius:16px;padding:16px;margin-top:18px}
327
  """
328
 
 
 
329
  # ---------- واجهة Gradio ----------
330
  with gr.Blocks(title="Question Generator", css=CSS) as demo:
331
+ gr.Markdown("<h2 class='top-title'>Question Generator</h2>")
332
 
333
+ # لوحة الإدخال (ثابتة)
334
+ with gr.Group(elem_classes=["panel"]):
335
+ gr.Markdown("**ارفع ملفك أو ألصق نصًا، حدّد عدد الأسئلة، ثم اضغط توليد.**", elem_classes=["small"])
336
  with gr.Row():
 
 
 
 
337
  with gr.Column(scale=1):
338
+ file_comp = gr.File(label="اختر ملف PDF أو TXT", file_count="single",
339
+ file_types=[".pdf",".txt"], type="filepath", elem_classes=["upload-like"])
 
 
340
  with gr.Accordion("خيارات متقدمة (لـ PDF المصوّر)", open=False):
341
  trocr_model = gr.Dropdown(
342
  choices=[
 
348
  value=DEFAULT_TROCR_MODEL, label="نموذج TrOCR"
349
  )
350
  trocr_zoom = gr.Slider(2.0, 3.5, value=DEFAULT_TROCR_ZOOM, step=0.1, label="Zoom OCR")
351
+ with gr.Column(scale=1):
352
+ text_area = gr.Textbox(lines=6, placeholder="ألصق هنا مقطع نصي...", label="أدخل نصًا أو ارفع ملفًا")
353
+ num_q = gr.Slider(4, 20, value=DEFAULT_NUM_QUESTIONS, step=1, label="عدد الأسئلة")
354
  btn_build = gr.Button("توليد الأسئلة", elem_classes=["button-primary"])
355
+ toast = gr.Markdown("", elem_classes=["small"])
356
 
357
+ # حالة عامة + مكان عرض الاختبار
358
  state = gr.State(None)
359
+ quiz_html = gr.HTML("") # كل الأسئلة ستُعرض هنا دفعة واحدة
360
 
361
+ # زر الإرسال + حقل سري لتجميع الإجابات + مكان النتيجة
362
+ btn_submit = gr.Button("إنهاء وإرسال الإجابات", elem_classes=["button-primary"])
363
+ answers_box = gr.Textbox(visible=False)
364
+ score_md = gr.Markdown("")
365
+ mistakes_md = gr.Markdown("")
366
+
367
+ # توليد الأسئلة
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  btn_build.click(
369
  build_quiz,
370
  inputs=[text_area, file_comp, num_q, trocr_model, trocr_zoom],
371
+ outputs=[state, quiz_html, toast]
 
 
 
372
  )
373
 
374
+ # JS لالتقاط الإجابات من الـ HTML (جميع الأسئلة)
375
+ js_collect = """
376
+ function () {
377
+ const data = {};
378
+ document.querySelectorAll('.q-card').forEach(card => {
379
+ const qid = card.getAttribute('data-qid');
380
+ const checked = card.querySelector('input[type="radio"]:checked');
381
+ data[qid] = checked ? checked.value : null;
382
+ });
383
+ return JSON.stringify(data);
384
+ }
385
+ """
386
+
387
+ # عند Submit: نجمع الإجابات بالـJS ثم نقيّمها في الباك إند
388
+ btn_submit.click(
389
+ None, inputs=None, outputs=[answers_box], js=js_collect
390
+ ).then(
391
+ grade, inputs=[state, answers_box], outputs=[score_md, mistakes_md]
392
  )
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  if __name__ == "__main__":
395
  demo.queue().launch()