ButterM40's picture
Deploy actual React-style frontend with FastAPI backend
c2db707
// REST API-based Chat Application
let currentCharacter = 'moses';
let conversationHistory = [];
let voiceEnabled = false; // Disabled by default
let voiceAvailable = false;
let currentAudio = null;
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
setupCharacterSelection();
setupEventListeners();
loadSettings();
checkVoiceStatus();
showWelcomeMessage(currentCharacter);
updateConnectionStatus(true); // Always connected in REST mode
});
function setupCharacterSelection() {
const characterCards = document.querySelectorAll('.character-card');
characterCards.forEach(card => {
card.addEventListener('click', function() {
const characterId = this.dataset.character;
switchCharacter(characterId);
});
});
}
function setupEventListeners() {
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-btn');
messageInput.addEventListener('keydown', handleKeyPress);
sendButton.addEventListener('click', sendMessage);
// Auto-resize textarea
messageInput.addEventListener('input', function() {
autoResize(this);
});
}
function handleKeyPress(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
async function sendMessage() {
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
if (!message) return;
// Clear input and show user message
messageInput.value = '';
autoResize(messageInput);
displayMessage(message, 'user');
showTypingIndicator();
try {
// Send REST API request
const requestData = {
text: message,
timestamp: Date.now(),
conversation_history: conversationHistory.slice(-4), // Last 4 messages for context
include_voice: voiceEnabled && voiceAvailable
};
console.log('Sending request:', {
character: currentCharacter,
includeVoice: requestData.include_voice,
voiceEnabled: voiceEnabled,
voiceAvailable: voiceAvailable
});
const response = await fetch(`/api/chat/${currentCharacter}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
hideTypingIndicator();
if (response.ok) {
const data = await response.json();
console.log('Response data:', {
hasVoiceData: !!data.voice_data,
voiceEnabled: voiceEnabled,
voiceAvailable: voiceAvailable,
voiceDataLength: data.voice_data ? data.voice_data.length : 0
});
displayMessage(data.response, 'assistant', data.character_id);
// Play voice if available
if (data.voice_data && voiceEnabled) {
console.log('Attempting to play voice...');
playVoice(data.voice_data);
} else if (!data.voice_data) {
console.log('No voice data in response');
} else if (!voiceEnabled) {
console.log('Voice is disabled');
}
// Update conversation history
conversationHistory.push(
{ role: 'user', content: message },
{ role: 'assistant', content: data.response }
);
// Limit history size
if (conversationHistory.length > 20) {
conversationHistory = conversationHistory.slice(-20);
}
} else {
const error = await response.text();
displayMessage(`Sorry, I encountered an error: ${error}`, 'error');
}
} catch (error) {
hideTypingIndicator();
console.error('Error sending message:', error);
displayMessage('Sorry, I could not connect to the server. Please try again.', 'error');
}
}
function displayMessage(content, type, characterId = null) {
const chatMessages = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${type}`;
if (type === 'user') {
messageDiv.innerHTML = `
<div class="message-avatar">
<img src="/static/avatars/user.svg" alt="User" onerror="this.src='/static/avatars/default.svg'">
</div>
<div class="message-content">
<div class="message-text">${escapeHtml(content)}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
</div>
`;
} else if (type === 'assistant') {
const char = characterId || currentCharacter;
const charName = getCharacterName(char);
messageDiv.innerHTML = `
<div class="message-avatar">
<img src="/static/avatars/${char}.svg" alt="${charName}" onerror="this.src='/static/avatars/default.svg'">
</div>
<div class="message-content">
<div class="message-text">${escapeHtml(content)}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
</div>
`;
} else if (type === 'error') {
messageDiv.className = 'message error';
messageDiv.innerHTML = `
<div class="message-content">
<div class="message-text error-text">❌ ${escapeHtml(content)}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
</div>
`;
}
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getCharacterName(characterId) {
const names = {
'moses': 'Moses',
'samsung_employee': 'Samsung Employee',
'jinx': 'Jinx'
};
return names[characterId] || 'Character';
}
function switchCharacter(characterId) {
// Update character selection UI
document.querySelectorAll('.character-card').forEach(card => {
card.classList.remove('active');
});
document.querySelector(`[data-character="${characterId}"]`).classList.add('active');
currentCharacter = characterId;
updateCharacterDisplay(characterId);
// Clear current chat and show welcome message
clearChat();
showWelcomeMessage(characterId);
}
function updateCharacterDisplay(characterId) {
const characterInfo = {
'moses': {
name: 'Moses',
description: 'Biblical Prophet and Lawgiver',
avatar: '/static/avatars/moses.svg'
},
'samsung_employee': {
name: 'Samsung Employee',
description: 'Tech-savvy Corporate Representative',
avatar: '/static/avatars/samsung.svg'
},
'jinx': {
name: 'Jinx',
description: 'Chaotic Genius from Arcane',
avatar: '/static/avatars/jinx.svg'
}
};
const info = characterInfo[characterId];
if (info) {
document.getElementById('current-character-name').textContent = info.name;
document.getElementById('current-character-desc').textContent = info.description;
document.getElementById('current-avatar').src = info.avatar;
// Update placeholder
const messageInput = document.getElementById('message-input');
messageInput.placeholder = `Message ${info.name}...`;
}
}
function showWelcomeMessage(characterId) {
const welcomeMessages = {
'moses': "Peace be with you, my child. I am Moses, prophet and lawgiver. How may I guide you in righteousness?",
'samsung_employee': "Hello! I'm your Samsung technology expert, ready to provide authentic product knowledge and enthusiasm. What amazing Galaxy features can I share with you today?",
'jinx': "*spins around excitedly* Hey there! I'm Jinx - the real me, not some AI pretending! Pure chaotic genius with no boring assistant stuff. Ready for some explosive fun?"
};
const message = welcomeMessages[characterId];
if (message) {
displayMessage(message, 'assistant', characterId);
}
}
function showTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
const characterName = document.getElementById('typing-character');
characterName.textContent = getCharacterName(currentCharacter);
indicator.style.display = 'flex';
}
function hideTypingIndicator() {
document.getElementById('typing-indicator').style.display = 'none';
}
function clearChat() {
const chatMessages = document.getElementById('chat-messages');
chatMessages.innerHTML = '';
conversationHistory = [];
}
function startNewChat() {
clearChat();
showWelcomeMessage(currentCharacter);
}
function updateConnectionStatus(connected) {
// In REST API mode, we're always "connected"
const indicator = document.querySelector('.connection-status');
if (indicator) {
indicator.textContent = connected ? 'REST API Connected' : 'Disconnected';
indicator.className = connected ? 'connection-status connected' : 'connection-status disconnected';
}
}
function toggleVoice() {
if (!voiceAvailable) {
showNotification('Voice synthesis is not available on this server', 'error');
return;
}
voiceEnabled = !voiceEnabled;
updateVoiceIcon();
// Save setting
localStorage.setItem('voiceEnabled', voiceEnabled.toString());
// Show notification
const status = voiceEnabled ? 'enabled' : 'disabled';
showNotification(`Voice output ${status}`, 'success');
}
function loadSettings() {
// Load any saved settings
const savedCharacter = localStorage.getItem('selectedCharacter');
if (savedCharacter && ['moses', 'samsung_employee', 'jinx'].includes(savedCharacter)) {
switchCharacter(savedCharacter);
}
// Load voice setting (default to false)
const savedVoiceEnabled = localStorage.getItem('voiceEnabled');
voiceEnabled = savedVoiceEnabled === 'true';
}
function showResources() {
document.getElementById('resources-modal').style.display = 'flex';
}
function showSettings() {
document.getElementById('settings-modal').style.display = 'flex';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function changeTheme() {
const theme = document.getElementById('theme-select').value;
document.body.className = theme === 'light' ? 'light-theme' : '';
}
function updateVoiceSetting() {
const checkbox = document.getElementById('voice-enabled');
if (voiceAvailable) {
checkbox.checked = voiceEnabled;
checkbox.disabled = false;
checkbox.onchange = function() {
voiceEnabled = this.checked;
updateVoiceIcon();
localStorage.setItem('voiceEnabled', voiceEnabled.toString());
};
} else {
checkbox.checked = false;
checkbox.disabled = true;
}
}
// Close modals when clicking outside
window.addEventListener('click', function(event) {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
});
// Save character selection
window.addEventListener('beforeunload', function() {
localStorage.setItem('selectedCharacter', currentCharacter);
});
// Voice-related functions
async function checkVoiceStatus() {
try {
const response = await fetch('/api/voice/status');
if (response.ok) {
const data = await response.json();
voiceAvailable = data.voice_enabled && data.voice_model_loaded;
} else {
voiceAvailable = false;
}
} catch (error) {
console.log('Voice status check failed:', error);
voiceAvailable = false;
}
updateVoiceIcon();
updateVoiceSetting();
}
function updateVoiceIcon() {
const icon = document.getElementById('voice-icon');
const button = icon.closest('.voice-toggle');
if (!voiceAvailable) {
icon.className = 'fas fa-volume-mute';
button.classList.remove('active');
button.title = 'Voice synthesis not available';
} else if (voiceEnabled) {
icon.className = 'fas fa-volume-up';
button.classList.add('active');
button.title = 'Voice enabled - Click to disable';
} else {
icon.className = 'fas fa-volume-off';
button.classList.remove('active');
button.title = 'Voice disabled - Click to enable';
}
}
function playVoice(audioDataUrl) {
try {
console.log('Playing voice audio, data length:', audioDataUrl.length);
console.log('Audio data preview:', audioDataUrl.substring(0, 50));
// Stop any currently playing audio
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
}
// Create and play new audio
currentAudio = new Audio(audioDataUrl);
// Add event listeners for debugging
currentAudio.addEventListener('loadstart', () => console.log('Audio loading started'));
currentAudio.addEventListener('canplay', () => console.log('Audio can play'));
currentAudio.addEventListener('playing', () => console.log('Audio is playing'));
currentAudio.addEventListener('ended', () => console.log('Audio ended'));
currentAudio.addEventListener('error', (e) => console.error('Audio error:', e));
currentAudio.play().then(() => {
console.log('Audio play() succeeded');
showNotification('Voice playback started', 'success');
}).catch(error => {
console.error('Error playing voice:', error);
showNotification('Failed to play voice audio: ' + error.message, 'error');
});
} catch (error) {
console.error('Error setting up voice playback:', error);
showNotification('Voice playback error: ' + error.message, 'error');
}
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<i class="fas ${
type === 'success' ? 'fa-check-circle' :
type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'
}"></i>
<span>${message}</span>
<button onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
// Add to page
document.body.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 3000);
}
// Add connection status indicator
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('.chat-header');
if (header && !document.querySelector('.connection-status')) {
const statusDiv = document.createElement('div');
statusDiv.className = 'connection-status connected';
statusDiv.textContent = 'REST API Ready';
statusDiv.style.cssText = 'font-size: 12px; color: #4CAF50; margin-left: 10px;';
header.appendChild(statusDiv);
}
});