big update

This commit is contained in:
gpatruno
2026-04-22 22:42:36 +02:00
parent 68cf59ae75
commit 7b2135bfed
25 changed files with 2661 additions and 564 deletions
+443
View File
@@ -87,6 +87,9 @@ async function loadInitialData() {
loadStatus(),
loadSubtitles(),
loadGenerations(),
loadInteractionConfig(),
refreshInteractionLog(),
loadSettings(),
checkAutoSubtitleStatus(), // Ajouter la vérification du statut auto
checkAutoMessageStatus(), // Ajouter la vérification du statut auto messages
checkChatMessageStatus(), // Ajouter la vérification du statut chat messages
@@ -812,6 +815,382 @@ window.addEventListener('unhandledrejection', function(e) {
showToast('Erreur de communication avec le serveur', 'error');
});
// === PARAMÈTRES ===
async function loadSettings() {
try {
const response = await fetch(`${API_BASE}/api/config/settings`);
const result = await response.json();
if (!result.success) return;
const settings = result.settings || {};
const maxCharsEl = document.getElementById('settings-message-max-chars');
if (maxCharsEl) {
maxCharsEl.value = Number.isFinite(settings.twitch_message_max_chars) ? settings.twitch_message_max_chars : 100;
}
} catch (e) {
console.error('loadSettings error:', e);
}
}
async function saveSettings() {
try {
const maxCharsEl = document.getElementById('settings-message-max-chars');
const twitch_message_max_chars = maxCharsEl ? parseInt(maxCharsEl.value || '100', 10) : 100;
const response = await fetch(`${API_BASE}/api/config/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings: { twitch_message_max_chars } })
});
const result = await response.json();
if (result.success) {
showToast('Paramètres sauvegardés', 'success');
} else {
showToast(result.error || 'Erreur sauvegarde paramètres', 'error');
}
} catch (e) {
console.error('saveSettings error:', e);
showToast('Erreur sauvegarde paramètres', 'error');
}
}
// === NAVBAR SOMMAIRE (tabs) ===
function activateMainTab(tabButtonId) {
try {
const el = document.getElementById(tabButtonId);
if (!el) return;
const tab = new bootstrap.Tab(el);
tab.show();
} catch (e) {
console.error('activateMainTab error:', e);
}
}
function syncNavbarSitemapActive(tabButtonId) {
try {
const links = document.querySelectorAll('.navbar-sitemap-link[data-main-tab]');
links.forEach(a => {
const target = a.getAttribute('data-main-tab');
a.classList.toggle('active', target === tabButtonId);
});
} catch (e) {
console.error('syncNavbarSitemapActive error:', e);
}
}
document.addEventListener('DOMContentLoaded', function() {
// Sync initial
const activeMain = document.querySelector('#mainTabs .nav-link.active');
if (activeMain && activeMain.id) syncNavbarSitemapActive(activeMain.id);
// Sync on tab changes
const mainTabButtons = document.querySelectorAll('#mainTabs .nav-link[data-bs-toggle="tab"]');
mainTabButtons.forEach(btn => {
btn.addEventListener('shown.bs.tab', function(ev) {
if (ev && ev.target && ev.target.id) syncNavbarSitemapActive(ev.target.id);
});
});
});
// === INTERACTION CHAT ===
let interactionConfig = null;
let interactionRegisteredAccounts = [];
async function loadInteractionConfig() {
try {
const response = await fetch(`${API_BASE}/api/interaction/config`);
const result = await response.json();
if (!result.success) {
console.error('Erreur interaction config:', result.error);
return;
}
interactionConfig = result.config || {};
interactionRegisteredAccounts = result.registered_accounts || [];
renderInteractionConfig();
} catch (error) {
console.error('Erreur lors du chargement interaction config:', error);
}
}
function renderInteractionConfig() {
if (!interactionConfig) return;
const enabledEl = document.getElementById('interaction-enabled');
const modeEl = document.getElementById('interaction-mode');
const tgptEnabledEl = document.getElementById('interaction-tgpt-enabled');
const tgptPrepromptEl = document.getElementById('interaction-tgpt-preprompt');
const tgptMaxCharsEl = document.getElementById('interaction-tgpt-max-chars');
const cooldownEl = document.getElementById('interaction-cooldown');
const defaultsEl = document.getElementById('interaction-default-responses');
const accountsEl = document.getElementById('interaction-registered-accounts');
if (enabledEl) enabledEl.checked = !!interactionConfig.enabled;
if (modeEl) modeEl.value = interactionConfig.mode || 'predefined';
if (tgptEnabledEl) tgptEnabledEl.checked = !!interactionConfig.tgpt_enabled;
if (tgptPrepromptEl) tgptPrepromptEl.value = interactionConfig.tgpt_preprompt || '';
if (tgptMaxCharsEl) tgptMaxCharsEl.value = Number.isFinite(interactionConfig.tgpt_max_chars) ? interactionConfig.tgpt_max_chars : 100;
if (cooldownEl) cooldownEl.value = Number.isFinite(interactionConfig.cooldown_seconds) ? interactionConfig.cooldown_seconds : 8;
if (defaultsEl) {
const defaults = Array.isArray(interactionConfig.default_responses) ? interactionConfig.default_responses : ['salut'];
defaultsEl.value = defaults.join('\n');
}
if (accountsEl) {
if (!interactionRegisteredAccounts || interactionRegisteredAccounts.length === 0) {
accountsEl.textContent = 'Aucun compte enregistré (config/user.json)';
} else {
accountsEl.innerHTML = interactionRegisteredAccounts
.map(a => `<div>@${escapeHtml(String(a))}</div>`)
.join('');
}
}
renderInteractionRules();
}
function renderInteractionRules() {
const container = document.getElementById('interaction-rules');
if (!container) return;
const rules = Array.isArray(interactionConfig.rules) ? interactionConfig.rules : [];
if (rules.length === 0) {
container.innerHTML = `
<div class="text-muted text-center py-3 border rounded bg-dark">
Aucune règle. La réponse par défaut sera utilisée.
</div>
`;
return;
}
container.innerHTML = rules.map((r, idx) => {
const id = r.id || `rule_${idx}`;
return `
<div class="border rounded p-2 bg-dark">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-3 align-items-center">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="rule-enabled-${id}" ${r.enabled !== false ? 'checked' : ''} onchange="onInteractionRuleChange('${id}')">
<label class="form-check-label small text-muted" for="rule-enabled-${id}">Actif</label>
</div>
<div class="small text-muted">#${idx + 1}</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-success btn-sm" onclick="saveInteractionRule('${id}')">
<i class="fas fa-save me-1"></i>Enregistrer
</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteInteractionRule('${id}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small text-muted">Utilisateur source (optionnel)</label>
<input class="form-control form-control-sm" type="text" id="rule-from-${id}" value="${escapeAttr(r.from_username || '')}" placeholder="cammenbert" oninput="onInteractionRuleChange('${id}')">
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Compte mentionné (optionnel)</label>
<input class="form-control form-control-sm" type="text" id="rule-mention-${id}" value="${escapeAttr(r.mention_account || '')}" placeholder="@exoticnaturees" oninput="onInteractionRuleChange('${id}')">
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Contient (optionnel)</label>
<input class="form-control form-control-sm" type="text" id="rule-contains-${id}" value="${escapeAttr(r.contains_text || '')}" placeholder="etoile etoile" oninput="onInteractionRuleChange('${id}')">
</div>
</div>
<div class="mt-2">
<label class="form-label small text-muted">Réponse (sans @user)</label>
<input class="form-control form-control-sm" type="text" id="rule-response-${id}" value="${escapeAttr(r.response_text || '')}" placeholder="filante" oninput="onInteractionRuleChange('${id}')">
</div>
<div class="mt-2">
<label class="form-label small text-muted">Préprompt TGPT (optionnel, override)</label>
<textarea class="form-control form-control-sm" id="rule-tgpt-preprompt-${id}" rows="2" placeholder="Ex: Réponds en une phrase." oninput="onInteractionRuleChange('${id}')">${escapeHtml(r.tgpt_preprompt || '')}</textarea>
</div>
</div>
`;
}).join('');
}
function onInteractionRuleChange(ruleId) {
const rules = Array.isArray(interactionConfig.rules) ? interactionConfig.rules : [];
const rule = rules.find(x => String(x.id) === String(ruleId));
if (!rule) return;
const enabledEl = document.getElementById(`rule-enabled-${ruleId}`);
const fromEl = document.getElementById(`rule-from-${ruleId}`);
const mentionEl = document.getElementById(`rule-mention-${ruleId}`);
const containsEl = document.getElementById(`rule-contains-${ruleId}`);
const responseEl = document.getElementById(`rule-response-${ruleId}`);
const tgptPrepromptEl = document.getElementById(`rule-tgpt-preprompt-${ruleId}`);
rule.enabled = enabledEl ? enabledEl.checked : true;
rule.from_username = fromEl ? fromEl.value.trim() : '';
rule.mention_account = mentionEl ? mentionEl.value.trim() : '';
rule.contains_text = containsEl ? containsEl.value.trim() : '';
rule.response_text = responseEl ? responseEl.value.trim() : '';
rule.tgpt_preprompt = tgptPrepromptEl ? tgptPrepromptEl.value : '';
}
async function saveInteractionRule(ruleId) {
// S'assurer que les champs du DOM sont bien remontés dans l'objet
onInteractionRuleChange(ruleId);
await saveInteractionConfig();
}
function addInteractionRule() {
if (!interactionConfig) interactionConfig = {};
if (!Array.isArray(interactionConfig.rules)) interactionConfig.rules = [];
const id = `r_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
interactionConfig.rules.unshift({
id,
enabled: true,
from_username: '',
mention_account: '',
contains_text: '',
response_text: '',
tgpt_preprompt: '',
});
renderInteractionRules();
}
function deleteInteractionRule(ruleId) {
if (!interactionConfig || !Array.isArray(interactionConfig.rules)) return;
interactionConfig.rules = interactionConfig.rules.filter(r => String(r.id) !== String(ruleId));
renderInteractionRules();
}
async function saveInteractionConfig() {
try {
const enabledEl = document.getElementById('interaction-enabled');
const modeEl = document.getElementById('interaction-mode');
const tgptEnabledEl = document.getElementById('interaction-tgpt-enabled');
const tgptPrepromptEl = document.getElementById('interaction-tgpt-preprompt');
const tgptMaxCharsEl = document.getElementById('interaction-tgpt-max-chars');
const cooldownEl = document.getElementById('interaction-cooldown');
const defaultsEl = document.getElementById('interaction-default-responses');
interactionConfig = interactionConfig || {};
interactionConfig.enabled = enabledEl ? !!enabledEl.checked : true;
interactionConfig.mode = modeEl ? modeEl.value : 'predefined';
interactionConfig.tgpt_enabled = tgptEnabledEl ? !!tgptEnabledEl.checked : false;
interactionConfig.tgpt_preprompt = tgptPrepromptEl ? tgptPrepromptEl.value : '';
interactionConfig.tgpt_max_chars = tgptMaxCharsEl ? parseInt(tgptMaxCharsEl.value || '100', 10) : 100;
interactionConfig.cooldown_seconds = cooldownEl ? parseInt(cooldownEl.value || '8', 10) : 8;
const defaults = defaultsEl ? defaultsEl.value.split('\n').map(x => x.trim()).filter(Boolean) : ['salut'];
interactionConfig.default_responses = defaults.length ? defaults : ['salut'];
// Nettoyage minimal des règles
if (Array.isArray(interactionConfig.rules)) {
interactionConfig.rules = interactionConfig.rules.map(r => ({
id: String(r.id || ''),
enabled: r.enabled !== false,
from_username: (r.from_username || '').trim() || null,
mention_account: (r.mention_account || '').trim() || null,
contains_text: (r.contains_text || '').trim() || null,
response_text: (r.response_text || '').trim(),
tgpt_preprompt: (r.tgpt_preprompt || '').trim() || null,
})).filter(r => r.id);
} else {
interactionConfig.rules = [];
}
const response = await fetch(`${API_BASE}/api/interaction/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: interactionConfig })
});
const result = await response.json();
if (result.success) {
interactionConfig = result.config;
renderInteractionConfig();
showToast('Interaction chat sauvegardée', 'success');
} else {
showToast(result.error || 'Erreur sauvegarde interaction chat', 'error');
}
} catch (error) {
console.error('Erreur save interaction:', error);
showToast('Erreur sauvegarde interaction chat', 'error');
}
}
async function refreshInteractionConfig() {
await loadInteractionConfig();
}
async function refreshInteractionLog() {
try {
const response = await fetch(`${API_BASE}/api/interaction/log?limit=120`);
const result = await response.json();
const container = document.getElementById('interaction-log');
if (!container) return;
if (!result.success) {
container.innerHTML = `<div class="text-danger small">Erreur: ${escapeHtml(String(result.error || 'unknown'))}</div>`;
return;
}
const logs = Array.isArray(result.logs) ? result.logs : [];
if (logs.length === 0) {
container.innerHTML = `<div class="text-muted text-center">Aucun log</div>`;
return;
}
container.innerHTML = logs.slice().reverse().map(l => {
const t = escapeHtml(String(l.ts || ''));
const typ = escapeHtml(String(l.type || ''));
const from = escapeHtml(String(l.from || ''));
const content = escapeHtml(String(l.content || ''));
const response = escapeHtml(String(l.response || ''));
const ruleId = l.rule_id ? `<span class="badge bg-info ms-2">rule ${escapeHtml(String(l.rule_id))}</span>` : '';
let body = '';
if (typ === 'responded') {
body = `<div class="small"><strong>@${from}</strong> → <span class="text-muted">${content}</span></div>
<div class="small text-success">${response}</div>`;
} else if (typ === 'error') {
body = `<div class="small text-danger">${escapeHtml(String(l.error || 'error'))}</div>`;
} else {
body = `<div class="small text-muted">${escapeHtml(JSON.stringify(l))}</div>`;
}
return `
<div class="subtitle-item fade-in">
<div class="d-flex justify-content-between align-items-center">
<div class="subtitle-time text-muted small">${t}</div>
<div><span class="badge bg-secondary">${typ}</span>${ruleId}</div>
</div>
${body}
</div>
`;
}).join('');
} catch (error) {
console.error('Erreur logs interaction:', error);
}
}
function escapeHtml(text) {
return String(text)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function escapeAttr(text) {
return escapeHtml(text).replaceAll('`', '&#096;');
}
// === GESTION DU CHAT ===
// Variables globales pour le chat
@@ -1160,6 +1539,18 @@ function renderUsers() {
</div>
</div>
<div class="user-controls">
<div class="d-flex align-items-center me-2" title="Activer/désactiver cet utilisateur">
<span class="small text-muted me-1">Actif</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="user-enabled-${index}" ${user.enabled === false ? '' : 'checked'} onchange="toggleUserEnabled(${index})">
</div>
</div>
<div class="d-flex align-items-center me-2" title="Bypass anti-boucle Interaction chat">
<span class="small text-muted me-1">Bypass</span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="user-bypass-${index}" ${user.interaction_bypass_antiloop ? 'checked' : ''} onchange="toggleUserInteractionBypass(${index})">
</div>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="editUser(${index})">
<i class="fas fa-edit"></i>
</button>
@@ -1175,6 +1566,58 @@ function renderUsers() {
updateUserSelectors();
}
async function toggleUserEnabled(userId) {
try {
const checkbox = document.getElementById(`user-enabled-${userId}`);
if (!checkbox) return;
const enabled = checkbox.checked;
const response = await fetch(`${API_BASE}/api/config/users/${userId}/enabled`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
const result = await response.json();
if (result.success) {
currentUsers[userId] = result.user;
showToast(`Utilisateur ${enabled ? 'activé' : 'désactivé'}`, 'success');
} else {
checkbox.checked = !enabled;
showToast(result.error || 'Erreur lors du changement d’état', 'error');
}
} catch (error) {
console.error('Erreur toggleUserEnabled:', error);
showToast('Erreur lors du changement d’état utilisateur', 'error');
}
}
async function toggleUserInteractionBypass(userId) {
try {
const checkbox = document.getElementById(`user-bypass-${userId}`);
if (!checkbox) return;
const interaction_bypass_antiloop = checkbox.checked;
const response = await fetch(`${API_BASE}/api/config/users/${userId}/interaction-bypass`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interaction_bypass_antiloop })
});
const result = await response.json();
if (result.success) {
currentUsers[userId] = result.user;
showToast(`Bypass anti-boucle: ${interaction_bypass_antiloop ? 'ON' : 'OFF'}`, 'success');
} else {
checkbox.checked = !interaction_bypass_antiloop;
showToast(result.error || 'Erreur lors du changement', 'error');
}
} catch (error) {
console.error('Erreur toggleUserInteractionBypass:', error);
showToast('Erreur lors du changement bypass', 'error');
}
}
// Mettre à jour les sélecteurs d'utilisateurs dans l'interface
function updateUserSelectors() {
console.log('Debug - updateUserSelectors called');