Multi-Rag / static /chat.js
VashuTheGreat's picture
Clean commit without images
1f725d8
// static/chat.js
// Requires: constants.js (loaded by base.html before this script)
// =====================================================
// SESSION MANAGEMENT
// =====================================================
let uploadedFile = false;
let isConnected = false;
/**
* Initiates a connection by generating a UUID and storing it in sessionStorage.
* Reveals the upload panel and chat interface.
*/
function initiateConnection() {
const btn = document.getElementById('btnConnect');
btn.classList.add('loading');
btn.disabled = true;
// Simulate a brief connection delay for UX
setTimeout(() => {
isConnected = true;
setConnectedState();
}, 600);
}
function setConnectedState() {
// Update status indicator
const indicator = document.getElementById('statusIndicator');
indicator.className = 'status-indicator connected';
indicator.innerHTML = `<span class="status-dot"></span><span id="statusText">Connected</span>`;
// Hide connect button, show upload + session panels
document.getElementById('btnConnect').style.display = 'none';
document.getElementById('uploadPanel').style.display = 'flex';
document.getElementById('sessionInfo').style.display = 'block';
// Display truncated session ID
document.getElementById('sessionIdDisplay').textContent = userId;
// Remove overlay to reveal chat
const overlay = document.getElementById('chatOverlay');
overlay.classList.add('hidden');
// Enable input
document.getElementById('messageInput').disabled = false;
document.getElementById('sendBtn').disabled = false;
// Show a welcome message
appendMessage('assistant', '👋 Connected! Upload a PDF or TXT file to give me context, then ask me anything about it.');
}
/**
* Resets the session – clears storage and reloads the page.
*/
function resetSession() {
location.reload();
}
// =====================================================
// FILE UPLOAD
// =====================================================
async function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const statusEl = document.getElementById('uploadStatus');
const zoneEl = document.getElementById('uploadZone');
const labelEl = document.getElementById('uploadLabel');
// Validate file type
const allowed = ['application/pdf', 'text/plain'];
const ext = file.name.split('.').pop().toLowerCase();
if (!allowed.includes(file.type) && !['pdf', 'txt'].includes(ext)) {
statusEl.textContent = '✗ Only PDF or TXT files are allowed.';
statusEl.className = 'upload-status error';
return;
}
// Validate single file per session
if (uploadedFile) {
statusEl.textContent = '✗ One file per session. Start a new session to upload another.';
statusEl.className = 'upload-status error';
return;
}
statusEl.textContent = 'Uploading...';
statusEl.className = 'upload-status';
zoneEl.classList.remove('uploaded');
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(ROUTES.UPLOAD_FILE, {
method: 'POST',
headers: { 'user_id': getUserId() },
body: formData
});
const data = await res.json();
if (res.ok) {
uploadedFile = true;
zoneEl.classList.add('uploaded');
labelEl.textContent = `✓ ${file.name}`;
statusEl.textContent = `Uploaded successfully! The agent is ready.`;
statusEl.className = 'upload-status';
appendMessage('assistant', `📄 Document **${file.name}** uploaded successfully! You can now ask me questions about it.`);
} else {
statusEl.textContent = `✗ ${data.message || 'Upload failed. Try again.'}`;
statusEl.className = 'upload-status error';
}
} catch (err) {
statusEl.textContent = '✗ Network error. Please check the server.';
statusEl.className = 'upload-status error';
console.error('Upload error:', err);
}
}
// Drag & Drop support
(function setupDropZone() {
window.addEventListener('DOMContentLoaded', () => {
const zone = document.getElementById('uploadZone');
if (!zone) return;
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.classList.add('dragover');
});
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) {
const input = document.getElementById('fileInput');
// Create a DataTransfer to assign file to the input
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
handleFileUpload({ target: input });
}
});
});
})();
// =====================================================
// CHAT MESSAGES
// =====================================================
function appendMessage(role, text) {
const container = document.getElementById('messagesContainer');
const msgEl = document.createElement('div');
msgEl.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'AI';
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
if (role === 'assistant' && typeof marked !== 'undefined') {
// Render markdown for AI responses (###, **, *, lists etc.)
bubble.innerHTML = marked.parse(text);
} else {
// Plain text for user messages (safe, no XSS risk)
bubble.textContent = text;
}
msgEl.appendChild(avatar);
msgEl.appendChild(bubble);
container.appendChild(msgEl);
container.scrollTop = container.scrollHeight;
return msgEl;
}
function showTypingIndicator() {
const container = document.getElementById('messagesContainer');
const msgEl = document.createElement('div');
msgEl.className = 'message assistant typing-indicator';
msgEl.id = 'typingIndicator';
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = 'AI';
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
bubble.innerHTML = `<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>`;
msgEl.appendChild(avatar);
msgEl.appendChild(bubble);
container.appendChild(msgEl);
container.scrollTop = container.scrollHeight;
}
function removeTypingIndicator() {
const el = document.getElementById('typingIndicator');
if (el) el.remove();
}
// =====================================================
// SEND MESSAGE
// =====================================================
async function sendMessage() {
if (!isConnected || !userId) return;
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (!text) return;
// Clear input
input.value = '';
input.style.height = 'auto';
// Disable while waiting
const sendBtn = document.getElementById('sendBtn');
sendBtn.disabled = true;
input.disabled = true;
appendMessage('user', text);
showTypingIndicator();
try {
const res = await fetch(ROUTES.CHAT_MESSAGE(text), {
method: 'POST',
headers: AUTH_HEADERS()
});
const data = await res.json();
removeTypingIndicator();
if (res.ok) {
appendMessage('assistant', data.data || 'No response received.');
} else {
appendMessage('assistant', `⚠️ Error: ${data.data || 'Something went wrong.'}`);
}
} catch (err) {
removeTypingIndicator();
appendMessage('assistant', '⚠️ Could not reach the server. Please check your connection.');
console.error('Chat error:', err);
} finally {
sendBtn.disabled = false;
input.disabled = false;
input.focus();
}
}
// =====================================================
// KEYBOARD & AUTO-RESIZE
// =====================================================
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 150) + 'px';
}
// =====================================================
// RESTORE SESSION ON PAGE LOAD
// =====================================================
// No session restore needed – getUserId() always returns the same server-injected APP_USER_ID