Dooratre commited on
Commit
94cba3f
·
verified ·
1 Parent(s): b5d0134

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1153 -1161
templates/index.html CHANGED
@@ -1,1162 +1,1154 @@
1
- <!DOCTYPE html>
2
- <html lang="ar" dir="rtl">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
- <title>مساعد ITGS226 - مقدمة في الإنترنت</title>
7
-
8
- <!-- Highlight.js -->
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
11
-
12
- <!-- Arabic font -->
13
- <link rel="preconnect" href="https://fonts.googleapis.com">
14
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
- <link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
16
-
17
- <style>
18
- :root{
19
- --bg0:#0b1220;
20
- --bg1:#0f1a2e;
21
- --text:#eaf0ff;
22
- --muted:#a9b7d6;
23
- --brand:#6ea8ff;
24
- --brand2:#7c5cff;
25
-
26
- --shadow: 0 10px 30px rgba(0,0,0,.35);
27
- --radius: 18px;
28
- }
29
-
30
- *{ box-sizing:border-box; margin:0; padding:0; }
31
- html, body { height:100%; }
32
- body{
33
- font-family: "Tajawal", system-ui, -apple-system, Segoe UI, Arial, sans-serif;
34
- background:
35
- radial-gradient(1200px 700px at 20% 10%, rgba(124,92,255,.25), transparent 60%),
36
- radial-gradient(900px 600px at 80% 20%, rgba(110,168,255,.22), transparent 55%),
37
- linear-gradient(180deg, var(--bg0), var(--bg1));
38
- color: var(--text);
39
- overflow: hidden;
40
- }
41
-
42
- .app{
43
- height: 100dvh;
44
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
45
- display:flex;
46
- flex-direction:column;
47
- }
48
-
49
- .topbar{
50
- flex: 0 0 auto;
51
- padding: 14px 14px 10px;
52
- display:flex;
53
- align-items:center;
54
- justify-content:space-between;
55
- gap: 10px;
56
- }
57
-
58
- .brand{
59
- display:flex;
60
- align-items:center;
61
- gap: 10px;
62
- min-width: 0;
63
- }
64
-
65
- .logo{
66
- width: 42px; height: 42px;
67
- border-radius: 14px;
68
- background: linear-gradient(135deg, rgba(110,168,255,.95), rgba(124,92,255,.95));
69
- box-shadow: 0 10px 25px rgba(124,92,255,.25);
70
- display:grid;
71
- place-items:center;
72
- font-weight:800;
73
- letter-spacing:.5px;
74
- }
75
-
76
- .brandText{
77
- min-width:0;
78
- display:flex;
79
- flex-direction:column;
80
- gap:2px;
81
- }
82
- .brandText .title{
83
- font-size: 16px;
84
- font-weight: 800;
85
- white-space:nowrap;
86
- overflow:hidden;
87
- text-overflow:ellipsis;
88
- }
89
- .brandText .subtitle{
90
- font-size: 12px;
91
- color: var(--muted);
92
- white-space:nowrap;
93
- overflow:hidden;
94
- text-overflow:ellipsis;
95
- }
96
-
97
- .actions{
98
- display:flex;
99
- gap: 8px;
100
- flex: 0 0 auto;
101
- }
102
-
103
- .btn{
104
- border: 1px solid rgba(255,255,255,.10);
105
- background: rgba(15,27,51,.55);
106
- color: var(--text);
107
- padding: 10px 12px;
108
- border-radius: 12px;
109
- cursor:pointer;
110
- font-weight:700;
111
- font-size: 13px;
112
- transition: transform .12s ease, border-color .12s ease;
113
- backdrop-filter: blur(10px);
114
- -webkit-backdrop-filter: blur(10px);
115
- display:inline-flex;
116
- align-items:center;
117
- gap: 8px;
118
- user-select:none;
119
- }
120
- .btn:hover{ transform: translateY(-1px); border-color: rgba(110,168,255,.35); }
121
- .btn:active{ transform: translateY(0px); }
122
- .btn.danger:hover{ border-color: rgba(251,113,133,.45); }
123
-
124
- .chatCard{
125
- flex: 1 1 auto;
126
- margin: 0 14px 12px;
127
- border-radius: var(--radius);
128
- border: 1px solid rgba(255,255,255,.10);
129
- background: rgba(15,27,51,.35);
130
- box-shadow: var(--shadow);
131
- overflow:hidden;
132
- display:flex;
133
- flex-direction:column;
134
- min-height: 0;
135
- backdrop-filter: blur(12px);
136
- -webkit-backdrop-filter: blur(12px);
137
- }
138
-
139
- .messages{
140
- flex: 1 1 auto;
141
- padding: 16px 14px;
142
- overflow:auto;
143
- scroll-behavior:smooth;
144
- min-height: 0;
145
- }
146
-
147
- .messages::-webkit-scrollbar{ width: 10px; }
148
- .messages::-webkit-scrollbar-track{ background: transparent; }
149
- .messages::-webkit-scrollbar-thumb{
150
- background: rgba(255,255,255,.12);
151
- border-radius: 10px;
152
- border: 2px solid transparent;
153
- background-clip: content-box;
154
- }
155
-
156
- .empty{
157
- padding: 22px 14px;
158
- border: 1px dashed rgba(255,255,255,.18);
159
- border-radius: 16px;
160
- background: rgba(0,0,0,.12);
161
- color: var(--muted);
162
- line-height: 1.9;
163
- }
164
- .empty h2{
165
- color: var(--text);
166
- font-size: 18px;
167
- margin-bottom: 6px;
168
- font-weight: 800;
169
- }
170
-
171
- .msgRow{
172
- display:flex;
173
- gap: 10px;
174
- margin-bottom: 12px;
175
- align-items:flex-end;
176
- }
177
- .msgRow.user{ justify-content:flex-start; flex-direction: row-reverse; }
178
- .msgRow.ai{ justify-content:flex-start; }
179
-
180
- /* ===== Pro avatar + name ===== */
181
- .avatarCol{
182
- width: 56px;
183
- flex: 0 0 auto;
184
- display:flex;
185
- flex-direction:column;
186
- align-items:center;
187
- gap: 6px;
188
- }
189
-
190
- .avatar{
191
- width: 44px; height: 44px;
192
- border-radius: 16px;
193
- display:grid;
194
- place-items:center;
195
- border: 1px solid rgba(255,255,255,.12);
196
- box-shadow: 0 10px 25px rgba(0,0,0,.25);
197
- overflow:hidden;
198
- position: relative;
199
- user-select:none;
200
- }
201
-
202
- .avatarName{
203
- max-width: 56px;
204
- font-size: 11px;
205
- color: rgba(234,240,255,.78);
206
- text-align:center;
207
- white-space:nowrap;
208
- overflow:hidden;
209
- text-overflow:ellipsis;
210
- line-height: 1.2;
211
- }
212
-
213
- .avatar.ai{
214
- background: linear-gradient(135deg, rgba(110,168,255,.25), rgba(124,92,255,.25));
215
- color: rgba(234,240,255,.95);
216
- font-weight: 900;
217
- letter-spacing: .5px;
218
- }
219
-
220
- /* Normal user: emoji avatar (not text) */
221
- .avatar.userDefault{
222
- background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.25), transparent 55%),
223
- linear-gradient(135deg, rgba(110,168,255,.75), rgba(124,92,255,.35));
224
- border-color: rgba(110,168,255,.35);
225
- color: #081022;
226
- font-size: 20px;
227
- }
228
-
229
- /* Bissan: flower + pink bg + subtle sparkle */
230
- .avatar.userBissan{
231
- background:
232
- radial-gradient(circle at 25% 25%, rgba(255,255,255,.45), transparent 55%),
233
- radial-gradient(circle at 70% 30%, rgba(255,255,255,.25), transparent 60%),
234
- linear-gradient(135deg, rgba(255,105,180,.75), rgba(255,182,193,.35));
235
- border-color: rgba(255,105,180,.40);
236
- color: #2b0b1a;
237
- font-size: 20px;
238
- }
239
- .avatar.userBissan::after{
240
- content:"";
241
- position:absolute;
242
- inset:-40%;
243
- background: conic-gradient(from 180deg, transparent, rgba(255,255,255,.25), transparent);
244
- animation: spin 3.8s linear infinite;
245
- opacity:.55;
246
- }
247
- .avatar.userBissan > span{
248
- position: relative;
249
- z-index: 2;
250
- filter: drop-shadow(0 6px 10px rgba(0,0,0,.25));
251
- }
252
- @keyframes spin{
253
- to{ transform: rotate(360deg); }
254
- }
255
-
256
- .bubbleWrap{
257
- max-width: min(760px, 86%);
258
- display:flex;
259
- flex-direction:column;
260
- gap: 6px;
261
- min-width: 0;
262
- }
263
-
264
- .bubble{
265
- border-radius: 16px;
266
- padding: 12px 14px;
267
- line-height: 1.95;
268
- border: 1px solid rgba(255,255,255,.10);
269
- background: rgba(0,0,0,.12);
270
- word-break: break-word;
271
- overflow-wrap: anywhere;
272
- white-space: normal;
273
- }
274
- .user .bubble{
275
- background: linear-gradient(135deg, rgba(110,168,255,.18), rgba(124,92,255,.18));
276
- border-color: rgba(110,168,255,.22);
277
- }
278
- .ai .bubble{
279
- background: rgba(15,26,46,.55);
280
- border-color: rgba(255,255,255,.10);
281
- }
282
-
283
- .meta{
284
- font-size: 12px;
285
- color: var(--muted);
286
- padding: 0 6px;
287
- display:flex;
288
- gap: 10px;
289
- align-items:center;
290
- flex-wrap:wrap;
291
- }
292
- .pill{
293
- font-size: 12px;
294
- padding: 4px 10px;
295
- border-radius: 999px;
296
- border: 1px solid rgba(255,255,255,.10);
297
- background: rgba(0,0,0,.12);
298
- color: var(--muted);
299
- }
300
-
301
- .arabicText{
302
- direction: rtl;
303
- text-align: right;
304
- unicode-bidi: plaintext;
305
- }
306
-
307
- /* Code blocks */
308
- .codeBlock{
309
- margin: 10px 0;
310
- border-radius: 14px;
311
- overflow:hidden;
312
- border: 1px solid rgba(255,255,255,.10);
313
- background: #1e1e1e;
314
- }
315
- .codeHeader{
316
- display:flex;
317
- align-items:center;
318
- justify-content:space-between;
319
- padding: 8px 10px;
320
- background: #252526;
321
- border-bottom: 1px solid #2a2a2a;
322
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
323
- direction:ltr;
324
- }
325
- .codeLeft{ display:flex; align-items:center; gap: 10px; min-width:0; }
326
- .dots{ display:flex; gap:6px; }
327
- .dot{ width:10px; height:10px; border-radius:50%; display:inline-block; }
328
- .dot.r{ background:#ff5f56; }
329
- .dot.y{ background:#ffbd2e; }
330
- .dot.g{ background:#27c93f; }
331
- .lang{
332
- font-size: 12px;
333
- color:#cfcfcf;
334
- opacity:.9;
335
- white-space:nowrap;
336
- overflow:hidden;
337
- text-overflow:ellipsis;
338
- max-width: 220px;
339
- text-transform: lowercase;
340
- }
341
- .copyBtn{
342
- border: 1px solid #3c3c3c;
343
- background: transparent;
344
- color:#cfcfcf;
345
- padding: 6px 10px;
346
- border-radius: 10px;
347
- cursor:pointer;
348
- font-size: 12px;
349
- }
350
- .copyBtn:hover{ background:#2a2d2e; border-color:#4c4c4c; }
351
- .copyBtn.copied{ background:#0e639c; border-color:#0e639c; color:#fff; }
352
-
353
- pre{ margin:0; overflow:auto; }
354
- pre code{
355
- display:block;
356
- padding: 14px 14px;
357
- font-size: 13px;
358
- line-height: 1.7;
359
- direction:ltr;
360
- text-align:left;
361
- white-space: pre;
362
- tab-size: 4;
363
- }
364
-
365
- /* Composer */
366
- .composer{
367
- flex: 0 0 auto;
368
- padding: 12px;
369
- border-top: 1px solid rgba(255,255,255,.10);
370
- background: rgba(15,27,51,.45);
371
- backdrop-filter: blur(12px);
372
- -webkit-backdrop-filter: blur(12px);
373
- }
374
- .composerRow{
375
- display:flex;
376
- gap: 10px;
377
- align-items:flex-end;
378
- }
379
- textarea{
380
- flex: 1 1 auto;
381
- resize:none;
382
- max-height: 160px;
383
- min-height: 46px;
384
- padding: 12px 12px;
385
- border-radius: 14px;
386
- border: 1px solid rgba(255,255,255,.12);
387
- background: rgba(0,0,0,.18);
388
- color: var(--text);
389
- outline:none;
390
- font-size: 14px;
391
- line-height: 1.8;
392
- font-family: inherit;
393
- }
394
- textarea::placeholder{ color: rgba(234,240,255,.55); }
395
-
396
- .sendBtn{
397
- flex: 0 0 auto;
398
- padding: 12px 14px;
399
- border-radius: 14px;
400
- border: 1px solid rgba(110,168,255,.35);
401
- background: linear-gradient(135deg, rgba(110,168,255,.85), rgba(124,92,255,.85));
402
- color: #081022;
403
- font-weight: 900;
404
- cursor:pointer;
405
- min-width: 92px;
406
- }
407
- .sendBtn:hover{ transform: translateY(-1px); filter: brightness(1.05); }
408
- .sendBtn:disabled{ opacity:.55; cursor:not-allowed; transform:none; }
409
-
410
- /* Thinking bubble */
411
- .thinking{
412
- display:inline-flex;
413
- align-items:center;
414
- gap: 10px;
415
- color: var(--muted);
416
- font-weight: 700;
417
- }
418
- .dotsAnim{ display:inline-flex; gap: 4px; align-items:center; }
419
- .dotsAnim span{
420
- width: 6px; height: 6px;
421
- border-radius: 50%;
422
- background: rgba(234,240,255,.55);
423
- display:inline-block;
424
- animation: bounce 1.1s infinite ease-in-out;
425
- }
426
- .dotsAnim span:nth-child(2){ animation-delay: .15s; }
427
- .dotsAnim span:nth-child(3){ animation-delay: .30s; }
428
- @keyframes bounce{
429
- 0%, 80%, 100%{ transform: translateY(0); opacity:.55; }
430
- 40%{ transform: translateY(-5px); opacity:1; }
431
- }
432
-
433
- /* Typing cursor */
434
- .typeCursor{
435
- display:inline-block;
436
- width: 10px;
437
- margin-right: 2px;
438
- opacity: .9;
439
- animation: blink 1s infinite;
440
- }
441
- @keyframes blink{ 0%,50%{opacity:1} 51%,100%{opacity:0} }
442
-
443
- /* MCQ */
444
- .mcq{
445
- margin-top: 10px;
446
- border-radius: 16px;
447
- border: 1px solid rgba(255,255,255,.10);
448
- background: rgba(0,0,0,.12);
449
- overflow:hidden;
450
- }
451
- .mcqHead{
452
- padding: 12px 12px;
453
- border-bottom: 1px solid rgba(255,255,255,.10);
454
- display:flex;
455
- align-items:center;
456
- justify-content:space-between;
457
- gap: 10px;
458
- }
459
- .mcqTitle{ font-weight: 900; color: var(--text); line-height: 1.7; }
460
- .mcqBadge{
461
- font-size: 12px;
462
- padding: 4px 10px;
463
- border-radius: 999px;
464
- border: 1px solid rgba(255,255,255,.10);
465
- color: var(--muted);
466
- background: rgba(15,26,46,.55);
467
- white-space:nowrap;
468
- }
469
- .mcqBody{ padding: 10px 10px 12px; display:flex; flex-direction:column; gap: 8px; }
470
- .opt{
471
- padding: 12px 12px;
472
- border-radius: 14px;
473
- border: 1px solid rgba(255,255,255,.10);
474
- background: rgba(15,26,46,.45);
475
- cursor:pointer;
476
- display:flex;
477
- align-items:center;
478
- gap: 10px;
479
- user-select:none;
480
- }
481
- .opt:hover{ transform: translateY(-1px); border-color: rgba(110,168,255,.35); }
482
- .opt.disabled{ cursor: default; opacity: .95; }
483
- .opt.correct{ border-color: rgba(45,212,191,.55); background: rgba(45,212,191,.12); }
484
- .opt.wrong{ border-color: rgba(251,113,133,.55); background: rgba(251,113,133,.10); }
485
- .optKey{
486
- width: 30px; height: 30px;
487
- border-radius: 12px;
488
- display:grid;
489
- place-items:center;
490
- font-weight: 900;
491
- background: rgba(255,255,255,.08);
492
- border: 1px solid rgba(255,255,255,.10);
493
- flex: 0 0 auto;
494
- }
495
- .optText{ flex: 1 1 auto; line-height: 1.8; }
496
- .optIcon{ flex: 0 0 auto; font-weight: 900; }
497
-
498
- @media (max-width: 520px){
499
- .topbar{ padding: 12px 12px 8px; }
500
- .chatCard{ margin: 0 10px 10px; border-radius: 16px; }
501
- .messages{ padding: 14px 12px; }
502
- .bubbleWrap{ max-width: 92%; }
503
- .sendBtn{ min-width: 78px; }
504
- .avatarCol{ width: 52px; }
505
- .avatarName{ max-width: 52px; }
506
- }
507
- </style>
508
- </head>
509
-
510
- <body>
511
- <div class="app">
512
- <div class="topbar">
513
- <div class="brand">
514
- <div class="logo">IT</div>
515
- <div class="brandText">
516
- <div class="title">مساعد ITGS226</div>
517
- <div class="subtitle">مقدمة في الإنترنت • دردشة ذ��ية + أسئلة اختيار من متعدد</div>
518
- </div>
519
- </div>
520
-
521
- <div class="actions">
522
- <button class="btn danger" onclick="clearHistory()">مسح المحادثة</button>
523
- </div>
524
- </div>
525
-
526
- <div class="chatCard">
527
- <div class="messages" id="messages">
528
- <div class="empty" id="emptyState">
529
- <h2>أهلًا بك</h2>
530
- <p>
531
- اكتب سؤالك في مادة <b>ITGS226 مقدمة في الإنترنت</b> وسأساعدك بشرح واضح ومنظم.
532
- <br/>يمكنني أيضًا إنشاء أسئلة اختيار من متعدد (MCQ) للتدريب.
533
- </p>
534
- <p style="margin-top:10px;color:rgba(234,240,255,.75)">
535
- لتحديد اسم المستخدم من الرابط:
536
- <br>
537
- <code style="direction:ltr;unicode-bidi:plaintext">/?name=Ahmed</code>
538
- <br>
539
- أو
540
- <code style="direction:ltr;unicode-bidi:plaintext">/?name=بيسان</code>
541
- </p>
542
- </div>
543
- </div>
544
-
545
- <div class="composer">
546
- <div class="composerRow">
547
- <textarea id="input" rows="1" placeholder="اكتب سؤالك هنا... (Enter للإرسال، Shift+Enter لسطر جديد)"></textarea>
548
- <button class="sendBtn" id="sendBtn" onclick="sendMessage()">إرسال</button>
549
- </div>
550
- </div>
551
- </div>
552
- </div>
553
-
554
- <script>
555
- const messagesEl = document.getElementById('messages');
556
- const inputEl = document.getElementById('input');
557
- const sendBtn = document.getElementById('sendBtn');
558
- const emptyState = document.getElementById('emptyState');
559
-
560
- // Loaded from backend (/session_info)
561
- let USER_PROFILE = {
562
- display_name: 'أنت',
563
- is_bissan: false
564
- };
565
-
566
- // MCQ state
567
- let mcqAnswers = {};
568
- let mcqCounter = 0;
569
-
570
- // Auto-resize textarea
571
- inputEl.addEventListener('input', function(){
572
- this.style.height = 'auto';
573
- this.style.height = Math.min(this.scrollHeight, 160) + 'px';
574
- });
575
-
576
- inputEl.addEventListener('keydown', function(e){
577
- if (e.key === 'Enter' && !e.shiftKey){
578
- e.preventDefault();
579
- sendMessage();
580
- }
581
- });
582
-
583
- function scrollToBottom(){
584
- messagesEl.scrollTop = messagesEl.scrollHeight;
585
- }
586
-
587
- function escapeHtml(text){
588
- const div = document.createElement('div');
589
- div.textContent = text;
590
- return div.innerHTML;
591
- }
592
-
593
- function normalizeTextClient(text){
594
- if (!text) return '';
595
- return text
596
- .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069]/g, '')
597
- .replace(/\r\n/g, '\n')
598
- .replace(/\r/g, '\n')
599
- .replace(/\n{3,}/g, '\n\n')
600
- .trim();
601
- }
602
-
603
- function parseCodeBlocks(text){
604
- const codeRegex = /<code_(\w+)>([\s\S]*?)<\/code_\1>/g;
605
- const parts = [];
606
- let lastIndex = 0;
607
- let match;
608
-
609
- while ((match = codeRegex.exec(text)) !== null){
610
- if (match.index > lastIndex){
611
- parts.push({ type:'text', content: text.substring(lastIndex, match.index) });
612
- }
613
- parts.push({ type:'code', language: match[1], content: match[2].trim() });
614
- lastIndex = match.index + match[0].length;
615
- }
616
- if (lastIndex < text.length){
617
- parts.push({ type:'text', content: text.substring(lastIndex) });
618
- }
619
- return parts.length ? parts : [{ type:'text', content:text }];
620
- }
621
-
622
- function createCodeBlock(language, code){
623
- const wrap = document.createElement('div');
624
- wrap.className = 'codeBlock';
625
-
626
- const header = document.createElement('div');
627
- header.className = 'codeHeader';
628
-
629
- const left = document.createElement('div');
630
- left.className = 'codeLeft';
631
-
632
- const dots = document.createElement('div');
633
- dots.className = 'dots';
634
- dots.innerHTML = `<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>`;
635
-
636
- const lang = document.createElement('span');
637
- lang.className = 'lang';
638
- lang.textContent = (language || 'text').toLowerCase();
639
-
640
- left.appendChild(dots);
641
- left.appendChild(lang);
642
-
643
- const copyBtn = document.createElement('button');
644
- copyBtn.className = 'copyBtn';
645
- copyBtn.textContent = 'نسخ';
646
- copyBtn.addEventListener('click', () => copyCode(code, copyBtn));
647
-
648
- header.appendChild(left);
649
- header.appendChild(copyBtn);
650
-
651
- const pre = document.createElement('pre');
652
- const codeEl = document.createElement('code');
653
- const safeLang = (language || 'plaintext').toLowerCase();
654
- codeEl.className = `language-${safeLang}`;
655
- codeEl.textContent = code;
656
-
657
- pre.appendChild(codeEl);
658
- wrap.appendChild(header);
659
- wrap.appendChild(pre);
660
-
661
- setTimeout(() => { try { hljs.highlightElement(codeEl); } catch(e){} }, 0);
662
- return wrap;
663
- }
664
-
665
- async function copyCode(code, btn){
666
- try{
667
- await navigator.clipboard.writeText(code);
668
- btn.classList.add('copied');
669
- btn.textContent = 'تم النسخ';
670
- setTimeout(() => {
671
- btn.classList.remove('copied');
672
- btn.textContent = 'نسخ';
673
- }, 1200);
674
- }catch(e){
675
- btn.textContent = 'فشل النسخ';
676
- setTimeout(() => btn.textContent = 'نسخ', 1200);
677
- }
678
- }
679
-
680
- function parseMCQ(text){
681
- const mcqRegex = /<mcq>([\s\S]*?)<\/mcq>/g;
682
- const mcqs = [];
683
- let match;
684
-
685
- while ((match = mcqRegex.exec(text)) !== null){
686
- const mcqContent = match[1];
687
- const qMatch = mcqContent.match(/<q>([\s\S]*?)<\/q>/);
688
- const question = qMatch ? qMatch[1].trim() : '';
689
-
690
- const options = [];
691
- const optionRegex = /<(true|false)>([\s\S]*?)<\/(true|false)>/g;
692
- let optMatch;
693
- while ((optMatch = optionRegex.exec(mcqContent)) !== null){
694
- options.push({ text: optMatch[2].trim(), isCorrect: optMatch[1] === 'true' });
695
- }
696
-
697
- if (question && options.length) mcqs.push({ question, options });
698
- }
699
- return mcqs;
700
- }
701
-
702
- function removeMCQTags(text){
703
- return text.replace(/<mcq>[\s\S]*?<\/mcq>/g, '').trim();
704
- }
705
-
706
- function createMCQElement(mcqData, mcqId){
707
- const box = document.createElement('div');
708
- box.className = 'mcq';
709
- box.dataset.mcqId = mcqId;
710
-
711
- const head = document.createElement('div');
712
- head.className = 'mcqHead';
713
-
714
- const title = document.createElement('div');
715
- title.className = 'mcqTitle arabicText';
716
- title.textContent = `سؤال ${mcqId + 1}: ${mcqData.question}`;
717
-
718
- const badge = document.createElement('div');
719
- badge.className = 'mcqBadge';
720
- badge.textContent = 'اختر الإجابة الصحيحة';
721
-
722
- head.appendChild(title);
723
- head.appendChild(badge);
724
-
725
- const body = document.createElement('div');
726
- body.className = 'mcqBody';
727
-
728
- mcqData.options.forEach((opt, idx) => {
729
- const row = document.createElement('div');
730
- row.className = 'opt arabicText';
731
- row.dataset.correct = opt.isCorrect ? 'true' : 'false';
732
- row.dataset.index = idx;
733
-
734
- const key = document.createElement('div');
735
- key.className = 'optKey';
736
- key.textContent = String.fromCharCode(65 + idx);
737
-
738
- const text = document.createElement('div');
739
- text.className = 'optText';
740
- text.textContent = opt.text;
741
-
742
- const icon = document.createElement('div');
743
- icon.className = 'optIcon';
744
- icon.textContent = '';
745
-
746
- row.appendChild(key);
747
- row.appendChild(text);
748
- row.appendChild(icon);
749
-
750
- row.addEventListener('click', () => handleMCQClick(mcqId, idx, row, body));
751
- body.appendChild(row);
752
- });
753
-
754
- box.appendChild(head);
755
- box.appendChild(body);
756
- return box;
757
- }
758
-
759
- function handleMCQClick(mcqId, selectedIndex, selectedRow, body){
760
- if (body.dataset.answered === 'true') return;
761
-
762
- const rows = body.querySelectorAll('.opt');
763
- body.dataset.answered = 'true';
764
-
765
- const isCorrect = selectedRow.dataset.correct === 'true';
766
-
767
- rows.forEach(r => r.classList.add('disabled'));
768
-
769
- if (isCorrect){
770
- selectedRow.classList.add('correct');
771
- selectedRow.querySelector('.optIcon').textContent = '✓';
772
- } else {
773
- selectedRow.classList.add('wrong');
774
- selectedRow.querySelector('.optIcon').textContent = '✗';
775
-
776
- rows.forEach(r => {
777
- if (r.dataset.correct === 'true'){
778
- r.classList.add('correct');
779
- r.querySelector('.optIcon').textContent = '✓';
780
- }
781
- });
782
- }
783
-
784
- mcqAnswers[mcqId] = selectedIndex + 1;
785
- }
786
-
787
- function getMCQAnswersText(){
788
- if (!Object.keys(mcqAnswers).length) return '';
789
- let t = '\n\n[إجابات أسئلة الاختيار السابقة:\n';
790
- Object.keys(mcqAnswers).sort((a,b)=>parseInt(a)-parseInt(b)).forEach(id=>{
791
- t += `${parseInt(id)+1}. ${mcqAnswers[id]}\n`;
792
- });
793
- t += ']';
794
- return t;
795
- }
796
-
797
- function createAvatarCol(role){
798
- const col = document.createElement('div');
799
- col.className = 'avatarCol';
800
-
801
- const avatar = document.createElement('div');
802
- const nameEl = document.createElement('div');
803
- nameEl.className = 'avatarName';
804
-
805
- if (role === 'ai'){
806
- avatar.className = 'avatar ai';
807
- avatar.textContent = 'AI';
808
- nameEl.textContent = 'المساعد';
809
- } else {
810
- if (USER_PROFILE.is_bissan){
811
- avatar.className = 'avatar userBissan';
812
- avatar.innerHTML = '<span>🌸</span>';
813
- nameEl.textContent = USER_PROFILE.display_name || 'بيسان';
814
- } else {
815
- avatar.className = 'avatar userDefault';
816
- avatar.textContent = '👤'; // emoji (not text)
817
- nameEl.textContent = USER_PROFILE.display_name || 'أنت';
818
- }
819
- }
820
-
821
- col.appendChild(avatar);
822
- col.appendChild(nameEl);
823
- return col;
824
- }
825
-
826
- function createMessageRow(role){
827
- if (emptyState) emptyState.remove();
828
-
829
- const row = document.createElement('div');
830
- row.className = `msgRow ${role}`;
831
-
832
- const avatarCol = createAvatarCol(role);
833
-
834
- const wrap = document.createElement('div');
835
- wrap.className = 'bubbleWrap';
836
-
837
- const bubble = document.createElement('div');
838
- bubble.className = 'bubble arabicText';
839
-
840
- const meta = document.createElement('div');
841
- meta.className = 'meta';
842
-
843
- wrap.appendChild(bubble);
844
- wrap.appendChild(meta);
845
-
846
- row.appendChild(avatarCol);
847
- row.appendChild(wrap);
848
-
849
- messagesEl.appendChild(row);
850
- scrollToBottom();
851
-
852
- return { row, bubble, meta, wrap };
853
- }
854
-
855
- function setMeta(metaEl, sheet, time){
856
- metaEl.innerHTML = '';
857
- if (sheet){
858
- const p1 = document.createElement('span');
859
- p1.className = 'pill';
860
- p1.textContent = `ورقة: ${sheet}`;
861
- metaEl.appendChild(p1);
862
- }
863
- if (time){
864
- const p2 = document.createElement('span');
865
- p2.className = 'pill';
866
- p2.textContent = `الوقت: ${time}`;
867
- metaEl.appendChild(p2);
868
- }
869
- }
870
-
871
- function addUserMessage(text){
872
- const { bubble } = createMessageRow('user');
873
- bubble.innerHTML = escapeHtml(normalizeTextClient(text)).replace(/\n/g,'<br>');
874
- }
875
-
876
- function showThinking(){
877
- const { row, bubble } = createMessageRow('ai');
878
- row.id = 'thinkingRow';
879
- bubble.innerHTML = `
880
- <div class="thinking arabicText">
881
- <span>جاري التفكير</span>
882
- <span class="dotsAnim" aria-hidden="true">
883
- <span></span><span></span><span></span>
884
- </span>
885
- </div>
886
- `;
887
- }
888
-
889
- function hideThinking(){
890
- const r = document.getElementById('thinkingRow');
891
- if (r) r.remove();
892
- }
893
-
894
- /* Mini-Markdown */
895
- function renderMiniMarkdownToHtml(text){
896
- let s = normalizeTextClient(text);
897
- s = escapeHtml(s);
898
-
899
- s = s.replace(/^\s*---\s*$/gm, '<hr>');
900
- s = s.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
901
- s = s.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
902
- s = s.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
903
- s = s.replace(/`([^`]+)`/g, '<code class="inlineCode">$1</code>');
904
-
905
- const lines = s.split('\n');
906
- let out = '';
907
- let inUl = false;
908
- let inOl = false;
909
-
910
- const closeLists = () => {
911
- if (inUl){ out += '</ul>'; inUl = false; }
912
- if (inOl){ out += '</ol>'; inOl = false; }
913
- };
914
-
915
- for (let line of lines){
916
- const ulMatch = line.match(/^\s*-\s+(.+)$/);
917
- const olMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
918
-
919
- if (ulMatch){
920
- if (inOl){ out += '</ol>'; inOl = false; }
921
- if (!inUl){ out += '<ul>'; inUl = true; }
922
- out += `<li>${ulMatch[1]}</li>`;
923
- continue;
924
- }
925
-
926
- if (olMatch){
927
- if (inUl){ out += '</ul>'; inUl = false; }
928
- if (!inOl){ out += '<ol>'; inOl = true; }
929
- out += `<li>${olMatch[2]}</li>`;
930
- continue;
931
- }
932
-
933
- closeLists();
934
-
935
- if (line.trim() === ''){
936
- out += '<br>';
937
- } else if (line.startsWith('<h2>') || line.startsWith('<h3>') || line.startsWith('<hr>')){
938
- out += line;
939
- } else {
940
- out += `<p>${line}</p>`;
941
- }
942
- }
943
-
944
- closeLists();
945
- return out;
946
- }
947
-
948
- function renderAssistantParts(containerBubble, parts){
949
- containerBubble.innerHTML = '';
950
- parts.forEach(part => {
951
- if (part.type === 'code'){
952
- containerBubble.appendChild(createCodeBlock(part.language, part.content));
953
- } else {
954
- const block = document.createElement('div');
955
- block.className = 'arabicText';
956
- block.innerHTML = renderMiniMarkdownToHtml(part.content);
957
- containerBubble.appendChild(block);
958
- }
959
- });
960
- }
961
-
962
- function tokenizeHtml(html){
963
- const tokens = [];
964
- const re = /(<[^>]+>)/g;
965
- const parts = html.split(re).filter(Boolean);
966
-
967
- for (const part of parts){
968
- if (part.startsWith('<') && part.endsWith('>')){
969
- tokens.push(part);
970
- } else {
971
- for (const ch of part) tokens.push(ch);
972
- }
973
- }
974
- return tokens;
975
- }
976
-
977
- async function typeAssistantMessage(containerBubble, fullText){
978
- const normalized = normalizeTextClient(fullText);
979
-
980
- const mcqs = parseMCQ(normalized);
981
- const withoutMCQ = removeMCQTags(normalized);
982
- const parts = parseCodeBlocks(withoutMCQ);
983
-
984
- const textOnly = parts
985
- .filter(p => p.type === 'text')
986
- .map(p => p.content)
987
- .join('')
988
- .trim();
989
-
990
- const formattedHtml = renderMiniMarkdownToHtml(textOnly);
991
- const tokens = tokenizeHtml(formattedHtml);
992
-
993
- containerBubble.innerHTML = '';
994
-
995
- const typingWrap = document.createElement('div');
996
- typingWrap.className = 'arabicText';
997
- containerBubble.appendChild(typingWrap);
998
-
999
- const cursor = document.createElement('span');
1000
- cursor.className = 'typeCursor';
1001
- cursor.textContent = '▍';
1002
- containerBubble.appendChild(cursor);
1003
-
1004
- let out = '';
1005
- const speed = 6;
1006
-
1007
- for (let i = 0; i < tokens.length; i++){
1008
- const tok = tokens[i];
1009
- out += tok;
1010
- typingWrap.innerHTML = out;
1011
-
1012
- const isTag = tok.startsWith('<') && tok.endsWith('>');
1013
- if (!isTag){
1014
- scrollToBottom();
1015
- await new Promise(r => setTimeout(r, speed));
1016
- }
1017
- }
1018
-
1019
- cursor.remove();
1020
- renderAssistantParts(containerBubble, parts);
1021
- return mcqs;
1022
- }
1023
-
1024
- // IMPORTANT: send name to AI as prefix: "Name: message"
1025
- function buildMessageForAI(rawMsg){
1026
- const name = (USER_PROFILE.display_name || 'User').trim();
1027
- // Example: "bissan: hi there"
1028
- return `${name}: ${rawMsg}`;
1029
- }
1030
-
1031
- async function sendMessage(){
1032
- const msg = inputEl.value.trim();
1033
- if (!msg) return;
1034
-
1035
- inputEl.disabled = true;
1036
- sendBtn.disabled = true;
1037
-
1038
- addUserMessage(msg);
1039
-
1040
- // Send to AI with name prefix + MCQ answers
1041
- const fullMessage = buildMessageForAI(msg) + getMCQAnswersText();
1042
-
1043
- inputEl.value = '';
1044
- inputEl.style.height = 'auto';
1045
- mcqAnswers = {};
1046
-
1047
- showThinking();
1048
-
1049
- try{
1050
- const res = await fetch('/send_message', {
1051
- method: 'POST',
1052
- headers: { 'Content-Type': 'application/json' },
1053
- body: JSON.stringify({ message: fullMessage })
1054
- });
1055
-
1056
- const data = await res.json();
1057
- hideThinking();
1058
-
1059
- if (!data.success){
1060
- const { bubble } = createMessageRow('ai');
1061
- bubble.innerHTML = escapeHtml(data.error || 'حدث خطأ').replace(/\n/g,'<br>');
1062
- return;
1063
- }
1064
-
1065
- // Update profile if backend returns it
1066
- if (data.user){
1067
- USER_PROFILE = data.user;
1068
- }
1069
-
1070
- const { bubble, meta, wrap } = createMessageRow('ai');
1071
- setMeta(meta, data.sheet_number, data.timestamp);
1072
-
1073
- const mcqs = await typeAssistantMessage(bubble, data.response);
1074
-
1075
- if (mcqs && mcqs.length){
1076
- mcqs.forEach(mcq => {
1077
- const el = createMCQElement(mcq, mcqCounter);
1078
- wrap.appendChild(el);
1079
- mcqCounter++;
1080
- });
1081
- }
1082
-
1083
- scrollToBottom();
1084
-
1085
- }catch(e){
1086
- hideThinking();
1087
- const { bubble } = createMessageRow('ai');
1088
- bubble.innerHTML = 'تعذر الاتصال بالخادم. تأكد من تشغيل السيرفر ثم أعد المحاولة.';
1089
- console.error(e);
1090
- }finally{
1091
- inputEl.disabled = false;
1092
- sendBtn.disabled = false;
1093
- inputEl.focus();
1094
- }
1095
- }
1096
-
1097
- async function clearHistory(){
1098
- if (!confirm('هل تريد مسح المحادثة بالكامل؟')) return;
1099
-
1100
- try{
1101
- const res = await fetch('/clear_history', { method:'POST', headers:{'Content-Type':'application/json'} });
1102
- const data = await res.json();
1103
- if (data.success){
1104
- messagesEl.innerHTML = `
1105
- <div class="empty" id="emptyState">
1106
- <h2>تم مسح المحادثة</h2>
1107
- <p>ابدأ بسؤال جديد في <b>ITGS226 مقدمة في الإنترنت</b>.</p>
1108
- </div>
1109
- `;
1110
- mcqAnswers = {};
1111
- mcqCounter = 0;
1112
- }
1113
- }catch(e){
1114
- alert('فشل مسح المحادثة');
1115
- console.error(e);
1116
- }
1117
- }
1118
-
1119
- async function showHistory(){
1120
- try{
1121
- const res = await fetch('/get_history');
1122
- const data = await res.json();
1123
- if (data.history && data.history.length){
1124
- let t = 'سجل المحادثة:\n\n';
1125
- data.history.forEach((m, i) => {
1126
- const who = m.role === 'user' ? (USER_PROFILE.display_name || 'المستخدم') : 'المساعد';
1127
- const snippet = (m.content || '').replace(/\s+/g,' ').slice(0, 120);
1128
- t += `${i+1}) ${who}: ${snippet}${snippet.length>=120?'...':''}\n\n`;
1129
- });
1130
- alert(t);
1131
- } else {
1132
- alert('لا يوجد سجل بعد');
1133
- }
1134
- }catch(e){
1135
- alert('فشل تحميل السجل');
1136
- console.error(e);
1137
- }
1138
- }
1139
-
1140
- function downloadChatJson(){
1141
- window.open('/bissan/download', '_blank');
1142
- }
1143
-
1144
- async function loadSessionInfo(){
1145
- try{
1146
- const res = await fetch('/session_info');
1147
- const data = await res.json();
1148
- if (data && data.user){
1149
- USER_PROFILE = data.user;
1150
- }
1151
- }catch(e){
1152
- console.warn('Failed to load session_info', e);
1153
- }
1154
- }
1155
-
1156
- window.addEventListener('load', async () => {
1157
- await loadSessionInfo();
1158
- inputEl.focus();
1159
- });
1160
- </script>
1161
- </body>
1162
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <title>مساعد ITGS226 - مقدمة في الإنترنت</title>
7
+
8
+ <!-- Highlight.js -->
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
11
+
12
+ <!-- Arabic font -->
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
16
+
17
+ <style>
18
+ :root{
19
+ --bg0:#0b1220;
20
+ --bg1:#0f1a2e;
21
+ --text:#eaf0ff;
22
+ --muted:#a9b7d6;
23
+ --brand:#6ea8ff;
24
+ --brand2:#7c5cff;
25
+
26
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
27
+ --radius: 18px;
28
+ }
29
+
30
+ *{ box-sizing:border-box; margin:0; padding:0; }
31
+ html, body { height:100%; }
32
+ body{
33
+ font-family: "Tajawal", system-ui, -apple-system, Segoe UI, Arial, sans-serif;
34
+ background:
35
+ radial-gradient(1200px 700px at 20% 10%, rgba(124,92,255,.25), transparent 60%),
36
+ radial-gradient(900px 600px at 80% 20%, rgba(110,168,255,.22), transparent 55%),
37
+ linear-gradient(180deg, var(--bg0), var(--bg1));
38
+ color: var(--text);
39
+ overflow: hidden;
40
+ }
41
+
42
+ .app{
43
+ height: 100dvh;
44
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
45
+ display:flex;
46
+ flex-direction:column;
47
+ }
48
+
49
+ .topbar{
50
+ flex: 0 0 auto;
51
+ padding: 14px 14px 10px;
52
+ display:flex;
53
+ align-items:center;
54
+ justify-content:space-between;
55
+ gap: 10px;
56
+ }
57
+
58
+ .brand{
59
+ display:flex;
60
+ align-items:center;
61
+ gap: 10px;
62
+ min-width: 0;
63
+ }
64
+
65
+ .logo{
66
+ width: 42px; height: 42px;
67
+ border-radius: 14px;
68
+ background: linear-gradient(135deg, rgba(110,168,255,.95), rgba(124,92,255,.95));
69
+ box-shadow: 0 10px 25px rgba(124,92,255,.25);
70
+ display:grid;
71
+ place-items:center;
72
+ font-weight:800;
73
+ letter-spacing:.5px;
74
+ }
75
+
76
+ .brandText{
77
+ min-width:0;
78
+ display:flex;
79
+ flex-direction:column;
80
+ gap:2px;
81
+ }
82
+ .brandText .title{
83
+ font-size: 16px;
84
+ font-weight: 800;
85
+ white-space:nowrap;
86
+ overflow:hidden;
87
+ text-overflow:ellipsis;
88
+ }
89
+ .brandText .subtitle{
90
+ font-size: 12px;
91
+ color: var(--muted);
92
+ white-space:nowrap;
93
+ overflow:hidden;
94
+ text-overflow:ellipsis;
95
+ }
96
+
97
+ .actions{
98
+ display:flex;
99
+ gap: 8px;
100
+ flex: 0 0 auto;
101
+ }
102
+
103
+ .btn{
104
+ border: 1px solid rgba(255,255,255,.10);
105
+ background: rgba(15,27,51,.55);
106
+ color: var(--text);
107
+ padding: 10px 12px;
108
+ border-radius: 12px;
109
+ cursor:pointer;
110
+ font-weight:700;
111
+ font-size: 13px;
112
+ transition: transform .12s ease, border-color .12s ease;
113
+ backdrop-filter: blur(10px);
114
+ -webkit-backdrop-filter: blur(10px);
115
+ display:inline-flex;
116
+ align-items:center;
117
+ gap: 8px;
118
+ user-select:none;
119
+ }
120
+ .btn:hover{ transform: translateY(-1px); border-color: rgba(110,168,255,.35); }
121
+ .btn:active{ transform: translateY(0px); }
122
+ .btn.danger:hover{ border-color: rgba(251,113,133,.45); }
123
+
124
+ .chatCard{
125
+ flex: 1 1 auto;
126
+ margin: 0 14px 12px;
127
+ border-radius: var(--radius);
128
+ border: 1px solid rgba(255,255,255,.10);
129
+ background: rgba(15,27,51,.35);
130
+ box-shadow: var(--shadow);
131
+ overflow:hidden;
132
+ display:flex;
133
+ flex-direction:column;
134
+ min-height: 0;
135
+ backdrop-filter: blur(12px);
136
+ -webkit-backdrop-filter: blur(12px);
137
+ }
138
+
139
+ .messages{
140
+ flex: 1 1 auto;
141
+ padding: 16px 14px;
142
+ overflow:auto;
143
+ scroll-behavior:smooth;
144
+ min-height: 0;
145
+ }
146
+
147
+ .messages::-webkit-scrollbar{ width: 10px; }
148
+ .messages::-webkit-scrollbar-track{ background: transparent; }
149
+ .messages::-webkit-scrollbar-thumb{
150
+ background: rgba(255,255,255,.12);
151
+ border-radius: 10px;
152
+ border: 2px solid transparent;
153
+ background-clip: content-box;
154
+ }
155
+
156
+ .empty{
157
+ padding: 22px 14px;
158
+ border: 1px dashed rgba(255,255,255,.18);
159
+ border-radius: 16px;
160
+ background: rgba(0,0,0,.12);
161
+ color: var(--muted);
162
+ line-height: 1.9;
163
+ }
164
+ .empty h2{
165
+ color: var(--text);
166
+ font-size: 18px;
167
+ margin-bottom: 6px;
168
+ font-weight: 800;
169
+ }
170
+
171
+ .msgRow{
172
+ display:flex;
173
+ gap: 10px;
174
+ margin-bottom: 12px;
175
+ align-items:flex-end;
176
+ }
177
+ .msgRow.user{ justify-content:flex-start; flex-direction: row-reverse; }
178
+ .msgRow.ai{ justify-content:flex-start; }
179
+
180
+ /* ===== Pro avatar + name ===== */
181
+ .avatarCol{
182
+ width: 56px;
183
+ flex: 0 0 auto;
184
+ display:flex;
185
+ flex-direction:column;
186
+ align-items:center;
187
+ gap: 6px;
188
+ }
189
+
190
+ .avatar{
191
+ width: 44px; height: 44px;
192
+ border-radius: 16px;
193
+ display:grid;
194
+ place-items:center;
195
+ border: 1px solid rgba(255,255,255,.12);
196
+ box-shadow: 0 10px 25px rgba(0,0,0,.25);
197
+ overflow:hidden;
198
+ position: relative;
199
+ user-select:none;
200
+ }
201
+
202
+ .avatarName{
203
+ max-width: 56px;
204
+ font-size: 11px;
205
+ color: rgba(234,240,255,.78);
206
+ text-align:center;
207
+ white-space:nowrap;
208
+ overflow:hidden;
209
+ text-overflow:ellipsis;
210
+ line-height: 1.2;
211
+ }
212
+
213
+ .avatar.ai{
214
+ background: linear-gradient(135deg, rgba(110,168,255,.25), rgba(124,92,255,.25));
215
+ color: rgba(234,240,255,.95);
216
+ font-weight: 900;
217
+ letter-spacing: .5px;
218
+ }
219
+
220
+ /* Normal user: emoji avatar (not text) */
221
+ .avatar.userDefault{
222
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.25), transparent 55%),
223
+ linear-gradient(135deg, rgba(110,168,255,.75), rgba(124,92,255,.35));
224
+ border-color: rgba(110,168,255,.35);
225
+ color: #081022;
226
+ font-size: 20px;
227
+ }
228
+
229
+ /* Bissan: flower + pink bg + subtle sparkle */
230
+ .avatar.userBissan{
231
+ background:
232
+ radial-gradient(circle at 25% 25%, rgba(255,255,255,.45), transparent 55%),
233
+ radial-gradient(circle at 70% 30%, rgba(255,255,255,.25), transparent 60%),
234
+ linear-gradient(135deg, rgba(255,105,180,.75), rgba(255,182,193,.35));
235
+ border-color: rgba(255,105,180,.40);
236
+ color: #2b0b1a;
237
+ font-size: 20px;
238
+ }
239
+ .avatar.userBissan::after{
240
+ content:"";
241
+ position:absolute;
242
+ inset:-40%;
243
+ background: conic-gradient(from 180deg, transparent, rgba(255,255,255,.25), transparent);
244
+ animation: spin 3.8s linear infinite;
245
+ opacity:.55;
246
+ }
247
+ .avatar.userBissan > span{
248
+ position: relative;
249
+ z-index: 2;
250
+ filter: drop-shadow(0 6px 10px rgba(0,0,0,.25));
251
+ }
252
+ @keyframes spin{
253
+ to{ transform: rotate(360deg); }
254
+ }
255
+
256
+ .bubbleWrap{
257
+ max-width: min(760px, 86%);
258
+ display:flex;
259
+ flex-direction:column;
260
+ gap: 6px;
261
+ min-width: 0;
262
+ }
263
+
264
+ .bubble{
265
+ border-radius: 16px;
266
+ padding: 12px 14px;
267
+ line-height: 1.95;
268
+ border: 1px solid rgba(255,255,255,.10);
269
+ background: rgba(0,0,0,.12);
270
+ word-break: break-word;
271
+ overflow-wrap: anywhere;
272
+ white-space: normal;
273
+ }
274
+ .user .bubble{
275
+ background: linear-gradient(135deg, rgba(110,168,255,.18), rgba(124,92,255,.18));
276
+ border-color: rgba(110,168,255,.22);
277
+ }
278
+ .ai .bubble{
279
+ background: rgba(15,26,46,.55);
280
+ border-color: rgba(255,255,255,.10);
281
+ }
282
+
283
+ .meta{
284
+ font-size: 12px;
285
+ color: var(--muted);
286
+ padding: 0 6px;
287
+ display:flex;
288
+ gap: 10px;
289
+ align-items:center;
290
+ flex-wrap:wrap;
291
+ }
292
+ .pill{
293
+ font-size: 12px;
294
+ padding: 4px 10px;
295
+ border-radius: 999px;
296
+ border: 1px solid rgba(255,255,255,.10);
297
+ background: rgba(0,0,0,.12);
298
+ color: var(--muted);
299
+ }
300
+
301
+ .arabicText{
302
+ direction: rtl;
303
+ text-align: right;
304
+ unicode-bidi: plaintext;
305
+ }
306
+
307
+ /* Code blocks */
308
+ .codeBlock{
309
+ margin: 10px 0;
310
+ border-radius: 14px;
311
+ overflow:hidden;
312
+ border: 1px solid rgba(255,255,255,.10);
313
+ background: #1e1e1e;
314
+ }
315
+ .codeHeader{
316
+ display:flex;
317
+ align-items:center;
318
+ justify-content:space-between;
319
+ padding: 8px 10px;
320
+ background: #252526;
321
+ border-bottom: 1px solid #2a2a2a;
322
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
323
+ direction:ltr;
324
+ }
325
+ .codeLeft{ display:flex; align-items:center; gap: 10px; min-width:0; }
326
+ .dots{ display:flex; gap:6px; }
327
+ .dot{ width:10px; height:10px; border-radius:50%; display:inline-block; }
328
+ .dot.r{ background:#ff5f56; }
329
+ .dot.y{ background:#ffbd2e; }
330
+ .dot.g{ background:#27c93f; }
331
+ .lang{
332
+ font-size: 12px;
333
+ color:#cfcfcf;
334
+ opacity:.9;
335
+ white-space:nowrap;
336
+ overflow:hidden;
337
+ text-overflow:ellipsis;
338
+ max-width: 220px;
339
+ text-transform: lowercase;
340
+ }
341
+ .copyBtn{
342
+ border: 1px solid #3c3c3c;
343
+ background: transparent;
344
+ color:#cfcfcf;
345
+ padding: 6px 10px;
346
+ border-radius: 10px;
347
+ cursor:pointer;
348
+ font-size: 12px;
349
+ }
350
+ .copyBtn:hover{ background:#2a2d2e; border-color:#4c4c4c; }
351
+ .copyBtn.copied{ background:#0e639c; border-color:#0e639c; color:#fff; }
352
+
353
+ pre{ margin:0; overflow:auto; }
354
+ pre code{
355
+ display:block;
356
+ padding: 14px 14px;
357
+ font-size: 13px;
358
+ line-height: 1.7;
359
+ direction:ltr;
360
+ text-align:left;
361
+ white-space: pre;
362
+ tab-size: 4;
363
+ }
364
+
365
+ /* Composer */
366
+ .composer{
367
+ flex: 0 0 auto;
368
+ padding: 12px;
369
+ border-top: 1px solid rgba(255,255,255,.10);
370
+ background: rgba(15,27,51,.45);
371
+ backdrop-filter: blur(12px);
372
+ -webkit-backdrop-filter: blur(12px);
373
+ }
374
+ .composerRow{
375
+ display:flex;
376
+ gap: 10px;
377
+ align-items:flex-end;
378
+ }
379
+ textarea{
380
+ flex: 1 1 auto;
381
+ resize:none;
382
+ max-height: 160px;
383
+ min-height: 46px;
384
+ padding: 12px 12px;
385
+ border-radius: 14px;
386
+ border: 1px solid rgba(255,255,255,.12);
387
+ background: rgba(0,0,0,.18);
388
+ color: var(--text);
389
+ outline:none;
390
+ font-size: 14px;
391
+ line-height: 1.8;
392
+ font-family: inherit;
393
+ }
394
+ textarea::placeholder{ color: rgba(234,240,255,.55); }
395
+
396
+ .sendBtn{
397
+ flex: 0 0 auto;
398
+ padding: 12px 14px;
399
+ border-radius: 14px;
400
+ border: 1px solid rgba(110,168,255,.35);
401
+ background: linear-gradient(135deg, rgba(110,168,255,.85), rgba(124,92,255,.85));
402
+ color: #081022;
403
+ font-weight: 900;
404
+ cursor:pointer;
405
+ min-width: 92px;
406
+ }
407
+ .sendBtn:hover{ transform: translateY(-1px); filter: brightness(1.05); }
408
+ .sendBtn:disabled{ opacity:.55; cursor:not-allowed; transform:none; }
409
+
410
+ /* Thinking bubble */
411
+ .thinking{
412
+ display:inline-flex;
413
+ align-items:center;
414
+ gap: 10px;
415
+ color: var(--muted);
416
+ font-weight: 700;
417
+ }
418
+ .dotsAnim{ display:inline-flex; gap: 4px; align-items:center; }
419
+ .dotsAnim span{
420
+ width: 6px; height: 6px;
421
+ border-radius: 50%;
422
+ background: rgba(234,240,255,.55);
423
+ display:inline-block;
424
+ animation: bounce 1.1s infinite ease-in-out;
425
+ }
426
+ .dotsAnim span:nth-child(2){ animation-delay: .15s; }
427
+ .dotsAnim span:nth-child(3){ animation-delay: .30s; }
428
+ @keyframes bounce{
429
+ 0%, 80%, 100%{ transform: translateY(0); opacity:.55; }
430
+ 40%{ transform: translateY(-5px); opacity:1; }
431
+ }
432
+
433
+ /* Typing cursor */
434
+ .typeCursor{
435
+ display:inline-block;
436
+ width: 10px;
437
+ margin-right: 2px;
438
+ opacity: .9;
439
+ animation: blink 1s infinite;
440
+ }
441
+ @keyframes blink{ 0%,50%{opacity:1} 51%,100%{opacity:0} }
442
+
443
+ /* MCQ */
444
+ .mcq{
445
+ margin-top: 10px;
446
+ border-radius: 16px;
447
+ border: 1px solid rgba(255,255,255,.10);
448
+ background: rgba(0,0,0,.12);
449
+ overflow:hidden;
450
+ }
451
+ .mcqHead{
452
+ padding: 12px 12px;
453
+ border-bottom: 1px solid rgba(255,255,255,.10);
454
+ display:flex;
455
+ align-items:center;
456
+ justify-content:space-between;
457
+ gap: 10px;
458
+ }
459
+ .mcqTitle{ font-weight: 900; color: var(--text); line-height: 1.7; }
460
+ .mcqBadge{
461
+ font-size: 12px;
462
+ padding: 4px 10px;
463
+ border-radius: 999px;
464
+ border: 1px solid rgba(255,255,255,.10);
465
+ color: var(--muted);
466
+ background: rgba(15,26,46,.55);
467
+ white-space:nowrap;
468
+ }
469
+ .mcqBody{ padding: 10px 10px 12px; display:flex; flex-direction:column; gap: 8px; }
470
+ .opt{
471
+ padding: 12px 12px;
472
+ border-radius: 14px;
473
+ border: 1px solid rgba(255,255,255,.10);
474
+ background: rgba(15,26,46,.45);
475
+ cursor:pointer;
476
+ display:flex;
477
+ align-items:center;
478
+ gap: 10px;
479
+ user-select:none;
480
+ }
481
+ .opt:hover{ transform: translateY(-1px); border-color: rgba(110,168,255,.35); }
482
+ .opt.disabled{ cursor: default; opacity: .95; }
483
+ .opt.correct{ border-color: rgba(45,212,191,.55); background: rgba(45,212,191,.12); }
484
+ .opt.wrong{ border-color: rgba(251,113,133,.55); background: rgba(251,113,133,.10); }
485
+ .optKey{
486
+ width: 30px; height: 30px;
487
+ border-radius: 12px;
488
+ display:grid;
489
+ place-items:center;
490
+ font-weight: 900;
491
+ background: rgba(255,255,255,.08);
492
+ border: 1px solid rgba(255,255,255,.10);
493
+ flex: 0 0 auto;
494
+ }
495
+ .optText{ flex: 1 1 auto; line-height: 1.8; }
496
+ .optIcon{ flex: 0 0 auto; font-weight: 900; }
497
+
498
+ @media (max-width: 520px){
499
+ .topbar{ padding: 12px 12px 8px; }
500
+ .chatCard{ margin: 0 10px 10px; border-radius: 16px; }
501
+ .messages{ padding: 14px 12px; }
502
+ .bubbleWrap{ max-width: 92%; }
503
+ .sendBtn{ min-width: 78px; }
504
+ .avatarCol{ width: 52px; }
505
+ .avatarName{ max-width: 52px; }
506
+ }
507
+ </style>
508
+ </head>
509
+
510
+ <body>
511
+ <div class="app">
512
+ <div class="topbar">
513
+ <div class="brand">
514
+ <div class="logo">IT</div>
515
+ <div class="brandText">
516
+ <div class="title">مساعد ITGS226</div>
517
+ <div class="subtitle">مقدمة في الإنترنت • دردشة ذكية + أسئلة اختيار من متعدد</div>
518
+ </div>
519
+ </div>
520
+
521
+ <div class="actions">
522
+ <button class="btn danger" onclick="clearHistory()">مسح المحادثة</button>
523
+ </div>
524
+ </div>
525
+
526
+ <div class="chatCard">
527
+ <div class="messages" id="messages">
528
+ <div class="empty" id="emptyState">
529
+ <h2>أهلًا بك</h2>
530
+ <p>
531
+ اكتب سؤالك في مادة <b>ITGS226 مقدمة في الإنترنت</b> وسأساعدك بشرح واضح ومنظم.
532
+ <br/>يمكنني أيضًا إنشاء أسئلة اختيار من متعدد (MCQ) للتدريب.
533
+ </p>
534
+ </div>
535
+ </div>
536
+
537
+ <div class="composer">
538
+ <div class="composerRow">
539
+ <textarea id="input" rows="1" placeholder="اكتب سؤالك هنا... (Enter للإرسال، Shift+Enter لسطر جديد)"></textarea>
540
+ <button class="sendBtn" id="sendBtn" onclick="sendMessage()">إرسال</button>
541
+ </div>
542
+ </div>
543
+ </div>
544
+ </div>
545
+
546
+ <script>
547
+ const messagesEl = document.getElementById('messages');
548
+ const inputEl = document.getElementById('input');
549
+ const sendBtn = document.getElementById('sendBtn');
550
+ const emptyState = document.getElementById('emptyState');
551
+
552
+ // Loaded from backend (/session_info)
553
+ let USER_PROFILE = {
554
+ display_name: 'أنت',
555
+ is_bissan: false
556
+ };
557
+
558
+ // MCQ state
559
+ let mcqAnswers = {};
560
+ let mcqCounter = 0;
561
+
562
+ // Auto-resize textarea
563
+ inputEl.addEventListener('input', function(){
564
+ this.style.height = 'auto';
565
+ this.style.height = Math.min(this.scrollHeight, 160) + 'px';
566
+ });
567
+
568
+ inputEl.addEventListener('keydown', function(e){
569
+ if (e.key === 'Enter' && !e.shiftKey){
570
+ e.preventDefault();
571
+ sendMessage();
572
+ }
573
+ });
574
+
575
+ function scrollToBottom(){
576
+ messagesEl.scrollTop = messagesEl.scrollHeight;
577
+ }
578
+
579
+ function escapeHtml(text){
580
+ const div = document.createElement('div');
581
+ div.textContent = text;
582
+ return div.innerHTML;
583
+ }
584
+
585
+ function normalizeTextClient(text){
586
+ if (!text) return '';
587
+ return text
588
+ .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069]/g, '')
589
+ .replace(/\r\n/g, '\n')
590
+ .replace(/\r/g, '\n')
591
+ .replace(/\n{3,}/g, '\n\n')
592
+ .trim();
593
+ }
594
+
595
+ function parseCodeBlocks(text){
596
+ const codeRegex = /<code_(\w+)>([\s\S]*?)<\/code_\1>/g;
597
+ const parts = [];
598
+ let lastIndex = 0;
599
+ let match;
600
+
601
+ while ((match = codeRegex.exec(text)) !== null){
602
+ if (match.index > lastIndex){
603
+ parts.push({ type:'text', content: text.substring(lastIndex, match.index) });
604
+ }
605
+ parts.push({ type:'code', language: match[1], content: match[2].trim() });
606
+ lastIndex = match.index + match[0].length;
607
+ }
608
+ if (lastIndex < text.length){
609
+ parts.push({ type:'text', content: text.substring(lastIndex) });
610
+ }
611
+ return parts.length ? parts : [{ type:'text', content:text }];
612
+ }
613
+
614
+ function createCodeBlock(language, code){
615
+ const wrap = document.createElement('div');
616
+ wrap.className = 'codeBlock';
617
+
618
+ const header = document.createElement('div');
619
+ header.className = 'codeHeader';
620
+
621
+ const left = document.createElement('div');
622
+ left.className = 'codeLeft';
623
+
624
+ const dots = document.createElement('div');
625
+ dots.className = 'dots';
626
+ dots.innerHTML = `<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>`;
627
+
628
+ const lang = document.createElement('span');
629
+ lang.className = 'lang';
630
+ lang.textContent = (language || 'text').toLowerCase();
631
+
632
+ left.appendChild(dots);
633
+ left.appendChild(lang);
634
+
635
+ const copyBtn = document.createElement('button');
636
+ copyBtn.className = 'copyBtn';
637
+ copyBtn.textContent = 'نسخ';
638
+ copyBtn.addEventListener('click', () => copyCode(code, copyBtn));
639
+
640
+ header.appendChild(left);
641
+ header.appendChild(copyBtn);
642
+
643
+ const pre = document.createElement('pre');
644
+ const codeEl = document.createElement('code');
645
+ const safeLang = (language || 'plaintext').toLowerCase();
646
+ codeEl.className = `language-${safeLang}`;
647
+ codeEl.textContent = code;
648
+
649
+ pre.appendChild(codeEl);
650
+ wrap.appendChild(header);
651
+ wrap.appendChild(pre);
652
+
653
+ setTimeout(() => { try { hljs.highlightElement(codeEl); } catch(e){} }, 0);
654
+ return wrap;
655
+ }
656
+
657
+ async function copyCode(code, btn){
658
+ try{
659
+ await navigator.clipboard.writeText(code);
660
+ btn.classList.add('copied');
661
+ btn.textContent = 'تم النسخ';
662
+ setTimeout(() => {
663
+ btn.classList.remove('copied');
664
+ btn.textContent = 'نسخ';
665
+ }, 1200);
666
+ }catch(e){
667
+ btn.textContent = 'فشل النسخ';
668
+ setTimeout(() => btn.textContent = 'نسخ', 1200);
669
+ }
670
+ }
671
+
672
+ function parseMCQ(text){
673
+ const mcqRegex = /<mcq>([\s\S]*?)<\/mcq>/g;
674
+ const mcqs = [];
675
+ let match;
676
+
677
+ while ((match = mcqRegex.exec(text)) !== null){
678
+ const mcqContent = match[1];
679
+ const qMatch = mcqContent.match(/<q>([\s\S]*?)<\/q>/);
680
+ const question = qMatch ? qMatch[1].trim() : '';
681
+
682
+ const options = [];
683
+ const optionRegex = /<(true|false)>([\s\S]*?)<\/(true|false)>/g;
684
+ let optMatch;
685
+ while ((optMatch = optionRegex.exec(mcqContent)) !== null){
686
+ options.push({ text: optMatch[2].trim(), isCorrect: optMatch[1] === 'true' });
687
+ }
688
+
689
+ if (question && options.length) mcqs.push({ question, options });
690
+ }
691
+ return mcqs;
692
+ }
693
+
694
+ function removeMCQTags(text){
695
+ return text.replace(/<mcq>[\s\S]*?<\/mcq>/g, '').trim();
696
+ }
697
+
698
+ function createMCQElement(mcqData, mcqId){
699
+ const box = document.createElement('div');
700
+ box.className = 'mcq';
701
+ box.dataset.mcqId = mcqId;
702
+
703
+ const head = document.createElement('div');
704
+ head.className = 'mcqHead';
705
+
706
+ const title = document.createElement('div');
707
+ title.className = 'mcqTitle arabicText';
708
+ title.textContent = `سؤال ${mcqId + 1}: ${mcqData.question}`;
709
+
710
+ const badge = document.createElement('div');
711
+ badge.className = 'mcqBadge';
712
+ badge.textContent = 'اختر الإجابة الصحيحة';
713
+
714
+ head.appendChild(title);
715
+ head.appendChild(badge);
716
+
717
+ const body = document.createElement('div');
718
+ body.className = 'mcqBody';
719
+
720
+ mcqData.options.forEach((opt, idx) => {
721
+ const row = document.createElement('div');
722
+ row.className = 'opt arabicText';
723
+ row.dataset.correct = opt.isCorrect ? 'true' : 'false';
724
+ row.dataset.index = idx;
725
+
726
+ const key = document.createElement('div');
727
+ key.className = 'optKey';
728
+ key.textContent = String.fromCharCode(65 + idx);
729
+
730
+ const text = document.createElement('div');
731
+ text.className = 'optText';
732
+ text.textContent = opt.text;
733
+
734
+ const icon = document.createElement('div');
735
+ icon.className = 'optIcon';
736
+ icon.textContent = '';
737
+
738
+ row.appendChild(key);
739
+ row.appendChild(text);
740
+ row.appendChild(icon);
741
+
742
+ row.addEventListener('click', () => handleMCQClick(mcqId, idx, row, body));
743
+ body.appendChild(row);
744
+ });
745
+
746
+ box.appendChild(head);
747
+ box.appendChild(body);
748
+ return box;
749
+ }
750
+
751
+ function handleMCQClick(mcqId, selectedIndex, selectedRow, body){
752
+ if (body.dataset.answered === 'true') return;
753
+
754
+ const rows = body.querySelectorAll('.opt');
755
+ body.dataset.answered = 'true';
756
+
757
+ const isCorrect = selectedRow.dataset.correct === 'true';
758
+
759
+ rows.forEach(r => r.classList.add('disabled'));
760
+
761
+ if (isCorrect){
762
+ selectedRow.classList.add('correct');
763
+ selectedRow.querySelector('.optIcon').textContent = '';
764
+ } else {
765
+ selectedRow.classList.add('wrong');
766
+ selectedRow.querySelector('.optIcon').textContent = '✗';
767
+
768
+ rows.forEach(r => {
769
+ if (r.dataset.correct === 'true'){
770
+ r.classList.add('correct');
771
+ r.querySelector('.optIcon').textContent = '✓';
772
+ }
773
+ });
774
+ }
775
+
776
+ mcqAnswers[mcqId] = selectedIndex + 1;
777
+ }
778
+
779
+ function getMCQAnswersText(){
780
+ if (!Object.keys(mcqAnswers).length) return '';
781
+ let t = '\n\n[إجابات أسئلة الاختيار السابقة:\n';
782
+ Object.keys(mcqAnswers).sort((a,b)=>parseInt(a)-parseInt(b)).forEach(id=>{
783
+ t += `${parseInt(id)+1}. ${mcqAnswers[id]}\n`;
784
+ });
785
+ t += ']';
786
+ return t;
787
+ }
788
+
789
+ function createAvatarCol(role){
790
+ const col = document.createElement('div');
791
+ col.className = 'avatarCol';
792
+
793
+ const avatar = document.createElement('div');
794
+ const nameEl = document.createElement('div');
795
+ nameEl.className = 'avatarName';
796
+
797
+ if (role === 'ai'){
798
+ avatar.className = 'avatar ai';
799
+ avatar.textContent = 'AI';
800
+ nameEl.textContent = 'المساعد';
801
+ } else {
802
+ if (USER_PROFILE.is_bissan){
803
+ avatar.className = 'avatar userBissan';
804
+ avatar.innerHTML = '<span>🌸</span>';
805
+ nameEl.textContent = USER_PROFILE.display_name || 'بيسان';
806
+ } else {
807
+ avatar.className = 'avatar userDefault';
808
+ avatar.textContent = '👤'; // emoji (not text)
809
+ nameEl.textContent = USER_PROFILE.display_name || 'أنت';
810
+ }
811
+ }
812
+
813
+ col.appendChild(avatar);
814
+ col.appendChild(nameEl);
815
+ return col;
816
+ }
817
+
818
+ function createMessageRow(role){
819
+ if (emptyState) emptyState.remove();
820
+
821
+ const row = document.createElement('div');
822
+ row.className = `msgRow ${role}`;
823
+
824
+ const avatarCol = createAvatarCol(role);
825
+
826
+ const wrap = document.createElement('div');
827
+ wrap.className = 'bubbleWrap';
828
+
829
+ const bubble = document.createElement('div');
830
+ bubble.className = 'bubble arabicText';
831
+
832
+ const meta = document.createElement('div');
833
+ meta.className = 'meta';
834
+
835
+ wrap.appendChild(bubble);
836
+ wrap.appendChild(meta);
837
+
838
+ row.appendChild(avatarCol);
839
+ row.appendChild(wrap);
840
+
841
+ messagesEl.appendChild(row);
842
+ scrollToBottom();
843
+
844
+ return { row, bubble, meta, wrap };
845
+ }
846
+
847
+ function setMeta(metaEl, sheet, time){
848
+ metaEl.innerHTML = '';
849
+ if (sheet){
850
+ const p1 = document.createElement('span');
851
+ p1.className = 'pill';
852
+ p1.textContent = `ورقة: ${sheet}`;
853
+ metaEl.appendChild(p1);
854
+ }
855
+ if (time){
856
+ const p2 = document.createElement('span');
857
+ p2.className = 'pill';
858
+ p2.textContent = `الوقت: ${time}`;
859
+ metaEl.appendChild(p2);
860
+ }
861
+ }
862
+
863
+ function addUserMessage(text){
864
+ const { bubble } = createMessageRow('user');
865
+ bubble.innerHTML = escapeHtml(normalizeTextClient(text)).replace(/\n/g,'<br>');
866
+ }
867
+
868
+ function showThinking(){
869
+ const { row, bubble } = createMessageRow('ai');
870
+ row.id = 'thinkingRow';
871
+ bubble.innerHTML = `
872
+ <div class="thinking arabicText">
873
+ <span>جاري التفكير</span>
874
+ <span class="dotsAnim" aria-hidden="true">
875
+ <span></span><span></span><span></span>
876
+ </span>
877
+ </div>
878
+ `;
879
+ }
880
+
881
+ function hideThinking(){
882
+ const r = document.getElementById('thinkingRow');
883
+ if (r) r.remove();
884
+ }
885
+
886
+ /* Mini-Markdown */
887
+ function renderMiniMarkdownToHtml(text){
888
+ let s = normalizeTextClient(text);
889
+ s = escapeHtml(s);
890
+
891
+ s = s.replace(/^\s*---\s*$/gm, '<hr>');
892
+ s = s.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
893
+ s = s.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
894
+ s = s.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
895
+ s = s.replace(/`([^`]+)`/g, '<code class="inlineCode">$1</code>');
896
+
897
+ const lines = s.split('\n');
898
+ let out = '';
899
+ let inUl = false;
900
+ let inOl = false;
901
+
902
+ const closeLists = () => {
903
+ if (inUl){ out += '</ul>'; inUl = false; }
904
+ if (inOl){ out += '</ol>'; inOl = false; }
905
+ };
906
+
907
+ for (let line of lines){
908
+ const ulMatch = line.match(/^\s*-\s+(.+)$/);
909
+ const olMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
910
+
911
+ if (ulMatch){
912
+ if (inOl){ out += '</ol>'; inOl = false; }
913
+ if (!inUl){ out += '<ul>'; inUl = true; }
914
+ out += `<li>${ulMatch[1]}</li>`;
915
+ continue;
916
+ }
917
+
918
+ if (olMatch){
919
+ if (inUl){ out += '</ul>'; inUl = false; }
920
+ if (!inOl){ out += '<ol>'; inOl = true; }
921
+ out += `<li>${olMatch[2]}</li>`;
922
+ continue;
923
+ }
924
+
925
+ closeLists();
926
+
927
+ if (line.trim() === ''){
928
+ out += '<br>';
929
+ } else if (line.startsWith('<h2>') || line.startsWith('<h3>') || line.startsWith('<hr>')){
930
+ out += line;
931
+ } else {
932
+ out += `<p>${line}</p>`;
933
+ }
934
+ }
935
+
936
+ closeLists();
937
+ return out;
938
+ }
939
+
940
+ function renderAssistantParts(containerBubble, parts){
941
+ containerBubble.innerHTML = '';
942
+ parts.forEach(part => {
943
+ if (part.type === 'code'){
944
+ containerBubble.appendChild(createCodeBlock(part.language, part.content));
945
+ } else {
946
+ const block = document.createElement('div');
947
+ block.className = 'arabicText';
948
+ block.innerHTML = renderMiniMarkdownToHtml(part.content);
949
+ containerBubble.appendChild(block);
950
+ }
951
+ });
952
+ }
953
+
954
+ function tokenizeHtml(html){
955
+ const tokens = [];
956
+ const re = /(<[^>]+>)/g;
957
+ const parts = html.split(re).filter(Boolean);
958
+
959
+ for (const part of parts){
960
+ if (part.startsWith('<') && part.endsWith('>')){
961
+ tokens.push(part);
962
+ } else {
963
+ for (const ch of part) tokens.push(ch);
964
+ }
965
+ }
966
+ return tokens;
967
+ }
968
+
969
+ async function typeAssistantMessage(containerBubble, fullText){
970
+ const normalized = normalizeTextClient(fullText);
971
+
972
+ const mcqs = parseMCQ(normalized);
973
+ const withoutMCQ = removeMCQTags(normalized);
974
+ const parts = parseCodeBlocks(withoutMCQ);
975
+
976
+ const textOnly = parts
977
+ .filter(p => p.type === 'text')
978
+ .map(p => p.content)
979
+ .join('')
980
+ .trim();
981
+
982
+ const formattedHtml = renderMiniMarkdownToHtml(textOnly);
983
+ const tokens = tokenizeHtml(formattedHtml);
984
+
985
+ containerBubble.innerHTML = '';
986
+
987
+ const typingWrap = document.createElement('div');
988
+ typingWrap.className = 'arabicText';
989
+ containerBubble.appendChild(typingWrap);
990
+
991
+ const cursor = document.createElement('span');
992
+ cursor.className = 'typeCursor';
993
+ cursor.textContent = '';
994
+ containerBubble.appendChild(cursor);
995
+
996
+ let out = '';
997
+ const speed = 6;
998
+
999
+ for (let i = 0; i < tokens.length; i++){
1000
+ const tok = tokens[i];
1001
+ out += tok;
1002
+ typingWrap.innerHTML = out;
1003
+
1004
+ const isTag = tok.startsWith('<') && tok.endsWith('>');
1005
+ if (!isTag){
1006
+ scrollToBottom();
1007
+ await new Promise(r => setTimeout(r, speed));
1008
+ }
1009
+ }
1010
+
1011
+ cursor.remove();
1012
+ renderAssistantParts(containerBubble, parts);
1013
+ return mcqs;
1014
+ }
1015
+
1016
+ // ✅ IMPORTANT: send name to AI as prefix: "Name: message"
1017
+ function buildMessageForAI(rawMsg){
1018
+ const name = (USER_PROFILE.display_name || 'User').trim();
1019
+ // Example: "bissan: hi there"
1020
+ return `${name}: ${rawMsg}`;
1021
+ }
1022
+
1023
+ async function sendMessage(){
1024
+ const msg = inputEl.value.trim();
1025
+ if (!msg) return;
1026
+
1027
+ inputEl.disabled = true;
1028
+ sendBtn.disabled = true;
1029
+
1030
+ addUserMessage(msg);
1031
+
1032
+ // Send to AI with name prefix + MCQ answers
1033
+ const fullMessage = buildMessageForAI(msg) + getMCQAnswersText();
1034
+
1035
+ inputEl.value = '';
1036
+ inputEl.style.height = 'auto';
1037
+ mcqAnswers = {};
1038
+
1039
+ showThinking();
1040
+
1041
+ try{
1042
+ const res = await fetch('/send_message', {
1043
+ method: 'POST',
1044
+ headers: { 'Content-Type': 'application/json' },
1045
+ body: JSON.stringify({ message: fullMessage })
1046
+ });
1047
+
1048
+ const data = await res.json();
1049
+ hideThinking();
1050
+
1051
+ if (!data.success){
1052
+ const { bubble } = createMessageRow('ai');
1053
+ bubble.innerHTML = escapeHtml(data.error || 'حدث خطأ').replace(/\n/g,'<br>');
1054
+ return;
1055
+ }
1056
+
1057
+ // Update profile if backend returns it
1058
+ if (data.user){
1059
+ USER_PROFILE = data.user;
1060
+ }
1061
+
1062
+ const { bubble, meta, wrap } = createMessageRow('ai');
1063
+ setMeta(meta, data.sheet_number, data.timestamp);
1064
+
1065
+ const mcqs = await typeAssistantMessage(bubble, data.response);
1066
+
1067
+ if (mcqs && mcqs.length){
1068
+ mcqs.forEach(mcq => {
1069
+ const el = createMCQElement(mcq, mcqCounter);
1070
+ wrap.appendChild(el);
1071
+ mcqCounter++;
1072
+ });
1073
+ }
1074
+
1075
+ scrollToBottom();
1076
+
1077
+ }catch(e){
1078
+ hideThinking();
1079
+ const { bubble } = createMessageRow('ai');
1080
+ bubble.innerHTML = 'تعذر الاتصال بالخادم. تأكد من تشغيل السيرفر ثم أعد المحاولة.';
1081
+ console.error(e);
1082
+ }finally{
1083
+ inputEl.disabled = false;
1084
+ sendBtn.disabled = false;
1085
+ inputEl.focus();
1086
+ }
1087
+ }
1088
+
1089
+ async function clearHistory(){
1090
+ if (!confirm('هل تريد مسح المحادثة بالكامل؟')) return;
1091
+
1092
+ try{
1093
+ const res = await fetch('/clear_history', { method:'POST', headers:{'Content-Type':'application/json'} });
1094
+ const data = await res.json();
1095
+ if (data.success){
1096
+ messagesEl.innerHTML = `
1097
+ <div class="empty" id="emptyState">
1098
+ <h2>تم مسح المحادثة</h2>
1099
+ <p>ابدأ بسؤال جديد في <b>ITGS226 مقدمة في الإنترنت</b>.</p>
1100
+ </div>
1101
+ `;
1102
+ mcqAnswers = {};
1103
+ mcqCounter = 0;
1104
+ }
1105
+ }catch(e){
1106
+ alert('فشل مسح المحادثة');
1107
+ console.error(e);
1108
+ }
1109
+ }
1110
+
1111
+ async function showHistory(){
1112
+ try{
1113
+ const res = await fetch('/get_history');
1114
+ const data = await res.json();
1115
+ if (data.history && data.history.length){
1116
+ let t = 'سجل المحادثة:\n\n';
1117
+ data.history.forEach((m, i) => {
1118
+ const who = m.role === 'user' ? (USER_PROFILE.display_name || 'المستخدم') : 'المساعد';
1119
+ const snippet = (m.content || '').replace(/\s+/g,' ').slice(0, 120);
1120
+ t += `${i+1}) ${who}: ${snippet}${snippet.length>=120?'...':''}\n\n`;
1121
+ });
1122
+ alert(t);
1123
+ } else {
1124
+ alert('لا يوجد سجل بعد');
1125
+ }
1126
+ }catch(e){
1127
+ alert('فشل تحميل السجل');
1128
+ console.error(e);
1129
+ }
1130
+ }
1131
+
1132
+ function downloadChatJson(){
1133
+ window.open('/bissan/download', '_blank');
1134
+ }
1135
+
1136
+ async function loadSessionInfo(){
1137
+ try{
1138
+ const res = await fetch('/session_info');
1139
+ const data = await res.json();
1140
+ if (data && data.user){
1141
+ USER_PROFILE = data.user;
1142
+ }
1143
+ }catch(e){
1144
+ console.warn('Failed to load session_info', e);
1145
+ }
1146
+ }
1147
+
1148
+ window.addEventListener('load', async () => {
1149
+ await loadSessionInfo();
1150
+ inputEl.focus();
1151
+ });
1152
+ </script>
1153
+ </body>
 
 
 
 
 
 
 
 
1154
  </html>