update update
This commit is contained in:
+492
-129
@@ -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('`', '`');
|
||||
}
|
||||
|
||||
// === 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
|
||||
|
||||
Reference in New Issue
Block a user