const chatBox = document.getElementById('chatBox'); const chatInput = document.getElementById('chatInput'); const sendButton = document.getElementById('sendButton'); const chatList = document.getElementById('chatList'); const progressBar = document.getElementById('progress-bar'); const usageText = document.getElementById('usage-text'); const loadingIndicator = document.getElementById('loadingIndicator'); let currentAssistantMessageDiv = null; // To hold the streaming message div export function displayMessage(text, sender, options = { isMarkdown: false, isStreaming: false }) { // Sanitize text before inserting, especially if using innerHTML // For user messages, simple textContent is safer. For Markdown, use DOMPurify. const sanitizedText = sender === 'user' ? text : (options.isMarkdown ? DOMPurify.sanitize(marked.parse(text)) : text); const messageElement = document.createElement('div'); messageElement.classList.add('chat-message', sender); if (sender === 'assistant' && options.isMarkdown) { messageElement.innerHTML = sanitizedText; // Use sanitized HTML } else { messageElement.textContent = sanitizedText; // Safer for plain text } const isScrolledToBottom = chatBox.scrollHeight - chatBox.clientHeight <= chatBox.scrollTop + 5; // Tolerance // If starting a new streaming message, create the container if (sender === 'assistant' && options.isStreaming && !currentAssistantMessageDiv) { currentAssistantMessageDiv = document.createElement('div'); currentAssistantMessageDiv.classList.add('chat-message', 'assistant'); chatBox.appendChild(currentAssistantMessageDiv); } // Update or append message if (currentAssistantMessageDiv && sender === 'assistant' && options.isStreaming) { currentAssistantMessageDiv.innerHTML = sanitizedText; // Update streaming div } else { chatBox.appendChild(messageElement); // Append regular message or finalize stream currentAssistantMessageDiv = null; // Reset stream div reference } // Scroll to bottom only if the user was already near the bottom if (isScrolledToBottom) { chatBox.scrollTop = chatBox.scrollHeight; } } // Function to update the streaming message div content incrementally export function updateStreamingMessage(contentChunk, isMarkdown = true) { if (!currentAssistantMessageDiv) { // Create the div if it doesn't exist (e.g., first chunk) currentAssistantMessageDiv = document.createElement('div'); currentAssistantMessageDiv.classList.add('chat-message', 'assistant'); chatBox.appendChild(currentAssistantMessageDiv); } // Append chunk (if plain text) or re-render markdown (simpler for now) // For true streaming of markdown, need more complex parsing let currentContent = currentAssistantMessageDiv.dataset.rawContent || ''; currentContent += contentChunk; currentAssistantMessageDiv.dataset.rawContent = currentContent; // Store raw text if (isMarkdown && window.marked && window.DOMPurify) { currentAssistantMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(currentContent)); } else { currentAssistantMessageDiv.textContent = currentContent; } // Keep scrolled to bottom during streaming chatBox.scrollTop = chatBox.scrollHeight; } // Function to finalize the streaming message div export function finalizeStreamingMessage() { if (currentAssistantMessageDiv) { delete currentAssistantMessageDiv.dataset.rawContent; // Clean up temp data } currentAssistantMessageDiv = null; } export function clearChatBox() { chatBox.innerHTML = ''; // Optionally add a default message // displayMessage("Neuer Chat gestartet.", "assistant"); } export function clearChatInput() { chatInput.value = ''; autoResizeTextarea(); // Reset height } export function disableInput(isLoading = true) { chatInput.disabled = true; sendButton.disabled = true; chatInput.placeholder = isLoading ? 'Warte auf Antwort...' : 'Schreibe eine Nachricht...'; if (isLoading && loadingIndicator) loadingIndicator.style.display = 'inline-block'; // Show spinner if (isLoading) sendButton.style.display = 'none'; // Hide send button } export function enableInput() { chatInput.disabled = false; sendButton.disabled = false; chatInput.placeholder = 'Schreibe eine Nachricht...'; if (loadingIndicator) loadingIndicator.style.display = 'none'; // Hide spinner sendButton.style.display = 'flex'; // Show send button chatInput.focus(); } export function addChatToList(chatData, isActive, loadChatCallback, deleteChatCallback) { const container = document.createElement('div'); container.classList.add('chat-session-button-container'); container.dataset.chatId = chatData.id; // Add ID to container for easier selection if (isActive) { container.classList.add('active'); } const button = document.createElement('button'); button.classList.add('chat-session-button'); button.textContent = chatData.name || `Chat ${chatData.id.slice(-4)}`; // Use name or part of ID button.addEventListener('click', () => loadChatCallback(chatData.id)); const deleteBtn = document.createElement('button'); deleteBtn.classList.add('delete-chat-button'); deleteBtn.setAttribute('aria-label', `Delete chat ${chatData.name || chatData.id}`); deleteBtn.innerHTML = '🗑️'; // Use trash icon deleteBtn.addEventListener('click', (event) => { event.stopPropagation(); // Prevent chat loading deleteChatCallback(chatData.id); }); container.appendChild(button); container.appendChild(deleteBtn); // Prepend to the top of the list for newest first chatList.insertBefore(container, chatList.firstChild); } export function removeChatFromList(chatId) { const chatElement = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"]`); if (chatElement) { chatElement.remove(); } } export function updateChatNameInList(chatId, name) { const button = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"] .chat-session-button`); const deleteButton = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"] .delete-chat-button`); if (button) { button.textContent = name || `Chat ${chatId.slice(-4)}`; } if (deleteButton) { deleteButton.setAttribute('aria-label', `Delete chat ${name || chatId}`); } } export function setActiveChatInList(chatId) { document.querySelectorAll('.chat-session-button-container').forEach(container => { container.classList.toggle('active', container.dataset.chatId === chatId); }); } export function updateProgressBar(currentUsage, maxUsage) { // Ensure values are numbers and maxUsage is positive currentUsage = Number(currentUsage) || 0; maxUsage = Number(maxUsage) || 1; // Avoid division by zero maxUsage = Math.max(1, maxUsage); // Ensure maxUsage is at least 1 const percentage = Math.min(100, Math.max(0, (currentUsage / maxUsage) * 100)); // Clamp between 0 and 100 if (progressBar) { progressBar.style.width = `${percentage}%`; } if (usageText) { // Format maxUsage for readability if large? usageText.textContent = `Token Usage: ${currentUsage} / ${maxUsage}`; } } // Auto-resize textarea helper export function autoResizeTextarea() { chatInput.style.overflowY = 'hidden'; // Prevent scrollbar flash chatInput.style.height = 'auto'; const scrollHeight = chatInput.scrollHeight; const maxHeight = parseInt(window.getComputedStyle(chatInput).maxHeight, 10); if (maxHeight > 0 && scrollHeight > maxHeight) { chatInput.style.height = `${maxHeight}px`; chatInput.style.overflowY = 'auto'; } else { chatInput.style.height = `${scrollHeight}px`; chatInput.style.overflowY = 'hidden'; } }