| <!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> |
| |
| <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"> |
|
|
| |
| <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"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6"> |
|
|
| |
| <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> |
| |
| <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> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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 = []; |
| |
| |
| 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(); |
| |
| |
| 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"; |
| } |
| } |
| |
| |
| 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) { |
| |
| 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'}`; |
| |
| |
| 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, '"')}">${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> |