/** * app.js — Sebayhi * * Handles: * - Mode selection (knowledge / i3rab) with sidebar toggle * - Conversation history stored in memory (sidebar list) * - SSE streaming from /chat/stream * - New chat / history switching */ // ── DOM ─────────────────────────────────────────────────────────────────────── const messageList = document.getElementById('messageList'); const chatScroll = document.getElementById('chatScroll'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const modeKnowledge = document.getElementById('modeKnowledge'); const modeI3rab = document.getElementById('modeI3rab'); const newChatBtn = document.getElementById('newChatBtn'); const historyList = document.getElementById('historyList'); const historyEmpty = document.getElementById('historyEmpty'); // ── State ───────────────────────────────────────────────────────────────────── let currentMode = 'knowledge'; let previousMode = null; let isStreaming = false; // In-memory history: [{ id, mode, title, messages: [{role, text, mode}] }] let conversations = []; let activeConvId = null; const PLACEHOLDERS = { knowledge: 'Ask a grammar question… / اسأل سؤالاً نحوياً', i3rab: 'Enter a sentence for I3rab… / اكتب الجملة للإعراب', }; const MODE_LABELS = { knowledge: 'Switched to Knowledge · معرفة', i3rab: 'Switched to I3rab · إعراب', }; // ═════════════════════════════════════════════════════════════════════════════ // MODE // ═════════════════════════════════════════════════════════════════════════════ function setMode(mode) { currentMode = mode; document.body.dataset.mode = mode; [modeKnowledge, modeI3rab].forEach(btn => { const active = btn.dataset.mode === mode; btn.classList.toggle('mode-btn--active', active); btn.setAttribute('aria-pressed', active); }); userInput.placeholder = PLACEHOLDERS[mode]; userInput.focus(); } modeKnowledge.addEventListener('click', () => setMode('knowledge')); modeI3rab.addEventListener('click', () => setMode('i3rab')); setMode('knowledge'); // ═════════════════════════════════════════════════════════════════════════════ // HISTORY // ═════════════════════════════════════════════════════════════════════════════ function createConversation(mode, firstMessage) { const id = Date.now().toString(); const conv = { id, mode, title: firstMessage.slice(0, 36) + (firstMessage.length > 36 ? '…' : ''), messages: [], }; conversations.unshift(conv); activeConvId = id; renderHistory(); return conv; } function renderHistory() { // Clear all items but keep the empty placeholder [...historyList.querySelectorAll('.history-item')].forEach(el => el.remove()); if (conversations.length === 0) { historyEmpty.style.display = ''; return; } historyEmpty.style.display = 'none'; conversations.forEach(conv => { const item = document.createElement('div'); item.className = 'history-item' + (conv.id === activeConvId ? ' history-item--active' : ''); item.dataset.id = conv.id; item.innerHTML = ` ${conv.mode === 'i3rab' ? 'I3' : 'KN'} ${escapeHtml(conv.title)}`; item.addEventListener('click', () => loadConversation(conv.id)); historyList.appendChild(item); }); } function loadConversation(id) { const conv = conversations.find(c => c.id === id); if (!conv || id === activeConvId) return; activeConvId = id; previousMode = null; // Restore mode setMode(conv.mode); // Re-render messages messageList.innerHTML = ''; conv.messages.forEach(msg => { if (msg.role === 'user') { _appendUserBubble(msg.text); } else { _appendAssistantBubble(msg.text, msg.mode); } }); renderHistory(); scrollToBottom(); } // ═════════════════════════════════════════════════════════════════════════════ // NEW CHAT // ═════════════════════════════════════════════════════════════════════════════ function startNewChat() { activeConvId = null; previousMode = null; messageList.innerHTML = `
`; renderHistory(); userInput.focus(); } newChatBtn.addEventListener('click', startNewChat); // ═════════════════════════════════════════════════════════════════════════════ // SEND // ═════════════════════════════════════════════════════════════════════════════ //* look at this func later async function sendMessage() { const text = userInput.value.trim(); if (!text || isStreaming) return; // Mode switch divider if (previousMode !== null && previousMode !== currentMode) { appendModeDivider(currentMode); } // Start or continue conversation let conv; if (!activeConvId) { conv = createConversation(currentMode, text); } else { conv = conversations.find(c => c.id === activeConvId); } previousMode = currentMode; // Save user message conv.messages.push({ role: 'user', text, mode: currentMode }); // Render user bubble _appendUserBubble(text); userInput.value = ''; autoResizeTextarea(); // Lock isStreaming = true; sendBtn.disabled = true; const thinkingEl = appendThinking(); try { const response = await fetch('/chat/stream', { method: 'POST', credentials: "include", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, intent: currentMode }), }); if (!response.ok) throw new Error(`Server error: ${response.status}`); thinkingEl.remove(); const { textEl, cursorEl } = _appendStreamingBubble(currentMode); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let replyText = ''; outer: while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop(); for (const part of parts) { if (!part.startsWith('data: ')) continue; const payload = part.slice(6); if (payload === '[DONE]') { cursorEl.remove(); break outer; } textEl.textContent += payload; replyText += payload; scrollToBottom(); } } // Save assistant reply to history conv.messages.push({ role: 'assistant', text: replyText, mode: currentMode }); } catch (err) { thinkingEl?.remove(); appendErrorBubble(err.message); console.error(err); } finally { isStreaming = false; sendBtn.disabled = false; userInput.focus(); scrollToBottom(); } } // ═════════════════════════════════════════════════════════════════════════════ // RENDER HELPERS // ═════════════════════════════════════════════════════════════════════════════ function _appendUserBubble(text) { const el = document.createElement('div'); el.className = 'message message--user'; el.innerHTML = `