CheddahBot/cheddahbot/static/app.js

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();
}
});