285 lines
8.1 KiB
JavaScript
285 lines
8.1 KiB
JavaScript
/* CheddahBot Frontend JS */
|
|
|
|
// ── Session Management ──
|
|
const SESSION_KEY = 'cheddahbot_session';
|
|
|
|
function getSession() {
|
|
try { return JSON.parse(localStorage.getItem(SESSION_KEY) || '{}'); }
|
|
catch { return {}; }
|
|
}
|
|
|
|
function saveSession(data) {
|
|
const s = getSession();
|
|
Object.assign(s, data);
|
|
localStorage.setItem(SESSION_KEY, JSON.stringify(s));
|
|
}
|
|
|
|
function getActiveAgent() {
|
|
return getSession().agent_name || document.getElementById('input-agent-name')?.value || 'default';
|
|
}
|
|
|
|
// ── Agent Switching ──
|
|
function switchAgent(name) {
|
|
// Update UI
|
|
document.querySelectorAll('.agent-btn').forEach(b => {
|
|
b.classList.toggle('active', b.dataset.agent === name);
|
|
});
|
|
document.getElementById('input-agent-name').value = name;
|
|
document.getElementById('input-conv-id').value = '';
|
|
saveSession({ agent_name: name, conv_id: null });
|
|
|
|
// Clear chat and load new sidebar
|
|
document.getElementById('chat-messages').innerHTML = '';
|
|
refreshSidebar();
|
|
}
|
|
|
|
function setActiveAgent(name) {
|
|
document.querySelectorAll('.agent-btn').forEach(b => {
|
|
b.classList.toggle('active', b.dataset.agent === name);
|
|
});
|
|
const agentInput = document.getElementById('input-agent-name');
|
|
if (agentInput) agentInput.value = name;
|
|
}
|
|
|
|
// ── Sidebar ──
|
|
function refreshSidebar() {
|
|
const agent = getActiveAgent();
|
|
htmx.ajax('GET', '/chat/conversations?agent_name=' + agent, {
|
|
target: '#sidebar-conversations',
|
|
swap: 'innerHTML'
|
|
});
|
|
}
|
|
|
|
// ── Conversation Loading ──
|
|
function loadConversation(convId) {
|
|
const agent = getActiveAgent();
|
|
document.getElementById('input-conv-id').value = convId;
|
|
saveSession({ conv_id: convId });
|
|
|
|
htmx.ajax('GET', '/chat/load/' + convId + '?agent_name=' + agent, {
|
|
target: '#chat-messages',
|
|
swap: 'innerHTML'
|
|
}).then(() => {
|
|
scrollChat();
|
|
renderAllMarkdown();
|
|
});
|
|
}
|
|
|
|
// ── Chat Input ──
|
|
function handleKeydown(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
document.getElementById('chat-form').requestSubmit();
|
|
}
|
|
}
|
|
|
|
function autoResize(el) {
|
|
el.style.height = 'auto';
|
|
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
|
|
}
|
|
|
|
function afterSend(event) {
|
|
const input = document.getElementById('chat-input');
|
|
input.value = '';
|
|
input.style.height = 'auto';
|
|
|
|
// Clear file input and preview
|
|
const fileInput = document.querySelector('input[type="file"]');
|
|
if (fileInput) fileInput.value = '';
|
|
const preview = document.getElementById('file-preview');
|
|
if (preview) { preview.style.display = 'none'; preview.innerHTML = ''; }
|
|
|
|
scrollChat();
|
|
}
|
|
|
|
function scrollChat() {
|
|
const el = document.getElementById('chat-messages');
|
|
if (el) {
|
|
requestAnimationFrame(() => {
|
|
el.scrollTop = el.scrollHeight;
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── File Upload Preview ──
|
|
function showFileNames(input) {
|
|
const preview = document.getElementById('file-preview');
|
|
if (!input.files.length) {
|
|
preview.style.display = 'none';
|
|
return;
|
|
}
|
|
let html = '';
|
|
for (const f of input.files) {
|
|
html += '<span class="file-tag">' + f.name + '</span>';
|
|
}
|
|
preview.innerHTML = html;
|
|
preview.style.display = 'block';
|
|
}
|
|
|
|
// Drag and drop
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const chatMain = document.querySelector('.chat-main');
|
|
if (!chatMain) return;
|
|
|
|
chatMain.addEventListener('dragover', e => {
|
|
e.preventDefault();
|
|
chatMain.style.outline = '2px dashed var(--accent)';
|
|
});
|
|
chatMain.addEventListener('dragleave', () => {
|
|
chatMain.style.outline = '';
|
|
});
|
|
chatMain.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
chatMain.style.outline = '';
|
|
const fileInput = document.querySelector('input[type="file"]');
|
|
if (fileInput && e.dataTransfer.files.length) {
|
|
fileInput.files = e.dataTransfer.files;
|
|
showFileNames(fileInput);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── SSE Streaming ──
|
|
// Handle SSE chunks for chat streaming
|
|
let streamBuffer = '';
|
|
let activeSSE = null;
|
|
|
|
document.addEventListener('htmx:sseBeforeMessage', function(e) {
|
|
// This fires for each SSE event received by htmx
|
|
});
|
|
|
|
// Watch for SSE trigger divs being added to the DOM
|
|
const observer = new MutationObserver(mutations => {
|
|
for (const m of mutations) {
|
|
for (const node of m.addedNodes) {
|
|
if (node.id === 'sse-trigger') {
|
|
setupStream(node);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const chatMessages = document.getElementById('chat-messages');
|
|
if (chatMessages) {
|
|
observer.observe(chatMessages, { childList: true, subtree: true });
|
|
}
|
|
});
|
|
|
|
function setupStream(triggerDiv) {
|
|
const sseUrl = triggerDiv.getAttribute('sse-connect');
|
|
if (!sseUrl) return;
|
|
|
|
// Remove the htmx SSE to manage manually
|
|
triggerDiv.remove();
|
|
|
|
const responseDiv = document.getElementById('assistant-response');
|
|
if (!responseDiv) return;
|
|
|
|
streamBuffer = '';
|
|
|
|
// Show typing indicator
|
|
responseDiv.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
|
|
const source = new EventSource(sseUrl);
|
|
activeSSE = source;
|
|
|
|
source.addEventListener('chunk', function(e) {
|
|
if (streamBuffer === '') {
|
|
// Remove typing indicator on first chunk
|
|
responseDiv.innerHTML = '';
|
|
}
|
|
streamBuffer += e.data;
|
|
// Render markdown
|
|
try {
|
|
responseDiv.innerHTML = marked.parse(streamBuffer);
|
|
} catch {
|
|
responseDiv.textContent = streamBuffer;
|
|
}
|
|
scrollChat();
|
|
});
|
|
|
|
source.addEventListener('done', function(e) {
|
|
source.close();
|
|
activeSSE = null;
|
|
// Final markdown render
|
|
if (streamBuffer) {
|
|
try {
|
|
responseDiv.innerHTML = marked.parse(streamBuffer);
|
|
} catch {
|
|
responseDiv.textContent = streamBuffer;
|
|
}
|
|
}
|
|
streamBuffer = '';
|
|
|
|
// Update conv_id from done event data
|
|
const convId = e.data;
|
|
if (convId) {
|
|
document.getElementById('input-conv-id').value = convId;
|
|
saveSession({ conv_id: convId });
|
|
}
|
|
|
|
// Refresh sidebar
|
|
refreshSidebar();
|
|
scrollChat();
|
|
});
|
|
|
|
source.onerror = function() {
|
|
source.close();
|
|
activeSSE = null;
|
|
if (!streamBuffer) {
|
|
responseDiv.innerHTML = '<span class="text-err">Connection lost</span>';
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Markdown Rendering ──
|
|
function renderAllMarkdown() {
|
|
document.querySelectorAll('.message-content').forEach(el => {
|
|
const raw = el.textContent;
|
|
if (raw && typeof marked !== 'undefined') {
|
|
try {
|
|
el.innerHTML = marked.parse(raw);
|
|
} catch { /* keep raw text */ }
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Mobile Sidebar ──
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('chat-sidebar');
|
|
const overlay = document.getElementById('sidebar-overlay');
|
|
if (sidebar) {
|
|
sidebar.classList.toggle('open');
|
|
}
|
|
if (overlay) {
|
|
overlay.classList.toggle('visible');
|
|
}
|
|
}
|
|
|
|
// ── Notification Banner (chat page) ──
|
|
function setupChatNotifications() {
|
|
const banner = document.getElementById('notification-banner');
|
|
if (!banner) return;
|
|
|
|
const source = new EventSource('/sse/notifications');
|
|
source.addEventListener('notification', function(e) {
|
|
const notif = JSON.parse(e.data);
|
|
banner.textContent = notif.message;
|
|
banner.style.display = 'block';
|
|
// Auto-hide after 15s
|
|
setTimeout(() => { banner.style.display = 'none'; }, 15000);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', setupChatNotifications);
|
|
|
|
// ── HTMX Events ──
|
|
document.addEventListener('scrollChat', scrollChat);
|
|
document.addEventListener('htmx:afterSwap', function(e) {
|
|
if (e.target.id === 'chat-messages') {
|
|
renderAllMarkdown();
|
|
scrollChat();
|
|
}
|
|
});
|