Spaces:
Running
Running
Update app.py
Browse files
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, "
|
| 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 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 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,
|
| 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,
|
| 280 |
-
|
| 281 |
-
# ----------
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
-
|
| 291 |
-
.gradio-container { max-width: 980px; margin: 0 auto; }
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
box-shadow:0 25px 45px rgba(0,0,0,.5);
|
| 299 |
-
color:#f5efe6;
|
| 300 |
}
|
| 301 |
-
|
| 302 |
-
.
|
| 303 |
-
|
| 304 |
-
/* نص السؤال أكبر وتباعد مريح */
|
| 305 |
-
.card p { font-size:1.12rem; line-height:1.9; word-wrap:break-word; overflow-wrap:anywhere; }
|
| 306 |
|
| 307 |
-
|
| 308 |
-
.
|
| 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 |
-
|
| 316 |
-
border:
|
| 317 |
-
border-radius:16px; padding:14px; color:#ddd;
|
| 318 |
}
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
/*
|
| 326 |
-
.
|
| 327 |
-
.
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
.
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
.
|
|
|
|
|
|
|
|
|
|
| 334 |
"""
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
# ---------- واجهة Gradio ----------
|
| 339 |
with gr.Blocks(title="Question Generator", css=CSS) as demo:
|
| 340 |
-
gr.Markdown("<h2 class='top-title'
|
| 341 |
|
| 342 |
-
#
|
| 343 |
-
|
| 344 |
-
|
| 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 |
-
|
| 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=["
|
| 368 |
|
| 369 |
-
# حالة عامة
|
| 370 |
state = gr.State(None)
|
|
|
|
| 371 |
|
| 372 |
-
#
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 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,
|
| 397 |
-
).then(
|
| 398 |
-
fn=show, inputs=[state],
|
| 399 |
-
outputs=[q_md, choices, exp_md, feedback, progress]
|
| 400 |
)
|
| 401 |
|
| 402 |
-
#
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|