Ahmed-El-Sharkawy commited on
Commit
60fbdcb
·
verified ·
1 Parent(s): 41d8abc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +406 -406
app.py CHANGED
@@ -1,406 +1,406 @@
1
- import os, sys, time, asyncio, json, re
2
- from typing import List, Dict, Optional
3
- import gradio as gr
4
- from dotenv import load_dotenv
5
- import base64
6
- from openai import OpenAI
7
-
8
- if sys.platform.startswith("win"):
9
- try:
10
- asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
11
- except Exception:
12
- pass
13
-
14
- load_dotenv()
15
- APP_Name = os.getenv("APP_Name", "منصة دروس تفاعلية بالعربية")
16
- APP_Version = os.getenv("APP_Version", "0.2.0")
17
- API_KEY = os.getenv("API_KEY", "")
18
-
19
- MODELS = [m.strip() for m in os.getenv("Models", "").split(",") if m.strip()] or [
20
- "QwQ-32B",
21
- "zai-org/GLM-4.5-Air",
22
- ]
23
-
24
- MODEL_INFO = {
25
- "QwQ-32B": "QwQ-32B — مُعلّم استدلالي قوي للإجابات المفصّلة.",
26
- "zai-org/GLM-4.5-Air": "GLM-4.5-Air — سريع وفعّال للشرح خطوة بخطوة.",
27
- }
28
-
29
- LOGO_PATH = "download.jpeg"
30
- COMPANY_LOGO = LOGO_PATH
31
- OWNER_NAME = "ENG. Ahmed Yasser El Sharkawy"
32
-
33
- BASE_URL = "https://genai.ghaymah.systems"
34
- client = OpenAI(api_key=API_KEY, base_url=BASE_URL) if API_KEY else None
35
-
36
- CSS = """
37
- :root { direction: rtl; }
38
- * { font-family: "Tajawal", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
39
- .app-header{display:flex;align-items:center;gap:12px;justify-content:center;margin:6px 0 16px}
40
- .app-header img{height:60px;border-radius:12px}
41
- .app-title{font-weight:800;font-size:28px;line-height:1.1}
42
- .app-sub{opacity:.7;font-size:14px}
43
- .gradio-container { direction: rtl; }
44
- .markdown-body { direction: rtl; text-align: right; }
45
- """
46
-
47
- SYSTEM_SEED = (
48
- "أنت معلّم عربي متميز. قدّم شروحًا تعليمية دقيقة ومبسطة، وراعِ مستوى الطالب،"
49
- " وقدّم الحلول خطوة بخطوة عند الطلب. استخدم LaTeX للمعادلات بين $$...$$."
50
- )
51
-
52
- BACKOFF = [3, 6, 12]
53
-
54
- def logo_data_uri(path: str) -> str:
55
- if not os.path.exists(path):
56
- return ""
57
- ext = os.path.splitext(path)[1].lower()
58
- mime = {
59
- ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
60
- ".webp": "image/webp", ".gif": "image/gif"
61
- }.get(ext, "image/png")
62
- with open(path, "rb") as f:
63
- b64 = base64.b64encode(f.read()).decode("utf-8")
64
- return f"data:{mime};base64,{b64}"
65
-
66
- DIALECT_MAP = {
67
- "فصحى": "استخدم العربية الفصحى الواضحة.",
68
- "مصري": "استخدم لهجة مصرية خفيفة مفهومة على نطاق واسع دون إسراف.",
69
- "شامي": "استخدم لهجة شامية مبسطة ومفهومة.",
70
- "خليجي": "استخدم لهجة خليجية مبسطة ومفهومة.",
71
- "مغربي": "استخدم لهجة مغاربية مبسطة ومفهومة، وتوضيح المصطلحات إن لزم.",
72
- }
73
-
74
- SUBJECT_MAP = {
75
- "رياضيات": "math",
76
- "فيزياء": "physics",
77
- "كيمياء": "chemistry",
78
- "أحياء": "biology",
79
- }
80
-
81
- LEVEL_MAP = {
82
- "ابتدائي": "elementary",
83
- "إعدادي": "middle",
84
- "ثانوي": "high",
85
- "جامعي": "university",
86
- }
87
-
88
- STYLE_MAP = {
89
- "خطوة بخطوة": "step_by_step",
90
- "تلميحات أولًا": "hints_first",
91
- "إجابة نهائية فقط": "final_only",
92
- }
93
-
94
- SCHEMA_GUIDE = {
95
- "mode": "solve|explain|practice",
96
- "subject": "math|physics|chemistry|biology",
97
- "grade": "elementary|middle|high|university",
98
- "dialect": "fosha|masri|shami|khaleeji|maghrebi",
99
- "answer_style": "step_by_step|hints_first|final_only",
100
- "steps": [{"title": "", "explanation": "", "math": ""}],
101
- "final_answer": "",
102
- "tips": [""],
103
- "common_mistakes": [""],
104
- "similar_exercises": [""],
105
- "confidence": 0.0,
106
- }
107
-
108
- DIALECT_CODE = {
109
- "فصحى": "fosha",
110
- "مصري": "masri",
111
- "شامي": "shami",
112
- "خليجي": "khaleeji",
113
- "مغربي": "maghrebi",
114
- }
115
-
116
- def build_messages(
117
- prompt: str,
118
- subject_ar: str,
119
- level_ar: str,
120
- dialect_ar: str,
121
- style_ar: str,
122
- mode: str,
123
- ) -> List[Dict[str, str]]:
124
- subject = SUBJECT_MAP.get(subject_ar, "math")
125
- grade = LEVEL_MAP.get(level_ar, "university")
126
- answer_style = STYLE_MAP.get(style_ar, "step_by_step")
127
- dialect_note = DIALECT_MAP.get(dialect_ar, DIALECT_MAP["فصحى"])
128
- dialect_code = DIALECT_CODE.get(dialect_ar, "masri")
129
-
130
- system = (
131
- f"{SYSTEM_SEED} {dialect_note} ركّز على الدقة والمنطق التربوي."
132
- " إذا طلب الطالب برهانًا أو اشتقاقًا فاذكر الأفكار الرئيسية بإيجاز."
133
- " تجنّب الحشو واطرح أسئلة فاحصة عند الحاجة."
134
- )
135
-
136
- user = (
137
- "أنت الآن داخل منصة دروس عربية. أعد لي إخراجًا بصيغة JSON فقط دون أي نص زائد.\n"
138
- "اتبع هذا المخطط الحرفي للمفاتيح: {"
139
- "\"mode\", \"subject\", \"grade\", \"dialect\", \"answer_style\", \"steps\", \"final_answer\", \"tips\", \"common_mistakes\", \"similar_exercises\", \"confidence\"} .\n"
140
- "- اكتب الشرح بالعربية وفق اللهجة المطلوبة.\n"
141
- "- اكتب المعادلات داخل الحقل math بين $$ بهذه الصيغة: $$..$$.\n"
142
- "- راعِ المستوى الدراسي.\n"
143
- f"- subject='{subject}', grade='{grade}', dialect='{dialect_code}', answer_style='{answer_style}', mode='{mode}'.\n"
144
- f"- مهمة الطالب: {prompt}\n"
145
- "أعد JSON الصحيح فقط."
146
- )
147
- return [{"role": "system", "content": system}, {"role": "user", "content": user}]
148
-
149
-
150
- def safe_chat_complete(model: str, messages: List[Dict], max_tokens: int = 1200) -> str:
151
- if not client:
152
- return "⚠️ لم يتم العثور على API_KEY في .env"
153
- attempt = 0
154
- while True:
155
- try:
156
- resp = client.chat.completions.create(
157
- model=model,
158
- messages=messages,
159
- max_tokens=max_tokens,
160
- temperature=0.2,
161
- timeout=90,
162
- )
163
- return resp.choices[0].message.content or ""
164
- except Exception as e:
165
- msg = str(e)
166
- if ("429" in msg or "Rate" in msg) and attempt < len(BACKOFF):
167
- time.sleep(BACKOFF[attempt]); attempt += 1
168
- continue
169
- return f"فشل الطلب مع النموذج `{model}`: {e}"
170
-
171
-
172
- def extract_json(text: str) -> Optional[Dict]:
173
- if not text:
174
- return None
175
- try:
176
- return json.loads(text)
177
- except Exception:
178
- pass
179
- m = re.search(r"\{[\s\S]*\}", text)
180
- if m:
181
- try:
182
- return json.loads(m.group(0))
183
- except Exception:
184
- return None
185
- return None
186
-
187
-
188
- def format_lesson(payload: Dict) -> str:
189
- """حوّل استجابة JSON إلى Markdown سهل القراءة مع بطاقة ملخص.
190
- متسامح مع تنسيقات steps/tips المختلفة.
191
- """
192
- if not isinstance(payload, dict):
193
- return payload if isinstance(payload, str) else "تعذر تنسيق الرد."
194
-
195
- subject = payload.get("subject", "math") or "math"
196
- grade = payload.get("grade", "") or ""
197
- answer_style = payload.get("answer_style", "") or ""
198
- dialect_code = payload.get("dialect", "fosha") or "fosha"
199
- mode = payload.get("mode", "solve") or "solve"
200
-
201
- steps_raw = payload.get("steps", []) or []
202
- final_answer = payload.get("final_answer", "") or ""
203
- tips = payload.get("tips", []) or []
204
- mistakes = payload.get("common_mistakes", []) or []
205
- similars = payload.get("similar_exercises", []) or []
206
-
207
- if isinstance(tips, str): tips = [tips]
208
- if isinstance(mistakes, str): mistakes = [mistakes]
209
- if isinstance(similars, str): similars = [similars]
210
-
211
- steps: List[Dict[str, str]] = []
212
- for i, st in enumerate(steps_raw, 1):
213
- if isinstance(st, dict):
214
- steps.append({
215
- "title": st.get("title", f"الخطوة {i}") or f"الخطوة {i}",
216
- "explanation": st.get("explanation", "") or "",
217
- "math": st.get("math", "") or "",
218
- })
219
- elif isinstance(st, str):
220
- steps.append({"title": f"الخطوة {i}", "explanation": st, "math": ""})
221
- else:
222
- continue
223
-
224
- subject_icon = {"math": "🧮", "physics": "🧪", "chemistry": "⚗️", "biology": "🧬"}.get(subject, "📘")
225
-
226
- # خرائط عرض عربية
227
- SUBJECT_AR = {"math": "رياضيات", "physics": "فيزياء", "chemistry": "كيمياء", "biology": "أحياء"}
228
- STYLE_AR = {"step_by_step": "خطوة بخطوة", "hints_first": "تلميحات أولًا", "final_only": "إجابة نهائية فقط"}
229
- DIALECT_AR = {"fosha": "فصحى", "masri": "مصري", "shami": "شامي", "khaleeji": "خليجي", "maghrebi": "مغربي"}
230
- MODE_AR = {"solve": "حل مسألة", "explain": "شرح مفهوم", "practice": "إنشاء تمارين"}
231
-
232
- md: List[str] = []
233
-
234
-
235
- # تفاصيل الشرح
236
- if steps:
237
- md.append("#### الخطوات")
238
- for i, st in enumerate(steps, 1):
239
- title = st.get("title", f"الخطوة {i}")
240
- expl = st.get("explanation", "")
241
- math = st.get("math", "")
242
- md.append(f"**{i}. {title}**\n\n{expl}")
243
- if math:
244
- md.append(f"\\[ {math.replace('$$','')} \\]")
245
-
246
- if final_answer:
247
- md.append("#### الإجابة النهائية")
248
- md.append(final_answer)
249
-
250
- if tips:
251
- md.append("#### نصائح للمذاكرة")
252
- for t in tips:
253
- md.append(f"- {t}")
254
-
255
- if mistakes:
256
- md.append("#### أخطاء شائعة")
257
- for m in mistakes:
258
- md.append(f"- {m}")
259
-
260
- if similars:
261
- md.append("#### تمارين مشابهة للتدريب")
262
- for s in similars[:5]:
263
- md.append(f"- {s}")
264
-
265
- return "\n\n".join(md)
266
-
267
-
268
- # Gradio
269
- with gr.Blocks(title=f"{APP_Name} v{APP_Version}", css=CSS, theme=gr.themes.Soft()) as demo:
270
- # MathJax لدعم LaTeX
271
- gr.HTML(
272
- """
273
- <script>
274
- if (!window.MathJax) {
275
- window.MathJax = {tex: {inlineMath: [['$','$'], ['\\(','\\)']]}};
276
- }
277
- </script>
278
- <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
279
- """
280
- )
281
-
282
- header_logo_src = logo_data_uri(COMPANY_LOGO)
283
- logo_html = f"<img src='{header_logo_src}' alt='logo'>" if header_logo_src else ""
284
- gr.HTML(f"""
285
- <div class="app-header">
286
- {logo_html}
287
- <div class="app-header-text">
288
- <div class="app-title">{APP_Name}</div>
289
- <div class="app-sub">v{APP_Version} • {OWNER_NAME}</div>
290
- </div>
291
- </div>
292
- """)
293
-
294
- with gr.Row():
295
- with gr.Column(scale=3):
296
- chat = gr.Chatbot(label="جلسة الدرس", height=520, type="messages", value=[])
297
- user_in = gr.Textbox(label="سؤال الطالب / المسألة", placeholder="اكتب السؤال هنا… مثلاً: احسب قيمة التعبير 2x+3 عند x=5", lines=2)
298
- with gr.Row():
299
- send_btn = gr.Button("إرسال ✨", variant="primary")
300
- clear_btn = gr.Button("مسح المحادثة")
301
- gr.Markdown(
302
- """
303
- > **ملاحظة:** تُستخدم هذه المنصة لأغراض تعليمية. تحقّق دائمًا من النتائج خاصة في المسائل المتقدمة.
304
-
305
- جرّب كتابة: **حلّل العبارة التربيعية x^2 - 5x + 6** أو **اشرح قانون نيوتن الثاني**.
306
- """
307
- )
308
-
309
-
310
- with gr.Column(scale=2, min_width=340):
311
- model_choice = gr.Radio(
312
- choices=MODELS,
313
- value=MODELS[0],
314
- label="النموذج",
315
- info="اختر QwQ-32B أو GLM-4.5-Air",
316
- )
317
- info_md = gr.Markdown(MODEL_INFO.get(MODELS[0], ""))
318
-
319
- def _update_info(m: str) -> str:
320
- title = f"**{m}**"
321
- desc = MODEL_INFO.get(m, "")
322
- return f"{title}\n\n{desc}"
323
- model_choice.change(_update_info, model_choice, info_md)
324
-
325
- subject_dd = gr.Dropdown(["رياضيات", "فيزياء", "كيمياء", "أحياء"], value="رياضيات", label="المادة")
326
- level_dd = gr.Dropdown(["ابتدائي", "إعدادي", "ثانوي", "جامعي"], value="جامعي", label="المستوى الدراسي")
327
- dialect_dd = gr.Dropdown(["فصحى", "مصري", "شامي", "خليجي", "مغربي"], value="مصري", label="الأسلوب/اللهجة")
328
- style_dd = gr.Radio(["خطوة بخطوة", "تلميحات أولًا", "إجابة نهائية فقط"], value="خطوة بخطوة", label="نمط الشرح")
329
- mode_dd = gr.Radio(["حل مسألة", "شرح مفهوم", "إنشاء تمارين"], value="حل مسألة", label="نوع الدرس")
330
-
331
- ex_label = gr.Markdown("**أمثلة سريعة**")
332
- examples = gr.Dropdown(
333
- [
334
- "أوجد قيمة x: 2x + 3 = 11",
335
- "اشتق الدالة f(x)=x^3 - 4x",
336
- "احسب عجلة جسم كتلته 2كج تؤثر عليه قوة 10ن",
337
- "ما الفرق بين الرابطة الأيونية والتساهمية؟",
338
- ], label="اختر مثالًا ثم اضغط إدراج")
339
- insert_btn = gr.Button("إدراج المثال")
340
-
341
- # gr.Image(LOGO_PATH, show_label=False, container=False)
342
-
343
- state = gr.State({"history": []})
344
-
345
- def on_insert(ex):
346
- return gr.update(value=ex or "")
347
-
348
- insert_btn.click(on_insert, examples, user_in)
349
-
350
- def on_submit(msg, chat_messages):
351
- if not msg:
352
- return "", (chat_messages or [])
353
- updated = (chat_messages or []) + [{"role": "user", "content": msg}]
354
- return "", updated
355
-
356
- def bot_step(chat_messages, chosen_model, st, subject_ar, level_ar, dialect_ar, style_ar, mode_label):
357
- if not chat_messages:
358
- return chat_messages, st
359
- last_user = None
360
- for m in reversed(chat_messages):
361
- if m.get("role") == "user":
362
- last_user = m.get("content")
363
- break
364
- if not last_user:
365
- return chat_messages, st
366
-
367
- mode = "solve" if mode_label == "حل مسألة" else ("explain" if mode_label == "شرح مفهوم" else "practice")
368
- msgs = build_messages(last_user, subject_ar, level_ar, dialect_ar, style_ar, mode)
369
- reply_raw = safe_chat_complete(chosen_model, msgs, max_tokens=1400)
370
-
371
- payload = extract_json(reply_raw)
372
- if payload is None:
373
- pretty = reply_raw or "تعذر الحصول على رد."
374
- else:
375
- pretty = format_lesson(payload)
376
-
377
- updated = (chat_messages or []) + [{"role": "assistant", "content": pretty}]
378
- st = st or {"history": []}
379
- st["history"] = (st.get("history") or []) + [{
380
- "user": last_user,
381
- "model": chosen_model,
382
- "subject": subject_ar,
383
- "level": level_ar,
384
- "dialect": dialect_ar,
385
- "style": style_ar,
386
- "mode": mode_label,
387
- "raw": reply_raw,
388
- }]
389
- return updated, st
390
-
391
- def on_clear():
392
- return [], {"history": []}
393
-
394
- user_in.submit(on_submit, [user_in, chat], [user_in, chat]) \
395
- .then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
396
-
397
- send_btn.click(on_submit, [user_in, chat], [user_in, chat]) \
398
- .then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
399
-
400
- clear_btn.click(on_clear, outputs=[chat, state])
401
-
402
-
403
-
404
- if __name__ == "__main__":
405
- demo.queue()
406
- demo.launch()
 
