document.addEventListener('DOMContentLoaded', () => { const chatBox = document.getElementById('chat-box'); const userInput = document.getElementById('user-input'); const sendTrigger = document.getElementById('send-trigger'); const uploadTrigger = document.getElementById('upload-trigger'); const docUpload = document.getElementById('doc-upload'); const providerSelect = document.getElementById('provider-select'); const categorySelect = document.getElementById('category-select'); const otherProviderGroup = document.getElementById('other-provider-group'); const modifyGroup = document.getElementById('modify-group'); const fileToModify = document.getElementById('file-to-modify'); const sidebar = document.getElementById('sidebar'); const sidebarToggle = document.getElementById('sidebar-toggle'); const statusBar = document.getElementById('status-bar'); const statusText = document.getElementById('status-text'); const clearChat = document.getElementById('clear-chat'); const audioUpload = document.getElementById('audio-upload'); const audioTrigger = document.getElementById('audio-trigger'); // --- Sidebar Toggle Logic --- const isSidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true'; if (isSidebarCollapsed) { sidebar.classList.add('collapsed'); } sidebarToggle.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed')); }); let chatHistory = []; let extractedEntities = {}; // State persistence let isProcessing = false; let configData = null; // --- Dynamic UI Initialization --- async function loadConfig() { try { const response = await fetch('/api/config'); configData = await response.json(); populateProviders(); } catch (e) { console.error("Could not load config", e); } } function populateProviders() { providerSelect.innerHTML = configData.providers.map(p => `` ).join(''); // Trigger initial load populateCategories(); } function populateCategories() { const selectedProviderName = providerSelect.value; const provider = configData.providers.find(p => p.name === selectedProviderName); let categories = provider ? [...provider.categories] : []; if (!categories.includes("Other...")) { categories.push("Other..."); } categorySelect.innerHTML = categories.map(c => `` ).join(''); const otherCategoryGroup = document.getElementById('other-category-group'); if (otherCategoryGroup) { otherCategoryGroup.style.display = categorySelect.value === 'Other...' ? 'block' : 'none'; } updateFileList(); } loadConfig(); // --- Chat Logic --- function addMessage(text, role) { if (role === 'bot') { typeMessage(text); } else { const msgDiv = document.createElement('div'); msgDiv.className = `message ${role}-message`; msgDiv.textContent = text; chatBox.appendChild(msgDiv); chatBox.scrollTop = chatBox.scrollHeight; } } async function typeMessage(fullText) { const msgDiv = document.createElement('div'); msgDiv.className = 'message bot-message'; chatBox.appendChild(msgDiv); chatBox.scrollTop = chatBox.scrollHeight; const words = fullText.split(' '); let currentText = ''; for (let i = 0; i < words.length; i++) { currentText += words[i] + ' '; msgDiv.innerHTML = marked.parse(currentText + '▌'); chatBox.scrollTop = chatBox.scrollHeight; await new Promise(resolve => setTimeout(resolve, 30)); } msgDiv.innerHTML = marked.parse(fullText); chatBox.scrollTop = chatBox.scrollHeight; } async function handleChat() { const prompt = userInput.value.trim(); if (!prompt || isProcessing) return; addMessage(prompt, 'user'); userInput.value = ''; isProcessing = true; const typingDiv = document.createElement('div'); typingDiv.className = 'message bot-message'; typingDiv.textContent = 'Typing...'; chatBox.appendChild(typingDiv); chatBox.scrollTop = chatBox.scrollHeight; try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, history: chatHistory, extracted_entities: extractedEntities }) }); const data = await response.json(); chatBox.removeChild(typingDiv); // Update state from backend response if (data.extracted_entities) { extractedEntities = data.extracted_entities; } if (data.answer) { addMessage(data.answer, 'bot'); chatHistory.push(prompt); } else if (data.error) { addMessage(`Error: ${data.error}`, 'bot'); } else { addMessage("I'm sorry, I encountered an unknown error.", 'bot'); } } catch (error) { chatBox.removeChild(typingDiv); addMessage("Connection error. Please try again later.", 'bot'); } finally { isProcessing = false; } } sendTrigger.addEventListener('click', handleChat); userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleChat(); }); clearChat.addEventListener('click', () => { chatBox.innerHTML = '
Chat history cleared. How can I help?
'; chatHistory = []; extractedEntities = {}; // Reset state }); // --- Audio Chat Logic --- audioTrigger.addEventListener('click', () => audioUpload.click()); audioUpload.addEventListener('change', async () => { if (!audioUpload.files.length || isProcessing) return; const file = audioUpload.files[0]; const audioUrl = URL.createObjectURL(file); const formData = new FormData(); formData.append('audio', file); formData.append('history', JSON.stringify(chatHistory)); formData.append('extracted_entities', JSON.stringify(extractedEntities)); isProcessing = true; const userMsgDiv = document.createElement('div'); userMsgDiv.className = 'message user-message audio-message-bubble'; userMsgDiv.innerHTML = `
Audio: ${file.name}
`; chatBox.appendChild(userMsgDiv); chatBox.scrollTop = chatBox.scrollHeight; const playBtn = userMsgDiv.querySelector('.play-btn'); const audio = new Audio(audioUrl); playBtn.addEventListener('click', () => { console.log("Play button clicked for:", file.name); if (audio.paused) { audio.play().then(() => { console.log("Playback started"); playBtn.innerHTML = ''; }).catch(err => { console.error("Playback failed:", err); alert("Playback failed: " + err.message); }); } else { audio.pause(); playBtn.innerHTML = ''; } }); audio.onended = () => { playBtn.innerHTML = ''; }; audio.onerror = (e) => { console.error("Audio error event:", e); alert("Error loading the audio file for playback."); }; const typingDiv = document.createElement('div'); typingDiv.className = 'message bot-message'; typingDiv.textContent = 'Transcribing audio...'; chatBox.appendChild(typingDiv); chatBox.scrollTop = chatBox.scrollHeight; try { const response = await fetch('/api/audio-chat', { method: 'POST', body: formData }); const data = await response.json(); chatBox.removeChild(typingDiv); // Update state from backend response if (data.extracted_entities) { extractedEntities = data.extracted_entities; } if (data.transcription) { const transDiv = document.createElement('div'); transDiv.innerHTML = `
Raw Transcript:
"${data.transcription}"
Refined Question:
"${data.summarized_question || data.transcription}"
`; transDiv.style.marginTop = '12px'; transDiv.style.borderTop = '1px solid rgba(255,255,255,0.1)'; transDiv.style.paddingTop = '12px'; userMsgDiv.appendChild(transDiv); if (data.answer) { addMessage(data.answer, 'bot'); chatHistory.push(data.summarized_question || data.transcription); } } else if (data.error) { addMessage(`Error: ${data.error}`, 'bot'); } } catch (error) { if (typingDiv.parentNode) chatBox.removeChild(typingDiv); addMessage("Audio processing error. Please try again.", 'bot'); } finally { isProcessing = false; audioUpload.value = ''; } }); // --- Upload and Ingestion Logic --- providerSelect.addEventListener('change', () => { otherProviderGroup.style.display = providerSelect.value === 'Other' ? 'block' : 'none'; populateCategories(); }); categorySelect.addEventListener('change', () => { const otherCategoryGroup = document.getElementById('other-category-group'); otherCategoryGroup.style.display = categorySelect.value === 'Other...' ? 'block' : 'none'; updateFileList(); }); const otherCategoryInput = document.getElementById('other-category'); if (otherCategoryInput) { otherCategoryInput.addEventListener('input', updateFileList); } const otherProviderInput = document.getElementById('other-provider'); if (otherProviderInput) { otherProviderInput.addEventListener('input', updateFileList); } categorySelect.addEventListener('change', updateFileList); document.querySelectorAll('input[name="upload-mode"]').forEach(radio => { radio.addEventListener('change', (e) => { modifyGroup.style.display = e.target.value === 'Modify Existing' ? 'block' : 'none'; if (e.target.value === 'Modify Existing') updateFileList(); }); }); async function updateFileList() { const provider = providerSelect.value === 'Other' ? document.getElementById('other-provider').value : providerSelect.value; const category = categorySelect.value === 'Other...' ? document.getElementById('other-category').value : categorySelect.value; if (!provider || !category || category === '') { fileToModify.innerHTML = ''; return; } try { const response = await fetch(`/api/files?provider=${encodeURIComponent(provider)}&category=${encodeURIComponent(category)}`); const data = await response.json(); fileToModify.innerHTML = ''; data.files.forEach(f => { const opt = document.createElement('option'); opt.value = f; opt.textContent = f; fileToModify.appendChild(opt); }); } catch (e) { console.error("Could not fetch files", e); } } uploadTrigger.addEventListener('click', () => docUpload.click()); docUpload.addEventListener('change', async () => { if (!docUpload.files.length) return; const formData = new FormData(); const provider = providerSelect.value === 'Other' ? document.getElementById('other-provider').value : providerSelect.value; const category = categorySelect.value === 'Other...' ? document.getElementById('other-category').value : categorySelect.value; const mode = document.querySelector('input[name="upload-mode"]:checked').value; if (!provider || !category) { alert("Please specify both Provider and Category."); return; } formData.append('file', docUpload.files[0]); formData.append('provider', provider); formData.append('category', category); formData.append('mode', mode); if (mode === 'Modify Existing') { formData.append('file_to_modify', fileToModify.value); } uploadTrigger.disabled = true; uploadTrigger.textContent = 'Uploading...'; try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.error) alert(data.error); else { // Refresh config to include new provider/category if added await loadConfig(); // Start polling status pollStatus(); } } catch (e) { alert("Upload failed."); } finally { uploadTrigger.disabled = false; uploadTrigger.textContent = 'Choose & Process'; docUpload.value = ''; } }); function pollStatus() { const interval = setInterval(async () => { try { const res = await fetch('/api/status'); const data = await res.json(); statusText.textContent = data.status; statusBar.style.width = data.progress + '%'; if (data.status === 'Completed Successfully!' || data.status.startsWith('Failed')) { clearInterval(interval); if (data.status === 'Completed Successfully!') { statusText.style.color = '#10b981'; // Green } else { statusText.style.color = '#ef4444'; // Red } } } catch (e) { clearInterval(interval); } }, 1000); } });