Chatty / static /js /chat.js
findEthics
Add clear chat functionality with redesigned UX
410a397
Raw
History Blame Contribute Delete
22.8 kB
/**
* Chatty - Ethics Chat Application
* Frontend JavaScript for the chat interface
*
* Features:
* - Real-time chat with findEthics-Atlas API
* - Message history management
* - Error handling and user feedback
* - Responsive UI interactions
*/
// Chat application JavaScript
class ChatApp {
constructor() {
this.chatMessages = document.getElementById('chatMessages');
this.chatForm = document.getElementById('chatForm');
this.messageInput = document.getElementById('messageInput');
this.sendButton = document.getElementById('sendButton');
this.loadingIndicator = document.getElementById('loadingIndicator');
this.chatHistory = [];
this.userId = null; // Will be set from API responses
this.historyLoaded = false;
this.sessionWarningShown = false;
// Check if anonymous mode
this.isAnonymous = window.chatConfig && window.chatConfig.isAnonymous;
this.anonymousRateLimit = window.chatConfig ? window.chatConfig.anonymousRateLimit : 0;
this.anonymousId = window.chatConfig ? window.chatConfig.anonymousId : '';
this.messageCount = 0; // Track message count for anonymous users
this.init();
}
init() {
// Add event listeners
this.chatForm.addEventListener('submit', (e) => this.handleSubmit(e));
this.messageInput.addEventListener('keypress', (e) => this.handleKeyPress(e));
this.messageInput.addEventListener('input', (e) => this.handleInputResize(e));
// Add clear chat button listener
const clearButton = document.getElementById('clearChatButton');
if (clearButton) {
clearButton.addEventListener('click', () => this.handleClearChat());
}
// Mobile-specific optimizations
this.setupMobileOptimizations();
// Load chat history
this.loadChatHistory();
// Set up session monitoring
this.setupSessionMonitoring();
// Focus on input when page loads (but not on mobile to prevent keyboard popup)
if (!this.isMobile()) {
this.messageInput.focus();
}
}
isMobile() {
return window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
setupMobileOptimizations() {
// Handle viewport changes for mobile keyboards
if (this.isMobile()) {
let initialViewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
const handleViewportChange = () => {
if (window.visualViewport) {
const currentHeight = window.visualViewport.height;
const heightDiff = initialViewportHeight - currentHeight;
// Adjust container when keyboard appears
if (heightDiff > 150) { // Keyboard is likely open
document.body.style.height = `${currentHeight}px`;
} else {
document.body.style.height = '100vh';
}
}
};
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', handleViewportChange);
}
// Prevent zoom on double tap
let lastTouchEnd = 0;
document.addEventListener('touchend', (event) => {
const now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
}
}
handleInputResize(e) {
// Auto-resize textarea
const input = e.target;
input.style.height = 'auto';
const newHeight = Math.min(input.scrollHeight, 120);
input.style.height = newHeight + 'px';
// Scroll to bottom if needed when textarea expands
if (newHeight > 44) {
setTimeout(() => this.scrollToBottom(), 100);
}
}
handleKeyPress(e) {
// Allow Enter key to submit form (but not on mobile where Enter should create new line)
if (e.key === 'Enter' && !e.shiftKey && !this.isMobile()) {
e.preventDefault();
this.chatForm.dispatchEvent(new Event('submit'));
}
}
async handleSubmit(e) {
e.preventDefault();
const message = this.messageInput.value.trim();
if (!message) return;
// Add user message to chat
this.addMessage(message, 'user');
// Clear input and disable send button
this.messageInput.value = '';
this.messageInput.style.height = 'auto'; // Reset textarea height
this.setLoading(true);
try {
// Send message to backend
const response = await this.sendMessage(message);
if (response.success) {
// Add assistant response to chat
this.addMessage(response.response, 'assistant');
// Update chat history
this.chatHistory.push({
user: message,
assistant: response.response
});
// Store user_id for future use (if provided)
if (response.user_id) {
this.userId = response.user_id;
}
// Update message counter for anonymous users
if (this.isAnonymous && response.message_count !== undefined) {
this.updateMessageCounter(response.message_count);
// Show warning if approaching limit
if (response.messages_remaining !== undefined && response.messages_remaining <= 3 && response.messages_remaining > 0) {
this.showRateLimitWarning(response.messages_remaining);
}
}
} else {
// Show error message
this.showError(response.error || 'Failed to get response');
}
} catch (error) {
// Error already handled with user-friendly messages
// Provide specific error messages based on error type
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
this.handleSessionExpiry();
this.showError('Your session has expired. Please log in again to continue.');
} else if (error.message.includes('429')) {
if (this.isAnonymous) {
this.showError(`Rate limit exceeded. Anonymous users are limited to ${this.anonymousRateLimit} messages per hour.`);
} else {
this.showError('You are sending messages too quickly. Please wait a moment and try again.');
}
} else if (error.message.includes('400')) {
this.showError('There was a problem with your message. Please check it and try again.');
} else if (error.message.includes('500')) {
this.showError('The chat service is temporarily unavailable. Please try again in a few moments.');
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
this.showError('Unable to connect to the chat service. Please check your internet connection.');
} else {
this.showError('An unexpected error occurred. Please try again.');
}
} finally {
this.setLoading(false);
}
}
async sendMessage(message) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
history: this.chatHistory
})
});
// Handle different response statuses
if (response.status === 401) {
this.handleSessionExpiry();
return { success: false, error: 'Your session has expired. Please log in again.' };
}
if (response.status === 429) {
return { success: false, error: 'You are sending messages too quickly. Please wait a moment and try again.' };
}
if (response.status === 400) {
const errorData = await response.json().catch(() => ({}));
return { success: false, error: errorData.error || 'There was a problem with your message. Please check it and try again.' };
}
if (response.status === 500) {
return { success: false, error: 'The chat service is temporarily unavailable. Please try again in a few moments.' };
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
addMessage(content, sender, shouldScroll = true) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}`;
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContent.textContent = content;
messageDiv.appendChild(messageContent);
this.chatMessages.appendChild(messageDiv);
// Scroll to bottom only if requested (not for history loading)
if (shouldScroll) {
this.scrollToBottom();
}
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
this.chatMessages.appendChild(errorDiv);
this.scrollToBottom();
// Remove error message after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}, 5000);
}
setLoading(loading) {
if (loading) {
this.sendButton.disabled = true;
this.messageInput.disabled = true;
this.loadingIndicator.style.display = 'flex';
} else {
this.sendButton.disabled = false;
this.messageInput.disabled = false;
this.loadingIndicator.style.display = 'none';
this.messageInput.focus();
}
}
async loadChatHistory() {
// Skip loading history for anonymous users (they start fresh)
if (this.isAnonymous) {
this.historyLoaded = true;
// Initialize message counter for anonymous users
this.updateMessageCounter(0);
return;
}
try {
// Show loading state
this.showHistoryLoading(true);
const response = await fetch('/api/user-history?limit=50');
if (response.status === 401) {
// Session expired - redirect to login
window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
return;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.history && data.history.length > 0) {
// Display history messages
this.displayHistoryMessages(data.history);
// Build chat history for API context (last 10 exchanges)
this.buildChatHistoryContext(data.history);
}
this.historyLoaded = true;
} catch (error) {
// Error handled with showHistoryError
this.showHistoryError('Failed to load chat history');
} finally {
this.showHistoryLoading(false);
}
}
displayHistoryMessages(history) {
// Clear any existing messages
this.chatMessages.innerHTML = '';
// Add each message pair from history
history.forEach(session => {
if (session.message) {
this.addMessage(session.message, 'user', false); // Don't scroll for history
}
if (session.response) {
this.addMessage(session.response, 'assistant', false); // Don't scroll for history
}
});
// Scroll to bottom after all messages are loaded
setTimeout(() => this.scrollToBottom(), 100);
}
buildChatHistoryContext(history) {
// Build context for API calls (last 10 exchanges)
this.chatHistory = [];
const recentHistory = history.slice(-10); // Get last 10 exchanges
recentHistory.forEach(session => {
if (session.message && session.response) {
this.chatHistory.push({
user: session.message,
assistant: session.response
});
}
});
}
showHistoryLoading(loading) {
if (loading) {
// Show loading message in chat area
const loadingDiv = document.createElement('div');
loadingDiv.className = 'history-loading';
loadingDiv.id = 'historyLoading';
loadingDiv.innerHTML = '<div class="loading-text">Loading chat history...</div>';
this.chatMessages.appendChild(loadingDiv);
} else {
// Remove loading message
const loadingDiv = document.getElementById('historyLoading');
if (loadingDiv) {
loadingDiv.remove();
}
}
}
showHistoryError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'history-error';
errorDiv.innerHTML = `<div class="error-text">${message}</div>`;
this.chatMessages.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.parentNode.removeChild(errorDiv);
}
}, 5000);
}
scrollToBottom() {
// Instant scroll to bottom (no animations)
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
setupSessionMonitoring() {
// Check session status periodically (every 5 minutes)
setInterval(() => {
this.checkSessionStatus();
}, 5 * 60 * 1000);
// Check session on page visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.checkSessionStatus();
}
});
}
async checkSessionStatus() {
try {
const response = await fetch('/api/user-history?limit=1');
if (response.status === 401) {
this.handleSessionExpiry();
} else if (response.status === 429) {
this.showSessionWarning('You are being rate limited. Please wait before making more requests.');
}
} catch (error) {
// Network error - don't show warning as it might be temporary
// Network error - don't show warning as it might be temporary
}
}
handleSessionExpiry() {
if (!this.sessionWarningShown) {
this.showSessionExpiryWarning();
this.sessionWarningShown = true;
}
}
showSessionExpiryWarning() {
// Create session expiry warning banner
const warningDiv = document.createElement('div');
warningDiv.className = 'session-expiry-warning';
if (this.isAnonymous) {
warningDiv.innerHTML = `
Your anonymous session has expired.
<button onclick="window.location.href='/'">
Start New Session
</button>
<button onclick="window.location.href='/register'">
Create Account
</button>
`;
} else {
warningDiv.innerHTML = `
Your session has expired. Please log in again to continue chatting.
<button onclick="window.location.href='/login?next=' + encodeURIComponent(window.location.pathname)">
Login Again
</button>
`;
}
// Insert at top of page
document.body.insertBefore(warningDiv, document.body.firstChild);
// Disable chat interface
this.messageInput.disabled = true;
this.sendButton.disabled = true;
this.messageInput.placeholder = this.isAnonymous ? 'Session expired - start a new anonymous session' : 'Session expired - please log in again';
}
showSessionWarning(message) {
const warningDiv = document.createElement('div');
warningDiv.className = 'session-warning';
warningDiv.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #fbbf24;
color: #92400e;
padding: 0.75rem;
text-align: center;
font-weight: 500;
z-index: 1000;
border-bottom: 2px solid #f59e0b;
`;
warningDiv.textContent = message;
document.body.insertBefore(warningDiv, document.body.firstChild);
// Remove warning after 5 seconds
setTimeout(() => {
if (warningDiv.parentNode) {
warningDiv.parentNode.removeChild(warningDiv);
}
}, 5000);
}
updateMessageCounter(count) {
if (!this.isAnonymous) return;
this.messageCount = count;
const messagesUsedEl = document.getElementById('messagesUsed');
const messageCounterEl = document.getElementById('messageCounter');
if (messagesUsedEl) {
messagesUsedEl.textContent = count;
}
// Show warning colors as approaching limit
if (messageCounterEl) {
if (count >= this.anonymousRateLimit) {
messageCounterEl.style.backgroundColor = '#ffcccc';
messageCounterEl.style.borderColor = '#ff6666';
} else if (count >= this.anonymousRateLimit * 0.8) {
messageCounterEl.style.backgroundColor = '#fff5cc';
messageCounterEl.style.borderColor = '#ffcc66';
} else {
messageCounterEl.style.backgroundColor = 'white';
messageCounterEl.style.borderColor = 'rgb(200, 200, 200)';
}
}
}
showRateLimitWarning(remaining) {
const warningDiv = document.createElement('div');
warningDiv.className = 'system-message warning';
warningDiv.innerHTML = `
<strong>⚠️ Rate limit approaching:</strong> You have ${remaining} message${remaining === 1 ? '' : 's'} remaining this hour.
<a href="/login">Login</a> or <a href="/register">Register</a> for unlimited messages.
`;
this.chatMessages.appendChild(warningDiv);
this.scrollToBottom();
}
async handleClearChat() {
try {
// Call the clear chat API
const response = await this.clearChatHistory();
if (response.success) {
// Clear the UI immediately
this.clearChatUI();
// Show success message
this.showSuccessMessage(response.message || 'Chat history cleared successfully');
} else {
// Show error message
this.showError(response.error || 'Failed to clear chat history');
}
} catch (error) {
// Handle network or other errors
this.showError('Unable to clear chat history. Please check your connection and try again.');
}
}
async clearChatHistory() {
const response = await fetch('/api/clear-chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
clearChatUI() {
// Clear the chat messages display
this.chatMessages.innerHTML = '';
// Clear the local chat history
this.chatHistory = [];
// Reset message counter for anonymous users
if (this.isAnonymous) {
this.updateMessageCounter(0);
}
}
showSuccessMessage(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
this.chatMessages.appendChild(successDiv);
this.scrollToBottom();
// Remove success message after 3 seconds
setTimeout(() => {
if (successDiv.parentNode) {
successDiv.parentNode.removeChild(successDiv);
}
}, 3000);
}
}
// Initialize the chat application when the page loads
document.addEventListener('DOMContentLoaded', () => {
new ChatApp();
});
// Optional: Register service worker for better mobile experience
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
window.addEventListener('load', () => {
// Only register if we have a service worker file
fetch('/static/sw.js', { method: 'HEAD' })
.then(response => {
if (response.ok) {
navigator.serviceWorker.register('/static/sw.js')
.then(registration => {
// Service worker registered successfully
})
.catch(registrationError => {
// Service worker registration failed - that's fine
});
}
})
.catch(() => {
// Service worker file doesn't exist, that's fine
});
});
}