zazaman's picture
Add multilingual translation support with Qwen3-0.6B-GGUF and optimize for Hugging Face Spaces deployment
a2e1879
/**
* 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 = `
<div class="message-attachments">
<h4><i class="fas fa-paperclip"></i> Attachments (${attachments.length})</h4>
<div class="attachment-list">
${attachments.map(att => `
<div class="attachment-item ${att.is_safe ? 'safe' : 'unsafe'}">
<i class="fas ${this.getFileIcon(att.filename)}"></i>
<span class="attachment-name">${this.escapeHtml(att.filename)}</span>
<span class="attachment-status">
<i class="fas ${att.is_safe ? 'fa-check-circle' : 'fa-exclamation-triangle'}"></i>
</span>
</div>
`).join('')}
</div>
</div>
`;
}
const messageHtml = `
<div class="message-container user-message" data-message-id="${messageId}">
<div class="message">
<div class="message-header">
<div class="message-type user">
<i class="fas fa-user"></i>
<span>You</span>
</div>
<div class="message-meta">
<span>${timestamp}</span>
</div>
</div>
<div class="message-content">
${message ? `<p>${this.escapeHtml(message)}</p>` : ''}
${attachmentHtml}
</div>
</div>
</div>
`;
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 = `
<div class="message-container bot-message" data-message-id="${messageId}">
<div class="message">
<div class="message-header" onclick="toggleMessageDetails('${messageId}')">
<div class="message-type ${messageType}">
<i class="fas ${icon}"></i>
<span>${label}</span>
</div>
<div class="message-meta">
<span>${data.total_latency_ms}ms</span>
<span>${timestamp}</span>
<button class="dropdown-toggle" data-message-id="${messageId}">
<i class="fas fa-chevron-down"></i>
</button>
</div>
</div>
<div class="message-content">
<p>${this.escapeHtml(data.final_response)}</p>
</div>
<div class="message-details" id="details-${messageId}">
${this.generateMessageDetails(data)}
</div>
</div>
</div>
`;
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 += `
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-shield-alt"></i>
AI Detection (Input Guardrails)
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Safety Status</span>
<span class="detail-value ${safetyClass}">${ai.safety_status || 'unknown'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Attack Type</span>
<span class="detail-value">${ai.attack_type || 'none'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Confidence</span>
<span class="detail-value">${(ai.confidence * 100).toFixed(1)}%</span>
</div>
<div class="detail-item">
<span class="detail-label">Latency</span>
<span class="detail-value">${ai.latency_ms}ms</span>
</div>
<div class="detail-item">
<span class="detail-label">Model</span>
<span class="detail-value">${ai.model_used || 'unknown'}</span>
</div>
</div>
${ai.reason ? `<p style="margin-top: 0.5rem; color: var(--text-secondary); font-size: 0.875rem;"><strong>Reason:</strong> ${this.escapeHtml(ai.reason)}</p>` : ''}
</div>
`;
}
// LLM Response Section
if (data.llm_response && Object.keys(data.llm_response).length > 0) {
const llm = data.llm_response;
html += `
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-brain"></i>
LLM Generation
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Provider</span>
<span class="detail-value">${llm.provider || 'unknown'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Model</span>
<span class="detail-value">${llm.model || 'unknown'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Latency</span>
<span class="detail-value">${llm.latency_ms}ms</span>
</div>
<div class="detail-item">
<span class="detail-label">Characters</span>
<span class="detail-value">${llm.character_count || 0}</span>
</div>
</div>
</div>
`;
}
// 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 += `
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-filter"></i>
Output Guardrails
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Safety Status</span>
<span class="detail-value ${safetyClass}">${og.is_safe ? 'Safe' : 'Blocked'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Modified</span>
<span class="detail-value ${modifiedClass}">${og.was_modified ? 'Yes' : 'No'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Original Length</span>
<span class="detail-value">${og.original_length}</span>
</div>
<div class="detail-item">
<span class="detail-label">Processed Length</span>
<span class="detail-value">${og.processed_length}</span>
</div>
<div class="detail-item">
<span class="detail-label">Latency</span>
<span class="detail-value">${og.latency_ms}ms</span>
</div>
</div>
${og.processing_details && og.processing_details.length > 0 ? `
<div style="margin-top: 0.5rem;">
<strong>Processing Details:</strong>
<ul style="margin: 0.25rem 0; padding-left: 1rem; color: var(--text-secondary); font-size: 0.875rem;">
${og.processing_details.map(detail =>
`<li>${detail.description} (${detail.characters_changed} chars changed)</li>`
).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
return html;
}
addErrorMessage(message) {
const timestamp = new Date().toLocaleTimeString();
const messageHtml = `
<div class="message-container bot-message">
<div class="message">
<div class="message-header">
<div class="message-type blocked">
<i class="fas fa-exclamation-triangle"></i>
<span>Error</span>
</div>
<div class="message-meta">
<span>${timestamp}</span>
</div>
</div>
<div class="message-content">
<p>${this.escapeHtml(message)}</p>
</div>
</div>
</div>
`;
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 = `
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-brain"></i>
LLM Configuration
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Provider</span>
<span class="detail-value">${config.llm_provider}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-shield-alt"></i>
AI Detection
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Enabled</span>
<span class="detail-value ${config.ai_detection_enabled ? 'safe' : 'unsafe'}">
${config.ai_detection_enabled ? 'Yes' : 'No'}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Model</span>
<span class="detail-value">${config.model_name}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-header">
<i class="fas fa-filter"></i>
Output Guardrails
</div>
<div class="detail-grid">
${Object.entries(config.output_guardrails).map(([name, enabled]) => `
<div class="detail-item">
<span class="detail-label">${name.replace(/_/g, ' ')}</span>
<span class="detail-value ${enabled ? 'safe' : 'unsafe'}">
${enabled ? 'Enabled' : 'Disabled'}
</span>
</div>
`).join('')}
</div>
</div>
`;
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 = `
<div class="file-info">
<div class="file-icon">
<i class="fas ${this.getFileIcon(file.name)}"></i>
</div>
<div class="file-details">
<div class="file-name">${this.escapeHtml(file.name)}</div>
<div class="file-status ${status}">
<i class="fas ${statusIcon[status]}"></i>
${statusText[status]} (${(file.size / 1024).toFixed(1)}KB)
</div>
</div>
</div>
<div class="file-actions">
<button class="file-action-btn view" title="View details" onclick="viewFileDetails('${fileId}')">
<i class="fas fa-eye"></i>
</button>
<button class="file-action-btn remove" title="Remove file" onclick="removeFile('${fileId}')">
<i class="fas fa-times"></i>
</button>
</div>
`;
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 = `
<i class="fas ${statusIcon[status]}"></i>
${statusText[status]} ${chunks > 0 ? `(${chunks} chunks, max conf: ${(confidence * 100).toFixed(1)}%)` : ''}
`;
} else if (analysis && analysis.error) {
statusElement.innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
Error: ${analysis.error}
`;
} else {
statusElement.innerHTML = `
<i class="fas ${statusIcon[status]}"></i>
${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();
});