| document.addEventListener("DOMContentLoaded", () => { |
| |
| const sidebar = document.getElementById("sidebar"); |
| const openSidebarBtn = document.getElementById("open-sidebar"); |
| const closeSidebarBtn = document.getElementById("close-sidebar"); |
| const chatMessages = document.getElementById("chat-messages"); |
| const chatViewport = document.getElementById("chat-viewport"); |
| const chatForm = document.getElementById("chat-form"); |
| const userInput = document.getElementById("user-input"); |
| const usageStats = document.getElementById("usage-stats"); |
| const authTrigger = document.getElementById("auth-trigger"); |
| const authModal = document.getElementById("auth-modal"); |
| const loginForm = document.getElementById("login-form"); |
| const signupForm = document.getElementById("signup-form"); |
| const logoutBtn = document.getElementById("logout-btn"); |
| const historyList = document.getElementById("history-list"); |
| const tabBtns = document.querySelectorAll(".tab-btn"); |
| const cpuVal = document.getElementById("cpu-val"); |
| const ramVal = document.getElementById("ram-val"); |
| const sysPills = document.getElementById("sys-pills"); |
| const totalQEl = document.getElementById("total-q"); |
|
|
| let isStreaming = false; |
|
|
| |
| openSidebarBtn.onclick = () => { |
| sidebar.classList.remove("sidebar-closed"); |
| fetchHistory(); |
| }; |
| closeSidebarBtn.onclick = () => sidebar.classList.add("sidebar-closed"); |
|
|
| |
| authTrigger.onclick = () => authModal.classList.remove("hidden"); |
| document.querySelector(".close-modal-btn").onclick = () => authModal.classList.add("hidden"); |
|
|
| tabBtns.forEach(btn => { |
| btn.onclick = () => { |
| tabBtns.forEach(b => b.classList.remove("active")); |
| btn.classList.add("active"); |
| if(btn.dataset.tab === "login") { |
| loginForm.classList.remove("hidden"); |
| signupForm.classList.add("hidden"); |
| } else { |
| loginForm.classList.add("hidden"); |
| signupForm.classList.remove("hidden"); |
| } |
| }; |
| }); |
|
|
| loginForm.onsubmit = (e) => handleAuth(e, "login"); |
| signupForm.onsubmit = (e) => handleAuth(e, "signup"); |
|
|
| async function handleAuth(e, action) { |
| e.preventDefault(); |
| const email = e.target.querySelector("input[type='email']").value; |
| const password = e.target.querySelector("input[type='password']").value; |
| try { |
| const res = await fetch(`/auth/${action}`, { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({email, password}) |
| }); |
| const data = await res.json(); |
| if(data.success) location.reload(); |
| else alert(data.error); |
| } catch(err) { alert("خطأ في الاتصال"); } |
| } |
|
|
| logoutBtn.onclick = async () => { |
| await fetch("/logout"); |
| location.reload(); |
| }; |
|
|
| |
| function appendMessage(text, type, meta = null) { |
| const div = document.createElement("div"); |
| div.className = `message ${type === 'user' ? 'user-msg' : 'bot-msg'}`; |
| |
| let metaHtml = meta ? ` |
| <div class="msg-footer"> |
| <span><i class="fas fa-bolt"></i> ${meta.duration.toFixed(2)}s</span> |
| <span><i class="fas fa-coins"></i> ${meta.token_count}</span> |
| </div> |
| ` : ''; |
|
|
| div.innerHTML = ` |
| <div class="msg-bubble">${text}</div> |
| ${metaHtml} |
| `; |
| chatMessages.appendChild(div); |
| chatViewport.scrollTop = chatViewport.scrollHeight; |
| return div; |
| } |
|
|
| chatForm.onsubmit = async (e) => { |
| e.preventDefault(); |
| const q = userInput.value.trim(); |
| const tier = document.getElementById("model-tier").value; |
| if(!q || isStreaming) return; |
|
|
| appendMessage(q, 'user'); |
| userInput.value = ""; |
| isStreaming = true; |
|
|
| const botMsgDiv = document.createElement("div"); |
| botMsgDiv.className = "message bot-msg"; |
| |
| const stepsContainer = document.createElement("div"); |
| stepsContainer.className = "steps-container"; |
| |
| const currentStepEl = document.createElement("div"); |
| currentStepEl.className = "generating-status active-step"; |
| currentStepEl.innerHTML = `<i class="fas fa-circle-notch fa-spin"></i> <span>جاري البدء (${tier})...</span>`; |
| |
| botMsgDiv.appendChild(stepsContainer); |
| botMsgDiv.appendChild(currentStepEl); |
| chatMessages.appendChild(botMsgDiv); |
|
|
| let completedSteps = new Set(); |
|
|
| try { |
| const res = await fetch("/chat", { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({question: q, tier: tier}) |
| }); |
|
|
| if(!res.ok) { |
| const err = await res.json(); |
| botMsgDiv.innerHTML = `<div class="msg-bubble">❌ ${err.error}</div>`; |
| isStreaming = false; return; |
| } |
|
|
| const reader = res.body.getReader(); |
| const decoder = new TextDecoder(); |
|
|
| while(true) { |
| const {value, done} = await reader.read(); |
| if(done) break; |
| |
| const lines = decoder.decode(value).split("\n"); |
| for(const line of lines) { |
| if(!line.trim()) continue; |
| const data = JSON.parse(line); |
| |
| if(data.status === "Generating") { |
| const stepText = data.step; |
| const currentText = currentStepEl.querySelector("span").textContent; |
| if (currentText !== stepText && currentText !== `جاري البدء (${tier})...`) { |
| if (!completedSteps.has(currentText)) { |
| const doneStep = document.createElement("div"); |
| doneStep.className = "step-item completed"; |
| doneStep.innerHTML = `<i class="fas fa-check-circle"></i> ${currentText}`; |
| stepsContainer.appendChild(doneStep); |
| completedSteps.add(currentText); |
| } |
| } |
| currentStepEl.querySelector("span").textContent = stepText; |
| } else if(data.status === "Complete" || data.response_text) { |
| currentStepEl.remove(); |
| const bubble = document.createElement("div"); |
| bubble.className = "msg-bubble fade-in"; |
| bubble.innerHTML = data.response_text; |
| |
| const footer = document.createElement("div"); |
| footer.className = "msg-footer"; |
| footer.innerHTML = ` |
| <span><i class="fas fa-bolt"></i> ${data.duration.toFixed(2)}s</span> |
| <span><i class="fas fa-coins"></i> ${data.token_count}</span> |
| `; |
| |
| botMsgDiv.appendChild(bubble); |
| botMsgDiv.appendChild(footer); |
| updateGlobalStatus(); |
| } |
| } |
| } |
| } catch(err) { |
| botMsgDiv.innerHTML = `<div class="msg-bubble">❌ فشل في الاتصال</div>`; |
| } finally { |
| isStreaming = false; |
| chatViewport.scrollTop = chatViewport.scrollHeight; |
| } |
| }; |
|
|
| async function updateGlobalStatus() { |
| try { |
| const res = await fetch("/status"); |
| const data = await res.json(); |
| |
| if(data.logged_in) { |
| authTrigger.classList.add("hidden"); |
| logoutBtn.classList.remove("hidden"); |
| sysPills.classList.remove("hidden"); |
| usageStats.innerHTML = ` |
| <span class="pill primary"><i class="fas fa-user"></i> ${data.user}</span> |
| <span class="pill"><i class="fas fa-coins"></i> ${data.tokens}/${data.max}</span> |
| `; |
| } else { |
| authTrigger.classList.remove("hidden"); |
| logoutBtn.classList.add("hidden"); |
| sysPills.classList.add("hidden"); |
| usageStats.innerHTML = ` |
| <span class="pill"><i class="fas fa-user-secret"></i> ضيف</span> |
| <span class="pill"><i class="fas fa-coins"></i> ${data.tokens}/${data.max}</span> |
| `; |
| } |
| |
| cpuVal.textContent = data.cpu.toFixed(0); |
| ramVal.textContent = data.ram.toFixed(0); |
| totalQEl.textContent = data.total_questions; |
| } catch(e) {} |
| } |
|
|
| async function fetchHistory() { |
| try { |
| const res = await fetch("/history"); |
| const data = await res.json(); |
| historyList.innerHTML = ""; |
| data.reverse().forEach(item => { |
| const div = document.createElement("div"); |
| div.className = "history-item"; |
| div.innerHTML = `<i class="fas fa-comment"></i> ${item.q.substring(0, 30)}...`; |
| div.onclick = () => { |
| chatMessages.innerHTML = ""; |
| appendMessage(item.q, 'user'); |
| appendMessage(item.a, 'bot', {duration: 0, token_count: item.tokens || 0}); |
| sidebar.classList.add("sidebar-closed"); |
| }; |
| historyList.appendChild(div); |
| }); |
| } catch(e) {} |
| } |
|
|
| setInterval(updateGlobalStatus, 10000); |
| updateGlobalStatus(); |
| }); |
|
|