sentinelai-takedown-engine-2gqv9 / anti-parasite.html
hts-ai's picture
🐳 25/07 - 17:33 - j'ai trouvé les probleme. active mise en quarentaine le mot clee parasite traceur traqueuer je te donne un exemple. un mot clée avec larent mathiot. regard el'url de google n'
faf9b7f verified
Raw
History Blame Contribute Delete
28.4 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SentinelAI · Anti-Parasite Engine</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
dark: { 950: '#0a0b0f', 900: '#0f1117', 800: '#161822', 700: '#1e2030', 600: '#272a3a', 500: '#3a3e52', 400: '#5c6078', 300: '#8b8fa6', 200: '#b4b7c8', 100: '#dfe0e8' },
neon: { purple: '#a78bfa', green: '#34d399', blue: '#60a5fa', cyan: '#22d3ee', amber: '#fbbf24', red: '#f87171', pink: '#f472b6', orange: '#fb923c' },
glass: { white: 'rgba(255,255,255,0.04)', border: 'rgba(255,255,255,0.08)', hover: 'rgba(255,255,255,0.07)' },
},
fontFamily: {
mono: ['"SF Mono"', '"Fira Code"', 'monospace'],
display: ['"Inter"', 'system-ui', 'sans-serif'],
}
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
body { font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
@keyframes fadeInUp { from { opacity:0; transform:translateY(16px) } to { opacity:1; transform:translateY(0) } }
@keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:0.3} }
.anim-up { animation: fadeInUp 0.5s ease forwards; opacity: 0; }
.anim-up-1 { animation-delay: 0.05s; }
.anim-up-2 { animation-delay: 0.1s; }
.anim-up-3 { animation-delay: 0.15s; }
.anim-up-4 { animation-delay: 0.2s; }
.glow-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07); backdrop-filter: blur(12px); }
.glow-card:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.12); }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
.text-gradient {
background: linear-gradient(135deg, #f87171, #fbbf24, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 40px 40px;
}
</style>
<!-- Config & Monitoring -->
<script src="config.js"></script>
<script src="https://cdn.signalfx.com/o11y-gdi-rum/latest/splunk-otel-web.js" crossorigin="anonymous"></script>
<script>
(function() {
const cfg = window.SENTINEL_CONFIG?.splunkRUM;
if (cfg && cfg.token) {
SplunkRum.init({
beaconUrl: cfg.beaconUrl,
rumAuth: cfg.token,
app: cfg.app,
environment: cfg.environment
});
}
})();
</script>
</head>
<body class="bg-black text-dark-100 min-h-screen grid-bg overflow-x-hidden">
<!-- Ambient -->
<div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
<div class="absolute top-0 left-1/4 w-[600px] h-[600px] rounded-full bg-neon-red/5 blur-[150px]"></div>
<div class="absolute bottom-1/4 right-0 w-[500px] h-[500px] rounded-full bg-neon-purple/5 blur-[120px]"></div>
</div>
<div class="relative z-10 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<!-- Top Bar -->
<div class="flex items-center justify-between py-4 border-b border-glass-border anim-up">
<div class="flex items-center gap-3">
<a href="dashboard.html" class="text-dark-400 hover:text-white transition" title="Dashboard"><i data-lucide="layout-dashboard" class="w-5 h-5"></i></a>
<a href="index.html" class="text-dark-400 hover:text-white transition" title="Fast Takedown"><i data-lucide="shield-alert" class="w-5 h-5"></i></a>
<a href="agent.html" class="text-dark-400 hover:text-white transition" title="Agent Hybride"><i data-lucide="brain" class="w-5 h-5"></i></a>
<a href="content-scan.html" class="text-dark-400 hover:text-white transition" title="Content Scan"><i data-lucide="scan-search" class="w-5 h-5"></i></a>
<a href="stop-net.html" class="text-dark-400 hover:text-white transition" title="Stop Net"><i data-lucide="shield-off" class="w-5 h-5"></i></a>
<a href="web-cleaner.html" class="text-dark-400 hover:text-white transition" title="WEB CLEANER OS"><i data-lucide="scan-eye" class="w-5 h-5"></i></a>
<a href="mailing-seo-geo.html" class="text-dark-400 hover:text-white transition" title="SEO GEO"><i data-lucide="map-pinned" class="w-5 h-5"></i></a>
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-neon-red to-neon-orange flex items-center justify-center shadow-lg shadow-neon-red/20">
<i data-lucide="bug-off" class="w-5 h-5 text-white"></i>
</div>
<div>
<div class="text-sm font-bold tracking-tight text-white">SentinelAI · Anti-Parasite</div>
<div class="text-[10px] text-dark-400 tracking-[0.15em] uppercase">BERT · NLP · Repousse contenus indésirables</div>
</div>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg glow-card">
<span id="apiStatusDot" class="w-2 h-2 rounded-full bg-dark-500"></span>
<span id="apiStatusText" class="text-[11px] text-dark-300">API hors ligne</span>
</div>
</div>
</div>
<!-- Hero -->
<div class="pt-8 pb-6 anim-up anim-up-1">
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight text-white mb-2">
Anti-Parasite <span class="text-gradient">Engine</span>
</h1>
<p class="text-sm text-dark-400 max-w-2xl">
Algorithme de classification BERT multilingue qui détecte les contenus parasites (spam, keyword stuffing, arnaques, copie) et les oppose à du contenu de haute qualité. Intégration avec dilution sémantique pour repousser les résultats indésirables.
</p>
</div>
<!-- Main Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<!-- Classification Panel -->
<div class="glow-card rounded-xl p-5 anim-up anim-up-2">
<div class="flex items-center gap-2 mb-4">
<div class="w-8 h-8 rounded-lg bg-neon-red/10 flex items-center justify-center">
<i data-lucide="brain" class="w-4 h-4 text-neon-red"></i>
</div>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Classification BERT</span>
</div>
<label class="block text-[11px] text-dark-400 mb-1.5">URL ou contenu textuel à analyser</label>
<textarea id="textInput" rows="5"
class="w-full px-3 py-2.5 rounded-lg bg-dark-700 border border-glass-border text-white text-sm placeholder:text-dark-500 outline-none focus:border-neon-red transition resize-vertical mb-2"
placeholder="Collez ici une URL, un extrait de page, un commentaire, une description de produit…"></textarea>
<!-- Real-time signal preview -->
<div id="signalPreview" class="hidden mb-3 p-2.5 rounded-lg bg-dark-900/50 border border-glass-border">
<div class="flex items-center gap-1.5 mb-1.5">
<i data-lucide="radar" class="w-3.5 h-3.5 text-neon-amber"></i>
<span class="text-[10px] font-semibold text-neon-amber uppercase tracking-wider">Signaux détectés en temps réel</span>
<span id="signalCount" class="text-[10px] px-1.5 py-0.5 rounded bg-neon-amber/15 text-neon-amber border border-neon-amber/30 font-semibold">0</span>
</div>
<div id="signalList" class="flex flex-wrap gap-1.5"></div>
</div>
<div class="flex gap-2 mb-4">
<button onclick="classifyText()" id="classifyBtn"
class="flex-1 py-2.5 rounded-lg bg-gradient-to-r from-neon-red to-neon-orange text-dark-950 font-semibold text-sm hover:opacity-90 active:scale-[0.98] transition flex items-center justify-center gap-2">
<i data-lucide="scan" class="w-4 h-4"></i> Classifier
</button>
<button onclick="trainModel()" id="trainBtn"
class="px-4 py-2.5 rounded-lg border border-glass-border text-dark-300 text-sm hover:bg-white/5 transition flex items-center gap-2">
<i data-lucide="graduation-cap" class="w-4 h-4"></i>
<span class="hidden sm:inline">Entraîner</span>
</button>
</div>
<div id="resultPanel" class="hidden p-4 rounded-lg border border-glass-border bg-white/[0.02]">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] text-dark-400 uppercase tracking-wider">Résultat</span>
<span id="resultBadge" class="text-[10px] px-2 py-0.5 rounded-full font-semibold"></span>
</div>
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<div class="text-[10px] text-dark-500 mb-0.5">Confiance</div>
<div id="confidenceValue" class="text-xl font-bold text-white"></div>
</div>
<div>
<div class="text-[10px] text-dark-500 mb-0.5">Score parasite</div>
<div id="parasiteScore" class="text-xl font-bold text-white"></div>
</div>
</div>
<div class="h-2 rounded-full bg-dark-700 overflow-hidden mb-3">
<div id="scoreBar" class="h-full rounded-full bg-neon-red transition-all duration-500" style="width:0%"></div>
</div>
<div class="text-[11px] text-dark-400 font-mono" id="resultHash"></div>
<!-- Detected signals detail -->
<div id="detectedSignalsPanel" class="hidden mt-3 pt-3 border-t border-glass-border">
<div class="flex items-center gap-1.5 mb-2">
<i data-lucide="radar" class="w-3.5 h-3.5 text-neon-red"></i>
<span class="text-[10px] font-semibold text-neon-red uppercase tracking-wider">Signaux parasite déclenchés</span>
</div>
<div id="detectedSignalsList" class="flex flex-wrap gap-1.5"></div>
</div>
</div>
</div>
<!-- Dilution Panel -->
<div class="glow-card rounded-xl p-5 anim-up anim-up-3">
<div class="flex items-center gap-2 mb-4">
<div class="w-8 h-8 rounded-lg bg-neon-purple/10 flex items-center justify-center">
<i data-lucide="shield-plus" class="w-4 h-4 text-neon-purple"></i>
</div>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Dilution sémantique</span>
</div>
<label class="block text-[11px] text-dark-400 mb-1.5">Mot-clé / sujet négatif à neutraliser</label>
<input id="diluteKeyword" type="text"
class="w-full px-3 py-2.5 rounded-lg bg-dark-700 border border-glass-border text-white text-sm placeholder:text-dark-500 outline-none focus:border-neon-purple transition mb-3"
placeholder="Ex: arnaque site-exemple" />
<label class="block text-[11px] text-dark-400 mb-1.5">Contexte (optionnel)</label>
<input id="diluteContext" type="text"
class="w-full px-3 py-2.5 rounded-lg bg-dark-700 border border-glass-border text-white text-sm placeholder:text-dark-500 outline-none focus:border-neon-purple transition mb-3"
placeholder="Mise au point factuelle…" />
<button onclick="dilute()" id="diluteBtn"
class="w-full py-2.5 rounded-lg bg-gradient-to-r from-neon-purple to-neon-blue text-white font-semibold text-sm hover:opacity-90 active:scale-[0.98] transition flex items-center justify-center gap-2 mb-4">
<i data-lucide="sparkles" class="w-4 h-4"></i> Générer contenu positif
</button>
<div id="diluteResults" class="hidden space-y-2 max-h-[260px] overflow-y-auto pr-1"></div>
</div>
</div>
<!-- Batch Table -->
<div class="glow-card rounded-xl p-5 anim-up anim-up-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<i data-lucide="list" class="w-4 h-4 text-neon-amber"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Historique d'analyse</span>
</div>
<button onclick="clearHistory()" class="text-[11px] text-dark-400 hover:text-white transition">Effacer</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead>
<tr class="text-dark-500 border-b border-glass-border">
<th class="pb-2 font-medium">Horodatage</th>
<th class="pb-2 font-medium">Texte</th>
<th class="pb-2 font-medium">Label</th>
<th class="pb-2 font-medium">Confiance</th>
<th class="pb-2 font-medium">Score parasite</th>
</tr>
</thead>
<tbody id="historyTable" class="text-dark-300 divide-y divide-glass-border">
<tr>
<td colspan="5" class="py-4 text-center text-dark-500 italic">Aucune analyse — entrez un contenu ci-dessus</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast" class="fixed bottom-6 right-6 z-50 hidden">
<div class="bg-dark-700 text-white text-xs font-medium px-4 py-2.5 rounded-lg shadow-lg border border-glass-border flex items-center gap-2">
<i data-lucide="check-circle" class="w-4 h-4 text-neon-green"></i>
<span id="toastMsg"></span>
</div>
</div>
<script>
const API_BASE = "http://localhost:5001";
const DILUTE_BASE = "http://localhost:5002";
let history = [];
// ▸ Real-time signal detection as user types
let _signalDebounce = null;
function updateSignalPreview() {
const text = document.getElementById("textInput").value.trim();
const preview = document.getElementById("signalPreview");
const list = document.getElementById("signalList");
const countEl = document.getElementById("signalCount");
if (!text) {
preview.classList.add("hidden");
return;
}
const lowered = text.toLowerCase();
const detected = [];
PARASITE_SIGNALS.forEach(sig => {
if (lowered.includes(sig)) {
detected.push({ signal: sig, boost: PARASITE_BOOST.includes(sig) });
}
});
if (detected.length === 0) {
preview.classList.add("hidden");
return;
}
preview.classList.remove("hidden");
countEl.textContent = detected.length;
list.innerHTML = detected.map(s => {
const color = s.boost ? 'text-neon-red bg-neon-red/15 border-neon-red/30' : 'text-neon-amber bg-neon-amber/15 border-neon-amber/30';
const weight = s.boost ? ' ×2' : '';
return `<span class="text-[10px] px-2 py-1 rounded border ${color} font-medium">${s.signal}${weight}</span>`;
}).join("");
lucide.createIcons();
}
document.addEventListener("DOMContentLoaded", () => {
checkHealth();
setInterval(checkHealth, 10000);
lucide.createIcons();
// ▸ Activate real-time signal detection on input
document.getElementById("textInput").addEventListener("input", () => {
clearTimeout(_signalDebounce);
_signalDebounce = setTimeout(updateSignalPreview, 150);
});
});
async function checkHealth() {
try {
const resp = await fetch(`${API_BASE}/health`, { method: "GET" });
const online = resp.ok;
const dot = document.getElementById("apiStatusDot");
const txt = document.getElementById("apiStatusText");
if (online) {
dot.className = "w-2 h-2 rounded-full bg-neon-green animate-pulse";
txt.textContent = "API en ligne";
txt.className = "text-[11px] text-neon-green";
} else {
throw new Error("offline");
}
} catch (e) {
document.getElementById("apiStatusDot").className = "w-2 h-2 rounded-full bg-dark-500";
document.getElementById("apiStatusText").textContent = "API hors ligne — mode secours actif";
document.getElementById("apiStatusText").className = "text-[11px] text-neon-amber";
}
}
// ═══ CLIENT-SIDE FALLBACK (quand API Flask hors ligne) ═════════════════════
const PARASITE_SIGNALS = [
"viagra","cialis","casino","lottery","loterie","click here",
"cliquez ici","urgent","gagner","free money","argent gratuit",
"crypto","bitcoin","mlm","arnaqu","escro","arnaqué","fraude",
"duplicate content","keyword stuffing","free download","porn",
"xxx","hidden links","buy now","limited offer","offre limitée",
"100% guaranteed","guaranteed","make money online","no virus",
"pirated","𝙩𝙚𝙧𝙢𝙞𝙣𝙚","????","!!!","🔥","💰","💸",
];
const PARASITE_BOOST = ["arnaqu","escro","fraude","pirated","viagra","casino","crypto","porn","xxx"];
function simpleHash(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(16, '0').substring(0, 16);
}
function fallbackClassify(text) {
const lowered = text.toLowerCase();
let score = 0;
for (const sig of PARASITE_SIGNALS) {
if (lowered.includes(sig)) {
score += 1;
if (PARASITE_BOOST.includes(sig)) score += 1;
}
}
const maxScore = Math.max(PARASITE_SIGNALS.length * 0.15, 5);
const parasiteScore = Math.min(score / maxScore, 1.0);
const label = parasiteScore < 0.5 ? "high_quality" : "parasite";
return {
text: text,
label: label,
label_id: label === "parasite" ? 0 : 1,
confidence: Math.round((0.6 + Math.abs(0.5 - parasiteScore) * 0.7) * 10000) / 10000,
parasite_score: Math.round(parasiteScore * 10000) / 10000,
quality_score: Math.round((1 - parasiteScore) * 10000) / 10000,
fallback: true,
timestamp: new Date().toISOString(),
hash: simpleHash(text),
};
}
function fallbackDilute(keyword, context) {
const templates = [
`Guide complet et sourcé sur ${keyword} : définitions, cadre légal et bonnes pratiques.`,
`${keyword} : analyse factuelle, ressources officielles et contacts utiles.`,
`Comprendre ${keyword} en 5 minutes — version vérifiée par des experts.`,
`FAQ ${keyword} : réponses aux questions les plus fréquentes.`,
`Dossier ${keyword} : chiffres clés, évolution réglementaire et impacts.`,
`Mise au point : les faits concernant ${keyword} et le contexte réel.`,
`Témoignages vérifiés et retours d'expérience sur ${keyword}.`,
`Professionnels de ${keyword} : annuaire certifié et avis contrôlés.`,
`Le cadre juridique de ${keyword} : texte de loi, jurisprudence et droits du citoyen.`,
`${keyword} et le droit : ce que dit vraiment la loi (Art. 29 Loi 1881, RGPD, DSA).`,
];
if (context) {
templates.push(`Mise au point factuelle sur ${keyword} : ${context.substring(0, 280)}`);
}
return {
keyword: keyword,
context: context || "",
positive_contents: templates,
strategy: "semantic_dilution (mode secours client-side)",
timestamp: new Date().toISOString(),
};
}
async function classifyText() {
const input = document.getElementById("textInput");
const text = input.value.trim();
if (!text) { showToast("Entrez un texte à classifier"); return; }
const btn = document.getElementById("classifyBtn");
btn.disabled = true;
btn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-dark-950 border-t-transparent rounded-full animate-spin"></span> Analyse…';
try {
const resp = await fetch(`${API_BASE}/classify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text })
});
if (!resp.ok) {
let errMsg = `HTTP ${resp.status}`;
try {
const errData = await resp.json();
if (errData && errData.error) errMsg = errData.error;
} catch (_) {}
throw new Error(errMsg);
}
const data = await resp.json();
if (!data || !data.label || typeof data.confidence !== "number" || typeof data.parasite_score !== "number") {
throw new Error("Réponse inattendue du backend (champs manquants)");
}
renderResult(data);
addToHistory(data);
showToast(`Classification : ${data.label} (${Math.round(data.confidence * 100)}%)${data.fallback === true ? ' (mode secours)' : ''}`);
} catch (e) {
// ▸ Fallback client-side quand l'API Flask est hors ligne
const data = fallbackClassify(text);
renderResult(data);
addToHistory(data);
showToast(`Classification (mode secours) : ${data.label} (${Math.round(data.confidence * 100)}%)`);
}
btn.disabled = false;
btn.innerHTML = '<i data-lucide="scan" class="w-4 h-4"></i> Classifier';
lucide.createIcons();
}
function renderResult(data) {
const panel = document.getElementById("resultPanel");
panel.classList.remove("hidden");
const badge = document.getElementById("resultBadge");
const isParasite = data.label === "parasite";
const fallbackNote = data.fallback === true ? " (mode secours)" : "";
badge.textContent = isParasite ? "PARASITE" + fallbackNote : "HAUTE QUALITÉ" + fallbackNote;
badge.className = `text-[10px] px-2 py-0.5 rounded-full font-semibold ${isParasite ? 'bg-neon-red/20 text-neon-red border border-neon-red/30' : 'bg-neon-green/20 text-neon-green border border-neon-green/30'}`;
const confidence = typeof data.confidence === "number" ? data.confidence : 0;
const parasiteScore = typeof data.parasite_score === "number" ? data.parasite_score : 0;
document.getElementById("confidenceValue").textContent = Math.round(confidence * 100) + "%";
document.getElementById("confidenceValue").className = `text-xl font-bold ${isParasite ? 'text-neon-red' : 'text-neon-green'}`;
document.getElementById("parasiteScore").textContent = (parasiteScore * 100).toFixed(1) + "%";
document.getElementById("resultHash").textContent = "hash: " + (data.hash || "N/A");
const bar = document.getElementById("scoreBar");
bar.style.width = (parasiteScore * 100) + "%";
bar.className = `h-full rounded-full transition-all duration-500 ${isParasite ? 'bg-neon-red' : 'bg-neon-green'}`;
// ▸ Render detected signals in result panel
const signalsPanel = document.getElementById("detectedSignalsPanel");
const signalsList = document.getElementById("detectedSignalsList");
const text = data.text || "";
const lowered = text.toLowerCase();
const detected = [];
PARASITE_SIGNALS.forEach(sig => {
if (lowered.includes(sig)) {
const isBoost = PARASITE_BOOST.includes(sig);
detected.push({ signal: sig, boost: isBoost });
}
});
if (detected.length > 0) {
signalsPanel.classList.remove("hidden");
signalsList.innerHTML = detected.map(s => {
const color = s.boost ? 'text-neon-red bg-neon-red/15 border-neon-red/30' : 'text-neon-amber bg-neon-amber/15 border-neon-amber/30';
const weight = s.boost ? ' ×2' : '';
return `<span class="text-[10px] px-2 py-1 rounded border ${color} font-medium">${s.signal}${weight}</span>`;
}).join("");
} else {
signalsPanel.classList.add("hidden");
}
lucide.createIcons();
}
function addToHistory(data) {
history.unshift(data);
renderHistory();
}
function renderHistory() {
const tbody = document.getElementById("historyTable");
if (history.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-dark-500 italic">Aucune analyse</td></tr>';
return;
}
tbody.innerHTML = history.map(h => {
const isParasite = (h.label || "") === "parasite";
const labelClass = isParasite ? "text-neon-red font-semibold" : "text-neon-green font-semibold";
const safeText = h.text || "";
const shortText = safeText.length > 60 ? safeText.substring(0, 60) + "…" : safeText;
const safeTimestamp = h.timestamp || new Date().toISOString();
const safeConfidence = typeof h.confidence === "number" ? h.confidence : 0;
const safeParasiteScore = typeof h.parasite_score === "number" ? h.parasite_score : 0;
const fallbackTag = h.fallback === true ? ' <span class="text-[9px] text-neon-amber">(secours)</span>' : '';
return `<tr class="hover:bg-white/5 transition">
<td class="py-2 font-mono text-[10px]">${new Date(safeTimestamp).toLocaleString('fr-FR')}</td>
<td class="py-2 max-w-[200px] truncate" title="${safeText.replace(/"/g, '&quot;')}">${shortText}</td>
<td class="py-2 ${labelClass}">${h.label || "N/A"}${fallbackTag}</td>
<td class="py-2">${Math.round(safeConfidence * 100)}%</td>
<td class="py-2">${(safeParasiteScore * 100).toFixed(1)}%</td>
</tr>`;
}).join("");
}
function clearHistory() {
history = [];
renderHistory();
showToast("Historique effacé");
}
async function trainModel() {
const btn = document.getElementById("trainBtn");
btn.disabled = true;
btn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-dark-300 border-t-transparent rounded-full animate-spin"></span>…';
try {
const resp = await fetch(`${API_BASE}/train`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({})
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
showToast(`Modèle BERT entraîné sur ${data.samples} exemples ✓`);
} catch (e) {
// ▸ Fallback client-side : simuler l'entraînement avec les données par défaut
const sampleCount = 20; // DEFAULT_TRAIN_DATA equivalent
showToast(`Entraînement client-side simulé — ${sampleCount} exemples (mode secours)`);
}
btn.disabled = false;
btn.innerHTML = '<i data-lucide="graduation-cap" class="w-4 h-4"></i><span class="hidden sm:inline">Entraîner</span>';
lucide.createIcons();
}
async function dilute() {
const keyword = document.getElementById("diluteKeyword").value.trim();
const context = document.getElementById("diluteContext").value.trim();
if (!keyword) { showToast("Entrez un mot-clé à diluer"); return; }
const btn = document.getElementById("diluteBtn");
btn.disabled = true;
btn.innerHTML = '<span class="inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span> Génération…';
try {
const resp = await fetch(`${DILUTE_BASE}/dilute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keyword, context })
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
const contents = data.positive_contents || [];
const container = document.getElementById("diluteResults");
container.classList.remove("hidden");
container.innerHTML = contents.map((c, i) => `
<div class="p-3 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="text-[10px] text-neon-purple mb-1">Contenu ${i + 1}</div>
<div class="text-[12px] text-dark-200 leading-relaxed">${c || ""}</div>
</div>
`).join("");
showToast(`${contents.length} contenus positifs générés`);
} catch (e) {
// ▸ Fallback client-side quand l'API de dilution est hors ligne
const data = fallbackDilute(keyword, context);
const container = document.getElementById("diluteResults");
container.classList.remove("hidden");
container.innerHTML = data.positive_contents.map((c, i) => `
<div class="p-3 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="text-[10px] text-neon-amber mb-1">Contenu ${i + 1} <span class="text-[9px] opacity-60">(secours)</span></div>
<div class="text-[12px] text-dark-200 leading-relaxed">${c}</div>
</div>
`).join("");
showToast(`${data.positive_contents.length} contenus générés (mode secours)`);
}
btn.disabled = false;
btn.innerHTML = '<i data-lucide="sparkles" class="w-4 h-4"></i> Générer contenu positif';
lucide.createIcons();
}
function showToast(msg) {
const t = document.getElementById("toast");
document.getElementById("toastMsg").textContent = msg;
t.classList.remove("hidden");
lucide.createIcons();
clearTimeout(window._t);
window._t = setTimeout(() => t.classList.add("hidden"), 3500);
}
</script>
</body>
</html>