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