update update

This commit is contained in:
gpatruno
2026-04-28 21:06:26 +02:00
parent 7b2135bfed
commit b4254c9e06
28 changed files with 2032 additions and 547 deletions
+492 -129
View File
@@ -43,8 +43,6 @@ function initializeSocketIO() {
// Synchroniser l'état de l'interface web
const iaGeneratorToggle = document.getElementById('iaGeneratorToggle');
const iaGeneratorStatus = document.getElementById('iaGeneratorStatus');
const chatMessageToggle = document.getElementById('chatMessageToggle');
const chatMessageStatus = document.getElementById('chatMessageStatus');
if (iaGeneratorToggle && iaGeneratorStatus) {
iaGeneratorToggle.checked = data.running;
@@ -52,13 +50,6 @@ function initializeSocketIO() {
iaGeneratorStatus.className = data.running ? 'text-success' : 'text-muted';
}
// Synchroniser aussi l'état de l'envoi de messages
if (chatMessageToggle && chatMessageStatus) {
chatMessageToggle.checked = data.running;
chatMessageStatus.textContent = data.running ? 'Activé' : 'Désactivé';
chatMessageStatus.className = data.running ? 'text-success' : 'text-muted';
}
console.log('IA Generator status changed:', data);
});
}
@@ -89,10 +80,11 @@ async function loadInitialData() {
loadGenerations(),
loadInteractionConfig(),
refreshInteractionLog(),
loadSubtitleRules(),
refreshSubtitleRulesLog(),
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
checkSystemStatus() // Ajouter la vérification du statut système
]);
} catch (error) {
@@ -108,6 +100,10 @@ function setupEventListeners() {
addFluxModal.addEventListener('hidden.bs.modal', function() {
document.getElementById('channel-name').value = '';
document.getElementById('record-audio').checked = true;
const sendMsg = document.getElementById('send-messages');
if (sendMsg) sendMsg.checked = true;
const enableIa = document.getElementById('enable-ia');
if (enableIa) enableIa.checked = true;
});
}
@@ -130,6 +126,7 @@ async function loadFluxList() {
const fluxList = await response.json();
bot_controller.flux_list = fluxList; // Mettre à jour la variable globale
renderFluxList(fluxList);
updateSubtitlesFluxSelector();
} catch (error) {
console.error('Erreur lors du chargement des flux:', error);
}
@@ -210,6 +207,8 @@ async function toggleFlux(fluxId) {
async function addFlux() {
const channelName = document.getElementById('channel-name').value.trim();
const recordAudio = document.getElementById('record-audio').checked;
const sendMessages = document.getElementById('send-messages') ? document.getElementById('send-messages').checked : true;
const enableIa = document.getElementById('enable-ia') ? document.getElementById('enable-ia').checked : true;
if (!channelName) {
showToast('Veuillez entrer un nom de canal', 'error');
@@ -224,7 +223,9 @@ async function addFlux() {
},
body: JSON.stringify({
channel_name: channelName,
record_audio: recordAudio
record_audio: recordAudio,
send_messages: sendMessages,
enable_ia: enableIa
})
});
@@ -520,7 +521,8 @@ function addToRecentMessages(message) {
// Chargement des sous-titres
async function loadSubtitles() {
try {
const response = await fetch(`${API_BASE}/api/subtitles`);
const qs = selectedSubtitlesFluxId != null ? `?flux_id=${encodeURIComponent(String(selectedSubtitlesFluxId))}` : '';
const response = await fetch(`${API_BASE}/api/subtitles${qs}`);
const subtitles = await response.json();
renderSubtitles(subtitles);
} catch (error) {
@@ -556,10 +558,18 @@ function renderSubtitles(subtitles) {
// 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') {
let text = '';
try {
const qs = selectedSubtitlesFluxId != null ? `?flux_id=${encodeURIComponent(String(selectedSubtitlesFluxId))}` : '';
const lastRes = await fetch(`${API_BASE}/api/subtitles/last${qs}`);
const lastJson = await lastRes.json();
text = (lastJson && lastJson.success) ? String(lastJson.last_subtitle || '') : '';
} catch (e) {
console.error('Erreur /api/subtitles/last:', e);
text = '';
}
if (!text || text.trim() === '') {
showToast('Aucun sous-titre disponible pour la génération', 'warning');
return;
}
@@ -629,7 +639,8 @@ async function clearSubtitlesHistory() {
}
try {
const response = await fetch('/api/subtitles/clear', {
const qs = selectedSubtitlesFluxId != null ? `?flux_id=${encodeURIComponent(String(selectedSubtitlesFluxId))}` : '';
const response = await fetch(`/api/subtitles/clear${qs}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -713,14 +724,6 @@ function updateDashboard(status) {
// 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();
}
@@ -920,20 +923,25 @@ function renderInteractionConfig() {
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 tgptSettingsWrap = document.getElementById('interaction-tgpt-settings');
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;
// Afficher les réglages TGPT uniquement si mode=tgpt
const mode = (interactionConfig.mode || 'predefined');
if (tgptSettingsWrap) {
tgptSettingsWrap.classList.toggle('d-none', mode !== 'tgpt');
}
if (defaultsEl) {
const defaults = Array.isArray(interactionConfig.default_responses) ? interactionConfig.default_responses : ['salut'];
defaultsEl.value = defaults.join('\n');
@@ -968,6 +976,7 @@ function renderInteractionRules() {
container.innerHTML = rules.map((r, idx) => {
const id = r.id || `rule_${idx}`;
const action = (r.action || 'response');
return `
<div class="border rounded p-2 bg-dark">
<div class="d-flex justify-content-between align-items-center">
@@ -1003,20 +1012,50 @@ function renderInteractionRules() {
</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 class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small text-muted">Action</label>
<select class="form-select form-select-sm" id="rule-action-${id}" onchange="onInteractionRuleActionChange('${id}')">
<option value="response" ${action === 'response' ? 'selected' : ''}>Réponse</option>
<option value="tgpt" ${action === 'tgpt' ? 'selected' : ''}>TGPT</option>
<option value="clip" ${action === 'clip' ? 'selected' : ''}>Créer un clip</option>
</select>
</div>
<div class="col-md-8">
<div id="rule-action-response-${id}" class="${action === 'response' ? '' : 'd-none'}">
<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 id="rule-action-tgpt-${id}" class="${action === 'tgpt' ? '' : 'd-none'}">
<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 id="rule-action-clip-${id}" class="${action === 'clip' ? '' : 'd-none'}">
<div class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" id="rule-clip-delay-${id}" ${r.clip_has_delay ? 'checked' : ''} onchange="onInteractionRuleChange('${id}')">
<label class="form-check-label small text-muted" for="rule-clip-delay-${id}">has_delay</label>
</div>
<div class="form-text text-muted">Crée un clip sur le canal du flux.</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
function onInteractionRuleActionChange(ruleId) {
const actionEl = document.getElementById(`rule-action-${ruleId}`);
const action = actionEl ? actionEl.value : 'response';
const wrapResp = document.getElementById(`rule-action-response-${ruleId}`);
const wrapTgpt = document.getElementById(`rule-action-tgpt-${ruleId}`);
const wrapClip = document.getElementById(`rule-action-clip-${ruleId}`);
if (wrapResp) wrapResp.classList.toggle('d-none', action !== 'response');
if (wrapTgpt) wrapTgpt.classList.toggle('d-none', action !== 'tgpt');
if (wrapClip) wrapClip.classList.toggle('d-none', action !== 'clip');
onInteractionRuleChange(ruleId);
}
function onInteractionRuleChange(ruleId) {
const rules = Array.isArray(interactionConfig.rules) ? interactionConfig.rules : [];
const rule = rules.find(x => String(x.id) === String(ruleId));
@@ -1028,13 +1067,17 @@ function onInteractionRuleChange(ruleId) {
const containsEl = document.getElementById(`rule-contains-${ruleId}`);
const responseEl = document.getElementById(`rule-response-${ruleId}`);
const tgptPrepromptEl = document.getElementById(`rule-tgpt-preprompt-${ruleId}`);
const actionEl = document.getElementById(`rule-action-${ruleId}`);
const clipDelayEl = document.getElementById(`rule-clip-delay-${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.action = actionEl ? actionEl.value : (rule.action || 'response');
rule.response_text = responseEl ? responseEl.value.trim() : '';
rule.tgpt_preprompt = tgptPrepromptEl ? tgptPrepromptEl.value : '';
rule.clip_has_delay = clipDelayEl ? !!clipDelayEl.checked : !!rule.clip_has_delay;
}
async function saveInteractionRule(ruleId) {
@@ -1054,8 +1097,10 @@ function addInteractionRule() {
from_username: '',
mention_account: '',
contains_text: '',
action: 'response',
response_text: '',
tgpt_preprompt: '',
clip_has_delay: false,
});
renderInteractionRules();
@@ -1071,7 +1116,6 @@ 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');
@@ -1080,7 +1124,7 @@ async function saveInteractionConfig() {
interactionConfig = interactionConfig || {};
interactionConfig.enabled = enabledEl ? !!enabledEl.checked : true;
interactionConfig.mode = modeEl ? modeEl.value : 'predefined';
interactionConfig.tgpt_enabled = tgptEnabledEl ? !!tgptEnabledEl.checked : false;
interactionConfig.tgpt_enabled = interactionConfig.mode === 'tgpt';
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;
@@ -1096,8 +1140,10 @@ async function saveInteractionConfig() {
from_username: (r.from_username || '').trim() || null,
mention_account: (r.mention_account || '').trim() || null,
contains_text: (r.contains_text || '').trim() || null,
action: String(r.action || 'response'),
response_text: (r.response_text || '').trim(),
tgpt_preprompt: (r.tgpt_preprompt || '').trim() || null,
clip_has_delay: !!r.clip_has_delay,
})).filter(r => r.id);
} else {
interactionConfig.rules = [];
@@ -1191,6 +1237,257 @@ function escapeAttr(text) {
return escapeHtml(text).replaceAll('`', '&#096;');
}
// === REGLES SOUS-TITRES ===
let subtitleRulesConfig = null;
async function refreshSubtitleRules() {
await loadSubtitleRules();
}
async function loadSubtitleRules() {
try {
const response = await fetch(`${API_BASE}/api/subtitles/rules/config`);
const result = await response.json();
if (!result.success) return;
subtitleRulesConfig = result.config || {};
renderSubtitleRules();
} catch (e) {
console.error('loadSubtitleRules error:', e);
}
}
function renderSubtitleRules() {
const enabledEl = document.getElementById('subtitle-rules-enabled');
const container = document.getElementById('subtitle-rules');
if (!enabledEl || !container) return;
subtitleRulesConfig = subtitleRulesConfig || {};
enabledEl.checked = !!subtitleRulesConfig.enabled;
const rules = Array.isArray(subtitleRulesConfig.rules) ? subtitleRulesConfig.rules : [];
if (rules.length === 0) {
container.innerHTML = `<div class="text-muted text-center py-3 border rounded bg-dark">Aucune règle.</div>`;
return;
}
container.innerHTML = rules.map((r, idx) => {
const id = r.id || `sr_${idx}`;
const action = r.action || 'clip';
const cooldown = Number.isFinite(r.cooldown_seconds) ? r.cooldown_seconds : 10;
const clipSend = !!r.clip_send_message;
const clipTpl = (r.clip_message_template || 'Clip: {url}');
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="sr-enabled-${id}" ${r.enabled !== false ? 'checked' : ''} onchange="onSubtitleRuleChange('${id}')">
<label class="form-check-label small text-muted" for="sr-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="saveSubtitleRules()"><i class="fas fa-save me-1"></i>Sauvegarder</button>
<button class="btn btn-outline-danger btn-sm" onclick="deleteSubtitleRule('${id}')"><i class="fas fa-trash"></i></button>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-5">
<label class="form-label small text-muted">Contient</label>
<input class="form-control form-control-sm" type="text" id="sr-contains-${id}" value="${escapeAttr(r.contains_text || '')}" placeholder="ex: clip" oninput="onSubtitleRuleChange('${id}')">
</div>
<div class="col-md-3">
<label class="form-label small text-muted">Action</label>
<select class="form-select form-select-sm" id="sr-action-${id}" onchange="onSubtitleRuleActionChange('${id}')">
<option value="clip" ${action === 'clip' ? 'selected' : ''}>Créer un clip</option>
<option value="send_message" ${action === 'send_message' ? 'selected' : ''}>Envoyer message</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small text-muted">Cooldown (sec)</label>
<input class="form-control form-control-sm" type="number" min="0" max="999" id="sr-cooldown-${id}" value="${cooldown}" oninput="onSubtitleRuleChange('${id}')">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-6">
<label class="form-label small text-muted">Compte (optionnel)</label>
<input class="form-control form-control-sm" type="text" id="sr-account-${id}" value="${escapeAttr(r.as_account || '')}" placeholder="ex: exoticnaturees" oninput="onSubtitleRuleChange('${id}')">
</div>
<div class="col-md-6">
<div id="sr-action-message-${id}" class="${action === 'send_message' ? '' : 'd-none'}">
<label class="form-label small text-muted">Message</label>
<input class="form-control form-control-sm" type="text" id="sr-message-${id}" value="${escapeAttr(r.message_text || '')}" placeholder="ex: wow" oninput="onSubtitleRuleChange('${id}')">
</div>
<div id="sr-action-clip-${id}" class="${action === 'clip' ? '' : 'd-none'}">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" id="sr-clip-delay-${id}" ${r.clip_has_delay ? 'checked' : ''} onchange="onSubtitleRuleChange('${id}')">
<label class="form-check-label small text-muted" for="sr-clip-delay-${id}">has_delay</label>
</div>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" id="sr-clip-sendmsg-${id}" ${clipSend ? 'checked' : ''} onchange="onSubtitleRuleChange('${id}')">
<label class="form-check-label small text-muted" for="sr-clip-sendmsg-${id}">Envoyer le lien du clip dans le chat</label>
</div>
<div class="mt-2 ${clipSend ? '' : 'd-none'}" id="sr-clip-template-wrap-${id}">
<label class="form-label small text-muted">Format du message</label>
<input class="form-control form-control-sm" type="text" id="sr-clip-template-${id}" value="${escapeAttr(clipTpl)}" placeholder="Clip: {url}" oninput="onSubtitleRuleChange('${id}')">
<div class="form-text text-muted">Variables: <code>{url}</code>, <code>{edit_url}</code>, <code>{id}</code></div>
</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
function onSubtitleRuleActionChange(ruleId) {
const actionEl = document.getElementById(`sr-action-${ruleId}`);
const action = actionEl ? actionEl.value : 'clip';
const msgWrap = document.getElementById(`sr-action-message-${ruleId}`);
const clipWrap = document.getElementById(`sr-action-clip-${ruleId}`);
if (msgWrap) msgWrap.classList.toggle('d-none', action !== 'send_message');
if (clipWrap) clipWrap.classList.toggle('d-none', action !== 'clip');
onSubtitleRuleChange(ruleId);
}
function onSubtitleRuleChange(ruleId) {
const rules = Array.isArray(subtitleRulesConfig?.rules) ? subtitleRulesConfig.rules : [];
const rule = rules.find(x => String(x.id) === String(ruleId));
if (!rule) return;
const enabledEl = document.getElementById(`sr-enabled-${ruleId}`);
const containsEl = document.getElementById(`sr-contains-${ruleId}`);
const actionEl = document.getElementById(`sr-action-${ruleId}`);
const cooldownEl = document.getElementById(`sr-cooldown-${ruleId}`);
const accountEl = document.getElementById(`sr-account-${ruleId}`);
const msgEl = document.getElementById(`sr-message-${ruleId}`);
const delayEl = document.getElementById(`sr-clip-delay-${ruleId}`);
const clipSendEl = document.getElementById(`sr-clip-sendmsg-${ruleId}`);
const clipTplEl = document.getElementById(`sr-clip-template-${ruleId}`);
const clipTplWrap = document.getElementById(`sr-clip-template-wrap-${ruleId}`);
rule.enabled = enabledEl ? enabledEl.checked : true;
rule.contains_text = containsEl ? containsEl.value.trim() : '';
rule.action = actionEl ? actionEl.value : (rule.action || 'clip');
rule.cooldown_seconds = cooldownEl ? parseInt(cooldownEl.value || '10', 10) : 10;
rule.as_account = accountEl ? accountEl.value.trim() : '';
rule.message_text = msgEl ? msgEl.value : '';
rule.clip_has_delay = delayEl ? !!delayEl.checked : !!rule.clip_has_delay;
rule.clip_send_message = clipSendEl ? !!clipSendEl.checked : !!rule.clip_send_message;
rule.clip_message_template = clipTplEl ? clipTplEl.value : (rule.clip_message_template || 'Clip: {url}');
if (clipTplWrap) clipTplWrap.classList.toggle('d-none', !rule.clip_send_message);
}
function addSubtitleRule() {
subtitleRulesConfig = subtitleRulesConfig || {};
if (!Array.isArray(subtitleRulesConfig.rules)) subtitleRulesConfig.rules = [];
// UX: si l'utilisateur ajoute une règle, on active la feature par défaut
subtitleRulesConfig.enabled = true;
const id = `sr_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
subtitleRulesConfig.rules.unshift({
id,
enabled: true,
contains_text: '',
action: 'clip',
as_account: '',
message_text: '',
clip_has_delay: false,
clip_send_message: false,
clip_message_template: 'Clip: {url}',
cooldown_seconds: 10,
});
renderSubtitleRules();
}
function deleteSubtitleRule(ruleId) {
if (!subtitleRulesConfig || !Array.isArray(subtitleRulesConfig.rules)) return;
subtitleRulesConfig.rules = subtitleRulesConfig.rules.filter(r => String(r.id) !== String(ruleId));
renderSubtitleRules();
}
async function saveSubtitleRules() {
try {
const enabledEl = document.getElementById('subtitle-rules-enabled');
subtitleRulesConfig = subtitleRulesConfig || {};
subtitleRulesConfig.enabled = enabledEl ? !!enabledEl.checked : false;
if (!Array.isArray(subtitleRulesConfig.rules)) subtitleRulesConfig.rules = [];
subtitleRulesConfig.rules = subtitleRulesConfig.rules.map(r => ({
id: String(r.id || ''),
enabled: r.enabled !== false,
contains_text: (r.contains_text || '').trim() || null,
action: String(r.action || 'clip'),
as_account: (r.as_account || '').trim() || null,
message_text: (r.message_text || '').trim(),
clip_has_delay: !!r.clip_has_delay,
clip_send_message: !!r.clip_send_message,
clip_message_template: String(r.clip_message_template || 'Clip: {url}'),
cooldown_seconds: parseInt(String(r.cooldown_seconds || '10'), 10) || 10,
})).filter(r => r.id);
const response = await fetch(`${API_BASE}/api/subtitles/rules/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: subtitleRulesConfig })
});
const result = await response.json();
if (result.success) {
subtitleRulesConfig = result.config;
renderSubtitleRules();
showToast('Règles sous-titres sauvegardées', 'success');
} else {
showToast(result.error || 'Erreur sauvegarde règles', 'error');
}
} catch (e) {
console.error('saveSubtitleRules error:', e);
showToast('Erreur sauvegarde règles', 'error');
}
}
async function refreshSubtitleRulesLog() {
try {
const response = await fetch(`${API_BASE}/api/subtitles/rules/log?limit=120`);
const result = await response.json();
const container = document.getElementById('subtitle-rules-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 ruleId = l.rule_id ? `<span class="badge bg-info ms-2">rule ${escapeHtml(String(l.rule_id))}</span>` : '';
const sub = escapeHtml(String(l.subtitle || ''));
let body = `<div class="small text-muted">${sub}</div>`;
if (typ === 'clip_created') {
const url = escapeHtml(String(l.clip_url || ''));
const chatMsg = l.chat_message ? `<div class="small text-success">${escapeHtml(String(l.chat_message))}</div>` : '';
body = `<div class="small">${sub}</div><div class="small text-success">${url}</div>${chatMsg}`;
} else if (typ === 'message_sent') {
body = `<div class="small">${sub}</div><div class="small text-success">${escapeHtml(String(l.message || ''))}</div>`;
} else if (typ === 'error') {
body = `<div class="small text-danger">${escapeHtml(String(l.error || 'error'))}</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 (e) {
console.error('refreshSubtitleRulesLog error:', e);
}
}
// === GESTION DU CHAT ===
// Variables globales pour le chat
@@ -1495,6 +1792,46 @@ document.addEventListener('DOMContentLoaded', function() {
// Variables globales pour les utilisateurs
let currentUsers = [];
// === SOUS-TITRES MULTI-FLUX ===
let selectedSubtitlesFluxId = null;
function updateSubtitlesFluxSelector() {
const sel = document.getElementById('subtitles-flux-select');
if (!sel) return;
const fluxList = (bot_controller && Array.isArray(bot_controller.flux_list)) ? bot_controller.flux_list : [];
const prev = sel.value;
sel.innerHTML = `<option value="-1">Flux…</option>` + fluxList.map(f => {
const name = escapeHtml(String(f.name || f.twitchname || `flux ${f.id}`));
return `<option value="${String(f.id)}">${name}</option>`;
}).join('');
if (selectedSubtitlesFluxId == null) {
const active = fluxList.find(f => f && f.active);
if (active) selectedSubtitlesFluxId = active.id;
}
if (selectedSubtitlesFluxId != null) {
sel.value = String(selectedSubtitlesFluxId);
} else if (prev && prev !== '-1') {
sel.value = prev;
}
}
function onSubtitlesFluxChange() {
const sel = document.getElementById('subtitles-flux-select');
if (!sel) return;
const v = parseInt(sel.value || '-1', 10);
selectedSubtitlesFluxId = (isNaN(v) || v < 0) ? null : v;
refreshSubtitlesAll();
}
async function refreshSubtitlesAll() {
await loadSubtitles();
await refreshSubtitleRulesLog();
}
// Chargement des utilisateurs
async function loadUsers() {
try {
@@ -1625,7 +1962,8 @@ function updateUserSelectors() {
const selectors = [
'custom-message-user',
'chat-user-select'
'chat-user-select',
'clips-user-select'
];
selectors.forEach(selectorId => {
@@ -1662,6 +2000,122 @@ function updateUserSelectors() {
console.log('Debug - Selectors updated');
}
// === CLIPS ===
function clearClipResult() {
const wrap = document.getElementById('clips-result');
const publicEl = document.getElementById('clips-public-url');
const editEl = document.getElementById('clips-edit-url');
const idEl = document.getElementById('clips-id');
if (wrap) wrap.classList.add('d-none');
if (publicEl) {
publicEl.textContent = '—';
publicEl.href = '#';
}
if (editEl) {
editEl.textContent = '—';
editEl.href = '#';
}
if (idEl) idEl.textContent = '—';
}
function prefillClipChannelFromActiveFlux() {
try {
const input = document.getElementById('clips-channel-login');
if (!input) return;
const active = bot_controller && bot_controller.flux_list
? bot_controller.flux_list.find(f => f && f.active)
: null;
if (active && active.twitchname) {
input.value = String(active.twitchname);
showToast('Chaîne pré-remplie depuis le flux actif', 'success');
} else {
showToast('Aucun flux actif trouvé', 'warning');
}
} catch (e) {
console.error('prefillClipChannelFromActiveFlux error:', e);
}
}
async function createClip() {
const userId = parseInt(document.getElementById('clips-user-select')?.value || '-1', 10);
const channelLogin = (document.getElementById('clips-channel-login')?.value || '').trim();
const hasDelay = !!document.getElementById('clips-has-delay')?.checked;
if (!channelLogin) {
showToast('Veuillez entrer un login de chaîne', 'error');
return;
}
if (isNaN(userId) || userId < 0 || userId >= currentUsers.length) {
showToast('Veuillez sélectionner un utilisateur valide', 'error');
return;
}
clearClipResult();
try {
const response = await fetch(`${API_BASE}/api/clips/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: userId,
channel_login: channelLogin,
has_delay: hasDelay,
})
});
const result = await response.json();
if (!result.success) {
showToast(result.error || 'Erreur lors de la création du clip', 'error');
return;
}
const clip = result.clip || {};
const wrap = document.getElementById('clips-result');
const publicEl = document.getElementById('clips-public-url');
const editEl = document.getElementById('clips-edit-url');
const idEl = document.getElementById('clips-id');
if (idEl) idEl.textContent = clip.id || '—';
if (publicEl) {
const url = clip.url || '#';
publicEl.textContent = url;
publicEl.href = url;
}
if (editEl) {
const editUrl = clip.edit_url || '#';
editEl.textContent = editUrl;
editEl.href = editUrl;
}
if (wrap) wrap.classList.remove('d-none');
showToast('Clip créé (ouvrez Edit pour publier)', 'success');
} catch (e) {
console.error('createClip error:', e);
showToast('Erreur de communication avec le serveur', 'error');
}
}
async function checkClipAutomationStatus() {
try {
const el = document.getElementById('clips-automation-status');
if (el) el.textContent = 'Chargement...';
const response = await fetch(`${API_BASE}/api/clips/automation/status`);
const result = await response.json();
if (!result.success) {
if (el) el.textContent = `Erreur: ${result.error || 'unknown'}`;
return;
}
const running = !!result.running;
const mode = result.mode || 'manual';
if (el) el.textContent = running ? `Actif (mode=${mode})` : `Inactif (mode=${mode})`;
} catch (e) {
console.error('checkClipAutomationStatus error:', e);
const el = document.getElementById('clips-automation-status');
if (el) el.textContent = 'Erreur de communication';
}
}
// Ouvrir le modal pour ajouter un utilisateur
function openAddUserModal() {
document.getElementById('userModalTitle').textContent = 'Ajouter un Utilisateur';
@@ -2082,97 +2536,6 @@ async function forceStopAutoSubtitle() {
}
}
// Fonction pour activer/désactiver l'envoi de messages dans le chat
async function toggleChatMessage() {
const toggle = document.getElementById('chatMessageToggle');
const statusElement = document.getElementById('chatMessageStatus');
if (toggle.checked) {
// Activer l'envoi de messages
try {
const response = await fetch('/api/chat/messages/enable', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
statusElement.textContent = 'Activé';
statusElement.className = 'text-success';
showAlert('Envoi de messages dans le chat activé', 'success');
} else {
toggle.checked = false;
showAlert('Erreur: ' + result.error, 'danger');
}
} catch (error) {
console.error('Erreur:', error);
toggle.checked = false;
showAlert('Erreur lors de l\'activation', 'danger');
}
} else {
// Désactiver l'envoi de messages
try {
const response = await fetch('/api/chat/messages/disable', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (result.success) {
statusElement.textContent = 'Désactivé';
statusElement.className = 'text-muted';
showAlert('Envoi de messages dans le chat désactivé', 'info');
} else {
toggle.checked = true;
showAlert('Erreur: ' + result.error, 'danger');
}
} catch (error) {
console.error('Erreur:', error);
toggle.checked = true;
showAlert('Erreur lors de la désactivation', 'danger');
}
}
}
// Fonction pour vérifier le statut de l'envoi de messages chat au chargement
async function checkChatMessageStatus() {
try {
const response = await fetch('/api/chat/messages/status');
const status = await response.json();
const toggle = document.getElementById('chatMessageToggle');
const statusElement = document.getElementById('chatMessageStatus');
if (toggle && statusElement) {
toggle.checked = status.enabled;
if (status.enabled) {
statusElement.textContent = 'Activé';
statusElement.className = 'text-success';
} else {
statusElement.textContent = 'Désactivé';
statusElement.className = 'text-muted';
}
}
} catch (error) {
console.error('Erreur lors de la vérification du statut chat:', error);
// En cas d'erreur, forcer la désactivation par défaut
const toggle = document.getElementById('chatMessageToggle');
const statusElement = document.getElementById('chatMessageStatus');
if (toggle && statusElement) {
toggle.checked = false;
statusElement.textContent = 'Désactivé';
statusElement.className = 'text-muted';
}
}
}
// === GESTION DES COMPOSANTS SYSTÈME ===
// Fonction pour activer/désactiver le générateur IA