Files
twitchBot-intelligent/static/js/app.js
T
2025-07-20 01:50:24 +02:00

662 lines
21 KiB
JavaScript

// Configuration et variables globales
const API_BASE = '';
let socket;
let currentPrompts = [];
let currentStatus = {};
// Initialisation de l'application
document.addEventListener('DOMContentLoaded', function() {
initializeSocketIO();
loadInitialData();
setupEventListeners();
startPeriodicUpdates();
});
// Initialisation de Socket.IO pour les mises à jour en temps réel
function initializeSocketIO() {
socket = io();
socket.on('connect', function() {
console.log('Connecté au serveur');
updateConnectionStatus(true);
});
socket.on('disconnect', function() {
console.log('Déconnecté du serveur');
updateConnectionStatus(false);
});
socket.on('status_update', function(data) {
updateDashboard(data);
});
socket.on('new_subtitle', function(data) {
addNewSubtitle(data);
});
socket.on('new_generation', function(data) {
updateNextMessage(data);
});
}
// Mise à jour du statut de connexion
function updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
const iconElement = statusElement.previousElementSibling;
if (connected) {
statusElement.textContent = 'Connecté';
iconElement.className = 'fas fa-circle text-success pulse';
} else {
statusElement.textContent = 'Déconnecté';
iconElement.className = 'fas fa-circle text-danger';
}
}
// Chargement des données initiales
async function loadInitialData() {
try {
await Promise.all([
loadFluxList(),
loadPrompts(),
loadStatus(),
loadSubtitles(),
loadGenerations()
]);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
showToast('Erreur lors du chargement des données', 'error');
}
}
// Configuration des écouteurs d'événements
function setupEventListeners() {
// Modal d'ajout de flux
const addFluxModal = document.getElementById('addFluxModal');
addFluxModal.addEventListener('hidden.bs.modal', function() {
document.getElementById('channel-name').value = '';
document.getElementById('record-audio').checked = true;
});
}
// Mises à jour périodiques
function startPeriodicUpdates() {
// Mise à jour des données toutes les 10 secondes
setInterval(async () => {
await loadStatus();
await loadSubtitles();
await loadGenerations();
}, 10000);
}
// === GESTION DES FLUX ===
// Chargement de la liste des flux
async function loadFluxList() {
try {
const response = await fetch(`${API_BASE}/api/flux`);
const flux = await response.json();
renderFluxList(flux);
} catch (error) {
console.error('Erreur lors du chargement des flux:', error);
}
}
// Rendu de la liste des flux
function renderFluxList(fluxList) {
const container = document.getElementById('flux-list');
if (fluxList.length === 0) {
container.innerHTML = `
<div class="text-center text-muted py-4">
<i class="fas fa-stream fa-3x mb-3"></i>
<p>Aucun flux configuré</p>
<p class="small">Cliquez sur "Ajouter Flux" pour commencer</p>
</div>
`;
return;
}
container.innerHTML = fluxList.map(flux => `
<div class="flux-item fade-in" data-flux-id="${flux.id}">
<div class="flux-controls">
<div class="flux-info">
<div class="flux-name">
<span class="flux-status ${flux.active ? 'active' : 'inactive'}"></span>
${flux.name}
<span class="badge bg-${flux.status === 'active' ? 'success' : flux.status === 'error' ? 'danger' : 'warning'} ms-2">${flux.status}</span>
</div>
<div class="flux-details">
${flux.record_audio ?
'<i class="fas fa-microphone text-success me-1"></i>Audio' :
'<i class="fas fa-microphone-slash text-muted me-1"></i>Pas d\'audio'
}
<i class="fas fa-comments text-info ms-2 me-1"></i>Chat
<span class="text-muted ms-2">${new Date(flux.created_at).toLocaleString()}</span>
${flux.error ? `<span class="text-danger ms-2">Erreur: ${flux.error}</span>` : ''}
</div>
</div>
<div class="flux-actions">
<button class="btn btn-sm btn-outline-warning" onclick="toggleFlux(${flux.id})" ${flux.status === 'error' ? 'disabled' : ''}>
<i class="fas fa-${flux.active ? 'pause' : 'play'}"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="removeFlux(${flux.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`).join('');
}
// Fonction pour activer/désactiver un flux
async function toggleFlux(fluxId) {
try {
const response = await fetch(`${API_BASE}/api/flux/${fluxId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
showToast(`Flux ${result.active ? 'activé' : 'désactivé'}`, 'success');
await loadFluxList();
} else {
showToast(result.error || 'Erreur lors du changement de statut', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors du changement de statut du flux', 'error');
}
}
// Ajout d'un nouveau flux
async function addFlux() {
const channelName = document.getElementById('channel-name').value.trim();
const recordAudio = document.getElementById('record-audio').checked;
if (!channelName) {
showToast('Veuillez entrer un nom de canal', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/flux`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel_name: channelName,
record_audio: recordAudio
})
});
const result = await response.json();
if (result.success) {
showToast(`Flux ${channelName} ajouté avec succès`, 'success');
bootstrap.Modal.getInstance(document.getElementById('addFluxModal')).hide();
await loadFluxList();
} else {
showToast(result.error || 'Erreur lors de l\'ajout du flux', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de l\'ajout du flux', 'error');
}
}
// Suppression d'un flux
async function removeFlux(fluxId) {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce flux ?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/api/flux/${fluxId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showToast('Flux supprimé avec succès', 'success');
await loadFluxList();
} else {
showToast(result.error || 'Erreur lors de la suppression', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de la suppression du flux', 'error');
}
}
// === GESTION DES PROMPTS ===
// Chargement des prompts
async function loadPrompts() {
try {
const response = await fetch(`${API_BASE}/api/config/prompts`);
currentPrompts = await response.json();
renderPrompts();
} catch (error) {
console.error('Erreur lors du chargement des prompts:', error);
}
}
// Rendu des prompts
function renderPrompts() {
const container = document.getElementById('prompts-list');
container.innerHTML = currentPrompts.map((prompt, index) => `
<div class="prompt-item fade-in">
<button class="btn btn-danger btn-sm" onclick="removePrompt(${index})">
<i class="fas fa-times"></i>
</button>
<div class="mb-2">
<label class="form-label small text-muted">Prompt ${index + 1}</label>
</div>
<textarea class="form-control" rows="2" data-prompt-index="${index}">${prompt}</textarea>
</div>
`).join('');
}
// Ajout d'un prompt
function addPrompt() {
currentPrompts.push('Nouveau prompt : ');
renderPrompts();
// Focus sur le nouveau prompt
setTimeout(() => {
const newPromptTextarea = document.querySelector(`textarea[data-prompt-index="${currentPrompts.length - 1}"]`);
if (newPromptTextarea) {
newPromptTextarea.focus();
newPromptTextarea.select();
}
}, 100);
}
// Suppression d'un prompt
function removePrompt(index) {
if (confirm('Êtes-vous sûr de vouloir supprimer ce prompt ?')) {
currentPrompts.splice(index, 1);
renderPrompts();
}
}
// Sauvegarde des prompts
async function savePrompts() {
// Récupérer les valeurs des textareas
const textareas = document.querySelectorAll('textarea[data-prompt-index]');
const updatedPrompts = Array.from(textareas).map(textarea => textarea.value.trim());
try {
const response = await fetch(`${API_BASE}/api/config/prompts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompts: updatedPrompts
})
});
const result = await response.json();
if (result.success) {
currentPrompts = updatedPrompts;
showToast('Prompts sauvegardés avec succès', 'success');
} else {
showToast('Erreur lors de la sauvegarde', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de la sauvegarde des prompts', 'error');
}
}
// === GESTION DES MESSAGES ===
// Envoi d'un message personnalisé
async function sendCustomMessage() {
const message = document.getElementById('custom-message').value.trim();
if (!message) {
showToast('Veuillez entrer un message', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/send-message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
channel: 'default'
})
});
const result = await response.json();
if (result.success) {
showToast('Message envoyé avec succès', 'success');
document.getElementById('custom-message').value = '';
addToRecentMessages(message);
} else {
showToast(result.error || 'Erreur lors de l\'envoi', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de l\'envoi du message', 'error');
}
}
// Envoi du prochain message
async function sendNextMessage() {
const nextMessageElement = document.getElementById('next-message');
const message = nextMessageElement.textContent.trim();
if (message === 'Aucun message en attente') {
showToast('Aucun message à envoyer', 'warning');
return;
}
try {
const response = await fetch(`${API_BASE}/api/send-message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
channel: 'default'
})
});
const result = await response.json();
if (result.success) {
showToast('Message envoyé avec succès', 'success');
addToRecentMessages(message);
nextMessageElement.textContent = 'Aucun message en attente';
} else {
showToast(result.error || 'Erreur lors de l\'envoi', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de l\'envoi du message', 'error');
}
}
// Envoi de la dernière génération
async function sendLastGeneration() {
try {
const response = await fetch(`${API_BASE}/api/generations`);
const generations = await response.json();
if (Object.keys(generations).length === 0) {
showToast('Aucune génération disponible', 'warning');
return;
}
const sortedKeys = Object.keys(generations).sort();
const lastGeneration = generations[sortedKeys[sortedKeys.length - 1]];
const sendResponse = await fetch(`${API_BASE}/api/send-message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: lastGeneration,
channel: 'default'
})
});
const result = await sendResponse.json();
if (result.success) {
showToast('Dernière génération envoyée', 'success');
addToRecentMessages(lastGeneration);
} else {
showToast(result.error || 'Erreur lors de l\'envoi', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de l\'envoi de la génération', 'error');
}
}
// Ajout d'un message aux messages récents
function addToRecentMessages(message) {
const container = document.getElementById('recent-messages');
const messageElement = document.createElement('div');
messageElement.className = 'message-item fade-in';
messageElement.innerHTML = `
<div class="message-content">${message}</div>
<div class="message-time">${new Date().toLocaleTimeString()}</div>
`;
container.insertBefore(messageElement, container.firstChild);
// Limiter à 10 messages récents
const messages = container.children;
if (messages.length > 10) {
container.removeChild(messages[messages.length - 1]);
}
}
// === GESTION DES SOUS-TITRES ===
// Chargement des sous-titres
async function loadSubtitles() {
try {
const response = await fetch(`${API_BASE}/api/subtitles`);
const subtitles = await response.json();
renderSubtitles(subtitles);
} catch (error) {
console.error('Erreur lors du chargement des sous-titres:', error);
}
}
// Rendu des sous-titres
function renderSubtitles(subtitles) {
const historyContainer = document.getElementById('subtitles-history');
const lastSubtitleElement = document.getElementById('last-subtitle');
if (Object.keys(subtitles).length === 0) {
lastSubtitleElement.textContent = 'Aucun texte détecté pour le moment';
historyContainer.innerHTML = '<p class="text-muted text-center">Aucun sous-titre disponible</p>';
return;
}
const sortedKeys = Object.keys(subtitles).sort();
const lastKey = sortedKeys[sortedKeys.length - 1];
// Mettre à jour le dernier sous-titre
lastSubtitleElement.textContent = subtitles[lastKey];
// Mettre à jour l'historique
historyContainer.innerHTML = sortedKeys.reverse().slice(0, 10).map(key => `
<div class="subtitle-item fade-in">
<div class="subtitle-content">${subtitles[key]}</div>
<div class="subtitle-time text-muted small">${key}</div>
</div>
`).join('');
}
// Génération d'une réponse à partir du sous-titre
async function generateFromSubtitle() {
const lastSubtitleElement = document.getElementById('last-subtitle');
const text = lastSubtitleElement.textContent.trim();
if (text === 'Aucun texte détecté pour le moment') {
showToast('Aucun sous-titre disponible pour la génération', 'warning');
return;
}
try {
const response = await fetch(`${API_BASE}/api/generate-response`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: text
})
});
const result = await response.json();
if (result.success) {
showToast('Génération de réponse lancée', 'success');
} else {
showToast(result.error || 'Erreur lors de la génération', 'error');
}
} catch (error) {
console.error('Erreur:', error);
showToast('Erreur lors de la génération de réponse', 'error');
}
}
// === GESTION DES GÉNÉRATIONS ===
// Chargement des générations
async function loadGenerations() {
try {
const response = await fetch(`${API_BASE}/api/generations`);
const generations = await response.json();
if (Object.keys(generations).length > 0) {
const sortedKeys = Object.keys(generations).sort();
const lastGeneration = generations[sortedKeys[sortedKeys.length - 1]];
updateNextMessage(lastGeneration);
}
} catch (error) {
console.error('Erreur lors du chargement des générations:', error);
}
}
// Mise à jour du prochain message
function updateNextMessage(message) {
const nextMessageElement = document.getElementById('next-message');
nextMessageElement.textContent = message || 'Aucun message en attente';
}
// === GESTION DU STATUT ===
// Chargement du statut
async function loadStatus() {
try {
const response = await fetch(`${API_BASE}/api/status`);
currentStatus = await response.json();
updateDashboard(currentStatus);
} catch (error) {
console.error('Erreur lors du chargement du statut:', error);
}
}
// Mise à jour du tableau de bord
function updateDashboard(status) {
document.getElementById('flux-count').textContent = status.flux_count || 0;
document.getElementById('recording-count').textContent = status.active_recordings || 0;
document.getElementById('chat-count').textContent = status.chat_connections || 0;
if (status.last_subtitle) {
document.getElementById('last-subtitle').textContent = status.last_subtitle;
}
if (status.next_message) {
updateNextMessage(status.next_message);
}
}
// === ACTIONS RAPIDES ===
// Génération d'une réponse
async function generateResponse() {
const lastSubtitleElement = document.getElementById('last-subtitle');
const text = lastSubtitleElement.textContent.trim();
if (text === 'Aucun texte détecté pour le moment') {
showToast('Aucun sous-titre disponible pour la génération', 'warning');
return;
}
await generateFromSubtitle();
}
// Actualisation des données
async function refreshData() {
showToast('Actualisation en cours...', 'info');
await loadInitialData();
showToast('Données actualisées', 'success');
}
// === UTILITAIRES ===
// Affichage des notifications toast
function showToast(message, type = 'info') {
// Créer le conteneur de toast s'il n'existe pas
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
const toastId = 'toast-' + Date.now();
const toastColors = {
success: 'text-bg-success',
error: 'text-bg-danger',
warning: 'text-bg-warning',
info: 'text-bg-info'
};
const toastElement = document.createElement('div');
toastElement.id = toastId;
toastElement.className = `toast ${toastColors[type] || 'text-bg-info'}`;
toastElement.setAttribute('role', 'alert');
toastElement.innerHTML = `
<div class="toast-header">
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : type === 'warning' ? 'exclamation-triangle' : 'info-circle'} me-2"></i>
<strong class="me-auto">TwitchBot</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
container.appendChild(toastElement);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: type === 'error' ? 5000 : 3000
});
toast.show();
// Supprimer l'élément après fermeture
toastElement.addEventListener('hidden.bs.toast', () => {
container.removeChild(toastElement);
});
}
// Gestion des erreurs globales
window.addEventListener('error', function(e) {
console.error('Erreur JavaScript:', e.error);
showToast('Une erreur inattendue s\'est produite', 'error');
});
// Gestion des erreurs de promesses non capturées
window.addEventListener('unhandledrejection', function(e) {
console.error('Promesse rejetée:', e.reason);
showToast('Erreur de communication avec le serveur', 'error');
});