2025-11-29 17:27:13 -03:00
|
|
|
// HTMX-based application initialization
|
2025-11-29 16:29:28 -03:00
|
|
|
(function() {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// Configuration
|
|
|
|
|
const config = {
|
2025-11-29 17:27:13 -03:00
|
|
|
wsUrl: '/ws',
|
|
|
|
|
apiBase: '/api',
|
|
|
|
|
reconnectDelay: 3000,
|
|
|
|
|
maxReconnectAttempts: 5
|
2025-11-29 16:29:28 -03:00
|
|
|
};
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// State
|
|
|
|
|
let reconnectAttempts = 0;
|
|
|
|
|
let wsConnection = null;
|
|
|
|
|
|
|
|
|
|
// Initialize HTMX extensions
|
2025-11-29 16:29:28 -03:00
|
|
|
function initHTMX() {
|
|
|
|
|
// Configure HTMX
|
|
|
|
|
htmx.config.defaultSwapStyle = 'innerHTML';
|
|
|
|
|
htmx.config.defaultSettleDelay = 100;
|
|
|
|
|
htmx.config.timeout = 10000;
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Add CSRF token to all requests if available
|
2025-11-29 16:29:28 -03:00
|
|
|
document.body.addEventListener('htmx:configRequest', (event) => {
|
2025-11-29 17:27:13 -03:00
|
|
|
const token = localStorage.getItem('csrf_token');
|
2025-11-29 16:29:28 -03:00
|
|
|
if (token) {
|
2025-11-29 17:27:13 -03:00
|
|
|
event.detail.headers['X-CSRF-Token'] = token;
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Handle errors globally
|
2025-11-29 16:29:28 -03:00
|
|
|
document.body.addEventListener('htmx:responseError', (event) => {
|
2025-11-29 17:27:13 -03:00
|
|
|
console.error('HTMX Error:', event.detail);
|
|
|
|
|
showNotification('Connection error. Please try again.', 'error');
|
2025-11-29 16:29:28 -03:00
|
|
|
});
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Handle successful swaps
|
2025-11-29 16:29:28 -03:00
|
|
|
document.body.addEventListener('htmx:afterSwap', (event) => {
|
2025-11-29 17:27:13 -03:00
|
|
|
// Auto-scroll messages if in chat
|
|
|
|
|
const messages = document.getElementById('messages');
|
|
|
|
|
if (messages && event.detail.target === messages) {
|
|
|
|
|
messages.scrollTop = messages.scrollHeight;
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Handle WebSocket messages
|
|
|
|
|
document.body.addEventListener('htmx:wsMessage', (event) => {
|
|
|
|
|
handleWebSocketMessage(JSON.parse(event.detail.message));
|
2025-11-29 16:29:28 -03:00
|
|
|
});
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Handle WebSocket connection events
|
|
|
|
|
document.body.addEventListener('htmx:wsConnecting', () => {
|
|
|
|
|
updateConnectionStatus('connecting');
|
|
|
|
|
});
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
document.body.addEventListener('htmx:wsOpen', () => {
|
|
|
|
|
updateConnectionStatus('connected');
|
|
|
|
|
reconnectAttempts = 0;
|
|
|
|
|
});
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
document.body.addEventListener('htmx:wsClose', () => {
|
|
|
|
|
updateConnectionStatus('disconnected');
|
|
|
|
|
attemptReconnect();
|
2025-11-29 16:29:28 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Handle WebSocket messages
|
|
|
|
|
function handleWebSocketMessage(message) {
|
|
|
|
|
switch(message.type) {
|
|
|
|
|
case 'message':
|
|
|
|
|
appendMessage(message);
|
|
|
|
|
break;
|
|
|
|
|
case 'notification':
|
|
|
|
|
showNotification(message.text, message.severity);
|
|
|
|
|
break;
|
|
|
|
|
case 'status':
|
|
|
|
|
updateStatus(message);
|
|
|
|
|
break;
|
|
|
|
|
case 'suggestion':
|
|
|
|
|
addSuggestion(message.text);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log('Unknown message type:', message.type);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Append message to chat
|
|
|
|
|
function appendMessage(message) {
|
|
|
|
|
const messagesEl = document.getElementById('messages');
|
|
|
|
|
if (!messagesEl) return;
|
|
|
|
|
|
|
|
|
|
const messageEl = document.createElement('div');
|
|
|
|
|
messageEl.className = `message ${message.sender === 'user' ? 'user' : 'bot'}`;
|
|
|
|
|
messageEl.innerHTML = `
|
|
|
|
|
<div class="message-content">
|
|
|
|
|
<span class="sender">${message.sender}</span>
|
|
|
|
|
<span class="text">${escapeHtml(message.text)}</span>
|
|
|
|
|
<span class="time">${formatTime(message.timestamp)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
messagesEl.appendChild(messageEl);
|
|
|
|
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Add suggestion chip
|
|
|
|
|
function addSuggestion(text) {
|
|
|
|
|
const suggestionsEl = document.getElementById('suggestions');
|
|
|
|
|
if (!suggestionsEl) return;
|
|
|
|
|
|
|
|
|
|
const chip = document.createElement('button');
|
|
|
|
|
chip.className = 'suggestion-chip';
|
|
|
|
|
chip.textContent = text;
|
|
|
|
|
chip.setAttribute('hx-post', '/api/sessions/current/message');
|
|
|
|
|
chip.setAttribute('hx-vals', JSON.stringify({content: text}));
|
|
|
|
|
chip.setAttribute('hx-target', '#messages');
|
|
|
|
|
chip.setAttribute('hx-swap', 'beforeend');
|
|
|
|
|
|
|
|
|
|
suggestionsEl.appendChild(chip);
|
|
|
|
|
htmx.process(chip);
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Update connection status
|
|
|
|
|
function updateConnectionStatus(status) {
|
|
|
|
|
const statusEl = document.getElementById('connectionStatus');
|
|
|
|
|
if (!statusEl) return;
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
statusEl.className = `connection-status ${status}`;
|
|
|
|
|
statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Update general status
|
|
|
|
|
function updateStatus(message) {
|
|
|
|
|
const statusEl = document.getElementById('status-' + message.id);
|
|
|
|
|
if (statusEl) {
|
|
|
|
|
statusEl.textContent = message.text;
|
|
|
|
|
statusEl.className = `status ${message.severity}`;
|
|
|
|
|
}
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show notification
|
2025-11-29 17:27:13 -03:00
|
|
|
function showNotification(text, type = 'info') {
|
2025-11-29 16:29:28 -03:00
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
notification.className = `notification ${type}`;
|
2025-11-29 17:27:13 -03:00
|
|
|
notification.textContent = text;
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
const container = document.getElementById('notifications') || document.body;
|
2025-11-29 16:29:28 -03:00
|
|
|
container.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
notification.classList.add('fade-out');
|
|
|
|
|
setTimeout(() => notification.remove(), 300);
|
2025-11-29 17:27:13 -03:00
|
|
|
}, 3000);
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Attempt to reconnect WebSocket
|
|
|
|
|
function attemptReconnect() {
|
|
|
|
|
if (reconnectAttempts >= config.maxReconnectAttempts) {
|
|
|
|
|
showNotification('Connection lost. Please refresh the page.', 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reconnectAttempts++;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log(`Reconnection attempt ${reconnectAttempts}...`);
|
|
|
|
|
htmx.trigger(document.body, 'htmx:wsReconnect');
|
|
|
|
|
}, config.reconnectDelay);
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Utility: Escape HTML
|
2025-11-29 16:29:28 -03:00
|
|
|
function escapeHtml(text) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Utility: Format timestamp
|
|
|
|
|
function formatTime(timestamp) {
|
|
|
|
|
if (!timestamp) return '';
|
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
|
return date.toLocaleTimeString('en-US', {
|
|
|
|
|
hour: 'numeric',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
hour12: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle navigation
|
|
|
|
|
function initNavigation() {
|
|
|
|
|
// Update active nav item on page change
|
|
|
|
|
document.addEventListener('htmx:pushedIntoHistory', (event) => {
|
|
|
|
|
const path = event.detail.path;
|
|
|
|
|
updateActiveNav(path);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle browser back/forward
|
|
|
|
|
window.addEventListener('popstate', (event) => {
|
|
|
|
|
updateActiveNav(window.location.pathname);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update active navigation item
|
|
|
|
|
function updateActiveNav(path) {
|
|
|
|
|
document.querySelectorAll('.nav-item, .app-item').forEach(item => {
|
|
|
|
|
const href = item.getAttribute('href');
|
|
|
|
|
if (href === path || (path === '/' && href === '/chat')) {
|
|
|
|
|
item.classList.add('active');
|
|
|
|
|
} else {
|
|
|
|
|
item.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize keyboard shortcuts
|
2025-11-29 16:29:28 -03:00
|
|
|
function initKeyboardShortcuts() {
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
2025-11-29 17:27:13 -03:00
|
|
|
// Send message on Enter (when in input)
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
const input = document.getElementById('messageInput');
|
|
|
|
|
if (input && document.activeElement === input) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = input.closest('form');
|
|
|
|
|
if (form) {
|
|
|
|
|
htmx.trigger(form, 'submit');
|
|
|
|
|
}
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Focus input on /
|
|
|
|
|
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const input = document.getElementById('messageInput');
|
|
|
|
|
if (input) input.focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Escape to blur input
|
2025-11-29 16:29:28 -03:00
|
|
|
if (e.key === 'Escape') {
|
2025-11-29 17:27:13 -03:00
|
|
|
const input = document.getElementById('messageInput');
|
|
|
|
|
if (input && document.activeElement === input) {
|
|
|
|
|
input.blur();
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Initialize scroll behavior
|
|
|
|
|
function initScrollBehavior() {
|
|
|
|
|
const scrollBtn = document.getElementById('scrollToBottom');
|
|
|
|
|
const messages = document.getElementById('messages');
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
if (scrollBtn && messages) {
|
|
|
|
|
// Show/hide scroll button
|
|
|
|
|
messages.addEventListener('scroll', () => {
|
|
|
|
|
const isAtBottom = messages.scrollHeight - messages.scrollTop <= messages.clientHeight + 100;
|
|
|
|
|
scrollBtn.style.display = isAtBottom ? 'none' : 'flex';
|
|
|
|
|
});
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Scroll to bottom on click
|
|
|
|
|
scrollBtn.addEventListener('click', () => {
|
|
|
|
|
messages.scrollTo({
|
|
|
|
|
top: messages.scrollHeight,
|
|
|
|
|
behavior: 'smooth'
|
2025-11-29 16:29:28 -03:00
|
|
|
});
|
2025-11-29 17:27:13 -03:00
|
|
|
});
|
|
|
|
|
}
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Initialize theme if ThemeManager exists
|
|
|
|
|
function initTheme() {
|
|
|
|
|
if (window.ThemeManager) {
|
|
|
|
|
ThemeManager.init();
|
|
|
|
|
}
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Main initialization
|
|
|
|
|
function init() {
|
|
|
|
|
console.log('Initializing HTMX application...');
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Initialize HTMX
|
2025-11-29 16:29:28 -03:00
|
|
|
initHTMX();
|
2025-11-29 17:27:13 -03:00
|
|
|
|
|
|
|
|
// Initialize navigation
|
|
|
|
|
initNavigation();
|
|
|
|
|
|
|
|
|
|
// Initialize keyboard shortcuts
|
2025-11-29 16:29:28 -03:00
|
|
|
initKeyboardShortcuts();
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Initialize scroll behavior
|
|
|
|
|
initScrollBehavior();
|
|
|
|
|
|
|
|
|
|
// Initialize theme
|
|
|
|
|
initTheme();
|
|
|
|
|
|
|
|
|
|
// Set initial active nav
|
|
|
|
|
updateActiveNav(window.location.pathname);
|
2025-11-29 16:29:28 -03:00
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
console.log('HTMX application initialized');
|
2025-11-29 16:29:28 -03:00
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Wait for DOM and HTMX to be ready
|
2025-11-29 16:29:28 -03:00
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-29 17:27:13 -03:00
|
|
|
// Expose public API
|
2025-11-29 16:29:28 -03:00
|
|
|
window.BotServerApp = {
|
|
|
|
|
showNotification,
|
2025-11-29 17:27:13 -03:00
|
|
|
appendMessage,
|
|
|
|
|
updateConnectionStatus,
|
2025-11-29 16:29:28 -03:00
|
|
|
config
|
|
|
|
|
};
|
|
|
|
|
})();
|