interface web
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
/* Variables CSS pour le thème dark */
|
||||
:root {
|
||||
--primary-bg: #1a1a1a;
|
||||
--secondary-bg: #2d2d2d;
|
||||
--tertiary-bg: #404040;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #cccccc;
|
||||
--accent-color: #007bff;
|
||||
--success-color: #28a745;
|
||||
--warning-color: #ffc107;
|
||||
--danger-color: #dc3545;
|
||||
--info-color: #17a2b8;
|
||||
--border-color: #495057;
|
||||
}
|
||||
|
||||
/* Body et éléments de base */
|
||||
body {
|
||||
background-color: var(--primary-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar-dark {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--secondary-bg) !important;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--tertiary-bg) !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Status Items */
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #007bff, #6610f2);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: linear-gradient(135deg, #17a2b8, #6f42c1);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #ffc107, #fd7e14);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #dc3545, #e83e8c);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
background-color: var(--secondary-bg);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px 8px 0 0;
|
||||
margin-right: 0.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
background-color: var(--tertiary-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-control {
|
||||
background-color: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: var(--secondary-bg);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: linear-gradient(135deg, rgba(23, 162, 184, 0.2), rgba(23, 162, 184, 0.1));
|
||||
color: #17a2b8;
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: linear-gradient(135deg, rgba(255, 193, 7, 0.2), rgba(255, 193, 7, 0.1));
|
||||
color: #ffc107;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, rgba(40, 167, 69, 0.2), rgba(40, 167, 69, 0.1));
|
||||
color: #28a745;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-content {
|
||||
background-color: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Flux Item */
|
||||
.flux-item {
|
||||
background-color: var(--tertiary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.flux-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.flux-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.flux-status.active {
|
||||
background-color: var(--success-color);
|
||||
box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
|
||||
}
|
||||
|
||||
.flux-status.inactive {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Message History */
|
||||
.message-history {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
background-color: var(--tertiary-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-left: 3px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Subtitle History */
|
||||
.subtitle-history {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle-item {
|
||||
background-color: var(--tertiary-bg);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-left: 3px solid var(--info-color);
|
||||
}
|
||||
|
||||
/* Prompt Item */
|
||||
.prompt-item {
|
||||
background-color: var(--tertiary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.prompt-item .btn-danger {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Scrollbar personnalisée */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--secondary-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container-fluid {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255,255,255,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Toast notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: var(--secondary-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
background-color: var(--tertiary-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Stats cards */
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Flux controls */
|
||||
.flux-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flux-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flux-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.flux-details {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid var(--accent-color);
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
// 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');
|
||||
});
|
||||
Reference in New Issue
Block a user