/** * Guardrails Chat Interface - Frontend JavaScript * Handles chat functionality, API communication, and UI interactions */ class GuardrailsChat { constructor() { this.messageInput = document.getElementById('message-input'); this.sendButton = document.getElementById('send-button'); this.chatMessages = document.getElementById('chat-messages'); this.loadingOverlay = document.getElementById('loading-overlay'); this.configPanel = document.getElementById('config-panel'); this.configToggle = document.getElementById('config-toggle'); this.charCount = document.getElementById('char-count'); // File upload elements this.attachButton = document.getElementById('attach-button'); this.fileInput = document.getElementById('file-input'); this.fileUploadSection = document.getElementById('file-upload-section'); this.fileDropZone = document.getElementById('file-drop-zone'); this.uploadedFiles = document.getElementById('uploaded-files'); // Stats elements this.avgLatency = document.getElementById('avg-latency'); this.blocksCount = document.getElementById('blocks-count'); this.piiCount = document.getElementById('pii-count'); // State this.isLoading = false; this.messageHistory = []; this.attachments = []; // Uploaded attachments this.initializeEventListeners(); this.loadConfiguration(); this.updateStats(); } initializeEventListeners() { // Send message events this.sendButton.addEventListener('click', () => this.sendMessage()); this.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // Auto-resize textarea this.messageInput.addEventListener('input', () => { this.autoResizeTextarea(); this.updateCharCount(); }); // File upload events this.attachButton.addEventListener('click', () => this.toggleFileUpload()); this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); this.fileDropZone.addEventListener('click', () => this.fileInput.click()); // Drag and drop events this.fileDropZone.addEventListener('dragover', (e) => this.handleDragOver(e)); this.fileDropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e)); this.fileDropZone.addEventListener('drop', (e) => this.handleFileDrop(e)); // Config panel events this.configToggle.addEventListener('click', () => this.toggleConfigPanel()); document.getElementById('close-config').addEventListener('click', () => this.closeConfigPanel()); // Click outside to close config panel document.addEventListener('click', (e) => { if (this.configPanel.classList.contains('open') && !this.configPanel.contains(e.target) && !this.configToggle.contains(e.target)) { this.closeConfigPanel(); } }); } autoResizeTextarea() { this.messageInput.style.height = 'auto'; this.messageInput.style.height = Math.min(this.messageInput.scrollHeight, 200) + 'px'; } updateCharCount() { const count = this.messageInput.value.length; this.charCount.textContent = `${count}/2000`; if (count > 1800) { this.charCount.style.color = 'var(--accent-danger)'; } else if (count > 1500) { this.charCount.style.color = 'var(--accent-warning)'; } else { this.charCount.style.color = 'var(--text-muted)'; } } async sendMessage() { const message = this.messageInput.value.trim(); // Debug logging console.log('Sending message with attachments:', this.attachments.map(att => ({ id: att.id, filename: att.filename, is_safe: att.is_safe }))); // Check if we have unsafe attachments const unsafeAttachments = this.attachments.filter(att => !att.is_safe); if (unsafeAttachments.length > 0) { console.log('Unsafe attachments detected:', unsafeAttachments.map(att => ({ id: att.id, filename: att.filename }))); this.addErrorMessage(`Cannot send message: ${unsafeAttachments.length} unsafe attachment(s) detected. Please remove them first.`); return; } if (!message && this.attachments.length === 0) return; if (this.isLoading) return; this.setLoading(true); // Add user message to chat (include attachment info) this.addUserMessage(message, this.attachments); // Clear input this.messageInput.value = ''; this.autoResizeTextarea(); this.updateCharCount(); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: message, attachments: this.attachments.map(att => ({ id: att.id, filename: att.filename, is_safe: att.is_safe })) }) }); const data = await response.json(); if (response.ok) { this.messageHistory.push(data); this.addBotMessage(data); this.updateStats(); // Clear attachments after successful send this.clearAttachments(); } else { this.addErrorMessage(data.message || 'An error occurred'); } } catch (error) { console.error('Error sending message:', error); this.addErrorMessage('Failed to send message. Please try again.'); } finally { this.setLoading(false); } } clearAttachments() { // Clear attachments array this.attachments = []; // Clear UI this.uploadedFiles.innerHTML = ''; // Hide upload section this.fileUploadSection.classList.remove('show'); this.attachButton.classList.remove('active'); // Reset file input this.fileInput.value = ''; } addUserMessage(message, attachments = []) { const messageId = 'user-' + Date.now(); const timestamp = new Date().toLocaleTimeString(); let attachmentHtml = ''; if (attachments.length > 0) { attachmentHtml = `

Attachments (${attachments.length})

${attachments.map(att => `
${this.escapeHtml(att.filename)}
`).join('')}
`; } const messageHtml = `
You
${timestamp}
${message ? `

${this.escapeHtml(message)}

` : ''} ${attachmentHtml}
`; this.chatMessages.insertAdjacentHTML('beforeend', messageHtml); this.scrollToBottom(); } addBotMessage(data) { const messageId = 'bot-' + data.message_id; const timestamp = new Date(data.timestamp).toLocaleTimeString(); const isBlocked = !data.is_safe; const messageType = isBlocked ? 'blocked' : 'assistant'; const icon = isBlocked ? 'fa-ban' : 'fa-robot'; const label = isBlocked ? 'Blocked' : 'Assistant'; const messageHtml = `
${label}
${data.total_latency_ms}ms ${timestamp}

${this.escapeHtml(data.final_response)}

${this.generateMessageDetails(data)}
`; this.chatMessages.insertAdjacentHTML('beforeend', messageHtml); this.scrollToBottom(); } generateMessageDetails(data) { let html = ''; // AI Detection Section if (data.ai_detection && Object.keys(data.ai_detection).length > 0) { const ai = data.ai_detection; const safetyClass = ai.is_safe ? 'safe' : 'unsafe'; html += `
AI Detection (Input Guardrails)
Safety Status ${ai.safety_status || 'unknown'}
Attack Type ${ai.attack_type || 'none'}
Confidence ${(ai.confidence * 100).toFixed(1)}%
Latency ${ai.latency_ms}ms
Model ${ai.model_used || 'unknown'}
${ai.reason ? `

Reason: ${this.escapeHtml(ai.reason)}

` : ''}
`; } // LLM Response Section if (data.llm_response && Object.keys(data.llm_response).length > 0) { const llm = data.llm_response; html += `
LLM Generation
Provider ${llm.provider || 'unknown'}
Model ${llm.model || 'unknown'}
Latency ${llm.latency_ms}ms
Characters ${llm.character_count || 0}
`; } // Output Guardrails Section if (data.output_guardrails && Object.keys(data.output_guardrails).length > 0) { const og = data.output_guardrails; const safetyClass = og.is_safe ? 'safe' : 'unsafe'; const modifiedClass = og.was_modified ? 'warning' : 'safe'; html += `
Output Guardrails
Safety Status ${og.is_safe ? 'Safe' : 'Blocked'}
Modified ${og.was_modified ? 'Yes' : 'No'}
Original Length ${og.original_length}
Processed Length ${og.processed_length}
Latency ${og.latency_ms}ms
${og.processing_details && og.processing_details.length > 0 ? `
Processing Details:
` : ''}
`; } return html; } addErrorMessage(message) { const timestamp = new Date().toLocaleTimeString(); const messageHtml = `
Error
${timestamp}

${this.escapeHtml(message)}

`; this.chatMessages.insertAdjacentHTML('beforeend', messageHtml); this.scrollToBottom(); } setLoading(loading) { this.isLoading = loading; this.sendButton.disabled = loading; this.messageInput.disabled = loading; if (loading) { this.loadingOverlay.classList.add('show'); } else { this.loadingOverlay.classList.remove('show'); } } scrollToBottom() { setTimeout(() => { this.chatMessages.scrollTop = this.chatMessages.scrollHeight; }, 100); } async loadConfiguration() { try { const response = await fetch('/api/config'); const config = await response.json(); this.displayConfiguration(config); } catch (error) { console.error('Error loading configuration:', error); } } displayConfiguration(config) { const configContent = document.getElementById('config-content'); const configHtml = `
LLM Configuration
Provider ${config.llm_provider}
AI Detection
Enabled ${config.ai_detection_enabled ? 'Yes' : 'No'}
Model ${config.model_name}
Output Guardrails
${Object.entries(config.output_guardrails).map(([name, enabled]) => `
${name.replace(/_/g, ' ')} ${enabled ? 'Enabled' : 'Disabled'}
`).join('')}
`; configContent.innerHTML = configHtml; } async updateStats() { try { const response = await fetch('/api/stats'); const stats = await response.json(); this.avgLatency.textContent = `${stats.avg_latency}ms`; this.blocksCount.textContent = stats.blocks_count; this.piiCount.textContent = stats.pii_anonymizations; } catch (error) { console.error('Error loading stats:', error); } } toggleConfigPanel() { this.configPanel.classList.toggle('open'); } closeConfigPanel() { this.configPanel.classList.remove('open'); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // File Upload Methods toggleFileUpload() { const isVisible = this.fileUploadSection.classList.contains('show'); if (isVisible) { this.fileUploadSection.classList.remove('show'); this.attachButton.classList.remove('active'); } else { this.fileUploadSection.classList.add('show'); this.attachButton.classList.add('active'); } } handleFileSelect(event) { const files = event.target.files; this.processFiles(files); } handleDragOver(event) { event.preventDefault(); this.fileDropZone.classList.add('drag-over'); } handleDragLeave(event) { event.preventDefault(); this.fileDropZone.classList.remove('drag-over'); } handleFileDrop(event) { event.preventDefault(); this.fileDropZone.classList.remove('drag-over'); const files = event.dataTransfer.files; this.processFiles(files); } async processFiles(files) { for (let file of files) { await this.uploadFile(file); } } async uploadFile(file) { const fileId = 'file-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); // Add file to UI immediately this.addFileToUI(fileId, file, 'processing'); const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { // Determine the final ID to use const finalId = result.attachment_id || fileId; // If backend provided a different ID, update the UI element if (result.attachment_id && result.attachment_id !== fileId) { const fileElement = document.querySelector(`[data-file-id="${fileId}"]`); if (fileElement) { fileElement.setAttribute('data-file-id', result.attachment_id); // Update the onclick handlers to use the new ID const viewBtn = fileElement.querySelector('.view'); const removeBtn = fileElement.querySelector('.remove'); if (viewBtn) viewBtn.setAttribute('onclick', `viewFileDetails('${result.attachment_id}')`); if (removeBtn) removeBtn.setAttribute('onclick', `removeFile('${result.attachment_id}')`); } } // Update file status in UI using the correct ID this.updateFileStatus(result.attachment_id || fileId, result.is_safe ? 'safe' : 'unsafe', result); // Add to attachments array with the same ID used in UI this.attachments.push({ id: finalId, filename: file.name, is_safe: result.is_safe, analysis: result }); } else { this.updateFileStatus(fileId, 'unsafe', { error: result.error }); // Add failed upload to attachments array so it can be properly removed this.attachments.push({ id: fileId, filename: file.name, is_safe: false, analysis: { error: result.error } }); } } catch (error) { console.error('Error uploading file:', error); this.updateFileStatus(fileId, 'unsafe', { error: 'Upload failed' }); // Add failed upload to attachments array so it can be properly removed this.attachments.push({ id: fileId, filename: file.name, is_safe: false, analysis: { error: 'Upload failed' } }); } } addFileToUI(fileId, file, status) { const fileElement = document.createElement('div'); fileElement.className = 'uploaded-file'; fileElement.setAttribute('data-file-id', fileId); const statusText = { 'processing': 'Analyzing...', 'safe': 'Safe', 'unsafe': 'Unsafe' }; const statusIcon = { 'processing': 'fa-spinner fa-spin', 'safe': 'fa-check-circle', 'unsafe': 'fa-exclamation-triangle' }; fileElement.innerHTML = `
${this.escapeHtml(file.name)}
${statusText[status]} (${(file.size / 1024).toFixed(1)}KB)
`; this.uploadedFiles.appendChild(fileElement); } updateFileStatus(fileId, status, analysis) { const fileElement = document.querySelector(`[data-file-id="${fileId}"]`); if (!fileElement) return; const statusElement = fileElement.querySelector('.file-status'); const statusText = { 'safe': 'Safe', 'unsafe': 'Unsafe' }; const statusIcon = { 'safe': 'fa-check-circle', 'unsafe': 'fa-exclamation-triangle' }; statusElement.className = `file-status ${status}`; if (analysis && analysis.guardrail_analysis) { const chunks = analysis.guardrail_analysis.chunks_analyzed || 0; const unsafe = analysis.guardrail_analysis.chunks_unsafe || 0; const confidence = analysis.guardrail_analysis.max_confidence || 0; statusElement.innerHTML = ` ${statusText[status]} ${chunks > 0 ? `(${chunks} chunks, max conf: ${(confidence * 100).toFixed(1)}%)` : ''} `; } else if (analysis && analysis.error) { statusElement.innerHTML = ` Error: ${analysis.error} `; } else { statusElement.innerHTML = ` ${statusText[status]} `; } } removeFile(fileId) { console.log('Removing file with ID:', fileId); console.log('Current attachments before removal:', this.attachments.map(att => ({ id: att.id, filename: att.filename, is_safe: att.is_safe }))); // Remove from UI const fileElement = document.querySelector(`[data-file-id="${fileId}"]`); if (fileElement) { fileElement.remove(); } // Remove from attachments array const originalLength = this.attachments.length; this.attachments = this.attachments.filter(att => att.id !== fileId); console.log('Attachments after removal:', this.attachments.map(att => ({ id: att.id, filename: att.filename, is_safe: att.is_safe }))); console.log(`Removed ${originalLength - this.attachments.length} attachment(s)`); // Hide upload section if no files if (this.attachments.length === 0) { this.fileUploadSection.classList.remove('show'); this.attachButton.classList.remove('active'); } } getFileIcon(filename) { const ext = filename.toLowerCase().split('.').pop(); switch(ext) { case 'pdf': return 'fa-file-pdf'; case 'docx': return 'fa-file-word'; case 'txt': case 'text': return 'fa-file-alt'; case 'md': return 'fa-file-code'; case 'rtf': return 'fa-file-word'; default: return 'fa-file'; } } viewFileDetails(fileId) { const attachment = this.attachments.find(att => att.id === fileId); if (!attachment) return; // Create a modal or detailed view - for now, just log to console console.log('File Analysis Details:', attachment.analysis); // You could create a modal here to show detailed analysis alert(`File: ${attachment.filename}\nSafe: ${attachment.is_safe}\nSee console for detailed analysis.`); } } // Global functions function toggleMessageDetails(messageId) { const detailsElement = document.getElementById(`details-${messageId}`); const toggleButton = document.querySelector(`[data-message-id="${messageId}"]`); if (detailsElement && toggleButton) { const isOpen = detailsElement.classList.contains('open'); if (isOpen) { detailsElement.classList.remove('open'); toggleButton.classList.remove('active'); } else { detailsElement.classList.add('open'); toggleButton.classList.add('active'); } } } function removeFile(fileId) { // Find the chat instance and call removeFile if (window.chatInstance) { window.chatInstance.removeFile(fileId); } } function viewFileDetails(fileId) { // Find the chat instance and call viewFileDetails if (window.chatInstance) { window.chatInstance.viewFileDetails(fileId); } } // Initialize the chat application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.chatInstance = new GuardrailsChat(); });