const API = ''; let currentSession = null; // DOM const sessionList = document.getElementById('session-list'); const messages = document.getElementById('messages'); const emptyState = document.getElementById('empty-state'); const inputArea = document.getElementById('input-area'); const inputMessage = document.getElementById('input-message'); const btnSend = document.getElementById('btn-send'); const btnNewSession = document.getElementById('btn-new-session'); const btnClear = document.getElementById('btn-clear'); const btnDelete = document.getElementById('btn-delete'); const btnMenu = document.getElementById('btn-menu'); const chatTitle = document.getElementById('chat-title'); const statsEl = document.getElementById('stats'); // ---- API helpers ---- async function api(path, opts = {}) { const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json' }, ...opts, }); return res.json(); } // ---- Sessions ---- async function loadSessions() { const sessions = await api('/api/sessions'); sessionList.innerHTML = sessions.map(s => `
${esc(s.title)} ${formatTime(s.updated_at)}
`).join(''); } async function createSession() { const s = await api('/api/sessions', { method: 'POST', body: '{}' }); currentSession = s; await loadSessions(); showChat(); } async function selectSession(id) { const sessions = await api('/api/sessions'); currentSession = sessions.find(s => s.id === id) || null; if (!currentSession) return; await loadSessions(); await loadMessages(id); showChat(); } async function deleteSession() { if (!currentSession) return; if (!confirm('Xóa phiên này?')) return; await api(`/api/sessions/${currentSession.id}`, { method: 'DELETE' }); currentSession = null; await loadSessions(); showEmpty(); } async function clearMessages() { if (!currentSession) return; if (!confirm('Xóa lịch sử chat trong phiên này?')) return; await api(`/api/sessions/${currentSession.id}/clear`, { method: 'POST' }); await loadMessages(currentSession.id); } async function loadMessages(sessionId) { const msgs = await api(`/api/sessions/${sessionId}/messages`); messages.innerHTML = ''; if (msgs.length === 0) { messages.innerHTML = `

Gõ câu hỏi bên dưới để bắt đầu

`; return; } msgs.forEach(m => appendMessage(m.role, m.content, m.sources)); scrollToBottom(); } // ---- Chat ---- async function sendMessage() { const text = inputMessage.value.trim(); if (!text || !currentSession) return; inputMessage.value = ''; autoResize(); appendMessage('user', text); scrollToBottom(); // Typing indicator const typingEl = document.createElement('div'); typingEl.className = 'message assistant'; typingEl.innerHTML = `
`; messages.appendChild(typingEl); scrollToBottom(); btnSend.disabled = true; try { const data = await api(`/api/sessions/${currentSession.id}/messages`, { method: 'POST', body: JSON.stringify({ content: text }), }); typingEl.remove(); appendMessage('assistant', data.content, data.sources); scrollToBottom(); loadSessions(); } catch (e) { typingEl.remove(); appendMessage('assistant', 'Lỗi kết nối: ' + e.message); scrollToBottom(); } finally { btnSend.disabled = false; inputMessage.focus(); } } function appendMessage(role, content, sources = null) { // Remove empty state if present const empty = messages.querySelector('.empty-state'); if (empty) empty.remove(); const div = document.createElement('div'); div.className = `message ${role}`; let html = `
${esc(content)}
`; if (sources && sources.length > 0) { html += `
`; sources.forEach(s => { html += `${esc(s.title.substring(0, 40))}`; }); html += `
`; } div.innerHTML = html; messages.appendChild(div); } // ---- UI state ---- function showChat() { emptyState.style.display = 'none'; inputArea.style.display = 'block'; chatTitle.textContent = currentSession?.title || ''; btnClear.style.display = currentSession ? '' : 'none'; btnDelete.style.display = currentSession ? '' : 'none'; } function showEmpty() { emptyState.style.display = 'flex'; inputArea.style.display = 'none'; chatTitle.textContent = 'Chọn hoặc tạo phiên mới'; btnClear.style.display = 'none'; btnDelete.style.display = 'none'; messages.innerHTML = ''; messages.appendChild(emptyState); } function scrollToBottom() { messages.scrollTop = messages.scrollHeight; } function autoResize() { inputMessage.style.height = 'auto'; inputMessage.style.height = Math.min(inputMessage.scrollHeight, 120) + 'px'; } // ---- Stats ---- async function loadStats() { try { const s = await api('/api/stats'); statsEl.innerHTML = `Chunks: ${s.chunks_indexed} | Phiên: ${s.sessions} | Tin nhắn: ${s.messages}`; } catch {} } // ---- Util ---- function esc(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } function formatTime(iso) { if (!iso) return ''; const d = new Date(iso); const now = new Date(); const diff = now - d; if (diff < 60000) return 'vừa xong'; if (diff < 3600000) return Math.floor(diff / 60000) + ' phút'; if (diff < 86400000) return Math.floor(diff / 3600000) + ' giờ'; return d.toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit' }); } // ---- Events ---- btnNewSession.onclick = createSession; btnClear.onclick = clearMessages; btnDelete.onclick = deleteSession; btnSend.onclick = sendMessage; btnMenu.onclick = () => { document.getElementById('sidebar').classList.toggle('open'); }; inputMessage.addEventListener('input', autoResize); inputMessage.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Close sidebar on mobile when clicking outside document.querySelector('.main').addEventListener('click', () => { document.getElementById('sidebar').classList.remove('open'); }); // Init loadSessions().then(loadStats);