1
+ import os, sys, time, asyncio, json, re
2
+ from typing import List, Dict, Optional
3
+ import gradio as gr
4
+ from dotenv import load_dotenv
5
+ import base64
6
+ from openai import OpenAI
7
+
8
+ if sys.platform.startswith("win"):
9
+ try:
10
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
11
+ except Exception:
12
+ pass
13
+
14
+ load_dotenv()
15
+ APP_Name = os.getenv("APP_Name", "منصة دروس تفاعلية بالعربية")
16
+ APP_Version = os.getenv("APP_Version", "0.2.0")
17
+ API_KEY = os.getenv("API_KEY", "")
18
+
19
+ MODELS = [m.strip() for m in os.getenv("Models", "").split(",") if m.strip()] or [
20
+ "QwQ-32B",
21
+ "zai-org/GLM-4.5-Air",
22
+ ]
23
+
24
+ MODEL_INFO = {
25
+ "QwQ-32B": "QwQ-32B — مُعلّم استدلالي قوي للإجابات المفصّلة.",
26
+ "zai-org/GLM-4.5-Air": "GLM-4.5-Air — سريع وفعّال للشرح خطوة بخطوة.",
27
+ }
28
+
29
+ LOGO_PATH = "download.jpeg"
30
+ COMPANY_LOGO = LOGO_PATH
31
+ OWNER_NAME = "ENG. Ahmed Yasser El Sharkawy"
32
+
33
+ BASE_URL = "https://genai.ghaymah.systems"
34
+ client = OpenAI(api_key=API_KEY, base_url=BASE_URL) if API_KEY else None
35
+
36
+ CSS = """
37
+ :root { direction: rtl; }
38
+ * { font-family: "Tajawal", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
39
+ .app-header{display:flex;align-items:center;gap:12px;justify-content:center;margin:6px 0 16px}
40
+ .app-header img{height:60px;border-radius:12px}
41
+ .app-title{font-weight:800;font-size:28px;line-height:1.1}
42
+ .app-sub{opacity:.7;font-size:14px}
43
+ .gradio-container { direction: rtl; }
44
+ .markdown-body { direction: rtl; text-align: right; }
45
+ """
46
+
47
+ SYSTEM_SEED = (
48
+ "أنت معلّم عربي متميز. قدّم شروحًا تعليمية دقيقة ومبسطة، وراعِ مستوى الطالب،"
49
+ " وقدّم الحلول خطوة بخطوة عند الطلب. استخدم LaTeX للمعادلات بين $$...$$."
50
+ )
51
+
52
+ BACKOFF = [3, 6, 12]
53
+
54
+ def logo_data_uri(path: str) -> str:
55
+ if not os.path.exists(path):
56
+ return ""
57
+ ext = os.path.splitext(path)[1].lower()
58
+ mime = {
59
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
60
+ ".webp": "image/webp", ".gif": "image/gif"
61
+ }.get(ext, "image/png")
62
+ with open(path, "rb") as f:
63
+ b64 = base64.b64encode(f.read()).decode("utf-8")
64
+ return f"data:{mime};base64,{b64}"
65
+
66
+ DIALECT_MAP = {
67
+ "فصحى": "استخدم العربية الفصحى الواضحة.",
68
+ "مصري": "استخدم لهجة مصرية خفيفة مفهومة على نطاق واسع دون إسراف.",
69
+ "شامي": "استخدم لهجة شامية مبسطة ومفهومة.",
70
+ "خليجي": "استخدم لهجة خليجية مبسطة ومفهومة.",
71
+ "مغربي": "استخدم لهجة مغاربية مبسطة ومفهومة، وتوضيح المصطلحات إن لزم.",
72
+ }
73
+
74
+ SUBJECT_MAP = {
75
+ "رياضيات": "math",
76
+ "فيزياء": "physics",
77
+ "كيمياء": "chemistry",
78
+ "أحياء": "biology",
79
+ }
80
+
81
+ LEVEL_MAP = {
82
+ "ابتدائي": "elementary",
83
+ "إعدادي": "middle",
84
+ "ثانوي": "high",
85
+ "جامعي": "university",
86
+ }
87
+
88
+ STYLE_MAP = {
89
+ "خطوة بخطوة": "step_by_step",
90
+ "تلميحات أولًا": "hints_first",
91
+ "إجابة نهائية فقط": "final_only",
92
+ }
93
+
94
+ SCHEMA_GUIDE = {
95
+ "mode": "solve|explain|practice",
96
+ "subject": "math|physics|chemistry|biology",
97
+ "grade": "elementary|middle|high|university",
98
+ "dialect": "fosha|masri|shami|khaleeji|maghrebi",
99
+ "answer_style": "step_by_step|hints_first|final_only",
100
+ "steps": [{"title": "", "explanation": "", "math": ""}],
101
+ "final_answer": "",
102
+ "tips": [""],
103
+ "common_mistakes": [""],
104
+ "similar_exercises": [""],
105
+ "confidence": 0.0,
106
+ }
107
+
108
+ DIALECT_CODE = {
109
+ "فصحى": "fosha",
110
+ "مصري": "masri",
111
+ "شامي": "shami",
112
+ "خليجي": "khaleeji",
113
+ "مغربي": "maghrebi",
114
+ }
115
+
116
+ def build_messages(
117
+ prompt: str,
118
+ subject_ar: str,
119
+ level_ar: str,
120
+ dialect_ar: str,
121
+ style_ar: str,
122
+ mode: str,
123
+ ) -> List[Dict[str, str]]:
124
+ subject = SUBJECT_MAP.get(subject_ar, "math")
125
+ grade = LEVEL_MAP.get(level_ar, "university")
126
+ answer_style = STYLE_MAP.get(style_ar, "step_by_step")
127
+ dialect_note = DIALECT_MAP.get(dialect_ar, DIALECT_MAP["فصحى"])
128
+ dialect_code = DIALECT_CODE.get(dialect_ar, "masri")
129
+
130
+ system = (
131
+ f"{SYSTEM_SEED} {dialect_note} ركّز على الدقة والمنطق التربوي."
132
+ " إذا طلب الطالب برهانًا أو اشتقاقًا فاذكر الأفكار الرئيسية بإيجاز."
133
+ " تجنّب الحشو واطرح أسئلة فاحصة عند الحاجة."
134
+ )
135
+
136
+ user = (
137
+ "أنت الآن داخل منصة دروس عربية. أعد لي إخراجًا بصيغة JSON فقط دون أي نص زائد.\n"
138
+ "اتبع هذا المخطط الحرفي للمفاتيح: {"
139
+ "\"mode\", \"subject\", \"grade\", \"dialect\", \"answer_style\", \"steps\", \"final_answer\", \"tips\", \"common_mistakes\", \"similar_exercises\", \"confidence\"} .\n"
140
+ "- اكتب الشرح بالعربية وفق اللهجة المطلوبة.\n"
141
+ "- اكتب المعادلات داخل الحقل math بين $$ بهذه الصيغة: $$..$$.\n"
142
+ "- راعِ المستوى الدراسي.\n"
143
+ f"- subject='{subject}', grade='{grade}', dialect='{dialect_code}', answer_style='{answer_style}', mode='{mode}'.\n"
144
+ f"- مهمة الطالب: {prompt}\n"
145
+ "أعد JSON الصحيح فقط."
146
+ )
147
+ return [{"role": "system", "content": system}, {"role": "user", "content": user}]
148
+
149
+
150
+ def safe_chat_complete(model: str, messages: List[Dict], max_tokens: int = 1200) -> str:
151
+ if not client:
152
+ return "⚠️ لم يتم العثور على API_KEY في .env"
153
+ attempt = 0
154
+ while True:
155
+ try:
156
+ resp = client.chat.completions.create(
157
+ model=model,
158
+ messages=messages,
159
+ max_tokens=max_tokens,
160
+ temperature=0.2,
161
+ timeout=90,
162
+ )
163
+ return resp.choices[0].message.content or ""
164
+ except Exception as e:
165
+ msg = str(e)
166
+ if ("429" in msg or "Rate" in msg) and attempt < len(BACKOFF):
167
+ time.sleep(BACKOFF[attempt]); attempt += 1
168
+ continue
169
+ return f"فشل الطلب مع النموذج `{model}`: {e}"
170
+
171
+
172
+ def extract_json(text: str) -> Optional[Dict]:
173
+ if not text:
174
+ return None
175
+ try:
176
+ return json.loads(text)
177
+ except Exception:
178
+ pass
179
+ m = re.search(r"\{[\s\S]*\}", text)
180
+ if m:
181
+ try:
182
+ return json.loads(m.group(0))
183
+ except Exception:
184
+ return None
185
+ return None
186
+
187
+
188
+ def format_lesson(payload: Dict) -> str:
189
+ """حوّل استجابة JSON إلى Markdown سهل القراءة مع بطاقة ملخص.
190
+ متسامح مع تنسيقات steps/tips المختلفة.
191
+ """
192
+ if not isinstance(payload, dict):
193
+ return payload if isinstance(payload, str) else "تعذر تنسيق الرد."
194
+
195
+ subject = payload.get("subject", "math") or "math"
196
+ grade = payload.get("grade", "") or ""
197
+ answer_style = payload.get("answer_style", "") or ""
198
+ dialect_code = payload.get("dialect", "fosha") or "fosha"
199
+ mode = payload.get("mode", "solve") or "solve"
200
+
201
+ steps_raw = payload.get("steps", []) or []
202
+ final_answer = payload.get("final_answer", "") or ""
203
+ tips = payload.get("tips", []) or []
204
+ mistakes = payload.get("common_mistakes", []) or []
205
+ similars = payload.get("similar_exercises", []) or []
206
+
207
+ if isinstance(tips, str): tips = [tips]
208
+ if isinstance(mistakes, str): mistakes = [mistakes]
209
+ if isinstance(similars, str): similars = [similars]
210
+
211
+ steps: List[Dict[str, str]] = []
212
+ for i, st in enumerate(steps_raw, 1):
213
+ if isinstance(st, dict):
214
+ steps.append({
215
+ "title": st.get("title", f"الخطوة {i}") or f"الخطوة {i}",
216
+ "explanation": st.get("explanation", "") or "",
217
+ "math": st.get("math", "") or "",
218
+ })
219
+ elif isinstance(st, str):
220
+ steps.append({"title": f"الخطوة {i}", "explanation": st, "math": ""})
221
+ else:
222
+ continue
223
+
224
+ subject_icon = {"math": "🧮", "physics": "🧪", "chemistry": "⚗️", "biology": "🧬"}.get(subject, "📘")
225
+
226
+ # خرائط عرض عربية
227
+ SUBJECT_AR = {"math": "رياضيات", "physics": "فيزياء", "chemistry": "كيمياء", "biology": "أحياء"}
228
+ STYLE_AR = {"step_by_step": "خطوة بخطوة", "hints_first": "تلميحات أولًا", "final_only": "إجابة نهائية فقط"}
229
+ DIALECT_AR = {"fosha": "فصحى", "masri": "مصري", "shami": "شامي", "khaleeji": "خليجي", "maghrebi": "مغربي"}
230
+ MODE_AR = {"solve": "حل مسألة", "explain": "شرح مفهوم", "practice": "إنشاء تمارين"}
231
+
232
+ md: List[str] = []
233
+
234
+
235
+ # تفاصيل الشرح
236
+ if steps:
237
+ md.append("#### الخطوات")
238
+ for i, st in enumerate(steps, 1):
239
+ title = st.get("title", f"الخطوة {i}")
240
+ expl = st.get("explanation", "")
241
+ math = st.get("math", "")
242
+ md.append(f"**{i}. {title}**\n\n{expl}")
243
+ if math:
244
+ md.append(f"\\[ {math.replace('$$','')} \\]")
245
+
246
+ if final_answer:
247
+ md.append("#### الإجابة النهائية")
248
+ md.append(final_answer)
249
+
250
+ if tips:
251
+ md.append("#### نصائح للمذاكرة")
252
+ for t in tips:
253
+ md.append(f"- {t}")
254
+
255
+ if mistakes:
256
+ md.append("#### أخطاء شائعة")
257
+ for m in mistakes:
258
+ md.append(f"- {m}")
259
+
260
+ if similars:
261
+ md.append("#### تمارين مشابهة للتدريب")
262
+ for s in similars[:5]:
263
+ md.append(f"- {s}")
264
+
265
+ return "\n\n".join(md)
266
+
267
+
268
+ # Gradio
269
+ with gr.Blocks(title=f"{APP_Name} v{APP_Version}", css=CSS, theme=gr.themes.Soft()) as demo:
270
+ # MathJax لدعم LaTeX
271
+ gr.HTML(
272
+ """
273
+ <script>
274
+ if (!window.MathJax) {
275
+ window.MathJax = {tex: {inlineMath: [['$','$'], ['\\(','\\)']]}};
276
+ }
277
+ </script>
278
+ <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
279
+ """
280
+ )
281
+
282
+ header_logo_src = logo_data_uri(COMPANY_LOGO)
283
+ logo_html = f"<img src='{header_logo_src}' alt='logo'>" if header_logo_src else ""
284
+ gr.HTML(f"""
285
+ <div class="app-header">
286
+ {logo_html}
287
+ <div class="app-header-text">
288
+ <div class="app-title">{APP_Name}</div>
289
+ <div class="app-sub">v{APP_Version} • {OWNER_NAME}</div>
290
+ </div>
291
+ </div>
292
+ """)
293
+
294
+ with gr.Row():
295
+ with gr.Column(scale=3):
296
+ chat = gr.Chatbot(label="جلسة الدرس", height=520, type="messages", value=[])
297
+ user_in = gr.Textbox(label="سؤال الطالب / المسألة", placeholder="اكتب السؤال هنا… مثلاً: احسب قيمة التعبير 2x+3 عند x=5", lines=2)
298
+ with gr.Row():
299
+ send_btn = gr.Button("إرسال ✨", variant="primary")
300
+ clear_btn = gr.Button("مسح المحادثة")
301
+ gr.Markdown(
302
+ """
303
+ > **ملاحظة:** تُستخدم هذه المنصة لأغراض تعليمية. تحقّق دائمًا من النتائج خاصة في المسائل المتقدمة.
304
+
305
+ جرّب كتابة: **حلّل العبارة التربيعية x^2 - 5x + 6** أو **اشرح قانون نيوتن الثاني**.
306
+ """
307
+ )
308
+
309
+
310
+ with gr.Column(scale=2, min_width=340):
311
+ model_choice = gr.Radio(
312
+ choices=MODELS,
313
+ value=MODELS[0],
314
+ label="النموذج",
315
+ info=" GLM-4.5-Air",
316
+ )
317
+ info_md = gr.Markdown(MODEL_INFO.get(MODELS[0], ""))
318
+
319
+ def _update_info(m: str) -> str:
320
+ title = f"**{m}**"
321
+ desc = MODEL_INFO.get(m, "")
322
+ return f"{title}\n\n{desc}"
323
+ model_choice.change(_update_info, model_choice, info_md)
324
+
325
+ subject_dd = gr.Dropdown(["رياضيات", "فيزياء", "كيمياء", "أحياء"], value="رياضيات", label="المادة")
326
+ level_dd = gr.Dropdown(["ابتدائي", "إعدادي", "ثانوي", "جامعي"], value="جامعي", label="المستوى الدراسي")
327
+ dialect_dd = gr.Dropdown(["فصحى", "مصري", "شامي", "خليجي", "مغربي"], value="مصري", label="الأسلوب/اللهجة")
328
+ style_dd = gr.Radio(["خطوة بخطوة", "تلميحات أولًا", "إجابة نهائية فقط"], value="خطوة بخطوة", label="نمط الشرح")
329
+ mode_dd = gr.Radio(["حل مسألة", "شرح مفهوم", "إنشاء تمارين"], value="حل مسألة", label="نوع الدرس")
330
+
331
+ ex_label = gr.Markdown("**أمثلة سريعة**")
332
+ examples = gr.Dropdown(
333
+ [
334
+ "أوجد قيمة x: 2x + 3 = 11",
335
+ "اشتق الدالة f(x)=x^3 - 4x",
336
+ "احسب عجلة جسم كتلته 2كج تؤثر عليه قوة 10ن",
337
+ "ما الفرق بين الرابطة الأيونية وال��ساهمية؟",
338
+ ], label="اختر مثالًا ثم اضغط إدراج")
339
+ insert_btn = gr.Button("إدراج المثال")
340
+
341
+ # gr.Image(LOGO_PATH, show_label=False, container=False)
342
+
343
+ state = gr.State({"history": []})
344
+
345
+ def on_insert(ex):
346
+ return gr.update(value=ex or "")
347
+
348
+ insert_btn.click(on_insert, examples, user_in)
349
+
350
+ def on_submit(msg, chat_messages):
351
+ if not msg:
352
+ return "", (chat_messages or [])
353
+ updated = (chat_messages or []) + [{"role": "user", "content": msg}]
354
+ return "", updated
355
+
356
+ def bot_step(chat_messages, chosen_model, st, subject_ar, level_ar, dialect_ar, style_ar, mode_label):
357
+ if not chat_messages:
358
+ return chat_messages, st
359
+ last_user = None
360
+ for m in reversed(chat_messages):
361
+ if m.get("role") == "user":
362
+ last_user = m.get("content")
363
+ break
364
+ if not last_user:
365
+ return chat_messages, st
366
+
367
+ mode = "solve" if mode_label == "حل مسألة" else ("explain" if mode_label == "شرح مفهوم" else "practice")
368
+ msgs = build_messages(last_user, subject_ar, level_ar, dialect_ar, style_ar, mode)
369
+ reply_raw = safe_chat_complete(chosen_model, msgs, max_tokens=1400)
370
+
371
+ payload = extract_json(reply_raw)
372
+ if payload is None:
373
+ pretty = reply_raw or "تعذر الحصول على رد."
374
+ else:
375
+ pretty = format_lesson(payload)
376
+
377
+ updated = (chat_messages or []) + [{"role": "assistant", "content": pretty}]
378
+ st = st or {"history": []}
379
+ st["history"] = (st.get("history") or []) + [{
380
+ "user": last_user,
381
+ "model": chosen_model,
382
+ "subject": subject_ar,
383
+ "level": level_ar,
384
+ "dialect": dialect_ar,
385
+ "style": style_ar,
386
+ "mode": mode_label,
387
+ "raw": reply_raw,
388
+ }]
389
+ return updated, st
390
+
391
+ def on_clear():
392
+ return [], {"history": []}
393
+
394
+ user_in.submit(on_submit, [user_in, chat], [user_in, chat]) \
395
+ .then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
396
+
397
+ send_btn.click(on_submit, [user_in, chat], [user_in, chat]) \
398
+ .then(bot_step, [chat, model_choice, state, subject_dd, level_dd, dialect_dd, style_dd, mode_dd], [chat, state])
399
+
400
+ clear_btn.click(on_clear, outputs=[chat, state])
401
+
402
+
403
+
404
+ if __name__ == "__main__":
405
+ demo.queue()
406
+ demo.launch()