Update index.html
Browse files- index.html +127 -15
index.html
CHANGED
|
@@ -111,10 +111,8 @@
|
|
| 111 |
|
| 112 |
.bubble{max-width:min(700px,80vw);padding:16px 22px;border-radius:var(--radius);
|
| 113 |
font-size:.98rem;line-height:1.75;word-break:break-word; box-shadow: 0 4px 15px rgba(0,0,0,0.03);}
|
| 114 |
-
.msg-row.bot .bubble{background:transparent;border:none;padding:8px 4px;box-shadow:none;border-top-left-radius:var(--radius);}
|
| 115 |
-
[dir="rtl"] .msg-row.
|
| 116 |
-
.msg-row.user .bubble{background:var(--user-bubble);border:none; border-top-right-radius:6px}
|
| 117 |
-
[dir="rtl"] .msg-row.user .bubble { border-top-right-radius:var(--radius); border-top-left-radius:6px; }
|
| 118 |
|
| 119 |
.bubble img {
|
| 120 |
max-width: 100%; height: auto; border-radius: 16px; margin: 12px 0;
|
|
@@ -181,14 +179,35 @@
|
|
| 181 |
font-family:'Cairo',sans-serif;font-size:.98rem;resize:none;max-height:160px;line-height:1.6; padding: 6px 0;}
|
| 182 |
.input-box textarea::placeholder{color:var(--text3)}
|
| 183 |
|
| 184 |
-
|
| 185 |
-
.btn-send
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
.btn-send:hover{opacity:.9;transform:scale(1.08) rotate(-10deg);}[dir="rtl"] .btn-send:hover{transform:scale(1.08) rotate(10deg);}
|
| 187 |
.btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none;}
|
| 188 |
|
| 189 |
.btn-stop {background:var(--surface3); color:var(--text); border:1px solid var(--border);}
|
| 190 |
.btn-stop:hover {background:var(--danger); color:#fff; transform:scale(1.05);}
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
/* MODAL */
|
| 193 |
.overlay{position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.6);display:flex;
|
| 194 |
align-items:center;justify-content:center;backdrop-filter:blur(8px);
|
|
@@ -280,6 +299,10 @@
|
|
| 280 |
<input type="file" id="file-input" multiple hidden onchange="handleFiles(this.files)" />
|
| 281 |
<button class="btn-attach" onclick="document.getElementById('file-input').click()">+</button>
|
| 282 |
<textarea id="msg-input" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)" placeholder="Type a message..."></textarea>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
<button class="btn-send" id="send-btn" onclick="sendMessage()">➤</button>
|
| 284 |
<button class="btn-stop" id="stop-btn" onclick="stopGeneration()" style="display:none;">⏹</button>
|
| 285 |
</div>
|
|
@@ -325,10 +348,10 @@
|
|
| 325 |
<script>
|
| 326 |
// ════════ i18n ════════
|
| 327 |
const i18n = {
|
| 328 |
-
en: { newChat: "✦ New Chat", chatsLabel: "CHATS", settings: "Settings", lang: "Language", dark: "Dark Mode", del: "Delete All Chats", delDesc: "This action cannot be undone", delBtn: "🗑 Delete", saveBtn: "💾 Close", placeholder: "Type your message... (Enter to send)", welcomeTitle: "Welcome to Genisi", welcomeSub: "Your smart assistant by AnesNT — Batna 🇩🇿", c1: "What is AI?", c2: "Write Python code", c3: "Summarize a topic", c4: "Design an image in space 🚀", errConnect: "Connection Error", memBadge: "Memory: {c}/{t} messages", stopMsg: "[Generation stopped by user]" },
|
| 329 |
-
ar: { newChat: "✦ محادثة جديدة", chatsLabel: "المحادثات", settings: "الإعدادات", lang: "اللغة", dark: "الوضع الليلي", del: "حذف جميع المحادثات", delDesc: "لا يمكن التراجع عن هذا الإجراء", delBtn: "🗑 حذف", saveBtn: "💾 إغلاق", placeholder: "اكتب رسالتك... (Enter للإرسال)", welcomeTitle: "مرحبًا في Genisi", welcomeSub: "أنا Genisi، مساعدك الذكي من AnesNT — باتنة 🇩🇿", c1: "ما هو الذكاء الاصطناعي؟", c2: "اكتب كود بايثون", c3: "لخص لي موضوعاً", c4: "صمم صورة بالفضاء 🚀", errConnect: "خطأ في الاتصال", memBadge: "الذاكرة: {c}/{t} رسائل", stopMsg: "[تم إيقاف التوليد]" },
|
| 330 |
-
fr: { newChat: "✦ Nouvelle Disc.", chatsLabel: "DISCUSSIONS", settings: "Paramètres", lang: "Langue", dark: "Mode Sombre", del: "Supprimer Tout", delDesc: "Action irréversible", delBtn: "🗑 Supprimer", saveBtn: "💾 Fermer", placeholder: "Écrivez votre message...", welcomeTitle: "Bienvenue sur Genisi", welcomeSub: "Votre assistant intelligent par AnesNT — Batna 🇩🇿", c1: "Qu'est-ce que l'IA?", c2: "Code Python", c3: "Résumer un sujet", c4: "Créer une image 🚀", errConnect: "Erreur de connexion", memBadge: "Mémoire: {c}/{t} msgs", stopMsg: "[Génération arrêtée]" },
|
| 331 |
-
es: { newChat: "✦ Nuevo Chat", chatsLabel: "CHATS", settings: "Ajustes", lang: "Idioma", dark: "Modo Oscuro", del: "Borrar Todo", delDesc: "Acción irreversible", delBtn: "🗑 Borrar", saveBtn: "💾 Cerrar", placeholder: "Escribe tu mensaje...", welcomeTitle: "Bienvenido a Genisi", welcomeSub: "Tu asistente por AnesNT — Batna 🇩🇿", c1: "¿Qué es la IA?", c2: "Código Python", c3: "Resumir", c4: "Diseñar imagen 🚀", errConnect: "Error de conexión", memBadge: "Memoria: {c}/{t} msgs", stopMsg: "[Generación detenida]" }
|
| 332 |
};
|
| 333 |
|
| 334 |
let currentLang = localStorage.getItem('genisi_lang') || 'en';
|
|
@@ -365,6 +388,11 @@ let isGenerating = false;
|
|
| 365 |
let pendingFiles =[];
|
| 366 |
let currentAbortController = null;
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
// ════════ MARKDOWN & PARSER ════════
|
| 369 |
const renderer = new marked.Renderer();
|
| 370 |
renderer.code = function(token) {
|
|
@@ -377,7 +405,7 @@ renderer.table = function(header, body) {
|
|
| 377 |
if (typeof header === 'object' && header !== null && 'header' in header) {
|
| 378 |
const token = header;
|
| 379 |
const hdr = token.header.map(cell => `<th>${cell.tokens.map(t=>t.raw||'').join('')}</th>`).join('');
|
| 380 |
-
const rows = (token.rows ||
|
| 381 |
return `<div class="table-wrapper"><table><thead><tr>${hdr}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
| 382 |
}
|
| 383 |
return `<div class="table-wrapper"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
|
|
@@ -393,7 +421,7 @@ function safeMarked(text) {
|
|
| 393 |
|
| 394 |
function renderKaTeX(el) {
|
| 395 |
if (window.renderMathInElement) {
|
| 396 |
-
renderMathInElement(el, { delimiters:
|
| 397 |
}
|
| 398 |
}
|
| 399 |
function copyCode(btn, encodedCode) {
|
|
@@ -504,9 +532,9 @@ function quickSend(text){ document.getElementById('msg-input').value=text; sendM
|
|
| 504 |
function handleKey(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();} }
|
| 505 |
function autoResize(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,160)+'px'; }
|
| 506 |
|
| 507 |
-
// ════════ MESSAGING & STREAMING ════════
|
| 508 |
async function sendMessage(){
|
| 509 |
-
if(isGenerating) return;
|
| 510 |
const input = document.getElementById('msg-input'); let text = input.value.trim();
|
| 511 |
if(!text && pendingFiles.length === 0) return;
|
| 512 |
const welcome = document.getElementById('welcome'); if(welcome) welcome.remove();
|
|
@@ -579,6 +607,90 @@ async function sendMessage(){
|
|
| 579 |
}
|
| 580 |
}
|
| 581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
function appendBubble(role, text, isMarkdown, filesList =[]){
|
| 583 |
const area=document.getElementById('chat-area');
|
| 584 |
const row=document.createElement('div'); row.className=`msg-row ${role}`;
|
|
@@ -588,7 +700,7 @@ function appendBubble(role, text, isMarkdown, filesList =[]){
|
|
| 588 |
const bub=document.createElement('div'); bub.className='bubble';
|
| 589 |
if(filesList && filesList.length > 0) filesList.forEach(name => { bub.innerHTML += `<div class="file-prev">📎 ${esc(name)}</div>`; });
|
| 590 |
const contentDiv = document.createElement('div');
|
| 591 |
-
|
| 592 |
contentDiv.dir = 'auto';
|
| 593 |
contentDiv.style.unicodeBidi = 'plaintext';
|
| 594 |
if(text) {
|
|
|
|
| 111 |
|
| 112 |
.bubble{max-width:min(700px,80vw);padding:16px 22px;border-radius:var(--radius);
|
| 113 |
font-size:.98rem;line-height:1.75;word-break:break-word; box-shadow: 0 4px 15px rgba(0,0,0,0.03);}
|
| 114 |
+
.msg-row.bot .bubble{background:transparent;border:none;padding:8px 4px;box-shadow:none;border-top-left-radius:var(--radius);}[dir="rtl"] .msg-row.bot .bubble { border-top-left-radius:var(--radius); border-top-right-radius:var(--radius); }
|
| 115 |
+
.msg-row.user .bubble{background:var(--user-bubble);border:none; border-top-right-radius:6px}[dir="rtl"] .msg-row.user .bubble { border-top-right-radius:var(--radius); border-top-left-radius:6px; }
|
|
|
|
|
|
|
| 116 |
|
| 117 |
.bubble img {
|
| 118 |
max-width: 100%; height: auto; border-radius: 16px; margin: 12px 0;
|
|
|
|
| 179 |
font-family:'Cairo',sans-serif;font-size:.98rem;resize:none;max-height:160px;line-height:1.6; padding: 6px 0;}
|
| 180 |
.input-box textarea::placeholder{color:var(--text3)}
|
| 181 |
|
| 182 |
+
/* أزرار الإرسال والميكروفون والإيقاف */
|
| 183 |
+
.btn-send, .btn-stop, .btn-voice {
|
| 184 |
+
width:42px;height:42px; border:none;border-radius:50%;cursor:pointer;
|
| 185 |
+
display:flex;align-items:center;justify-content:center;font-size:1.1rem;
|
| 186 |
+
transition:all var(--tr); flex-shrink:0; margin-bottom: 2px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.btn-send {background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#fff;}
|
| 190 |
.btn-send:hover{opacity:.9;transform:scale(1.08) rotate(-10deg);}[dir="rtl"] .btn-send:hover{transform:scale(1.08) rotate(10deg);}
|
| 191 |
.btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none;}
|
| 192 |
|
| 193 |
.btn-stop {background:var(--surface3); color:var(--text); border:1px solid var(--border);}
|
| 194 |
.btn-stop:hover {background:var(--danger); color:#fff; transform:scale(1.05);}
|
| 195 |
|
| 196 |
+
/* زر التحدث الجديد */
|
| 197 |
+
.btn-voice {background:var(--surface2); color:var(--text2);}
|
| 198 |
+
.btn-voice:hover {background:var(--surface3); color:var(--accent); transform:scale(1.05);}
|
| 199 |
+
|
| 200 |
+
/* أنيميشن التسجيل الصوتي */
|
| 201 |
+
.btn-voice.recording {
|
| 202 |
+
background:var(--danger); color:#fff;
|
| 203 |
+
animation: pulseRecord 1.5s infinite;
|
| 204 |
+
}
|
| 205 |
+
@keyframes pulseRecord {
|
| 206 |
+
0% { box-shadow: 0 0 0 0 rgba(247, 95, 95, 0.4); }
|
| 207 |
+
70% { box-shadow: 0 0 0 12px rgba(247, 95, 95, 0); }
|
| 208 |
+
100% { box-shadow: 0 0 0 0 rgba(247, 95, 95, 0); }
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
/* MODAL */
|
| 212 |
.overlay{position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.6);display:flex;
|
| 213 |
align-items:center;justify-content:center;backdrop-filter:blur(8px);
|
|
|
|
| 299 |
<input type="file" id="file-input" multiple hidden onchange="handleFiles(this.files)" />
|
| 300 |
<button class="btn-attach" onclick="document.getElementById('file-input').click()">+</button>
|
| 301 |
<textarea id="msg-input" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)" placeholder="Type a message..."></textarea>
|
| 302 |
+
|
| 303 |
+
<!-- زر التحدث الصوتي (مضاف حديثاً) -->
|
| 304 |
+
<button class="btn-voice" id="voice-btn" onclick="toggleVoiceRecord()" title="تحدث بالصوت">🎤</button>
|
| 305 |
+
|
| 306 |
<button class="btn-send" id="send-btn" onclick="sendMessage()">➤</button>
|
| 307 |
<button class="btn-stop" id="stop-btn" onclick="stopGeneration()" style="display:none;">⏹</button>
|
| 308 |
</div>
|
|
|
|
| 348 |
<script>
|
| 349 |
// ════════ i18n ════════
|
| 350 |
const i18n = {
|
| 351 |
+
en: { newChat: "✦ New Chat", chatsLabel: "CHATS", settings: "Settings", lang: "Language", dark: "Dark Mode", del: "Delete All Chats", delDesc: "This action cannot be undone", delBtn: "🗑 Delete", saveBtn: "💾 Close", placeholder: "Type your message... (Enter to send)", welcomeTitle: "Welcome to Genisi", welcomeSub: "Your smart assistant by AnesNT — Batna 🇩🇿", c1: "What is AI?", c2: "Write Python code", c3: "Summarize a topic", c4: "Design an image in space 🚀", errConnect: "Connection Error", memBadge: "Memory: {c}/{t} messages", stopMsg: "[Generation stopped by user]", vMsg: "🎤 Voice Message", vThink: "🔊 Thinking...", vPlay: "🔊 Playing Response...", vEnd: "🔊 Voice Response Ended" },
|
| 352 |
+
ar: { newChat: "✦ محادثة جديدة", chatsLabel: "المحادثات", settings: "الإعدادات", lang: "اللغة", dark: "الوضع الليلي", del: "حذف جميع المحادثات", delDesc: "لا يمكن التراجع عن هذا الإجراء", delBtn: "🗑 حذف", saveBtn: "💾 إغلاق", placeholder: "اكتب رسالتك... (Enter للإرسال)", welcomeTitle: "مرحبًا في Genisi", welcomeSub: "أنا Genisi، مساعدك الذكي من AnesNT — باتنة 🇩🇿", c1: "ما هو الذكاء الاصطناعي؟", c2: "اكتب كود بايثون", c3: "لخص لي موضوعاً", c4: "صمم صورة بالفضاء 🚀", errConnect: "خطأ في الاتصال", memBadge: "الذاكرة: {c}/{t} رسائل", stopMsg: "[تم إيقاف التوليد]", vMsg: "🎤 رسالة صوتية", vThink: "🔊 جاري التفكير...", vPlay: "🔊 يستمع ويتحدث الآن...", vEnd: "🔊 انتهى الرد الصوتي" },
|
| 353 |
+
fr: { newChat: "✦ Nouvelle Disc.", chatsLabel: "DISCUSSIONS", settings: "Paramètres", lang: "Langue", dark: "Mode Sombre", del: "Supprimer Tout", delDesc: "Action irréversible", delBtn: "🗑 Supprimer", saveBtn: "💾 Fermer", placeholder: "Écrivez votre message...", welcomeTitle: "Bienvenue sur Genisi", welcomeSub: "Votre assistant intelligent par AnesNT — Batna 🇩🇿", c1: "Qu'est-ce que l'IA?", c2: "Code Python", c3: "Résumer un sujet", c4: "Créer une image 🚀", errConnect: "Erreur de connexion", memBadge: "Mémoire: {c}/{t} msgs", stopMsg: "[Génération arrêtée]", vMsg: "🎤 Message Vocal", vThink: "🔊 Réflexion...", vPlay: "🔊 Lecture...", vEnd: "🔊 Fin de la réponse vocale" },
|
| 354 |
+
es: { newChat: "✦ Nuevo Chat", chatsLabel: "CHATS", settings: "Ajustes", lang: "Idioma", dark: "Modo Oscuro", del: "Borrar Todo", delDesc: "Acción irreversible", delBtn: "🗑 Borrar", saveBtn: "💾 Cerrar", placeholder: "Escribe tu mensaje...", welcomeTitle: "Bienvenido a Genisi", welcomeSub: "Tu asistente por AnesNT — Batna 🇩🇿", c1: "¿Qué es la IA?", c2: "Código Python", c3: "Resumir", c4: "Diseñar imagen 🚀", errConnect: "Error de conexión", memBadge: "Memoria: {c}/{t} msgs", stopMsg: "[Generación detenida]", vMsg: "🎤 Mensaje de Voz", vThink: "🔊 Pensando...", vPlay: "🔊 Reproduciendo...", vEnd: "🔊 Fin de la respuesta de voz" }
|
| 355 |
};
|
| 356 |
|
| 357 |
let currentLang = localStorage.getItem('genisi_lang') || 'en';
|
|
|
|
| 388 |
let pendingFiles =[];
|
| 389 |
let currentAbortController = null;
|
| 390 |
|
| 391 |
+
// المتغيرات الخاصة بالتسجيل الصوتي
|
| 392 |
+
let mediaRecorder;
|
| 393 |
+
let audioChunks =[];
|
| 394 |
+
let isRecording = false;
|
| 395 |
+
|
| 396 |
// ════════ MARKDOWN & PARSER ════════
|
| 397 |
const renderer = new marked.Renderer();
|
| 398 |
renderer.code = function(token) {
|
|
|
|
| 405 |
if (typeof header === 'object' && header !== null && 'header' in header) {
|
| 406 |
const token = header;
|
| 407 |
const hdr = token.header.map(cell => `<th>${cell.tokens.map(t=>t.raw||'').join('')}</th>`).join('');
|
| 408 |
+
const rows = (token.rows ||[]).map(row => `<tr>${row.map(cell => `<td>${cell.tokens.map(t=>t.raw||'').join('')}</td>`).join('')}</tr>`).join('');
|
| 409 |
return `<div class="table-wrapper"><table><thead><tr>${hdr}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
| 410 |
}
|
| 411 |
return `<div class="table-wrapper"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
|
|
|
|
| 421 |
|
| 422 |
function renderKaTeX(el) {
|
| 423 |
if (window.renderMathInElement) {
|
| 424 |
+
renderMathInElement(el, { delimiters:[{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false},{left: '\\(', right: '\\)', display: false},{left: '\\[', right: '\\]', display: true}], throwOnError: false });
|
| 425 |
}
|
| 426 |
}
|
| 427 |
function copyCode(btn, encodedCode) {
|
|
|
|
| 532 |
function handleKey(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();} }
|
| 533 |
function autoResize(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,160)+'px'; }
|
| 534 |
|
| 535 |
+
// ════════ MESSAGING & STREAMING (TEXT) ════════
|
| 536 |
async function sendMessage(){
|
| 537 |
+
if(isGenerating || isRecording) return;
|
| 538 |
const input = document.getElementById('msg-input'); let text = input.value.trim();
|
| 539 |
if(!text && pendingFiles.length === 0) return;
|
| 540 |
const welcome = document.getElementById('welcome'); if(welcome) welcome.remove();
|
|
|
|
| 607 |
}
|
| 608 |
}
|
| 609 |
|
| 610 |
+
// ════════ VOICE RECORDING (NEW FEATURE) ════════
|
| 611 |
+
async function toggleVoiceRecord() {
|
| 612 |
+
const voiceBtn = document.getElementById('voice-btn');
|
| 613 |
+
const t = i18n[currentLang];
|
| 614 |
+
|
| 615 |
+
// إذا كان يسجل حالياً، قم بإيقاف التسجيل
|
| 616 |
+
if (isRecording) {
|
| 617 |
+
mediaRecorder.stop();
|
| 618 |
+
isRecording = false;
|
| 619 |
+
voiceBtn.classList.remove('recording');
|
| 620 |
+
voiceBtn.textContent = '⏳';
|
| 621 |
+
voiceBtn.disabled = true;
|
| 622 |
+
return;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// إذا لم يكن يسجل، ابدأ التسجيل
|
| 626 |
+
try {
|
| 627 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 628 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 629 |
+
audioChunks =[];
|
| 630 |
+
|
| 631 |
+
mediaRecorder.ondataavailable = event => {
|
| 632 |
+
audioChunks.push(event.data);
|
| 633 |
+
};
|
| 634 |
+
|
| 635 |
+
mediaRecorder.onstop = async () => {
|
| 636 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
| 637 |
+
const reader = new FileReader();
|
| 638 |
+
reader.readAsDataURL(audioBlob);
|
| 639 |
+
reader.onloadend = async () => {
|
| 640 |
+
const base64data = reader.result.split(',')[1];
|
| 641 |
+
|
| 642 |
+
const welcome = document.getElementById('welcome');
|
| 643 |
+
if(welcome) welcome.remove();
|
| 644 |
+
|
| 645 |
+
// عرض مؤشر للمستخدم في المحادثة
|
| 646 |
+
appendBubble('user', t.vMsg, false);
|
| 647 |
+
const botContentDiv = appendBubble('bot', t.vThink, false);
|
| 648 |
+
|
| 649 |
+
try {
|
| 650 |
+
const response = await fetch('/voice-chat', {
|
| 651 |
+
method: 'POST',
|
| 652 |
+
headers: { 'Content-Type': 'application/json' },
|
| 653 |
+
body: JSON.stringify({ audio_b64: base64data, mime_type: 'audio/webm' })
|
| 654 |
+
});
|
| 655 |
+
|
| 656 |
+
const data = await response.json();
|
| 657 |
+
|
| 658 |
+
if (data.status === "success") {
|
| 659 |
+
botContentDiv.innerHTML = t.vPlay;
|
| 660 |
+
|
| 661 |
+
// تشغيل الصوت تلقائياً
|
| 662 |
+
const botAudio = new Audio(`data:${data.mime_type};base64,${data.audio_b64}`);
|
| 663 |
+
botAudio.play();
|
| 664 |
+
|
| 665 |
+
// عند انتهاء الصوت
|
| 666 |
+
botAudio.onended = () => {
|
| 667 |
+
botContentDiv.innerHTML = t.vEnd;
|
| 668 |
+
};
|
| 669 |
+
} else {
|
| 670 |
+
botContentDiv.innerHTML = `<span style="color:var(--danger)">⚠️ خطأ: ${data.message}</span>`;
|
| 671 |
+
}
|
| 672 |
+
} catch (err) {
|
| 673 |
+
botContentDiv.innerHTML = `<span style="color:var(--danger)">⚠️ ${t.errConnect}</span>`;
|
| 674 |
+
} finally {
|
| 675 |
+
voiceBtn.textContent = '🎤';
|
| 676 |
+
voiceBtn.disabled = false;
|
| 677 |
+
scrollToBottom();
|
| 678 |
+
stream.getTracks().forEach(track => track.stop()); // إغلاق المايكروفون
|
| 679 |
+
}
|
| 680 |
+
};
|
| 681 |
+
};
|
| 682 |
+
|
| 683 |
+
mediaRecorder.start();
|
| 684 |
+
isRecording = true;
|
| 685 |
+
voiceBtn.classList.add('recording');
|
| 686 |
+
voiceBtn.textContent = '⏹'; // أيقونة الإيقاف
|
| 687 |
+
} catch (err) {
|
| 688 |
+
alert('يرجى السماح باستخدام الميكروفون للتمكن من التحدث.');
|
| 689 |
+
console.error(err);
|
| 690 |
+
}
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
// ════════ UI BUBBLES ════════
|
| 694 |
function appendBubble(role, text, isMarkdown, filesList =[]){
|
| 695 |
const area=document.getElementById('chat-area');
|
| 696 |
const row=document.createElement('div'); row.className=`msg-row ${role}`;
|
|
|
|
| 700 |
const bub=document.createElement('div'); bub.className='bubble';
|
| 701 |
if(filesList && filesList.length > 0) filesList.forEach(name => { bub.innerHTML += `<div class="file-prev">📎 ${esc(name)}</div>`; });
|
| 702 |
const contentDiv = document.createElement('div');
|
| 703 |
+
|
| 704 |
contentDiv.dir = 'auto';
|
| 705 |
contentDiv.style.unicodeBidi = 'plaintext';
|
| 706 |
if(text) {
|