/**
* 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 = `
${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 = `
${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 += `
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 += `
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 += `
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:
${og.processing_details.map(detail =>
`- ${detail.description} (${detail.characters_changed} chars changed)
`
).join('')}
` : ''}
`;
}
return html;
}
addErrorMessage(message) {
const timestamp = new Date().toLocaleTimeString();
const messageHtml = `
${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 = `
Provider
${config.llm_provider}
Enabled
${config.ai_detection_enabled ? 'Yes' : 'No'}
Model
${config.model_name}
${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();
});