data-str / templates /chat.html
CORVO-AI's picture
Upload 135 files
b83571a verified
Raw
History Blame Contribute Delete
154 kB
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>المساعد التعليمي الذكي</title>
<script>
window.MathJax = {
tex: {
inlineMath: [['\\(', '\\)'], ['$', '$']],
displayMath: [['\\[', '\\]'], ['$$', '$$']],
processEscapes: true,
processEnvironments: true,
tags: 'ams'
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
ignoreHtmlClass: 'no-mathjax'
},
chtml: { mtextInheritFont: true },
startup: {
ready: () => {
MathJax.startup.defaultReady();
MathJax.startup.promise.then(() => { fixMathJaxArabic(); });
}
}
};
function fixMathJaxArabic() {
document.querySelectorAll('mjx-mtext, mjx-container mtext').forEach(el => {
const text = el.textContent || '';
if (/[\u0600-\u06FF]/.test(text)) {
el.style.direction = 'rtl';
el.style.unicodeBidi = 'embed';
el.style.display = 'inline-block';
}
});
}
function fixArabicInMath() {
document.querySelectorAll('mjx-mtext').forEach(el => {
const text = el.textContent || '';
if (/[\u0600-\u06FF]/.test(text)) {
el.style.direction = 'rtl';
el.style.unicodeBidi = 'embed';
el.style.display = 'inline-block';
el.style.fontFamily = 'Tajawal, Arial, sans-serif';
}
});
}
const mathObserver = new MutationObserver((mutations) => {
let hasMathJax = false;
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.tagName && node.tagName.toLowerCase().startsWith('mjx-')) hasMathJax = true;
if (node.querySelector && node.querySelector('mjx-mtext, mjx-container')) hasMathJax = true;
}
});
});
if (hasMathJax) setTimeout(fixArabicInMath, 50);
});
mathObserver.observe(document.body, { childList: true, subtree: true });
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=DM+Sans:wght@400;500;700&family=Tajawal:wght@300;400;500;700;800&family=Amiri:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
<!-- FontAwesome 6 for Professional Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ============================================================
CLAY DESIGN TOKENS
============================================================ */
:root {
/* Canvas & Surfaces */
--clay-canvas: #F4F1FA;
--clay-card-bg: rgba(255, 255, 255, 0.72);
--clay-card-solid: #FFFFFF;
--clay-input-bg: #EFEBF5;
--clay-input-focus: #FFFFFF;
/* Typography */
--clay-fg: #332F3A;
--clay-muted: #635F69;
--clay-subtle: #8B8494;
/* Accents */
--clay-accent: #7C3AED;
--clay-accent-light: #A78BFA;
--clay-accent-alt: #DB2777;
--clay-sky: #0EA5E9;
--clay-emerald: #10B981;
--clay-amber: #F59E0B;
/* Gradients */
--clay-btn-grad: linear-gradient(135deg, #A78BFA 0%, #7C3AED 100%);
--clay-btn-grad-alt: linear-gradient(135deg, #F472B6 0%, #DB2777 100%);
--clay-header-grad: linear-gradient(135deg, rgba(167,139,250,0.18) 0%, rgba(219,39,119,0.10) 100%);
/* ── 4-Layer Shadow Stacks ── */
/* Deep surface (large containers) */
--shadow-deep:
30px 30px 60px #cdc6d9,
-30px -30px 60px #ffffff,
inset 10px 10px 20px rgba(139,92,246,0.05),
inset -10px -10px 20px rgba(255,255,255,0.80);
/* Standard floating card */
--shadow-card:
16px 16px 32px rgba(160,150,180,0.22),
-10px -10px 24px rgba(255,255,255,0.92),
inset 6px 6px 12px rgba(139,92,246,0.03),
inset -6px -6px 12px rgba(255,255,255,1);
/* Card hover — amplified lift */
--shadow-card-hover:
22px 22px 44px rgba(139,92,246,0.18),
-12px -12px 28px rgba(255,255,255,0.95),
inset 6px 6px 12px rgba(139,92,246,0.04),
inset -6px -6px 12px rgba(255,255,255,1);
/* Convex button */
--shadow-btn:
12px 12px 24px rgba(139,92,246,0.32),
-8px -8px 16px rgba(255,255,255,0.45),
inset 4px 4px 8px rgba(255,255,255,0.42),
inset -4px -4px 8px rgba(0,0,0,0.10);
/* Button hover */
--shadow-btn-hover:
16px 16px 32px rgba(139,92,246,0.40),
-10px -10px 20px rgba(255,255,255,0.50),
inset 4px 4px 8px rgba(255,255,255,0.45),
inset -4px -4px 8px rgba(0,0,0,0.08);
/* Recessed / pressed */
--shadow-pressed:
inset 10px 10px 20px #d9d4e3,
inset -10px -10px 20px #ffffff;
/* Subtle pressed for inputs */
--shadow-input:
inset 8px 8px 16px #ddd8e8,
inset -8px -8px 16px #ffffff;
/* Input focus — raised white surface */
--shadow-input-focus:
16px 16px 32px rgba(160,150,180,0.18),
-10px -10px 24px rgba(255,255,255,0.90),
inset 2px 2px 4px rgba(139,92,246,0.06),
0 0 0 4px rgba(124,58,237,0.14);
/* User message bubble */
--shadow-user-msg:
10px 10px 20px rgba(139,92,246,0.28),
-6px -6px 14px rgba(255,255,255,0.20),
inset 3px 3px 6px rgba(255,255,255,0.30),
inset -3px -3px 6px rgba(0,0,0,0.12);
/* Bot message bubble */
--shadow-bot-msg:
10px 10px 20px rgba(160,150,180,0.18),
-6px -6px 14px rgba(255,255,255,0.90),
inset 4px 4px 8px rgba(139,92,246,0.03),
inset -4px -4px 8px rgba(255,255,255,0.95);
/* Header bar */
--shadow-header:
0 8px 32px rgba(160,150,180,0.18),
0 2px 8px rgba(139,92,246,0.08);
/* Status bar */
--shadow-status:
0 -4px 16px rgba(160,150,180,0.12);
/* Subject card */
--shadow-subject:
12px 12px 24px rgba(160,150,180,0.20),
-8px -8px 18px rgba(255,255,255,0.90),
inset 4px 4px 8px rgba(139,92,246,0.03),
inset -4px -4px 8px rgba(255,255,255,1);
/* Subject card hover */
--shadow-subject-hover:
18px 18px 36px rgba(139,92,246,0.22),
-10px -10px 22px rgba(255,255,255,0.95),
inset 4px 4px 8px rgba(139,92,246,0.05),
inset -4px -4px 8px rgba(255,255,255,1);
/* Subject card selected */
--shadow-subject-selected:
14px 14px 28px rgba(124,58,237,0.30),
-8px -8px 18px rgba(255,255,255,0.90),
inset 4px 4px 8px rgba(124,58,237,0.08),
inset -4px -4px 8px rgba(255,255,255,0.90),
0 0 0 3px rgba(124,58,237,0.30);
/* Modal */
--shadow-modal:
30px 30px 60px rgba(139,92,246,0.18),
-20px -20px 40px rgba(255,255,255,0.95),
inset 8px 8px 16px rgba(139,92,246,0.04),
inset -8px -8px 16px rgba(255,255,255,1);
/* Badge / pill */
--shadow-badge:
6px 6px 12px rgba(160,150,180,0.18),
-4px -4px 8px rgba(255,255,255,0.90),
inset 2px 2px 4px rgba(139,92,246,0.04),
inset -2px -2px 4px rgba(255,255,255,1);
/* Send button */
--shadow-send:
10px 10px 20px rgba(139,92,246,0.35),
-6px -6px 14px rgba(255,255,255,0.40),
inset 3px 3px 6px rgba(255,255,255,0.40),
inset -3px -3px 6px rgba(0,0,0,0.12);
/* Typing dots container */
--shadow-typing:
10px 10px 20px rgba(160,150,180,0.18),
-6px -6px 14px rgba(255,255,255,0.90),
inset 4px 4px 8px rgba(139,92,246,0.03),
inset -4px -4px 8px rgba(255,255,255,0.95);
}
/* ============================================================
RESET & BASE
============================================================ */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Tajawal', 'DM Sans', sans-serif;
background: var(--clay-canvas);
color: var(--clay-fg);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
/* Subtle canvas texture via radial gradient */
background-image:
radial-gradient(ellipse 80% 50% at 20% 10%, rgba(167,139,250,0.10) 0%, transparent 60%),
radial-gradient(ellipse 60% 40% at 80% 90%, rgba(219,39,119,0.07) 0%, transparent 55%),
radial-gradient(ellipse 50% 60% at 50% 50%, rgba(14,165,233,0.05) 0%, transparent 70%);
background-attachment: fixed;
}
/* MathJax RTL fix */
mjx-container {
direction: ltr !important;
}
mjx-container mtext {
unicode-bidi: plaintext !important;
direction: rtl !important;
}
mjx-container svg text {
direction: rtl !important;
}
/* ============================================================
ANIMATED BACKGROUND BLOBS
============================================================ */
.clay-blobs {
pointer-events: none;
position: fixed;
inset: 0;
overflow: hidden;
z-index: 0;
}
.clay-blob {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.55;
}
.clay-blob-1 {
width: 60vw;
height: 60vw;
max-width: 700px;
max-height: 700px;
background: radial-gradient(circle, rgba(167,139,250,0.35) 0%, rgba(124,58,237,0.12) 70%);
top: -15%;
right: -10%;
animation: clayFloat 10s ease-in-out infinite;
}
.clay-blob-2 {
width: 50vw;
height: 50vw;
max-width: 580px;
max-height: 580px;
background: radial-gradient(circle, rgba(219,39,119,0.22) 0%, rgba(244,114,182,0.08) 70%);
bottom: -10%;
left: -8%;
animation: clayFloatAlt 12s ease-in-out infinite;
animation-delay: -3s;
}
.clay-blob-3 {
width: 40vw;
height: 40vw;
max-width: 460px;
max-height: 460px;
background: radial-gradient(circle, rgba(14,165,233,0.18) 0%, rgba(56,189,248,0.06) 70%);
top: 40%;
left: 30%;
animation: clayFloat 14s ease-in-out infinite;
animation-delay: -6s;
}
.clay-blob-4 {
width: 35vw;
height: 35vw;
max-width: 380px;
max-height: 380px;
background: radial-gradient(circle, rgba(16,185,129,0.14) 0%, rgba(52,211,153,0.05) 70%);
bottom: 20%;
right: 5%;
animation: clayFloatAlt 9s ease-in-out infinite;
animation-delay: -1.5s;
}
@keyframes clayFloat {
0%, 100% { transform: translateY(0px) rotate(0deg) scale(1); }
33% { transform: translateY(-22px) rotate(2deg) scale(1.02); }
66% { transform: translateY(-10px) rotate(-1deg) scale(0.99); }
}
@keyframes clayFloatAlt {
0%, 100% { transform: translateY(0px) rotate(0deg) scale(1); }
40% { transform: translateY(-16px) rotate(-2.5deg) scale(1.015); }
70% { transform: translateY(-28px) rotate(1.5deg) scale(1.03); }
}
@keyframes clayBreathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.025); }
}
@keyframes claySpin {
to { transform: rotate(360deg); }
}
@keyframes clayPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(0.92); }
}
@keyframes clayBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes clayMessageIn {
from { opacity: 0; transform: translateY(14px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes clayTyping {
0%, 60%, 100% { transform: translateY(0); opacity: 0.45; }
30% { transform: translateY(-9px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.clay-blob,
.clay-blob-1,
.clay-blob-2,
.clay-blob-3,
.clay-blob-4 { animation: none !important; }
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* ============================================================
SCROLLBAR
============================================================ */
::-webkit-scrollbar { width: 7px; height: 7px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(124,58,237,0.22);
border-radius: 20px;
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(124,58,237,0.40); }
/* ============================================================
SUBJECT SELECTION PAGE
============================================================ */
.subject-page {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 28px 20px 36px;
overflow-y: auto;
}
.subject-page.hidden { display: none; }
/* Header */
.subject-header {
text-align: center;
margin-bottom: 44px;
}
.subject-header .main-logo {
font-size: 72px;
margin-bottom: 20px;
display: block;
filter: drop-shadow(0 8px 24px rgba(124,58,237,0.30));
animation: clayBreathe 6s ease-in-out infinite;
color: var(--clay-accent);
}
.subject-header h1 {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: clamp(26px, 5vw, 40px);
font-weight: 900;
color: var(--clay-fg);
margin-bottom: 12px;
letter-spacing: -0.5px;
line-height: 1.15;
}
.subject-header h1 span {
background: linear-gradient(135deg, var(--clay-accent) 20%, var(--clay-accent-alt) 80%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subject-header p {
font-size: 15px;
color: var(--clay-muted);
max-width: 420px;
margin: 0 auto;
line-height: 1.85;
font-weight: 500;
}
/* Subjects Grid */
.subjects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 18px;
max-width: 920px;
width: 100%;
margin-bottom: 36px;
}
/* Subject Card */
.subject-card {
background: var(--clay-card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1.5px solid rgba(255,255,255,0.70);
border-radius: 32px;
padding: 28px 20px 24px;
cursor: pointer;
transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.35s ease,
border-color 0.25s ease;
text-align: center;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-subject);
}
/* Decorative top shimmer */
.subject-card::before {
content: '';
position: absolute;
top: 0; left: 10%; right: 10%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(167,139,250,0.60), transparent);
border-radius: 0 0 4px 4px;
opacity: 0;
transition: opacity 0.3s ease;
}
.subject-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-subject-hover);
border-color: rgba(167,139,250,0.55);
}
.subject-card:hover::before { opacity: 1; }
.subject-card:active {
transform: translateY(-2px) scale(0.97);
box-shadow: var(--shadow-pressed);
}
.subject-card.selected {
border-color: rgba(124,58,237,0.50);
background: rgba(167,139,250,0.14);
box-shadow: var(--shadow-subject-selected);
transform: translateY(-4px);
}
.subject-card .subject-icon {
font-size: 44px;
margin-bottom: 14px;
display: block;
filter: drop-shadow(0 4px 10px rgba(124,58,237,0.20));
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1);
color: var(--clay-accent);
}
.subject-card:hover .subject-icon {
transform: scale(1.15) rotate(-4deg);
}
.subject-card .subject-name {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 16px;
font-weight: 800;
color: var(--clay-fg);
margin-bottom: 6px;
}
.subject-card .subject-info {
font-size: 12px;
color: var(--clay-muted);
font-weight: 500;
}
.subject-card .subject-chapters {
display: inline-block;
background: linear-gradient(135deg, rgba(167,139,250,0.22), rgba(124,58,237,0.14));
color: var(--clay-accent);
padding: 4px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
margin-top: 10px;
border: 1px solid rgba(124,58,237,0.18);
box-shadow: var(--shadow-badge);
}
/* Loading */
.loading-subjects {
grid-column: 1 / -1;
text-align: center;
color: var(--clay-muted);
padding: 52px 20px;
}
.loading-subjects .spinner {
width: 44px;
height: 44px;
border: 3px solid rgba(124,58,237,0.15);
border-top-color: var(--clay-accent);
border-radius: 50%;
animation: claySpin 0.85s linear infinite;
margin: 0 auto 18px;
}
.loading-subjects p {
font-weight: 600;
font-size: 14px;
color: var(--clay-muted);
}
/* Start Button */
.start-btn {
background: var(--clay-btn-grad);
border: none;
color: #fff;
padding: 0 40px;
height: 58px;
border-radius: 24px;
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 17px;
font-weight: 800;
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.25s ease,
opacity 0.2s ease;
display: inline-flex;
align-items: center;
gap: 10px;
box-shadow: var(--shadow-btn);
letter-spacing: 0.2px;
}
.start-btn:hover:not(:disabled) {
transform: translateY(-4px) scale(1.03);
box-shadow: var(--shadow-btn-hover);
}
.start-btn:active:not(:disabled) {
transform: scale(0.93);
box-shadow: var(--shadow-pressed);
}
.start-btn:disabled {
opacity: 0.48;
cursor: not-allowed;
box-shadow: none;
}
/* No subjects */
.no-subjects {
grid-column: 1 / -1;
text-align: center;
padding: 48px 20px;
color: var(--clay-muted);
}
.no-subjects .no-subjects-icon { font-size: 52px; margin-bottom: 16px; color: var(--clay-muted); }
.retry-btn {
background: var(--clay-card-bg);
backdrop-filter: blur(12px);
border: 1.5px solid rgba(124,58,237,0.22);
color: var(--clay-accent);
padding: 10px 26px;
border-radius: 20px;
cursor: pointer;
font-family: 'Tajawal', sans-serif;
font-size: 14px;
font-weight: 700;
margin-top: 18px;
transition: all 0.25s ease;
box-shadow: var(--shadow-badge);
display: inline-flex;
align-items: center;
gap: 8px;
}
.retry-btn:hover {
background: rgba(124,58,237,0.10);
transform: translateY(-2px);
box-shadow: var(--shadow-subject);
}
/* ============================================================
TRANSITION OVERLAY
============================================================ */
.transition-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(244,241,250,0.88);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
z-index: 500;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 20px;
}
.transition-overlay.active { display: flex; }
.transition-overlay .spinner {
width: 52px;
height: 52px;
border: 3px solid rgba(124,58,237,0.18);
border-top-color: var(--clay-accent);
border-radius: 50%;
animation: claySpin 0.85s linear infinite;
}
.transition-overlay p {
color: var(--clay-muted);
font-size: 15px;
font-weight: 600;
}
/* ============================================================
CHAT PAGE
============================================================ */
.chat-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
position: relative;
z-index: 1;
}
.chat-page.hidden { display: none; }
/* ============================================================
HEADER
============================================================ */
.header {
background: rgba(255,255,255,0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1.5px solid rgba(255,255,255,0.80);
padding: 12px 22px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
z-index: 10;
box-shadow: var(--shadow-header);
border-radius: 0 0 28px 28px;
}
.header-title {
display: flex;
align-items: center;
gap: 14px;
}
.header-title .logo {
width: 46px;
height: 46px;
background: var(--clay-btn-grad);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: var(--shadow-btn);
cursor: pointer;
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.3s ease;
flex-shrink: 0;
color: white;
}
.header-title .logo:hover {
transform: translateY(-3px) scale(1.08);
box-shadow: var(--shadow-btn-hover);
}
.header-title .logo:active {
transform: scale(0.92);
box-shadow: var(--shadow-pressed);
}
.header-title h1 {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 17px;
font-weight: 900;
background: linear-gradient(135deg, var(--clay-accent) 20%, var(--clay-accent-alt) 80%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
}
.header-title .subtitle {
font-size: 11.5px;
color: var(--clay-muted);
font-weight: 500;
margin-top: 1px;
}
.subject-badge-header {
display: inline-flex;
align-items: center;
gap: 5px;
background: linear-gradient(135deg, rgba(167,139,250,0.18), rgba(124,58,237,0.10));
border: 1px solid rgba(124,58,237,0.22);
color: var(--clay-accent);
padding: 3px 11px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
margin-top: 4px;
box-shadow: var(--shadow-badge);
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.btn-header {
background: rgba(255,255,255,0.70);
backdrop-filter: blur(8px);
border: 1.5px solid rgba(124,58,237,0.16);
color: var(--clay-muted);
padding: 8px 14px;
border-radius: 16px;
cursor: pointer;
font-family: 'Tajawal', sans-serif;
font-size: 13px;
font-weight: 600;
transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.25s ease,
background 0.2s ease,
color 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
box-shadow: var(--shadow-badge);
}
.btn-header:hover {
background: rgba(255,255,255,0.92);
border-color: rgba(124,58,237,0.35);
color: var(--clay-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-subject);
}
.btn-header:active {
transform: scale(0.94);
box-shadow: var(--shadow-pressed);
}
.btn-change-subject {
background: linear-gradient(135deg, rgba(167,139,250,0.18), rgba(124,58,237,0.10));
border-color: rgba(124,58,237,0.28);
color: var(--clay-accent);
}
.btn-change-subject:hover {
background: linear-gradient(135deg, rgba(167,139,250,0.30), rgba(124,58,237,0.18));
}
.session-badge {
background: linear-gradient(135deg, rgba(16,185,129,0.14), rgba(52,211,153,0.08));
border: 1px solid rgba(16,185,129,0.25);
color: #059669;
padding: 6px 12px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
gap: 5px;
box-shadow: var(--shadow-badge);
}
.session-badge .dot {
width: 7px;
height: 7px;
background: #10B981;
border-radius: 50%;
animation: clayPulse 2.2s ease-in-out infinite;
flex-shrink: 0;
}
/* ============================================================
CHAT CONTAINER
============================================================ */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 24px 20px;
display: flex;
flex-direction: column;
gap: 18px;
scroll-behavior: smooth;
}
/* ============================================================
WELCOME MESSAGE
============================================================ */
.welcome-message {
text-align: center;
padding: 48px 24px 36px;
color: var(--clay-muted);
max-width: 560px;
margin: 0 auto;
}
.welcome-message .welcome-icon {
font-size: 64px;
margin-bottom: 20px;
display: block;
filter: drop-shadow(0 8px 20px rgba(124,58,237,0.25));
animation: clayBreathe 5s ease-in-out infinite;
color: var(--clay-accent);
}
.welcome-message h2 {
font-family: 'Nunito', 'Tajawal', sans-serif;
color: var(--clay-fg);
font-size: 24px;
font-weight: 900;
margin-bottom: 10px;
}
.welcome-message p {
font-size: 14.5px;
line-height: 1.85;
max-width: 460px;
margin: 0 auto;
color: var(--clay-muted);
font-weight: 500;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 24px;
}
.quick-action {
background: rgba(255,255,255,0.72);
backdrop-filter: blur(12px);
border: 1.5px solid rgba(124,58,237,0.18);
color: var(--clay-accent);
padding: 9px 18px;
border-radius: 20px;
cursor: pointer;
font-family: 'Tajawal', sans-serif;
font-size: 13px;
font-weight: 700;
transition: transform 0.28s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.28s ease,
background 0.2s ease;
box-shadow: var(--shadow-badge);
display: flex;
align-items: center;
gap: 6px;
}
.quick-action:hover {
background: linear-gradient(135deg, rgba(167,139,250,0.22), rgba(124,58,237,0.12));
border-color: rgba(124,58,237,0.38);
transform: translateY(-3px) scale(1.04);
box-shadow: var(--shadow-subject);
}
.quick-action:active {
transform: scale(0.94);
box-shadow: var(--shadow-pressed);
}
/* ============================================================
MESSAGES
============================================================ */
.message {
display: flex;
gap: 10px;
max-width: 86%;
animation: clayMessageIn 0.35s cubic-bezier(0.34,1.56,0.64,1);
}
.message.user {
align-self: flex-start;
flex-direction: row;
}
.message.bot {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-avatar {
width: 38px;
height: 38px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
flex-shrink: 0;
margin-top: 4px;
}
.message.user .message-avatar {
background: var(--clay-btn-grad);
box-shadow: var(--shadow-btn);
color: white;
}
.message.bot .message-avatar {
background: rgba(255,255,255,0.80);
border: 1.5px solid rgba(124,58,237,0.18);
box-shadow: var(--shadow-badge);
color: var(--clay-accent);
}
.message-wrapper {
min-width: 0;
flex: 1;
}
.message-content {
padding: 14px 20px;
border-radius: 24px;
line-height: 1.85;
font-size: 14.5px;
position: relative;
word-wrap: break-word;
overflow-wrap: break-word;
font-weight: 500;
}
/* User bubble — convex clay */
.message.user .message-content {
background: var(--clay-btn-grad);
color: #fff;
border-bottom-right-radius: 6px;
box-shadow: var(--shadow-user-msg);
}
/* Bot bubble — floating card */
.message.bot .message-content {
background: rgba(255,255,255,0.78);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: var(--clay-fg);
border: 1.5px solid rgba(255,255,255,0.85);
border-bottom-left-radius: 6px;
box-shadow: var(--shadow-bot-msg);
}
.message-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 7px;
font-size: 11px;
color: var(--clay-subtle);
font-weight: 500;
}
.message.user .message-meta {
color: rgba(124,58,237,0.55);
}
.file-badge {
background: linear-gradient(135deg, rgba(167,139,250,0.20), rgba(124,58,237,0.12));
color: var(--clay-accent);
padding: 2px 9px;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
border: 1px solid rgba(124,58,237,0.18);
}
/* ============================================================
STREAMING CURSOR
============================================================ */
.streaming-cursor {
display: inline-block;
width: 2.5px;
height: 1em;
background: var(--clay-accent);
margin-right: 2px;
vertical-align: text-bottom;
border-radius: 2px;
animation: clayBlink 0.75s ease-in-out infinite;
}
/* ============================================================
BOT MESSAGE RICH CONTENT
============================================================ */
.message.bot .message-content h1,
.message.bot .message-content h2,
.message.bot .message-content h3,
.message.bot .message-content h4,
.message.bot .message-content h5,
.message.bot .message-content h6 {
font-family: 'Nunito', 'Tajawal', sans-serif;
color: var(--clay-fg);
margin: 18px 0 9px;
line-height: 1.25;
}
.message.bot .message-content h1 {
font-size: 20px;
font-weight: 900;
border-bottom: 2.5px solid rgba(124,58,237,0.25);
padding-bottom: 8px;
margin-bottom: 14px;
background: linear-gradient(135deg, var(--clay-accent), var(--clay-accent-alt));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.message.bot .message-content h2 {
font-size: 17px;
font-weight: 800;
border-bottom: 1.5px solid rgba(124,58,237,0.14);
padding-bottom: 6px;
color: var(--clay-accent);
}
.message.bot .message-content h3 {
font-size: 15.5px;
font-weight: 800;
color: #6D28D9;
}
.message.bot .message-content h4 {
font-size: 14.5px;
font-weight: 700;
color: #7C3AED;
}
.message.bot .message-content h5,
.message.bot .message-content h6 {
font-size: 13.5px;
font-weight: 700;
color: var(--clay-muted);
}
.message.bot .message-content > h1:first-child,
.message.bot .message-content > h2:first-child,
.message.bot .message-content > h3:first-child { margin-top: 0; }
.message.bot .message-content p {
margin: 9px 0;
line-height: 1.9;
color: var(--clay-fg);
}
.message.bot .message-content strong,
.message.bot .message-content b {
color: var(--clay-accent);
font-weight: 700;
}
.message.bot .message-content em,
.message.bot .message-content i {
color: var(--clay-accent-alt);
font-style: italic;
}
.message.bot .message-content a {
color: var(--clay-sky);
text-decoration: none;
border-bottom: 1.5px dashed rgba(14,165,233,0.45);
transition: all 0.2s ease;
font-weight: 600;
}
.message.bot .message-content a:hover {
color: var(--clay-accent);
border-bottom-style: solid;
border-bottom-color: var(--clay-accent);
}
.message.bot .message-content ul,
.message.bot .message-content ol {
margin: 10px 0;
padding-right: 26px;
}
.message.bot .message-content li {
margin: 7px 0;
line-height: 1.85;
color: var(--clay-fg);
}
.message.bot .message-content ul li::marker {
color: var(--clay-accent);
}
.message.bot .message-content ol li::marker {
color: var(--clay-sky);
font-weight: 700;
}
/* TABLE */
.message.bot .message-content .table-wrapper {
overflow-x: auto;
margin: 16px 0;
border-radius: 20px;
border: 1.5px solid rgba(124,58,237,0.14);
box-shadow: var(--shadow-card);
}
.message.bot .message-content table {
width: 100%;
border-collapse: collapse;
font-size: 13.5px;
min-width: 300px;
background: rgba(255,255,255,0.60);
}
.message.bot .message-content thead {
background: linear-gradient(135deg, rgba(167,139,250,0.18), rgba(124,58,237,0.10));
}
.message.bot .message-content th {
padding: 11px 16px;
text-align: right;
font-family: 'Nunito', 'Tajawal', sans-serif;
font-weight: 800;
color: var(--clay-accent);
border-bottom: 2px solid rgba(124,58,237,0.20);
white-space: nowrap;
font-size: 13px;
}
.message.bot .message-content td {
padding: 10px 16px;
text-align: right;
border-bottom: 1px solid rgba(124,58,237,0.07);
vertical-align: top;
color: var(--clay-fg);
font-weight: 500;
}
.message.bot .message-content tbody tr:nth-child(odd) {
background: rgba(167,139,250,0.04);
}
.message.bot .message-content tbody tr:nth-child(even) {
background: rgba(255,255,255,0.50);
}
.message.bot .message-content tbody tr:hover {
background: rgba(167,139,250,0.10);
}
.message.bot .message-content tbody tr:last-child td {
border-bottom: none;
}
/* INLINE CODE */
.message.bot .message-content code:not(pre code) {
background: rgba(124,58,237,0.10);
padding: 2px 8px;
border-radius: 8px;
font-size: 13px;
font-family: 'Courier New', monospace;
color: var(--clay-accent-alt);
border: 1px solid rgba(124,58,237,0.16);
font-weight: 600;
}
/* CODE BLOCK */
.message.bot .message-content pre {
background: #1e1a2e;
border: 1.5px solid rgba(124,58,237,0.22);
border-radius: 20px;
padding: 0;
margin: 14px 0;
overflow: hidden;
box-shadow:
10px 10px 20px rgba(124,58,237,0.15),
-6px -6px 14px rgba(255,255,255,0.60),
inset 0 1px 0 rgba(255,255,255,0.08);
}
.message.bot .message-content pre .code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 9px 16px;
background: rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(124,58,237,0.18);
font-size: 12px;
color: rgba(167,139,250,0.80);
font-weight: 600;
}
.message.bot .message-content pre .code-header .copy-btn {
background: rgba(167,139,250,0.18);
border: 1px solid rgba(167,139,250,0.30);
color: rgba(167,139,250,0.90);
padding: 4px 12px;
border-radius: 10px;
cursor: pointer;
font-size: 11px;
font-family: 'Tajawal', sans-serif;
font-weight: 700;
transition: all 0.2s ease;
}
.message.bot .message-content pre .code-header .copy-btn:hover {
background: rgba(167,139,250,0.32);
color: #fff;
transform: translateY(-1px);
}
.message.bot .message-content pre .code-header .copy-btn:active {
transform: scale(0.94);
}
.message.bot .message-content pre code {
display: block;
padding: 16px 20px;
overflow-x: auto;
font-size: 13px;
line-height: 1.65;
font-family: 'Courier New', monospace;
color: #e2d9f3;
background: transparent;
border: none;
}
/* BLOCKQUOTE */
.message.bot .message-content blockquote {
border-right: 4px solid var(--clay-accent);
background: linear-gradient(135deg, rgba(167,139,250,0.10), rgba(124,58,237,0.05));
padding: 13px 20px;
margin: 14px 0;
border-radius: 0 16px 16px 0;
color: var(--clay-muted);
font-style: italic;
font-weight: 500;
box-shadow:
6px 6px 12px rgba(124,58,237,0.08),
-4px -4px 8px rgba(255,255,255,0.80);
}
/* HR */
.message.bot .message-content hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(124,58,237,0.20), transparent);
margin: 18px 0;
border-radius: 2px;
}
/* MATH */
.message.bot .message-content .MathJax { font-size: 105% !important; }
.message.bot .message-content mjx-container {
overflow-x: auto;
overflow-y: hidden;
padding: 2px 0;
}
.message.bot .message-content mjx-container[display="true"] {
margin: 14px 0;
padding: 14px 18px;
background: rgba(255,255,255,0.65);
backdrop-filter: blur(8px);
border-radius: 16px;
border: 1.5px solid rgba(124,58,237,0.12);
box-shadow: var(--shadow-card);
}
/* ============================================================
TYPING INDICATOR
============================================================ */
.typing-indicator {
display: none;
align-self: flex-end;
gap: 10px;
flex-direction: row-reverse;
max-width: 85%;
}
.typing-indicator.active { display: flex; }
.typing-dots {
display: flex;
gap: 5px;
padding: 16px 22px;
background: rgba(255,255,255,0.78);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1.5px solid rgba(255,255,255,0.85);
border-radius: 24px;
border-bottom-left-radius: 6px;
box-shadow: var(--shadow-typing);
align-items: center;
}
.typing-dots span {
width: 9px;
height: 9px;
background: linear-gradient(135deg, var(--clay-accent-light), var(--clay-accent));
border-radius: 50%;
animation: clayTyping 1.4s ease-in-out infinite;
box-shadow: 0 2px 6px rgba(124,58,237,0.30);
}
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
/* ============================================================
STATUS BAR
============================================================ */
.status-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
padding: 7px 16px;
background: rgba(255,255,255,0.65);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
font-size: 11px;
color: var(--clay-subtle);
flex-shrink: 0;
border-top: 1.5px solid rgba(255,255,255,0.75);
box-shadow: var(--shadow-status);
font-weight: 600;
}
.status-item {
display: flex;
align-items: center;
gap: 5px;
background: rgba(255,255,255,0.60);
padding: 3px 10px;
border-radius: 12px;
border: 1px solid rgba(124,58,237,0.10);
box-shadow: var(--shadow-badge);
transition: transform 0.2s ease;
}
.status-item:hover {
transform: translateY(-1px);
border-color: rgba(124,58,237,0.22);
}
/* ============================================================
INPUT AREA
============================================================ */
.input-area {
padding: 14px 20px 18px;
background: rgba(255,255,255,0.72);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1.5px solid rgba(255,255,255,0.80);
flex-shrink: 0;
border-radius: 28px 28px 0 0;
box-shadow: var(--shadow-header);
}
.input-wrapper {
display: flex;
gap: 10px;
align-items: flex-end;
max-width: 900px;
margin: 0 auto;
}
.input-field {
flex: 1;
background: var(--clay-input-bg);
border: 1.5px solid rgba(124,58,237,0.14);
border-radius: 20px;
padding: 13px 20px;
color: var(--clay-fg);
font-family: 'Tajawal', sans-serif;
font-size: 15px;
font-weight: 500;
resize: none;
max-height: 120px;
min-height: 50px;
outline: none;
transition: box-shadow 0.25s ease,
background 0.25s ease,
border-color 0.25s ease;
line-height: 1.55;
box-shadow: var(--shadow-input);
}
.input-field:focus {
background: var(--clay-input-focus);
border-color: rgba(124,58,237,0.35);
box-shadow: var(--shadow-input-focus);
}
.input-field::placeholder {
color: var(--clay-subtle);
font-weight: 400;
}
.send-btn {
width: 50px;
height: 50px;
background: var(--clay-btn-grad);
border: none;
border-radius: 18px;
color: white;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.28s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.28s ease,
opacity 0.2s ease;
flex-shrink: 0;
box-shadow: var(--shadow-send);
}
.send-btn:hover:not(:disabled) {
transform: translateY(-3px) scale(1.08);
box-shadow: var(--shadow-btn-hover);
}
.send-btn:active:not(:disabled) {
transform: scale(0.92);
box-shadow: var(--shadow-pressed);
}
.send-btn:disabled {
opacity: 0.48;
cursor: not-allowed;
box-shadow: none;
}
/* ============================================================
MODAL
============================================================ */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(100,80,140,0.25);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-overlay.active { display: flex; }
.modal {
background: rgba(255,255,255,0.88);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1.5px solid rgba(255,255,255,0.90);
border-radius: 36px;
padding: 30px;
max-width: 620px;
width: 100%;
max-height: 78vh;
overflow-y: auto;
box-shadow: var(--shadow-modal);
animation: clayMessageIn 0.35s cubic-bezier(0.34,1,1.56,0.64,1);
}
.modal::-webkit-scrollbar { width: 5px; }
.modal::-webkit-scrollbar-thumb {
background: rgba(124,58,237,0.22);
border-radius: 10px;
}
.modal h3 {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 19px;
font-weight: 900;
color: var(--clay-fg);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 10px;
padding-bottom: 14px;
border-bottom: 2px solid rgba(124,58,237,0.12);
}
.modal-content {
font-size: 14px;
line-height: 1.85;
color: var(--clay-muted);
font-weight: 500;
}
/* Rich content inside modal */
.modal-content h1,
.modal-content h2,
.modal-content h3,
.modal-content h4 {
font-family: 'Nunito', 'Tajawal', sans-serif;
color: var(--clay-fg);
margin: 14px 0 8px;
font-weight: 800;
}
.modal-content h1 { font-size: 18px; color: var(--clay-accent); }
.modal-content h2 { font-size: 16px; color: var(--clay-accent); }
.modal-content h3 { font-size: 15px; color: #6D28D9; }
.modal-content p { margin: 8px 0; color: var(--clay-fg); }
.modal-content strong,
.modal-content b { color: var(--clay-accent); font-weight: 700; }
.modal-content ul,
.modal-content ol { padding-right: 22px; margin: 8px 0; }
.modal-content li { margin: 5px 0; color: var(--clay-fg); }
.modal-content ul li::marker { color: var(--clay-accent); }
.modal-content ol li::marker { color: var(--clay-sky); font-weight: 700; }
.modal-content code:not(pre code) {
background: rgba(124,58,237,0.10);
padding: 2px 7px;
border-radius: 7px;
font-size: 12.5px;
font-family: 'Courier New', monospace;
color: var(--clay-accent-alt);
border: 1px solid rgba(124,58,237,0.15);
}
.modal-content blockquote {
border-right: 4px solid var(--clay-accent);
background: rgba(167,139,250,0.08);
padding: 10px 16px;
margin: 10px 0;
border-radius: 0 12px 12px 0;
color: var(--clay-muted);
font-style: italic;
}
.modal-content hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(124,58,237,0.18), transparent);
margin: 14px 0;
}
.modal-content mjx-container[display="true"] {
margin: 12px 0;
padding: 12px 16px;
background: rgba(167,139,250,0.07);
border-radius: 14px;
border: 1px solid rgba(124,58,237,0.12);
}
.modal-close {
background: var(--clay-btn-grad);
border: none;
color: white;
padding: 0 24px;
height: 48px;
border-radius: 18px;
cursor: pointer;
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 15px;
font-weight: 800;
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
transition: transform 0.28s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.28s ease;
box-shadow: var(--shadow-btn);
}
.modal-close:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-btn-hover);
}
.modal-close:active {
transform: scale(0.93);
box-shadow: var(--shadow-pressed);
}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 768px) {
.subject-header h1 { font-size: 22px; }
.subject-header .main-logo { font-size: 56px; }
.subjects-grid {
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
gap: 13px;
}
.subject-card { padding: 20px 14px 18px; border-radius: 24px; }
.subject-card .subject-icon { font-size: 36px; }
.header { padding: 10px 14px; border-radius: 0 0 20px 20px; }
.header-title h1 { font-size: 14px; }
.header-title .logo { width: 40px; height: 40px; font-size: 19px; border-radius: 13px; }
.header-actions { gap: 5px; }
.btn-header { padding: 7px 10px; font-size: 12px; border-radius: 13px; }
.btn-header .btn-text { display: none; }
.chat-container { padding: 14px 12px; gap: 14px; }
.message { max-width: 94%; }
.message-content { padding: 12px 15px; font-size: 14px; border-radius: 20px; }
.input-area { padding: 10px 14px 14px; border-radius: 22px 22px 0 0; }
.input-field { font-size: 14px; padding: 11px 16px; border-radius: 17px; }
.send-btn { width: 46px; height: 46px; border-radius: 15px; font-size: 18px; }
.status-bar { gap: 8px; font-size: 10px; padding: 5px 10px; }
.status-item { padding: 2px 8px; border-radius: 10px; }
.modal { border-radius: 28px; padding: 22px; }
.modal h3 { font-size: 16px; }
.welcome-message { padding: 32px 16px 24px; }
.welcome-message .welcome-icon { font-size: 52px; }
.welcome-message h2 { font-size: 20px; }
}
@media (max-width: 480px) {
.session-badge { display: none; }
.subjects-grid { grid-template-columns: repeat(2, 1fr); gap: 11px; }
.subject-header .main-logo { font-size: 48px; }
.subject-header h1 { font-size: 20px; }
.start-btn { height: 52px; font-size: 15px; padding: 0 28px; border-radius: 20px; }
.message { max-width: 97%; }
.quick-actions { gap: 7px; }
.quick-action { padding: 8px 14px; font-size: 12px; }
}
/* ============================================================
UTILITY — force Nunito on key numeric/label elements
============================================================ */
#messageCount,
#totalMessages {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-weight: 800;
}
/* ============================================================
MEDIA BUTTONS (Voice & Image)
============================================================ */
.media-btn {
width: 50px;
height: 50px;
background: rgba(255,255,255,0.72);
border: 1.5px solid rgba(124,58,237,0.18);
border-radius: 18px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.28s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.28s ease,
background 0.2s ease,
border-color 0.2s ease;
box-shadow: var(--shadow-badge);
color: var(--clay-accent);
}
.media-btn:hover:not(:disabled) {
background: rgba(255,255,255,0.92);
border-color: rgba(124,58,237,0.35);
transform: translateY(-3px) scale(1.08);
box-shadow: var(--shadow-subject);
}
.media-btn:active:not(:disabled) {
transform: scale(0.92);
box-shadow: var(--shadow-pressed);
}
.media-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Voice button recording state */
.media-btn.voice-btn.recording {
background: linear-gradient(135deg, rgba(239,68,68,0.15), rgba(220,38,38,0.08));
border-color: rgba(239,68,68,0.45);
animation: voicePulse 1.2s ease-in-out infinite;
box-shadow:
0 0 0 0 rgba(239,68,68,0.4),
var(--shadow-badge);
color: #ef4444;
}
@keyframes voicePulse {
0%, 100% {
box-shadow:
0 0 0 0 rgba(239,68,68,0.35),
var(--shadow-badge);
}
50% {
box-shadow:
0 0 0 8px rgba(239,68,68,0.0),
var(--shadow-badge);
}
}
/* ============================================================
IMAGE PREVIEW BAR
============================================================ */
.image-preview-bar {
max-width: 900px;
margin: 0 auto 10px;
background: rgba(255,255,255,0.82);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1.5px solid rgba(124,58,237,0.18);
border-radius: 20px;
padding: 10px 14px;
box-shadow: var(--shadow-card);
animation: clayMessageIn 0.3s cubic-bezier(0.34,1.56,0.64,1);
}
.image-preview-inner {
display: flex;
align-items: center;
gap: 12px;
}
#imagePreviewThumb {
width: 52px;
height: 52px;
object-fit: cover;
border-radius: 12px;
border: 1.5px solid rgba(124,58,237,0.18);
box-shadow: var(--shadow-badge);
flex-shrink: 0;
}
.image-preview-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
#imagePreviewName {
font-size: 13px;
font-weight: 700;
color: var(--clay-fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-preview-status {
font-size: 11.5px;
font-weight: 600;
color: var(--clay-muted);
transition: color 0.2s ease;
}
.image-preview-status.status-analyzing {
color: var(--clay-amber);
animation: clayPulse 1.5s ease-in-out infinite;
}
.image-preview-status.status-ready {
color: var(--clay-emerald);
}
.image-preview-cancel {
width: 30px;
height: 30px;
background: rgba(239,68,68,0.10);
border: 1.5px solid rgba(239,68,68,0.22);
border-radius: 10px;
color: #ef4444;
font-size: 13px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
font-weight: 700;
}
.image-preview-cancel:hover {
background: rgba(239,68,68,0.18);
border-color: rgba(239,68,68,0.40);
transform: scale(1.08);
}
.image-preview-cancel:active {
transform: scale(0.92);
}
/* ============================================================
MESSAGE IMAGE (inside chat bubble)
============================================================ */
.message-image-wrapper {
margin-bottom: 8px;
}
.message-image {
max-width: 260px;
max-height: 200px;
width: 100%;
object-fit: cover;
border-radius: 16px;
border: 1.5px solid rgba(255,255,255,0.40);
box-shadow: var(--shadow-badge);
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.25s ease;
display: block;
}
.message-image:hover {
transform: scale(1.03);
box-shadow: var(--shadow-card-hover);
}
/* ============================================================
VOICE RECORDING UI
============================================================ */
.voice-recording-ui {
max-width: 900px;
margin: 0 auto 10px;
background: linear-gradient(135deg, rgba(239,68,68,0.08), rgba(220,38,38,0.04));
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1.5px solid rgba(239,68,68,0.25);
border-radius: 20px;
padding: 12px 16px;
box-shadow: var(--shadow-card);
animation: clayMessageIn 0.3s cubic-bezier(0.34,1.56,0.64,1);
}
.voice-recording-inner {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
/* Animated sound wave */
.voice-wave {
display: flex;
align-items: center;
gap: 3px;
height: 28px;
flex-shrink: 0;
}
.voice-wave span {
display: block;
width: 4px;
border-radius: 4px;
background: linear-gradient(180deg, #ef4444, #dc2626);
animation: voiceWave 0.9s ease-in-out infinite;
}
.voice-wave span:nth-child(1) { height: 10px; animation-delay: 0s; }
.voice-wave span:nth-child(2) { height: 22px; animation-delay: 0.15s; }
.voice-wave span:nth-child(3) { height: 16px; animation-delay: 0.3s; }
.voice-wave span:nth-child(4) { height: 26px; animation-delay: 0.45s; }
.voice-wave span:nth-child(5) { height: 12px; animation-delay: 0.6s; }
@keyframes voiceWave {
0%, 100% { transform: scaleY(1); opacity: 0.7; }
50% { transform: scaleY(1.6); opacity: 1; }
}
.voice-timer {
font-family: 'Nunito', 'Tajawal', sans-serif;
font-size: 15px;
font-weight: 800;
color: #ef4444;
flex-shrink: 0;
min-width: 44px;
}
.voice-label {
font-size: 12.5px;
font-weight: 600;
color: var(--clay-muted);
flex: 1;
}
.voice-stop-btn {
background: linear-gradient(135deg, #ef4444, #dc2626);
border: none;
color: white;
padding: 7px 16px;
border-radius: 14px;
cursor: pointer;
font-family: 'Tajawal', sans-serif;
font-size: 12.5px;
font-weight: 700;
transition: transform 0.25s cubic-bezier(0.34,1.56,0.64,1),
box-shadow 0.25s ease;
box-shadow:
6px 6px 12px rgba(239,68,68,0.30),
-4px -4px 8px rgba(255,255,255,0.40);
flex-shrink: 0;
}
.voice-stop-btn:hover {
transform: translateY(-2px) scale(1.04);
box-shadow:
8px 8px 16px rgba(239,68,68,0.38),
-5px -5px 10px rgba(255,255,255,0.45);
}
.voice-stop-btn:active {
transform: scale(0.93);
}
.voice-cancel-btn {
background: rgba(255,255,255,0.70);
border: 1.5px solid rgba(239,68,68,0.22);
color: #ef4444;
padding: 7px 14px;
border-radius: 14px;
cursor: pointer;
font-family: 'Tajawal', sans-serif;
font-size: 12.5px;
font-weight: 700;
transition: all 0.2s ease;
flex-shrink: 0;
}
.voice-cancel-btn:hover {
background: rgba(239,68,68,0.08);
border-color: rgba(239,68,68,0.40);
transform: translateY(-1px);
}
.voice-cancel-btn:active {
transform: scale(0.93);
}
/* ============================================================
UNSUPPORTED IMAGE NOTIFICATION
============================================================ */
.unsupported-image-notif {
align-self: center;
display: flex;
align-items: center;
gap: 10px;
background: linear-gradient(135deg, rgba(239,68,68,0.12), rgba(220,38,38,0.06));
border: 1.5px solid rgba(239,68,68,0.30);
border-radius: 20px;
padding: 12px 20px;
font-size: 13.5px;
font-weight: 600;
color: #dc2626;
max-width: 86%;
box-shadow:
8px 8px 16px rgba(239,68,68,0.12),
-6px -6px 12px rgba(255,255,255,0.80);
animation: clayMessageIn 0.35s cubic-bezier(0.34,1.56,0.64,1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.unsupported-icon {
font-size: 20px;
flex-shrink: 0;
color: #dc2626;
}
/* ============================================================
TOAST NOTIFICATION
============================================================ */
.toast {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: rgba(255,255,255,0.90);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1.5px solid rgba(124,58,237,0.18);
border-radius: 20px;
padding: 12px 24px;
font-size: 13.5px;
font-weight: 700;
color: var(--clay-fg);
box-shadow: var(--shadow-modal);
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease,
transform 0.3s cubic-bezier(0.34,1.56,0.64,1);
white-space: nowrap;
max-width: 90vw;
text-align: center;
font-family: 'Tajawal', sans-serif;
display: flex;
align-items: center;
gap: 8px;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
pointer-events: auto;
}
.toast.toast-success {
border-color: rgba(16,185,129,0.35);
background: linear-gradient(135deg, rgba(16,185,129,0.12), rgba(255,255,255,0.90));
color: #065f46;
}
.toast.toast-error {
border-color: rgba(239,68,68,0.35);
background: linear-gradient(135deg, rgba(239,68,68,0.10), rgba(255,255,255,0.90));
color: #991b1b;
}
.toast.toast-info {
border-color: rgba(14,165,233,0.30);
background: linear-gradient(135deg, rgba(14,165,233,0.10), rgba(255,255,255,0.90));
color: #0c4a6e;
}
/* ============================================================
RESPONSIVE — new elements
============================================================ */
@media (max-width: 768px) {
.media-btn {
width: 46px;
height: 46px;
font-size: 18px;
border-radius: 15px;
}
.voice-recording-inner { gap: 8px; }
.voice-label { display: none; }
.voice-stop-btn { padding: 7px 12px; font-size: 12px; }
.voice-cancel-btn { padding: 7px 10px; font-size: 12px; }
.image-preview-bar { border-radius: 16px; padding: 8px 12px; }
#imagePreviewThumb { width: 44px; height: 44px; border-radius: 10px; }
.message-image { max-width: 200px; max-height: 160px; }
.toast { font-size: 12.5px; padding: 10px 18px; bottom: 80px; }
.unsupported-image-notif { font-size: 12.5px; padding: 10px 16px; }
}
@media (max-width: 480px) {
.media-btn {
width: 42px;
height: 42px;
font-size: 17px;
border-radius: 13px;
}
.voice-recording-inner { gap: 6px; }
.voice-stop-btn { padding: 6px 10px; font-size: 11.5px; border-radius: 12px; }
.voice-cancel-btn { padding: 6px 8px; font-size: 11.5px; border-radius: 12px; }
.voice-wave span { width: 3px; }
.image-preview-bar { border-radius: 14px; }
#imagePreviewThumb { width: 38px; height: 38px; }
#imagePreviewName { font-size: 12px; }
.image-preview-status { font-size: 10.5px; }
.message-image { max-width: 170px; max-height: 130px; border-radius: 13px; }
.toast { font-size: 12px; padding: 9px 16px; border-radius: 16px; bottom: 70px; }
.unsupported-image-notif { font-size: 12px; padding: 9px 14px; border-radius: 16px; }
.unsupported-icon { font-size: 17px; }
}
</style>
</head>
<body>
<!-- ANIMATED BACKGROUND BLOBS -->
<div class="clay-blobs" aria-hidden="true">
<div class="clay-blob clay-blob-1"></div>
<div class="clay-blob clay-blob-2"></div>
<div class="clay-blob clay-blob-3"></div>
<div class="clay-blob clay-blob-4"></div>
</div>
<!-- SUBJECT SELECTION PAGE -->
<div class="subject-page hidden" id="subjectPage">
<div class="subject-header">
<div class="main-logo"><i class="fas fa-graduation-cap"></i></div>
<h1>المساعد <span>التعليمي الذكي</span></h1>
<div class="user-info-bar" id="userInfoBar">
<span class="user-greeting"><strong id="userDisplayName">-</strong></span>
<span class="user-track-badge" id="userTrackBadge">-</span>
</div>
<p>اختر المادة التي تريد الدراسة فيها وسيساعدك المساعد الذكي في فهم جميع المواضيع</p>
</div>
<div class="subjects-grid" id="subjectsGrid">
<div class="loading-subjects">
<div class="spinner"></div>
<p>جاري تحميل المواد...</p>
</div>
</div>
<button class="start-btn" id="startBtn" onclick="startChat()" disabled>
<span><i class="fas fa-play"></i></span>
<span>ابدأ المحادثة</span>
</button>
</div>
<!-- TRANSITION OVERLAY -->
<div class="transition-overlay" id="transitionOverlay">
<div class="spinner"></div>
<p id="transitionText">جاري تحميل المادة...</p>
</div>
<!-- CHAT PAGE -->
<div class="chat-page hidden" id="chatPage">
<!-- Header -->
<div class="header">
<div class="header-title">
<div class="logo"><i class="fas fa-graduation-cap"></i></div>
<div>
<h1 id="chatTitle">المساعد التعليمي الذكي</h1>
<div class="subtitle" id="chatSubtitle">نظام تعليمي ذكي بالذكاء الاصطناعي</div>
<div class="subject-badge-header" id="subjectBadgeHeader">
<i class="fas fa-book"></i> <span id="currentSubjectName">-</span>
</div>
</div>
</div>
<div class="header-actions">
<div class="user-chip" id="headerUserChip">
<span><i class="fas fa-user"></i></span>
<span id="headerUsername">-</span>
</div>
<div class="session-badge">
<span class="dot"></span>
<span id="messageCount">0 رسائل</span>
</div>
<button class="btn-header" onclick="showSummary()"><i class="fas fa-file-alt"></i> <span class="btn-text">الملخص</span></button>
<button class="btn-header" onclick="clearChat()"><i class="fas fa-trash-alt"></i> <span class="btn-text">مسح</span></button>
<a href="/dashboard" class="btn-header"><i class="fas fa-home"></i> <span class="btn-text">الرئيسية</span></a>
</div>
</div>
<!-- Chat Container -->
<div class="chat-container" id="chatContainer">
<div class="welcome-message" id="welcomeMessage">
<div class="welcome-icon" id="welcomeIcon"><i class="fas fa-book-open"></i></div>
<h2 id="welcomeTitle">مرحباً بك!</h2>
<p id="welcomeText">اطرح سؤالك وسأجيبك بالتفصيل مع المعادلات والشروحات!</p>
<div class="quick-actions" id="quickActions">
<button class="quick-action" onclick="sendQuick('مرحبا، عرفني بنفسك')"><i class="fas fa-hand-wave"></i> تعرف عليّ</button>
<button class="quick-action" onclick="sendQuick('ما هي المواضيع المتاحة؟')"><i class="fas fa-list"></i> المواضيع</button>
<button class="quick-action" onclick="sendQuick('اشرح لي الفصل الأول')"><i class="fas fa-book-open"></i> الفصل الأول</button>
</div>
</div>
<div class="history-loading" id="historyLoading" style="display:none;">
<div class="spinner"></div>
<p>جاري تحميل سجل المحادثة...</p>
</div>
<div class="typing-indicator" id="typingIndicator">
<div class="message-avatar" style="background:rgba(255,255,255,0.80);border:1.5px solid rgba(124,58,237,0.18);"><i class="fas fa-user-graduate"></i></div>
<div class="typing-dots"><span></span><span></span><span></span></div>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-item"><span><i class="fas fa-file-alt"></i></span><span id="currentFile">-</span></div>
<div class="status-item"><span><i class="fas fa-book"></i></span><span id="currentSubjectStatus">-</span></div>
<div class="status-item"><span><i class="fas fa-brain"></i></span><span id="summaryStatus">لا يوجد ملخص</span></div>
<div class="status-item"><span><i class="fas fa-comment-dots"></i></span><span id="totalMessages">0</span></div>
</div>
<!-- Input Area -->
<div class="input-area">
<!-- Image Preview Bar -->
<div class="image-preview-bar" id="imagePreviewBar" style="display:none;">
<div class="image-preview-inner">
<img id="imagePreviewThumb" src="" alt="preview" />
<div class="image-preview-info">
<span id="imagePreviewName">صورة</span>
<span class="image-preview-status" id="imagePreviewStatus">جاري التحليل...</span>
</div>
<button class="image-preview-cancel" onclick="cancelImage()" title="إلغاء"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="input-wrapper">
<input type="file" id="imageFileInput" accept="image/*" style="display:none;" onchange="handleImageSelected(event)" />
<button class="media-btn image-btn" id="imageBtn" onclick="document.getElementById('imageFileInput').click()" title="إرسال صورة">
<i class="fas fa-image"></i>
</button>
<button class="media-btn voice-btn" id="voiceBtn" onclick="toggleVoiceRecording()" title="تسجيل صوتي">
<i class="fas fa-microphone"></i>
</button>
<textarea
class="input-field"
id="messageInput"
placeholder="اكتب سؤالك هنا"
rows="1"
onkeydown="handleKeyDown(event)"
oninput="autoResize(this)"
></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()"><i class="fas fa-paper-plane"></i></button>
</div>
<!-- Voice Recording UI -->
<div class="voice-recording-ui" id="voiceRecordingUI" style="display:none;">
<div class="voice-recording-inner">
<div class="voice-wave">
<span></span><span></span><span></span><span></span><span></span>
</div>
<span class="voice-timer" id="voiceTimer">00:00</span>
<span class="voice-label">جاري التسجيل...</span>
<button class="voice-stop-btn" onclick="stopVoiceRecording()"><i class="fas fa-stop-circle"></i> إيقاف وإرسال</button>
<button class="voice-cancel-btn" onclick="cancelVoiceRecording()"><i class="fas fa-times"></i> إلغاء</button>
</div>
</div>
</div>
</div>
<!-- Summary Modal -->
<div class="modal-overlay" id="summaryModal">
<div class="modal">
<h3><i class="fas fa-file-alt"></i> ملخص المحادثة وتحليل مستوى الطالب</h3>
<div class="modal-content" id="summaryContent">جاري تحميل الملخص...</div>
<button class="modal-close" onclick="closeSummaryModal()">إغلاق</button>
</div>
</div>
<!-- Toast Notification -->
<div class="toast" id="toast"></div>
<script>
// ============================================================
// USERNAME FROM FLASK SESSION (injected by Jinja2)
// ============================================================
const USERNAME = "{{ username }}";
const SESSION_ID = "{{ session['session_id'] }}";
// ============================================================
// STATE
// ============================================================
let selectedSubject = null;
let selectedSubjectId = null;
let selectedSubjectName = null;
let isProcessing = false;
let messageCounter = 0;
let currentUserData = null;
let historyLoaded = false;
// Voice state
let mediaRecorder = null;
let audioChunks = [];
let voiceTimerInterval = null;
let voiceSeconds = 0;
let isRecording = false;
// Image state
let pendingImageUrl = null;
let pendingImageExtracted = null;
let pendingImageFile = null;
let imageAnalyzing = false;
// Icon map (Using FontAwesome Classes)
const subjectIconMap = {
'islamic': 'fa-book-quran',
'english': 'fa-language',
'math': 'fa-calculator',
'it': 'fa-laptop-code',
'statistics': 'fa-chart-pie',
'chemistry': 'fa-flask',
'physics_electric': 'fa-bolt',
'physics_mechanic': 'fa-cogs',
'biology': 'fa-dna',
'literary_studies': 'fa-feather',
'linguistic_studies': 'fa-pen-nib',
'literature': 'fa-book',
'philosophy': 'fa-brain',
'reading_writing': 'fa-pen',
'grammar': 'fa-spell-check',
'criticism': 'fa-comments',
'history': 'fa-landmark',
'geography': 'fa-globe',
'psychology': 'fa-head-side-virus',
'national_education': 'fa-flag',
'default': 'fa-book'
};
function getSubjectIcon(subjectId) {
return subjectIconMap[subjectId] || subjectIconMap['default'];
}
// ============================================================
// SOCKET.IO - FORCE LOGOUT & SESSION CHECK
// ============================================================
const socket = io();
socket.on('connect', function() {
socket.emit('register_session', {
username: USERNAME,
session_id: SESSION_ID
});
setInterval(function() {
socket.emit('check_session', {
username: USERNAME,
session_id: SESSION_ID
});
}, 30000);
});
socket.on('force_logout', function(data) {
alert(data.message || 'تم تسجيل خروجك من جهاز آخر.');
window.location.href = '/login';
});
socket.on('session_valid', function(data) {
console.log('✅ Session valid');
});
socket.on('disconnect', function() {
console.log('🔌 Socket disconnected');
});
// ============================================================
// SAFE FETCH - redirects to login on 401
// ============================================================
async function safeFetch(url, options = {}) {
const response = await fetch(url, options);
if (response.status === 401) {
window.location.href = '/login';
return null;
}
return response;
}
// ============================================================
// TOAST NOTIFICATION
// ============================================================
function showToast(message, type = 'info', duration = 3500) {
const toast = document.getElementById('toast');
// Allow HTML in message for icons
toast.innerHTML = message;
toast.className = `toast toast-${type} show`;
setTimeout(() => { toast.className = 'toast'; }, duration);
}
// ============================================================
// MARKDOWN PARSER
// ============================================================
class MarkdownParser {
static parse(text) {
if (!text) return '';
const protectedBlocks = [];
let blockIndex = 0;
function protect(content) {
const placeholder = `\x00BLOCK${blockIndex}\x00`;
protectedBlocks.push(content);
blockIndex++;
return placeholder;
}
text = text.replace(/\$\$([\s\S]*?)\$\$/g, (match) => protect(match));
text = text.replace(/\\\[([\s\S]*?)\\\]/g, (match) => protect(match));
text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => {
const escapedCode = MarkdownParser._escapeHtml(code.trim());
const codeId = `code_${Date.now()}_${blockIndex}`;
const html = `<pre><div class="code-header"><span>${lang || 'text'}</span><button class="copy-btn" onclick="copyCode('${codeId}')"><i class="fas fa-copy"></i> نسخ</button></div><code id="${codeId}" class="language-${lang || 'text'}">${escapedCode}</code></pre>`;
return protect(html);
});
text = text.replace(/`([^`\n]+)`/g, (match, code) => protect(`<code>${MarkdownParser._escapeHtml(code)}</code>`));
text = text.replace(/\\\([\s\S]*?\\\)/g, (match) => protect(match));
text = text.replace(/(?<![\\$])\$(?!\s)([^\$\n]+?)(?<!\s)\$(?!\$)/g, (match) => protect(match));
let lines = text.split('\n');
let html = '';
let inList = false;
let listType = '';
let inBlockquote = false;
let inTable = false;
let tableRows = [];
let i = 0;
while (i < lines.length) {
let line = lines[i];
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
if (!inTable) {
if (inList) { html += MarkdownParser._closeList(listType); inList = false; }
if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
inTable = true; tableRows = [];
}
tableRows.push(line.trim()); i++; continue;
} else if (inTable) {
html += MarkdownParser._renderTable(tableRows);
inTable = false; tableRows = []; continue;
}
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
if (inList) { html += MarkdownParser._closeList(listType); inList = false; }
if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
html += '<hr>'; i++; continue;
}
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
if (headingMatch) {
if (inList) { html += MarkdownParser._closeList(listType); inList = false; }
if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
const level = headingMatch[1].length;
html += `<h${level}>${MarkdownParser._parseInline(headingMatch[2])}</h${level}>`;
i++; continue;
}
if (line.trim().startsWith('>')) {
if (inList) { html += MarkdownParser._closeList(listType); inList = false; }
if (!inBlockquote) { html += '<blockquote>'; inBlockquote = true; }
html += `<p>${MarkdownParser._parseInline(line.trim().replace(/^>\s*/, ''))}</p>`;
i++; continue;
} else if (inBlockquote) {
html += '</blockquote>'; inBlockquote = false;
}
const ulMatch = line.match(/^(\s*)([-*+])\s+(.+)/);
if (ulMatch) {
if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
if (inList && listType !== 'ul') { html += MarkdownParser._closeList(listType); inList = false; }
if (!inList) { html += '<ul>'; inList = true; listType = 'ul'; }
html += `<li>${MarkdownParser._parseInline(ulMatch[3])}</li>`;
i++; continue;
}
const olMatch = line.match(/^(\s*)\d+[.)]\s+(.+)/);
if (olMatch) {
if (inBlockquote) { html += '</blockquote>'; inBlockquote = false; }
if (inList && listType !== 'ol') { html += MarkdownParser._closeList(listType); inList = false; }
if (!inList) { html += '<ol>'; inList = true; listType = 'ol'; }
html += `<li>${MarkdownParser._parseInline(olMatch[2])}</li>`;
i++; continue;
}
if (inList && line.trim() !== '') { html += MarkdownParser._closeList(listType); inList = false; }
if (line.trim() === '') { i++; continue; }
html += `<p>${MarkdownParser._parseInline(line)}</p>`;
i++;
}
if (inList) html += MarkdownParser._closeList(listType);
if (inBlockquote) html += '</blockquote>';
if (inTable) html += MarkdownParser._renderTable(tableRows);
html = html.replace(/\x00BLOCK(\d+)\x00/g, (match, idx) => protectedBlocks[parseInt(idx)] || match);
return html;
}
static _parseInline(text) {
if (!text) return '';
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/__(.+?)__/g, '<strong>$1</strong>');
text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
text = text.replace(/(?<![_\w])_(?!_)(.+?)(?<!_)_(?![_\w])/g, '<em>$1</em>');
text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
return text;
}
static _renderTable(rows) {
if (rows.length < 1) return '';
const parseRow = (row) => row.split('|').filter((_, i, arr) => i > 0 && i < arr.length - 1).map(cell => cell.trim());
const headerCells = parseRow(rows[0]);
let dataStartIndex = (rows.length > 1 && /^[\s|:-]+$/.test(rows[1])) ? 2 : 1;
let tableHtml = '<div class="table-wrapper"><table><thead><tr>';
headerCells.forEach(cell => { tableHtml += `<th>${MarkdownParser._parseInline(cell)}</th>`; });
tableHtml += '</tr></thead><tbody>';
for (let i = dataStartIndex; i < rows.length; i++) {
const cells = parseRow(rows[i]);
if (cells.length === 0) continue;
tableHtml += '<tr>';
cells.forEach(cell => { tableHtml += `<td>${MarkdownParser._parseInline(cell)}</td>`; });
tableHtml += '</tr>';
}
tableHtml += '</tbody></table></div>';
return tableHtml;
}
static _closeList(type) { return type === 'ol' ? '</ol>' : '</ul>'; }
static _escapeHtml(text) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
return text.replace(/[&<>"']/g, m => map[m]);
}
}
// ============================================================
// COPY CODE
// ============================================================
function copyCode(codeId) {
const el = document.getElementById(codeId);
if (el) {
navigator.clipboard.writeText(el.textContent).then(() => {
const btn = el.parentElement.querySelector('.copy-btn');
if (btn) { btn.innerHTML = '<i class="fas fa-check"></i> تم ✓'; setTimeout(() => { btn.innerHTML = '<i class="fas fa-copy"></i> نسخ'; }, 2000); }
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = el.textContent;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
}
function renderMath(element) {
if (window.MathJax && MathJax.typesetPromise) {
MathJax.typesetPromise([element]).then(() => fixArabicInMath()).catch(err => console.warn('MathJax:', err));
}
}
function highlightCode(element) {
element.querySelectorAll('pre code').forEach(block => { try { hljs.highlightElement(block); } catch(e) {} });
}
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }
}
function sendQuick(text) {
document.getElementById('messageInput').value = text;
sendMessage();
}
function scrollToBottom() {
const container = document.getElementById('chatContainer');
setTimeout(() => { container.scrollTop = container.scrollHeight; }, 50);
}
// ============================================================
// ADD USER MESSAGE
// ============================================================
function addUserMessage(content, imageUrl = null) {
const container = document.getElementById('chatContainer');
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const messageDiv = document.createElement('div');
messageDiv.className = 'message user';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = '<i class="fas fa-user"></i>';
const wrapper = document.createElement('div');
wrapper.className = 'message-wrapper';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
if (imageUrl) {
const imgWrapper = document.createElement('div');
imgWrapper.className = 'message-image-wrapper';
const img = document.createElement('img');
img.src = imageUrl;
img.className = 'message-image';
img.alt = 'صورة مرسلة';
img.onclick = () => window.open(imageUrl, '_blank');
imgWrapper.appendChild(img);
contentDiv.appendChild(imgWrapper);
}
if (content) {
const textSpan = document.createElement('span');
textSpan.textContent = content;
contentDiv.appendChild(textSpan);
}
const metaDiv = document.createElement('div');
metaDiv.className = 'message-meta';
const timeSpan = document.createElement('span');
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
metaDiv.appendChild(timeSpan);
wrapper.appendChild(contentDiv);
wrapper.appendChild(metaDiv);
messageDiv.appendChild(avatar);
messageDiv.appendChild(wrapper);
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(messageDiv, typingIndicator);
messageCounter++;
document.getElementById('messageCount').textContent = `${messageCounter} رسائل`;
scrollToBottom();
}
// ============================================================
// ADD USER MESSAGE FROM HISTORY (with timestamp)
// ============================================================
function addUserMessageFromHistory(content, timestamp = null) {
const container = document.getElementById('chatContainer');
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const messageDiv = document.createElement('div');
messageDiv.className = 'message user';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.innerHTML = '<i class="fas fa-user"></i>';
const wrapper = document.createElement('div');
wrapper.className = 'message-wrapper';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
if (content.startsWith('[صورة دراسية]')) {
const lines = content.split('\n');
const questionLine = lines.find(l => l.startsWith('سؤال الطالب:'));
const displayText = questionLine
? questionLine.replace('سؤال الطالب:', '').trim()
: 'أرسل صورة دراسية';
const imgNotice = document.createElement('div');
imgNotice.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(124,58,237,0.08);border-radius:8px;margin-bottom:6px;font-size:13px;color:#7c3aed;';
imgNotice.innerHTML = '<i class="fas fa-image"></i> صورة دراسية';
contentDiv.appendChild(imgNotice);
if (displayText && displayText !== 'أرسل صورة دراسية') {
const textSpan = document.createElement('span');
textSpan.textContent = displayText;
contentDiv.appendChild(textSpan);
}
} else {
const textSpan = document.createElement('span');
textSpan.textContent = content;
contentDiv.appendChild(textSpan);
}
const metaDiv = document.createElement('div');
metaDiv.className = 'message-meta';
const timeSpan = document.createElement('span');
if (timestamp) {
try {
const date = new Date(timestamp);
timeSpan.textContent = date.toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
} catch(e) {
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
}
} else {
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
}
metaDiv.appendChild(timeSpan);
wrapper.appendChild(contentDiv);
wrapper.appendChild(metaDiv);
messageDiv.appendChild(avatar);
messageDiv.appendChild(wrapper);
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(messageDiv, typingIndicator);
messageCounter++;
document.getElementById('messageCount').textContent = `${messageCounter} رسائل`;
}
// ============================================================
// ADD BOT MESSAGE FROM HISTORY
// ============================================================
function addBotMessageFromHistory(content, timestamp = null) {
const container = document.getElementById('chatContainer');
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.style.cssText = 'background:rgba(255,255,255,0.80);border:1.5px solid rgba(124,58,237,0.18);';
avatar.innerHTML = '<i class="fas fa-user-graduate"></i>';
const wrapper = document.createElement('div');
wrapper.className = 'message-wrapper';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
try {
const html = MarkdownParser.parse(content);
contentDiv.innerHTML = html;
highlightCode(contentDiv);
setTimeout(() => { renderMath(contentDiv); }, 50);
} catch(e) {
contentDiv.innerHTML = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
}
const metaDiv = document.createElement('div');
metaDiv.className = 'message-meta';
const timeSpan = document.createElement('span');
if (timestamp) {
try {
const date = new Date(timestamp);
timeSpan.textContent = date.toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
} catch(e) {
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
}
} else {
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
}
const historyBadge = document.createElement('span');
historyBadge.className = 'file-badge';
historyBadge.textContent = '📂 محفوظ';
historyBadge.style.cssText = 'opacity:0.6;font-size:10px;';
metaDiv.appendChild(timeSpan);
metaDiv.appendChild(historyBadge);
wrapper.appendChild(contentDiv);
wrapper.appendChild(metaDiv);
messageDiv.appendChild(avatar);
messageDiv.appendChild(wrapper);
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(messageDiv, typingIndicator);
messageCounter++;
document.getElementById('messageCount').textContent = `${messageCounter} رسائل`;
}
// ============================================================
// ADD HISTORY SEPARATOR
// ============================================================
function addHistorySeparator(text) {
const container = document.getElementById('chatContainer');
const sep = document.createElement('div');
sep.className = 'history-separator';
sep.innerHTML = `
<div class="history-separator-line"></div>
<span class="history-separator-text">${text}</span>
<div class="history-separator-line"></div>
`;
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(sep, typingIndicator);
}
// ============================================================
// ✅ RENDER HISTORY FROM MESSAGES ARRAY (no extra fetch needed)
// Used after /select-subject or /switch-subject returns chat_history
// ============================================================
function renderHistoryFromMessages(messages, subjectId, subjectName) {
if (!messages || messages.length === 0) {
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'block';
return false;
}
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const savedDate = messages[0]?.timestamp
? new Date(messages[0].timestamp).toLocaleDateString('ar-EG', {
year: 'numeric', month: 'long', day: 'numeric'
})
: 'محادثة سابقة';
addHistorySeparator(`<i class="fas fa-folder-open"></i> سجل محادثة ${subjectName} - ${savedDate}`);
for (const msg of messages) {
if (msg.role === 'user') {
addUserMessageFromHistory(msg.content, msg.timestamp);
} else if (msg.role === 'assistant') {
addBotMessageFromHistory(msg.content, msg.timestamp);
}
}
addHistorySeparator('<i class="fas fa-pen"></i> المحادثة الجديدة');
document.getElementById('messageCount').textContent = `${messageCounter} رسائل`;
setTimeout(() => { scrollToBottom(); }, 500);
console.log(`✅ Rendered ${messages.length} messages for subject: ${subjectId}`);
return true;
}
// ============================================================
// CREATE BOT MESSAGE
// ============================================================
function createBotMessage(file = null) {
const container = document.getElementById('chatContainer');
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const messageDiv = document.createElement('div');
messageDiv.className = 'message bot';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.style.cssText = 'background:rgba(255,255,255,0.80);border:1.5px solid rgba(124,58,237,0.18);';
avatar.innerHTML = '<i class="fas fa-user-graduate"></i>';
const wrapper = document.createElement('div');
wrapper.className = 'message-wrapper';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
const metaDiv = document.createElement('div');
metaDiv.className = 'message-meta';
const timeSpan = document.createElement('span');
timeSpan.textContent = new Date().toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
metaDiv.appendChild(timeSpan);
if (file) {
const fileBadge = document.createElement('span');
fileBadge.className = 'file-badge';
fileBadge.textContent = file;
metaDiv.appendChild(fileBadge);
}
wrapper.appendChild(contentDiv);
wrapper.appendChild(metaDiv);
messageDiv.appendChild(avatar);
messageDiv.appendChild(wrapper);
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(messageDiv, typingIndicator);
messageCounter++;
document.getElementById('messageCount').textContent = `${messageCounter} رسائل`;
scrollToBottom();
return contentDiv;
}
function renderBotContent(contentDiv, rawText) {
const html = MarkdownParser.parse(rawText);
contentDiv.innerHTML = html;
highlightCode(contentDiv);
renderMath(contentDiv);
scrollToBottom();
}
function showTyping() {
document.getElementById('typingIndicator').classList.add('active');
scrollToBottom();
}
function hideTyping() {
document.getElementById('typingIndicator').classList.remove('active');
}
function updateStatus(sessionInfo) {
if (sessionInfo) {
document.getElementById('currentFile').textContent = sessionInfo.last_file || '-';
document.getElementById('summaryStatus').textContent = sessionInfo.has_summary ? 'ملخص متوفر ✓' : 'لا يوجد ملخص';
document.getElementById('totalMessages').textContent = sessionInfo.total_messages || 0;
if (sessionInfo.subject) document.getElementById('currentSubjectStatus').textContent = sessionInfo.subject;
}
}
// ============================================================
// SEND MESSAGE
// ============================================================
async function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (pendingImageUrl && pendingImageExtracted && !imageAnalyzing) {
await sendImageMessage(message);
return;
}
if (!message || isProcessing) return;
isProcessing = true;
document.getElementById('sendBtn').disabled = true;
addUserMessage(message);
input.value = '';
input.style.height = 'auto';
showTyping();
try {
const response = await safeFetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message })
});
if (!response) return;
const data = await response.json();
if (data.error) {
hideTyping();
if (data.need_subject) { goToSubjectSelection(); return; }
if (data.code === 'no_access') {
const errorDiv = createBotMessage();
errorDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> ${data.error}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
return;
}
const errorDiv = createBotMessage();
errorDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> ${data.error}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
return;
}
if (data.session_info) updateStatus(data.session_info);
document.getElementById('currentFile').textContent = data.chosen_file || '-';
if (data.type === 'gpt') {
hideTyping();
typewriterEffect(data.response, data.chosen_file);
} else if (data.type === 'mistral') {
hideTyping();
await streamResponse(data.stream_url, data.chosen_file);
}
} catch (error) {
hideTyping();
const errorDiv = createBotMessage();
errorDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> خطأ في الاتصال: ${error.message}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
input.focus();
}
}
// ============================================================
// SEND IMAGE MESSAGE
// ============================================================
async function sendImageMessage(extraText = '') {
if (!pendingImageUrl || !pendingImageExtracted) return;
if (isProcessing) return;
isProcessing = true;
document.getElementById('sendBtn').disabled = true;
const userText = extraText
? `[صورة دراسية]\n\n${pendingImageExtracted}\n\nسؤال الطالب: ${extraText}`
: `[صورة دراسية]\n\n${pendingImageExtracted}`;
addUserMessage(extraText || 'أرسل صورة دراسية', pendingImageUrl);
pendingImageUrl = null;
pendingImageExtracted = null;
pendingImageFile = null;
hideImagePreview();
const input = document.getElementById('messageInput');
input.value = '';
input.style.height = 'auto';
showTyping();
try {
const response = await safeFetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userText })
});
if (!response) return;
const data = await response.json();
if (data.error) {
hideTyping();
if (data.need_subject) { goToSubjectSelection(); return; }
const errorDiv = createBotMessage();
errorDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> ${data.error}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
return;
}
if (data.session_info) updateStatus(data.session_info);
document.getElementById('currentFile').textContent = data.chosen_file || '-';
if (data.type === 'gpt') {
hideTyping();
typewriterEffect(data.response, data.chosen_file);
} else if (data.type === 'mistral') {
hideTyping();
await streamResponse(data.stream_url, data.chosen_file);
}
} catch (error) {
hideTyping();
const errorDiv = createBotMessage();
errorDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> خطأ في الاتصال: ${error.message}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
}
}
// ============================================================
// TYPEWRITER EFFECT
// ============================================================
function typewriterEffect(text, file) {
const contentDiv = createBotMessage(file);
let index = 0;
let renderTimer = null;
let isRendering = false;
contentDiv.innerHTML = '<span class="streaming-cursor"></span>';
function scheduleFullRender() {
if (renderTimer) clearTimeout(renderTimer);
renderTimer = setTimeout(() => {
if (!isRendering) {
isRendering = true;
try {
const html = MarkdownParser.parse(text.substr(0, index));
contentDiv.innerHTML = html + '<span class="streaming-cursor"></span>';
highlightCode(contentDiv);
renderMath(contentDiv);
} catch(e) {}
finally { isRendering = false; }
scrollToBottom();
}
}, 100);
}
function type() {
if (index < text.length) {
index += Math.min(6, text.length - index);
scheduleFullRender();
setTimeout(type, 10);
} else {
if (renderTimer) clearTimeout(renderTimer);
try { renderBotContent(contentDiv, text); } catch(e) { contentDiv.textContent = text; }
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
document.getElementById('messageInput').focus();
refreshSessionInfo();
}
}
type();
}
// ============================================================
// STREAM RESPONSE
// ============================================================
async function streamResponse(streamUrl, file) {
const contentDiv = createBotMessage(file);
let fullText = '';
let renderTimer = null;
let isRendering = false;
let streamDone = false;
contentDiv.innerHTML = '<span class="streaming-cursor"></span>';
function scheduleRender() {
if (renderTimer) clearTimeout(renderTimer);
renderTimer = setTimeout(() => {
if (!isRendering && fullText) {
isRendering = true;
const html = MarkdownParser.parse(fullText);
contentDiv.innerHTML = html + (streamDone ? '' : '<span class="streaming-cursor"></span>');
highlightCode(contentDiv);
renderMath(contentDiv);
isRendering = false;
scrollToBottom();
}
}, 120);
}
return new Promise((resolve) => {
try {
const eventSource = new EventSource(streamUrl);
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.error) {
eventSource.close();
streamDone = true;
if (renderTimer) clearTimeout(renderTimer);
contentDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> ${data.error}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
resolve(); return;
}
if (data.chunk) { fullText += data.chunk; scheduleRender(); }
if (data.done) {
eventSource.close();
streamDone = true;
if (renderTimer) clearTimeout(renderTimer);
renderBotContent(contentDiv, fullText);
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
refreshSessionInfo();
resolve();
}
} catch(e) { console.error('Stream parse error:', e); }
};
eventSource.onerror = function() {
eventSource.close();
streamDone = true;
if (renderTimer) clearTimeout(renderTimer);
if (!fullText) {
contentDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> خطأ في الاتصال. حاول مرة أخرى.</p>`;
} else {
renderBotContent(contentDiv, fullText);
}
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
resolve();
};
} catch(error) {
contentDiv.innerHTML = `<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> ${error.message}</p>`;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
resolve();
}
});
}
// ============================================================
// REFRESH SESSION INFO
// ============================================================
async function refreshSessionInfo() {
try {
const response = await safeFetch('/session-info', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (!response) return;
const info = await response.json();
updateStatus(info);
} catch(e) {}
}
// ============================================================
// VOICE RECORDING
// ============================================================
async function toggleVoiceRecording() {
if (isRecording) { stopVoiceRecording(); } else { await startVoiceRecording(); }
}
async function startVoiceRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioChunks = [];
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = async () => {
stream.getTracks().forEach(t => t.stop());
await processVoiceRecording();
};
mediaRecorder.start(100);
isRecording = true;
document.getElementById('voiceRecordingUI').style.display = 'block';
document.getElementById('voiceBtn').classList.add('recording');
document.getElementById('imageBtn').disabled = true;
document.getElementById('voiceBtn').innerHTML = '<i class="fas fa-microphone-slash"></i>';
voiceSeconds = 0;
updateVoiceTimer();
voiceTimerInterval = setInterval(() => {
voiceSeconds++;
updateVoiceTimer();
if (voiceSeconds >= 120) stopVoiceRecording();
}, 1000);
} catch(error) {
showToast('<i class="fas fa-microphone-slash"></i> لا يمكن الوصول إلى الميكروفون. تحقق من الأذونات.', 'error');
}
}
function updateVoiceTimer() {
const mins = String(Math.floor(voiceSeconds / 60)).padStart(2, '0');
const secs = String(voiceSeconds % 60).padStart(2, '0');
document.getElementById('voiceTimer').textContent = `${mins}:${secs}`;
}
function stopVoiceRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
clearInterval(voiceTimerInterval);
document.getElementById('voiceBtn').classList.remove('recording');
document.getElementById('voiceRecordingUI').style.display = 'none';
document.getElementById('imageBtn').disabled = false;
document.getElementById('voiceBtn').innerHTML = '<i class="fas fa-microphone"></i>';
}
}
function cancelVoiceRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.onstop = () => {};
mediaRecorder.stop();
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
isRecording = false;
audioChunks = [];
clearInterval(voiceTimerInterval);
document.getElementById('voiceBtn').classList.remove('recording');
document.getElementById('voiceRecordingUI').style.display = 'none';
document.getElementById('imageBtn').disabled = false;
document.getElementById('voiceBtn').innerHTML = '<i class="fas fa-microphone"></i>';
showToast('<i class="fas fa-times-circle"></i> تم إلغاء التسجيل', 'info');
}
}
async function processVoiceRecording() {
if (audioChunks.length === 0) {
showToast('<i class="fas fa-exclamation-circle"></i> لم يتم تسجيل أي صوت', 'error');
return;
}
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
audioChunks = [];
const voiceBtn = document.getElementById('voiceBtn');
voiceBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
voiceBtn.disabled = true;
document.getElementById('sendBtn').disabled = true;
showToast('<i class="fas fa-microphone"></i> جاري معالجة التسجيل...', 'info', 10000);
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
const uploadResponse = await safeFetch('/voice-upload', {
method: 'POST',
body: formData
});
if (!uploadResponse) return;
const uploadData = await uploadResponse.json();
if (uploadData.error) {
showToast(`<i class="fas fa-exclamation-circle"></i> ${uploadData.error}`, 'error');
resetVoiceBtn();
return;
}
const transcribeResponse = await safeFetch('/voice-transcribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_url: uploadData.file_url })
});
if (!transcribeResponse) return;
const transcribeData = await transcribeResponse.json();
if (transcribeData.error) {
showToast(`<i class="fas fa-exclamation-circle"></i> ${transcribeData.error}`, 'error');
resetVoiceBtn();
return;
}
const transcription = transcribeData.transcription.trim();
if (!transcription) {
showToast('<i class="fas fa-exclamation-circle"></i> لم يتم التعرف على أي كلام', 'error');
resetVoiceBtn();
return;
}
showToast('<i class="fas fa-check-circle"></i> تم التعرف على الصوت بنجاح', 'success');
document.getElementById('messageInput').value = transcription;
autoResize(document.getElementById('messageInput'));
resetVoiceBtn();
await sendMessage();
} catch(error) {
showToast(`<i class="fas fa-exclamation-circle"></i> خطأ في معالجة الصوت: ${error.message}`, 'error');
resetVoiceBtn();
}
}
function resetVoiceBtn() {
const voiceBtn = document.getElementById('voiceBtn');
voiceBtn.innerHTML = '<i class="fas fa-microphone"></i>';
voiceBtn.disabled = false;
document.getElementById('sendBtn').disabled = false;
}
// ============================================================
// IMAGE HANDLING
// ============================================================
async function handleImageSelected(event) {
const file = event.target.files[0];
if (!file) return;
event.target.value = '';
if (!file.type.startsWith('image/')) {
showToast('<i class="fas fa-exclamation-triangle"></i> يرجى اختيار ملف صورة صالح', 'error');
return;
}
if (file.size > 10 * 1024 * 1024) {
showToast('<i class="fas fa-exclamation-triangle"></i> حجم الصورة كبير جداً. الحد الأقصى 10 ميجابايت', 'error');
return;
}
pendingImageFile = file;
const reader = new FileReader();
reader.onload = (e) => { showImagePreview(e.target.result, file.name); };
reader.readAsDataURL(file);
imageAnalyzing = true;
document.getElementById('imageBtn').disabled = true;
document.getElementById('voiceBtn').disabled = true;
document.getElementById('sendBtn').disabled = true;
setImagePreviewStatus('جاري تحليل الصورة...', 'analyzing');
try {
const formData = new FormData();
formData.append('image', file, file.name);
const response = await safeFetch('/image-upload-analyze', {
method: 'POST',
body: formData
});
if (!response) {
hideImagePreview();
resetImageState();
return;
}
const data = await response.json();
if (data.error) {
if (data.need_subject) {
hideImagePreview();
goToSubjectSelection();
return;
}
showToast(`<i class="fas fa-exclamation-circle"></i> ${data.error}`, 'error');
hideImagePreview();
resetImageState();
return;
}
if (!data.supported) {
hideImagePreview();
resetImageState();
showToast(data.message || '<i class="fas fa-exclamation-triangle"></i> هذه الصورة لا تحتوي على محتوى دراسي', 'error', 5000);
const container = document.getElementById('chatContainer');
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'none';
const notifDiv = document.createElement('div');
notifDiv.className = 'unsupported-image-notif';
notifDiv.innerHTML = `<span class="unsupported-icon"><i class="fas fa-ban"></i></span>
<span>${data.message || 'هذه الصورة لا تحتوي على محتوى دراسي.'}</span>
`;
const typingIndicator = document.getElementById('typingIndicator');
container.insertBefore(notifDiv, typingIndicator);
scrollToBottom();
setTimeout(() => {
if (notifDiv.parentNode) notifDiv.parentNode.removeChild(notifDiv);
}, 5000);
return;
}
pendingImageUrl = data.image_url;
pendingImageExtracted = data.extracted_text;
imageAnalyzing = false;
setImagePreviewStatus('<i class="fas fa-check"></i> تم التحليل - اضغط إرسال', 'ready');
document.getElementById('imageBtn').disabled = false;
document.getElementById('voiceBtn').disabled = false;
document.getElementById('sendBtn').disabled = false;
showToast('<i class="fas fa-check-circle"></i> تم تحليل الصورة بنجاح! اضغط إرسال أو أضف سؤالاً', 'success');
document.getElementById('messageInput').focus();
document.getElementById('messageInput').placeholder = 'أضف سؤالاً عن الصورة (اختياري) ثم اضغط إرسال...';
} catch(error) {
showToast(`<i class="fas fa-exclamation-circle"></i> خطأ في معالجة الصورة: ${error.message}`, 'error');
hideImagePreview();
resetImageState();
}
}
function showImagePreview(src, filename) {
const bar = document.getElementById('imagePreviewBar');
const thumb = document.getElementById('imagePreviewThumb');
const name = document.getElementById('imagePreviewName');
thumb.src = src;
name.textContent = filename || 'صورة';
bar.style.display = 'block';
}
function hideImagePreview() {
document.getElementById('imagePreviewBar').style.display = 'none';
document.getElementById('imagePreviewThumb').src = '';
}
function setImagePreviewStatus(text, state) {
const statusEl = document.getElementById('imagePreviewStatus');
// HTML allowed for icons
statusEl.innerHTML = text;
statusEl.className = `image-preview-status status-${state}`;
}
function cancelImage() {
if (imageAnalyzing) {
showToast('<i class="fas fa-spinner fa-spin"></i> جاري التحليل، يرجى الانتظار...', 'info');
return;
}
hideImagePreview();
resetImageState();
document.getElementById('messageInput').placeholder = 'اكتب سؤالك هنا';
showToast('<i class="fas fa-times"></i> تم إلغاء الصورة', 'info');
}
function resetImageState() {
pendingImageUrl = null;
pendingImageExtracted = null;
pendingImageFile = null;
imageAnalyzing = false;
document.getElementById('imageBtn').disabled = false;
document.getElementById('voiceBtn').disabled = false;
document.getElementById('sendBtn').disabled = false;
}
// ============================================================
// LOAD USER DATA & SUBJECTS
// ============================================================
async function loadUserAndSubjects() {
const grid = document.getElementById('subjectsGrid');
const startBtn = document.getElementById('startBtn');
startBtn.disabled = true;
startBtn.innerHTML = `<span><i class="fas fa-play"></i></span><span>ابدأ المحادثة</span>`;
selectedSubjectId = null;
selectedSubjectName = null;
grid.innerHTML = `
<div class="loading-subjects" style="grid-column:1/-1;">
<div class="spinner"></div>
<p>جاري تحميل المواد...</p>
</div>`;
try {
const response = await safeFetch('/me');
if (!response) return;
const data = await response.json();
if (!response.ok || data.error) {
showAccessDenied(data.error || 'حدث خطأ غير متوقع');
return;
}
currentUserData = data;
document.getElementById('userDisplayName').textContent = data.username;
document.getElementById('userTrackBadge').textContent = data.student_type || '';
document.getElementById('headerUsername').textContent = data.username;
const subjects = data.subjects || [];
if (subjects.length === 0) {
grid.innerHTML = `
<div class="no-subjects" style="grid-column:1/-1;">
<div class="no-subjects-icon"><i class="fas fa-inbox"></i></div>
<p style="color:var(--clay-fg);font-weight:600;">لا توجد مواد متاحة حالياً</p>
<button class="retry-btn" onclick="loadUserAndSubjects()"><i class="fas fa-sync-alt"></i> إعادة المحاولة</button>
</div>`;
return;
}
grid.innerHTML = '';
subjects.forEach(subject => {
const iconClass = getSubjectIcon(subject.id);
const card = document.createElement('div');
if (subject.accessible) {
card.className = 'subject-card';
card.dataset.subjectId = subject.id;
card.dataset.subjectName = subject.name;
card.innerHTML = `
<span class="subject-icon"><i class="fas ${iconClass}"></i></span>
<div class="subject-name">${subject.name}</div>
<div class="subject-info">${subject.free ? 'مجاني' : 'مشترك'}</div>
${subject.free ? '<span class="subject-free-badge">مجاني</span>' : ''}
`;
card.onclick = () => selectSubjectCard(subject.id, subject.name, card);
} else {
card.className = 'subject-card locked';
card.dataset.subjectId = subject.id;
card.dataset.subjectName = subject.name;
card.innerHTML = `
<span class="subject-icon"><i class="fas ${iconClass}"></i></span>
<div class="subject-name">${subject.name}</div>
<div class="subject-info subject-locked-info"><i class="fas fa-lock"></i> يتطلب اشتراك</div>
<span class="subject-price-badge">${subject.price} $</span>
`;
card.onclick = () => showToast(`<i class="fas fa-lock"></i> يجب شراء مادة "${subject.name}" للوصول إليها`, 'error', 4000);
}
grid.appendChild(card);
});
// ✅ AUTO-SELECT from URL param - but DON'T call startChat yet
// startChat will be called from init() after this returns
const urlParams = new URLSearchParams(window.location.search);
const preSubject = urlParams.get('subject');
if (preSubject) {
const matchCard = grid.querySelector(`[data-subject-id="${preSubject}"]`);
if (matchCard && !matchCard.classList.contains('locked')) {
selectSubjectCard(preSubject, matchCard.dataset.subjectName, matchCard);
}
}
} catch(error) {
grid.innerHTML = `
<div class="no-subjects" style="grid-column:1/-1;">
<div class="no-subjects-icon"><i class="fas fa-exclamation-circle"></i></div>
<p style="color:var(--clay-fg);font-weight:600;">خطأ في تحميل المواد</p>
<p style="font-size:12px;margin-top:8px;color:#DB2777;">${error.message}</p>
<button class="retry-btn" onclick="loadUserAndSubjects()"><i class="fas fa-sync-alt"></i> إعادة المحاولة</button>
</div>`;
}
}
function showAccessDenied(message) {
document.getElementById('subjectPage').classList.add('hidden');
document.getElementById('chatPage').classList.add('hidden');
const accessPage = document.getElementById('accessDeniedPage');
if (accessPage) {
accessPage.classList.remove('hidden');
document.getElementById('accessDeniedTitle').textContent = '<i class="fas fa-lock"></i> غير مصرح';
document.getElementById('accessDeniedMsg').textContent = message;
}
}
function selectSubjectCard(subjectId, subjectName, cardElement) {
document.querySelectorAll('.subject-card').forEach(c => c.classList.remove('selected'));
cardElement.classList.add('selected');
selectedSubjectId = subjectId;
selectedSubjectName = subjectName;
const startBtn = document.getElementById('startBtn');
startBtn.disabled = false;
startBtn.innerHTML = `<span><i class="fas ${getSubjectIcon(subjectId)}"></i></span><span>ابدأ مع ${subjectName}</span>`;
}
// ============================================================
// ✅ START CHAT - calls /select-subject and uses returned
// chat_history directly (no second fetch needed)
// ============================================================
async function startChat() {
if (!selectedSubjectId) {
showToast('<i class="fas fa-exclamation-circle"></i> يرجى اختيار مادة أولاً', 'error');
return;
}
const overlay = document.getElementById('transitionOverlay');
document.getElementById('transitionText').textContent = `جاري تحميل مادة ${selectedSubjectName}...`;
overlay.classList.add('active');
try {
const response = await safeFetch('/select-subject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: selectedSubjectId })
});
if (!response) {
overlay.classList.remove('active');
return;
}
const data = await response.json();
if (!response.ok || data.error) {
overlay.classList.remove('active');
if (data.code === 'no_access') {
showToast(`<i class="fas fa-lock"></i> ${data.error}`, 'error', 5000);
} else {
showToast(`<i class="fas fa-exclamation-circle"></i> ${data.error}`, 'error');
}
return;
}
// ✅ Pass the chat_history from response directly
await switchToChatPage(data);
} catch(error) {
overlay.classList.remove('active');
showToast(`<i class="fas fa-exclamation-circle"></i> خطأ في الاتصال: ${error.message}`, 'error');
}
}
// ============================================================
// ✅ SWITCH TO CHAT PAGE - uses chat_history from /select-subject
// response directly, NO extra fetch to /get-chat-history
// ============================================================
async function switchToChatPage(data) {
const iconClass = getSubjectIcon(selectedSubjectId);
const subjectName = data.subject_name || selectedSubjectName;
document.getElementById('chatTitle').textContent = `مساعد ${subjectName} الذكي`;
document.getElementById('chatSubtitle').textContent = `${data.chapters_count} فصول متاحة`;
document.getElementById('currentSubjectName').textContent = subjectName;
document.getElementById('currentSubjectStatus').textContent = subjectName;
document.getElementById('welcomeIcon').innerHTML = `<i class="fas ${iconClass}"></i>`;
document.getElementById('welcomeTitle').textContent = `مرحباً بك في مساعد ${subjectName}`;
document.getElementById('welcomeText').textContent = `أنا مساعدك الذكي في مادة ${subjectName}. يمكنني مساعدتك في فهم جميع الفصول والمواضيع. اطرح سؤالك وسأجيبك بالتفصيل!`;
document.getElementById('subjectPage').classList.add('hidden');
document.getElementById('chatPage').classList.remove('hidden');
document.getElementById('transitionOverlay').classList.remove('active');
// ✅ Reset UI first (clears any old messages)
resetChatUI();
// ✅ Use chat_history from the /select-subject response directly
// This is THIS subject's history - no wrong subject possible
const chatHistory = data.chat_history || [];
if (chatHistory.length > 0) {
historyLoaded = true;
const hasHistory = renderHistoryFromMessages(
chatHistory,
selectedSubjectId,
subjectName
);
if (hasHistory) {
showToast(`<i class="fas fa-check-circle"></i> تم استعادة ${chatHistory.length} رسالة من سجل ${subjectName}`, 'success', 3000);
document.getElementById('chatSubtitle').textContent = `${data.chapters_count} فصول - سجل محادثة محمّل`;
}
} else {
historyLoaded = false;
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'block';
}
setTimeout(() => { document.getElementById('messageInput').focus(); }, 100);
}
function goToSubjectSelection() {
if (isProcessing) {
if (!confirm('هناك رسالة قيد المعالجة. هل تريد العودة لاختيار المادة؟')) return;
isProcessing = false;
document.getElementById('sendBtn').disabled = false;
}
if (imageAnalyzing) { resetImageState(); hideImagePreview(); }
if (isRecording) { cancelVoiceRecording(); }
document.getElementById('chatPage').classList.add('hidden');
document.getElementById('subjectPage').classList.remove('hidden');
historyLoaded = false;
loadUserAndSubjects();
}
function resetChatUI() {
const container = document.getElementById('chatContainer');
// ✅ Remove ALL old messages, separators, notifs
container.querySelectorAll(
'.message, .unsupported-image-notif, .history-separator'
).forEach(el => el.remove());
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'block';
const historyLoading = document.getElementById('historyLoading');
if (historyLoading) historyLoading.style.display = 'none';
messageCounter = 0;
document.getElementById('messageCount').textContent = '0 رسائل';
document.getElementById('currentFile').textContent = '-';
document.getElementById('summaryStatus').textContent = 'لا يوجد ملخص';
document.getElementById('totalMessages').textContent = '0';
hideImagePreview();
resetImageState();
document.getElementById('messageInput').placeholder = 'اكتب سؤالك هنا';
}
// ============================================================
// SUMMARY MODAL
// ============================================================
async function showSummary() {
document.getElementById('summaryModal').classList.add('active');
document.getElementById('summaryContent').textContent = 'جاري تحميل الملخص...';
try {
const response = await safeFetch('/summary', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (!response) return;
const data = await response.json();
const summaryDiv = document.getElementById('summaryContent');
summaryDiv.innerHTML = MarkdownParser.parse(data.summary);
renderMath(summaryDiv);
highlightCode(summaryDiv);
} catch(error) {
document.getElementById('summaryContent').innerHTML =
'<p style="color:#DB2777;font-weight:600;"><i class="fas fa-exclamation-circle"></i> خطأ في تحميل الملخص</p>';
}
}
function closeSummaryModal() {
document.getElementById('summaryModal').classList.remove('active');
}
document.getElementById('summaryModal').addEventListener('click', function(e) {
if (e.target === this) closeSummaryModal();
});
// ============================================================
// CLEAR CHAT
// ============================================================
async function clearChat() {
if (!confirm('هل تريد مسح جميع الرسائل والملخصات للمادة الحالية؟')) return;
try {
const response = await safeFetch('/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (!response) return;
// ✅ Clear UI
const container = document.getElementById('chatContainer');
container.querySelectorAll(
'.message, .unsupported-image-notif, .history-separator'
).forEach(el => el.remove());
const welcome = document.getElementById('welcomeMessage');
if (welcome) welcome.style.display = 'block';
messageCounter = 0;
historyLoaded = false;
document.getElementById('messageCount').textContent = '0 رسائل';
document.getElementById('currentFile').textContent = '-';
document.getElementById('summaryStatus').textContent = 'لا يوجد ملخص';
document.getElementById('totalMessages').textContent = '0';
hideImagePreview();
resetImageState();
// ✅ Re-select subject to keep server session active
// but DON'T load history (we just cleared it)
if (selectedSubjectId) {
const reSelectResponse = await safeFetch('/select-subject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: selectedSubjectId })
});
// We don't render history here since we just cleared
}
showToast('<i class="fas fa-check-circle"></i> تم مسح محادثة المادة الحالية بنجاح', 'success');
} catch(error) {
showToast('<i class="fas fa-exclamation-circle"></i> خطأ في مسح المحادثة', 'error');
}
}
// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeSummaryModal();
if (isRecording) cancelVoiceRecording();
}
});
// ============================================================
// ✅ INITIALIZE - Fixed to properly handle ?subject= URL param
// ============================================================
async function init() {
if (!USERNAME) {
window.location.href = '/login';
return;
}
const urlParams = new URLSearchParams(window.location.search);
const subjectFromUrl = urlParams.get('subject');
// ✅ CASE 1: URL has ?subject=xxx
// Always call /select-subject with that subject first
// This guarantees we load THAT subject's history, not whatever
// was last in memory
if (subjectFromUrl) {
console.log(`🔀 URL subject param detected: ${subjectFromUrl}`);
// Show transition overlay while loading
const overlay = document.getElementById('transitionOverlay');
document.getElementById('transitionText').textContent = 'جاري تحميل المادة...';
overlay.classList.add('active');
try {
// Load user data for header first
const meResponse = await safeFetch('/me');
if (!meResponse) {
overlay.classList.remove('active');
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
return;
}
const meData = await meResponse.json();
if (meData.error) {
overlay.classList.remove('active');
showAccessDenied(meData.error);
return;
}
currentUserData = meData;
document.getElementById('userDisplayName').textContent = meData.username;
document.getElementById('userTrackBadge').textContent = meData.student_type || '';
document.getElementById('headerUsername').textContent = meData.username;
// ✅ Find subject name from user's subjects list
const subjects = meData.subjects || [];
const subjectInfo = subjects.find(s => s.id === subjectFromUrl);
if (!subjectInfo) {
// Subject not found or not accessible - go to subject selection
overlay.classList.remove('active');
showToast('<i class="fas fa-exclamation-circle"></i> المادة غير متاحة، يرجى اختيار مادة من القائمة', 'error', 4000);
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
return;
}
if (!subjectInfo.accessible) {
overlay.classList.remove('active');
showToast(`<i class="fas fa-lock"></i> يجب شراء مادة "${subjectInfo.name}" للوصول إليها`, 'error', 4000);
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
return;
}
// ✅ Set selected subject state
selectedSubjectId = subjectFromUrl;
selectedSubjectName = subjectInfo.name;
// ✅ Call /select-subject - this loads THIS subject's
// history from DB into memory and returns it
const selectResponse = await safeFetch('/select-subject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: subjectFromUrl })
});
if (!selectResponse) {
overlay.classList.remove('active');
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
return;
}
const selectData = await selectResponse.json();
if (selectData.error) {
overlay.classList.remove('active');
showToast(`<i class="fas fa-exclamation-circle"></i> ${selectData.error}`, 'error');
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
return;
}
// ✅ Switch to chat page using the returned data
// chat_history here is ONLY for this subject
await switchToChatPage(selectData);
} catch(error) {
console.error('❌ Init error with subject param:', error);
overlay.classList.remove('active');
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
}
return;
}
// ✅ CASE 2: No URL param - check if server has a subject in memory
// This handles page refresh while already in a chat
try {
const sessionResponse = await safeFetch('/session-info', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (sessionResponse) {
const sessionData = await sessionResponse.json();
if (sessionData.subject && !sessionData.error) {
console.log(`🔄 Resuming subject from server session: ${sessionData.subject}`);
// Load user data
const meResponse = await safeFetch('/me');
if (meResponse) {
const meData = await meResponse.json();
if (!meData.error) {
currentUserData = meData;
document.getElementById('userDisplayName').textContent = meData.username;
document.getElementById('userTrackBadge').textContent = meData.student_type || '';
document.getElementById('headerUsername').textContent = meData.username;
const subjects = meData.subjects || [];
const subjectInfo = subjects.find(s => s.id === sessionData.subject);
if (subjectInfo && subjectInfo.accessible) {
selectedSubjectId = sessionData.subject;
selectedSubjectName = subjectInfo.name;
// ✅ Re-call /select-subject to reload
// this subject's history into memory
const overlay = document.getElementById('transitionOverlay');
document.getElementById('transitionText').textContent = 'جاري استعادة المحادثة...';
overlay.classList.add('active');
const selectResponse = await safeFetch('/select-subject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: selectedSubjectId })
});
if (selectResponse) {
const selectData = await selectResponse.json();
if (!selectData.error) {
await switchToChatPage(selectData);
return;
}
}
overlay.classList.remove('active');
}
}
}
}
}
} catch(e) {
console.warn('Could not check server session on init:', e);
}
// ✅ CASE 3: No subject anywhere - show subject selection page
console.log('📚 No subject found - showing subject selection');
document.getElementById('subjectPage').classList.remove('hidden');
await loadUserAndSubjects();
}
init();
console.log('✅ AI Chat Interface loaded');
console.log(`👤 Username: ${USERNAME}`);
console.log('🔐 Auth: Flask Session');
console.log('🔌 SocketIO: Active');
console.log('🎤 Voice Input: Active');
console.log('🖼️ Image Analysis: Active');
console.log('📚 Multi-subject: Active (Per-Subject History)');
console.log('🔄 Streaming: Active');
console.log('📂 Chat History: Active (Isolated Per Subject)');
</script>
</body>
</html>