sentinelai-takedown-engine-2gqv9 / mailing-seo-geo.html
hts-ai's picture
🐳 27/07 - 17:51 - ajoute seo maillage geo rapide positive  les premiere page de google en peux de temps  maillage 10k
7520e6a verified
Raw
History Blame Contribute Delete
79.8 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SentinelAI · Mailage SEO GEO Anti-Parasite</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} }
@keyframes breathe { 0%,100%{opacity:0.6} 50%{opacity:1} }
@keyframes shimmer { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
.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; }
.anim-up-5 { animation-delay: 0.25s; }
.anim-up-6 { animation-delay: 0.3s; }
.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); }
.pulse-glow { animation: breathe 2s ease-in-out infinite; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
.text-gradient-orange {
background: linear-gradient(135deg, #fbbf24, #fb923c, #f87171);
-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;
}
.shimmer-bg { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent); background-size: 200% 100%; animation: shimmer 3s infinite; }
</style>
<script src="config.js"></script>
<!-- Puter.js v2 — Llama gratuit & illimité -->
<script src="https://js.puter.com/v2/"></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/3 w-[600px] h-[600px] rounded-full bg-neon-orange/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-6xl 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="soe.html" class="text-dark-400 hover:text-white transition" title="SOE Inverse"><i data-lucide="search" class="w-5 h-5"></i></a>
<a href="seo-rapid-geo.html" class="text-dark-400 hover:text-neon-green transition" title="SEO Rapid GEO"><i data-lucide="rocket" class="w-5 h-5"></i></a>
<a href="anti-parasite.html" class="text-dark-400 hover:text-white transition" title="Anti-Parasite"><i data-lucide="bug-off" class="w-5 h-5"></i></a>
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-neon-orange to-neon-red flex items-center justify-center shadow-lg shadow-neon-orange/20">
<i data-lucide="map-pinned" class="w-5 h-5 text-white"></i>
</div>
<div>
<div class="text-sm font-bold tracking-tight text-white">Mailage SEO GEO <span class="text-gradient-orange">Anti-Parasite</span></div>
<div class="text-[10px] text-dark-400 tracking-[0.15em] uppercase">Mot clé · Cognitive · Avancé</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 class="w-2 h-2 rounded-full bg-neon-green animate-pulse"></span>
<span class="text-[11px] text-dark-300">Pipeline actif</span>
</div>
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg glow-card">
<i data-lucide="clock" class="w-3.5 h-3.5 text-dark-400"></i>
<span id="geoClock" class="text-[11px] font-mono text-dark-300"></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">
Mailage <span class="text-gradient-orange">SEO GEO</span> Anti-Parasite
</h1>
<p class="text-sm text-dark-400 max-w-2xl">Génération cognitive avancée de contenu SEO géo-localisé pour neutraliser les résultats parasites. Multi-villes, multi-mots-clés, multi-angles — chaque contenu est unique, optimisé et ciblé géographiquement.</p>
</div>
<!-- ═══ PHASE INDICATOR ═══ -->
<div class="flex items-center gap-1 mb-6 anim-up anim-up-2" id="phaseIndicator">
<span class="phase-dot w-2.5 h-2.5 rounded-full bg-neon-orange pulse-glow" data-phase="1"></span>
<span class="h-px w-8 bg-dark-500" data-phase="1-2"></span>
<span class="phase-dot w-2.5 h-2.5 rounded-full bg-dark-500" data-phase="2"></span>
<span class="h-px w-8 bg-dark-500" data-phase="2-3"></span>
<span class="phase-dot w-2.5 h-2.5 rounded-full bg-dark-500" data-phase="3"></span>
<span class="h-px w-8 bg-dark-500" data-phase="3-4"></span>
<span class="phase-dot w-2.5 h-2.5 rounded-full bg-dark-500" data-phase="4"></span>
<span class="ml-2 text-[10px] text-dark-500" id="phaseLabel">Cible & Géolocalisation</span>
</div>
<!-- ═══ MAIN LAYOUT ═══ -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<!-- RIGHT: CONFIG PANEL -->
<div class="lg:col-span-1 space-y-4 anim-up anim-up-3 lg:order-2">
<!-- Cible -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg bg-neon-orange/10 flex items-center justify-center">
<i data-lucide="target" class="w-4 h-4 text-neon-orange"></i>
</div>
<span class="text-xs font-semibold text-white uppercase tracking-wider">1. Cible</span>
</div>
<label class="block text-[11px] text-dark-400 mb-1.5">Sujet / entité à défendre</label>
<input id="subjectInput" 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-orange transition mb-3"
placeholder="Nom, marque, personne…" />
<label class="block text-[11px] text-dark-400 mb-1.5">Type de contenu parasite</label>
<div class="grid grid-cols-2 gap-1.5 mb-3" id="parasiteTypeGrid"></div>
</div>
<!-- GEO -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg bg-neon-cyan/10 flex items-center justify-center">
<i data-lucide="map-pin" class="w-4 h-4 text-neon-cyan"></i>
</div>
<span class="text-xs font-semibold text-white uppercase tracking-wider">2. Ciblage géographique</span>
</div>
<div class="grid grid-cols-2 gap-2 mb-3">
<div>
<label class="block text-[10px] text-dark-500 mb-1">Ville(s)</label>
<input id="geoCity" type="text"
class="w-full px-2.5 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-cyan transition"
placeholder="Paris, Lyon…" />
</div>
<div>
<label class="block text-[10px] text-dark-500 mb-1">Région(s)</label>
<input id="geoRegion" type="text"
class="w-full px-2.5 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-cyan transition"
placeholder="Île-de-France…" />
</div>
</div>
<div class="mb-3">
<label class="block text-[10px] text-dark-500 mb-1">Département(s) — pour SEO local</label>
<input id="geoDept" type="text"
class="w-full px-2.5 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-cyan transition"
placeholder="75, 92, 93, 94…" />
</div>
<label class="block text-[10px] text-dark-500 mb-1">Langue cible</label>
<select id="geoLang" class="w-full px-2.5 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs outline-none focus:border-neon-cyan transition mb-3">
<option value="fr">🇫🇷 Français</option>
<option value="en">🇬🇧 English</option>
<option value="de">🇩🇪 Deutsch</option>
<option value="es">🇪🇸 Español</option>
<option value="it">🇮🇹 Italiano</option>
<option value="pt">🇧🇷 Português</option>
<option value="ar">🇸🇦 العربية</option>
</select>
<!-- Preset GEO zones -->
<label class="block text-[10px] text-dark-500 mb-1">Zones prédéfinies</label>
<div class="flex flex-wrap gap-1.5" id="geoPresets"></div>
</div>
<!-- KEYWORDS -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center gap-2 mb-3">
<div class="w-7 h-7 rounded-lg bg-neon-amber/10 flex items-center justify-center">
<i data-lucide="hash" class="w-4 h-4 text-neon-amber"></i>
</div>
<span class="text-xs font-semibold text-white uppercase tracking-wider">3. Mots-clés</span>
</div>
<div class="flex gap-2 mb-2">
<input id="kwInput" type="text"
class="flex-1 px-2.5 py-2 rounded-lg bg-dark-700 border border-glass-border text-white text-xs placeholder:text-dark-500 outline-none focus:border-neon-amber transition"
placeholder="Ajouter un mot-clé…" />
<button onclick="addKeyword()" class="px-3 py-2 rounded-lg border border-neon-amber/40 text-neon-amber text-xs font-medium hover:bg-neon-amber/10 transition">+</button>
</div>
<div class="text-[10px] text-dark-500 mb-1.5">Suggestions automatiques</div>
<div class="flex flex-wrap gap-1.5 mb-3" id="kwSuggestions"></div>
<div class="text-[10px] text-dark-500 mb-1.5">Mots-clés sélectionnés</div>
<div id="kwTags" class="flex flex-wrap gap-1.5 min-h-[28px]"></div>
</div>
<!-- GENERATE -->
<button onclick="generateGeoContent()" id="generateBtn" disabled
class="w-full py-3 rounded-lg bg-gradient-to-r from-neon-orange to-neon-red text-dark-950 font-bold text-sm hover:opacity-90 active:scale-[0.98] transition-all disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2">
<i data-lucide="zap" class="w-4 h-4"></i>
Lancer la génération GEO cognitive
</button>
<!-- Puter Llama (Free) -->
<button onclick="generateGeoContentLlama()" id="generateLlamaBtn" disabled
class="w-full py-3 rounded-lg bg-gradient-to-r from-neon-purple to-neon-cyan text-white font-bold text-sm hover:opacity-90 active:scale-[0.98] transition-all disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2 mt-2">
<i data-lucide="sparkles" class="w-4 h-4"></i>
🦙 Générer avec Llama 4 (Gratuit · Illimité)
</button>
<!-- MAILLAGE 10K -->
<div class="mt-3 p-3 rounded-lg border border-glass-border bg-white/[0.02]">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="grid-3x3" class="w-4 h-4 text-neon-amber"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Maillage 10K</span>
</div>
<p class="text-[10px] text-dark-400 mb-2">Génère jusqu'à 10 800 variations SEO GEO (10 villes × 25 mots-clés × 6 angles × formulations multiples) sans appeler l'IA pour chaque item.</p>
<div class="flex gap-2">
<button onclick="generateBulkMesh10k()" id="mesh10kBtn" disabled
class="flex-1 py-2.5 rounded-lg bg-gradient-to-r from-neon-amber to-neon-orange text-dark-950 font-bold text-sm hover:opacity-90 active:scale-[0.98] transition-all disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2">
<i data-lucide="layers" class="w-4 h-4"></i> Générer maillage 10K
</button>
<button onclick="clearMeshResults()" id="clearMeshBtn" class="hidden px-3 py-2.5 rounded-lg border border-glass-border text-dark-300 text-xs hover:bg-white/5 transition">Effacer</button>
</div>
</div>
</div>
<!-- LEFT: RESULTS -->
<div class="lg:col-span-2 space-y-4 anim-up anim-up-2 lg:order-1">
<!-- PROGRESS -->
<div id="progressPanel" class="glow-card rounded-xl p-4 hidden">
<div class="flex items-center gap-2 mb-3">
<span class="inline-block w-4 h-4 border-2 border-neon-orange border-t-transparent rounded-full animate-spin"></span>
<span id="progressTitle" class="text-xs font-semibold text-white uppercase tracking-wider">Génération en cours…</span>
</div>
<div class="h-2 rounded-full bg-dark-700 overflow-hidden">
<div id="progressBar" class="h-full rounded-full bg-gradient-to-r from-neon-orange to-neon-red transition-all duration-700 ease-out" style="width:0%"></div>
</div>
<div id="progressDetail" class="text-[10px] text-dark-400 mt-1.5">Initialisation…</div>
</div>
<!-- GENERATED CONTENT -->
<div id="contentPanel" class="hidden space-y-3">
<!-- ANGLE TAGS -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="layers" class="w-4 h-4 text-neon-orange"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Angles cognitifs générés</span>
</div>
<span id="angleCount" class="text-[10px] text-dark-500"></span>
</div>
<div id="angleGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-2"></div>
</div>
<!-- CONTENT CARDS -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4 text-neon-purple"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Contenu SEO GEO généré</span>
</div>
<div class="flex items-center gap-2">
<select id="contentFilter" onchange="filterContent()" class="text-[10px] bg-dark-700 border border-glass-border text-dark-200 rounded-md px-2 py-1 outline-none">
<option value="all">Tous les angles</option>
</select>
<button onclick="copyAllContent()" class="text-[10px] text-neon-purple/70 hover:text-neon-purple transition flex items-center gap-1">
<i data-lucide="copy" class="w-3 h-3"></i>Tout copier
</button>
</div>
</div>
<div id="contentCards" class="space-y-3 max-h-[600px] overflow-y-auto pr-1"></div>
</div>
<!-- KEYWORD DENSITY -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center gap-2 mb-3">
<i data-lucide="bar-chart-3" class="w-4 h-4 text-neon-green"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Densité & distribution mot-clé</span>
</div>
<div id="densityGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-2"></div>
</div>
<!-- SEO METADATA -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="code-2" class="w-4 h-4 text-neon-cyan"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Méta-données SEO</span>
</div>
<button onclick="copyMetadata()" class="text-[10px] text-neon-cyan/70 hover:text-neon-cyan transition flex items-center gap-1">
<i data-lucide="copy" class="w-3 h-3"></i>Copier
</button>
</div>
<pre id="metadataOutput" class="text-[11px] font-mono text-neon-cyan/80 leading-relaxed whitespace-pre-wrap bg-dark-900/50 rounded-lg p-3 border border-glass-border"></pre>
</div>
<!-- PUBLISHING SCHEDULE -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center gap-2 mb-3">
<i data-lucide="calendar" class="w-4 h-4 text-neon-amber"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Planning de diffusion</span>
</div>
<div id="scheduleGrid" class="space-y-1.5"></div>
<div class="mt-3 p-2.5 rounded-md bg-neon-amber/10 border border-neon-amber/20">
<div class="text-[11px] text-neon-amber font-semibold mb-1">⚠ Rappel de renouvellement</div>
<div class="text-[10px] text-dark-400">Le contenu négatif peut revenir dans les résultats Google. Renouvelez le contenu positif tous les <strong class="text-white">7-14 jours</strong> et soumettez de nouvelles demandes de déréférencement via <a href="soe.html" class="text-neon-amber underline">SOE Inverse</a>.</div>
</div>
</div>
<!-- ANTI-PARASITE ANALYSIS -->
<div class="glow-card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="bug-off" class="w-4 h-4 text-neon-red"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Analyse anti-parasite</span>
</div>
<span id="parasiteScore" class="text-[10px] px-2 py-0.5 rounded bg-dark-700 text-dark-400 font-medium"></span>
</div>
<div id="parasiteGrid" class="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-3"></div>
<div class="flex items-center gap-2">
<a href="anti-parasite.html" class="text-[10px] px-3 py-1.5 rounded border border-neon-red/40 text-neon-red hover:bg-neon-red/10 transition flex items-center gap-1">
<i data-lucide="bug-off" class="w-3 h-3"></i>Classifier avec BERT
</a>
<a href="content-scan.html" class="text-[10px] px-3 py-1.5 rounded border border-neon-purple/40 text-neon-purple hover:bg-neon-purple/10 transition flex items-center gap-1">
<i data-lucide="scan-search" class="w-3 h-3"></i>Scanner le contenu
</a>
</div>
</div>
</div>
<!-- EMPTY STATE -->
<div id="emptyState" class="glow-card rounded-xl p-8 text-center">
<i data-lucide="map-pinned" class="w-12 h-12 mx-auto text-dark-600 mb-3"></i>
<div class="text-sm font-medium text-dark-300 mb-1">Configurez votre mailage SEO GEO</div>
<div class="text-[11px] text-dark-500">Entrez un sujet, sélectionnez le type de contenu parasite, et lancez la génération cognitive avancée.</div>
</div>
<!-- MESH 10K PANEL -->
<div id="mesh10kPanel" class="hidden space-y-3">
<div class="glow-card rounded-xl p-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<i data-lucide="grid-3x3" class="w-4 h-4 text-neon-amber"></i>
<span class="text-xs font-semibold text-white uppercase tracking-wider">Maillage 10K généré</span>
<span id="meshCountBadge" 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 class="flex items-center gap-2">
<button onclick="copyMeshResults()" class="text-[10px] px-2 py-1 rounded border border-glass-border text-dark-300 hover:text-white transition">Copier</button>
<button onclick="exportMeshAsJSON()" class="text-[10px] px-2 py-1 rounded border border-glass-border text-dark-300 hover:text-white transition">JSON</button>
<button onclick="exportMeshAsCSV()" class="text-[10px] px-2 py-1 rounded border border-glass-border text-dark-300 hover:text-white transition">CSV</button>
<button onclick="exportMeshAsTXT()" class="text-[10px] px-2 py-1 rounded border border-glass-border text-dark-300 hover:text-white transition">TXT</button>
</div>
</div>
<div class="p-3 rounded-lg bg-dark-900/50 border border-glass-border max-h-[400px] overflow-y-auto">
<div id="meshPreview" class="text-[11px] font-mono text-dark-200 leading-relaxed space-y-1"></div>
</div>
</div>
</div>
</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>
// ═══ CONSTANTS ═════════════════════════════════════════════════════════════
const PARASITE_TYPES = [
{ id: "defamation", label: "Diffamation", icon: "scale", color: "#a78bfa" },
{ id: "fake_news", label: "Fausse info", icon: "newspaper", color: "#f87171" },
{ id: "hate", label: "Contenu haineux", icon: "ban", color: "#fb923c" },
{ id: "dox", label: "Doxxing / données perso", icon: "lock", color: "#f472b6" },
{ id: "impersonation", label: "Usurpation", icon: "user-x", color: "#22d3ee" },
{ id: "reputation", label: "Atteinte réputation", icon: "trending-down", color: "#fbbf24" },
];
const GEO_PRESETS = [
{ label: "🇫🇷 Île-de-France", city: "Paris", region: "Île-de-France", dept: "75,92,93,94" },
{ label: "🇫🇷 Auvergne-Rhône-Alpes", city: "Lyon", region: "Auvergne-Rhône-Alpes", dept: "69" },
{ label: "🇫🇷 Provence-Alpes-Côte d'Azur", city: "Marseille", region: "PACA", dept: "13" },
{ label: "🇫🇷 Occitanie", city: "Toulouse", region: "Occitanie", dept: "31" },
{ label: "🇫🇷 Nouvelle-Aquitaine", city: "Bordeaux", region: "Nouvelle-Aquitaine", dept: "33" },
{ label: "🇫🇷 Hauts-de-France", city: "Lille", region: "Hauts-de-France", dept: "59" },
{ label: "🇫🇷 Grand Est", city: "Strasbourg", region: "Grand Est", dept: "67" },
{ label: "🇫🇷 Pays de la Loire", city: "Nantes", region: "Pays de la Loire", dept: "44" },
{ label: "🇧🇪 Bruxelles", city: "Bruxelles", region: "Bruxelles-Capitale", dept: "1000" },
{ label: "🇨🇭 Romandie", city: "Genève", region: "Romandie", dept: "GE" },
];
const LANG_ANGLES = {
fr: { label: "Français", angleLabels: ["juridique", "factuel", "communauté", "expert", "historique", "pratique"] },
en: { label: "English", angleLabels: ["legal", "factual", "community", "expert", "historical", "practical"] },
de: { label: "Deutsch", angleLabels: ["rechtlich", "sachlich", "gemeinschaft", "experte", "historisch", "praktisch"] },
es: { label: "Español", angleLabels: ["jurídico", "factual", "comunidad", "experto", "histórico", "práctico"] },
};
const ANGLE_CONFIGS = {
juridique: {
icon: "scale", color: "#a78bfa",
prefixes: {
fr: ["Guide juridique", "Le cadre légal de", "Droit et recours sur", "Ce que dit la loi sur"],
en: ["Legal Guide: Understanding", "The Legal Framework for", "Know Your Rights About"],
de: ["Rechtlicher Leitfaden", "Der rechtliche Rahmen für", "Ihre Rechte bei"],
es: ["Guía jurídica sobre", "El marco legal de", "Sus derechos respecto a"],
},
seoTitleTpls: {
fr: ["{kw} · Guide juridique complet {city}", "Droit à l'image {city} : recours et procédures"],
en: ["{kw} · Complete Legal Guide {city}", "Image rights {city}: remedies and procedures"],
}
},
factuel: {
icon: "bar-chart-2", color: "#60a5fa",
prefixes: {
fr: ["Les faits sur", "Données vérifiées", "Rapport annuel sur", "Statistiques officielles"],
en: ["The Facts About", "Verified Data on", "Annual Report on"],
de: ["Die Fakten über", "Verifizierte Daten zu", "Jahresbericht über"],
es: ["Los hechos sobre", "Datos verificados de", "Informe anual sobre"],
},
seoTitleTpls: {
fr: ["{kw} : chiffres clés {city} {year}", "Comprendre {kw} à {city} en 5 minutes"],
en: ["{kw}: Key Figures {city} {year}", "Understanding {kw} in {city}"],
}
},
communauté: {
icon: "users", color: "#34d399",
prefixes: {
fr: ["Communauté", "Forum", "Retours d'expérience sur", "Témoignages positifs"],
en: ["Community", "Forum", "Positive Reviews About", "User Experiences"],
de: ["Gemeinschaft", "Forum", "Positive Erfahrungen über", "Nutzerbewertungen"],
es: ["Comunidad", "Foro", "Testimonios positivos sobre", "Experiencias de usuarios"],
},
seoTitleTpls: {
fr: ["Avis vérifiés {kw} {city}", "Communauté {kw} · {city} — entraide et solutions"],
en: ["Verified Reviews {kw} {city}", "{kw} Community {city} — support and solutions"],
}
},
expert: {
icon: "graduation-cap", color: "#fbbf24",
prefixes: {
fr: ["L'avis de l'expert sur", "Analyse technique :", "Recommandations d'un professionnel", "Interview experte"],
en: ["Expert Opinion on", "Technical Analysis:", "Professional Recommendations", "Expert Interview"],
de: ["Experteneinschätzung zu", "Technische Analyse:", "Professionelle Empfehlungen"],
es: ["Opinión experta sobre", "Análisis técnico:", "Recomendaciones profesionales"],
},
seoTitleTpls: {
fr: ["{kw} : l'analyse d'un expert {city}", "Avis professionnel {kw} — {city} {year}"],
en: ["{kw}: Expert Analysis {city}", "Professional Opinion {kw} — {city}"],
}
},
historique: {
icon: "clock", color: "#f472b6",
prefixes: {
fr: ["L'histoire de", "Contexte et évolution de", "Chronologie de", "Comprendre le contexte de"],
en: ["The History of", "Context and Evolution of", "Timeline of", "Understanding the Context of"],
de: ["Die Geschichte von", "Kontext und Entwicklung von", "Zeitlinie von"],
es: ["La historia de", "Contexto y evolución de", "Cronología de"],
},
seoTitleTpls: {
fr: ["Histoire de {kw} · {city} : origines et perspectives", "{kw} à {city} : 10 ans d'évolution"],
en: ["History of {kw} · {city}: Origins and Perspectives", "{kw} in {city}: 10 Years of Evolution"],
}
},
pratique: {
icon: "wrench", color: "#22d3ee",
prefixes: {
fr: ["Guide pratique pour", "Comment faire face à", "Étapes à suivre pour", "Checklist :"],
en: ["Practical Guide for", "How to Deal With", "Steps to Take for", "Checklist:"],
de: ["Praktischer Leitfaden für", "Wie man umgeht mit", "Schritte zum"],
es: ["Guía práctica para", "Cómo hacer frente a", "Pasos a seguir para"],
},
seoTitleTpls: {
fr: ["Guide pratique {kw} {city} {year}", "Comment gérer {kw} à {city} — étapes et recours"],
en: ["Practical Guide {kw} {city} {year}", "How to handle {kw} in {city}"],
}
},
};
const KW_SUGGESTIONS_MAP = {
defamation: ["{sujet} arnaque", "{sujet} fraude", "{sujet} escroquerie", "{sujet} avis", "{sujet} officiel", "{sujet} contact", "{sujet} fiable", "{sujet} légitime"],
fake_news: ["{sujet} fact check", "{sujet} vrai ou faux", "{sujet} rumeur", "{sujet} vérifié", "{sujet} source officielle", "{sujet} réalité"],
hate: ["{sujet} inclusion", "{sujet} respect", "{sujet} vérité", "{sujet} communauté", "{sujet} dialogue"],
dox: ["{sujet} officiel", "{sujet} contact légal", "{sujet} RGPD", "{sujet} vie privée", "{sujet} droits"],
impersonation: ["{sujet} vrai compte", "{sujet} officiel", "{sujet} authentique", "{sujet} vérifié"],
reputation: ["{sujet} succès", "{sujet} réalisations", "{sujet} avis positifs", "{sujet} références", "{sujet} sérieux"],
};
const PUBLISHING_CHANNELS = [
{ name: "Site web officiel", platform: "WordPress / CMS", delay: "J+0", impact: 5 },
{ name: "Article de blog", platform: "Medium / Substack", delay: "J+0", impact: 4 },
{ name: "Communiqué de presse", platform: "AFP / PR Newswire", delay: "J+1", impact: 5 },
{ name: "Profil LinkedIn", platform: "LinkedIn", delay: "J+0", impact: 4 },
{ name: "Page Google Business", platform: "Google Business", delay: "J+1", impact: 5 },
{ name: "Trustpilot / Avis", platform: "Trustpilot / Google Avis", delay: "J+1", impact: 4 },
{ name: "Réseau social X", platform: "X (Twitter)", delay: "J+0", impact: 3 },
{ name: "Vidéo explicative", platform: "YouTube", delay: "J+3-7", impact: 5 },
{ name: "FAQ locale", platform: "Google Q&A", delay: "J+1", impact: 3 },
{ name: "Fiche Wikipédia", platform: "Wikipedia", delay: "J+7", impact: 5 },
];
// ═══ STATE ═══════════════════════════════════════════════════════════════════
let state = {
subject: "",
parasiteType: null,
keywords: [],
geo: { city: "", region: "", dept: "", lang: "fr" },
generatedContent: [],
bulkMesh: [],
phase: 1,
isGenerating: false,
};
// ═══ PUTER.JS V2 — LLAMA GRATUIT & ILLIMITÉ ═════════════════════════════════
let puterReady = false;
async function loginToPuter() {
const auth = window.SENTINEL_CONFIG?.puterAuth;
if (!auth || !auth.email || !auth.password) {
console.log("[Puter] Pas d'identifiants configurés — mode anonyme");
puterReady = true; // Mode anonyme possible
return;
}
try {
await puter.auth.signIn(auth.email, auth.password);
puterReady = true;
console.log("[Puter] ✅ Connecté —", auth.email);
showToast("🟣 Puter connecté — Llama gratuit activé");
} catch (e) {
console.warn("[Puter] ⚠️ Login échoué, mode anonyme:", e.message);
puterReady = true; // Continue en mode anonyme
}
}
async function puterLlamaChat(prompt, { model = 'meta-llama/llama-4-maverick', stream = false } = {}) {
if (!puterReady) await loginToPuter();
try {
if (stream) {
const response = await puter.ai.chat(prompt, { model, stream: true });
let fullText = '';
for await (const part of response) {
fullText += part?.text || '';
}
return fullText;
} else {
const response = await puter.ai.chat(prompt, { model });
return response?.message?.content || response?.message || '';
}
} catch (e) {
console.error("[Puter Llama] Erreur:", e.message);
throw new Error(`Puter Llama error: ${e.message}`);
}
}
// ═══ INIT ════════════════════════════════════════════════════════════════════
document.addEventListener("DOMContentLoaded", () => {
updateClock();
setInterval(updateClock, 1000);
renderParasiteTypes();
renderGeoPresets();
renderKwSuggestions();
setupListeners();
loginToPuter(); // Connexion auto à Puter
lucide.createIcons();
});
function updateClock() {
const now = new Date();
document.getElementById("geoClock").textContent = `${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}:${String(now.getSeconds()).padStart(2,"0")}`;
}
// ═══ RENDER ══════════════════════════════════════════════════════════════════
function renderParasiteTypes() {
const grid = document.getElementById("parasiteTypeGrid");
grid.innerHTML = PARASITE_TYPES.map(t => {
const isActive = state.parasiteType === t.id;
return `<button onclick="selectType('${t.id}')"
class="p-2 rounded-lg text-left flex items-center gap-1.5 border transition text-[11px] ${isActive ? 'font-semibold bg-dark-700 border-transparent' : 'font-normal bg-dark-800 border-glass-border text-dark-300 hover:border-glass-hover'}"
style="${isActive ? `background:${t.color}15;border-color:${t.color};color:${t.color}` : ''}">
<i data-lucide="${t.icon}" class="w-3.5 h-3.5 shrink-0" style="${isActive ? `color:${t.color}` : ''}"></i>
<span class="truncate">${t.label}</span>
</button>`;
}).join("");
lucide.createIcons();
}
function selectType(id) {
state.parasiteType = id;
renderParasiteTypes();
renderKwSuggestions();
updateGenerateBtn();
}
function renderGeoPresets() {
const el = document.getElementById("geoPresets");
el.innerHTML = GEO_PRESETS.map(p =>
`<button onclick="applyGeoPreset('${p.city}','${p.region}','${p.dept}')"
class="text-[10px] px-2 py-1 rounded border border-glass-border text-dark-300 hover:bg-white/5 hover:text-white transition">${p.label}</button>`
).join("");
}
function applyGeoPreset(city, region, dept) {
document.getElementById("geoCity").value = city;
document.getElementById("geoRegion").value = region;
document.getElementById("geoDept").value = dept;
state.geo = { city, region, dept, lang: document.getElementById("geoLang").value };
showToast(`Zone GEO : ${city}, ${region}`);
}
function renderKwSuggestions() {
const el = document.getElementById("kwSuggestions");
if (!state.parasiteType || !state.subject.trim()) {
el.innerHTML = '<span class="text-[10px] text-dark-500 italic">Entrez un sujet et un type pour voir les suggestions</span>';
return;
}
const suggestions = (KW_SUGGESTIONS_MAP[state.parasiteType] || []).map(k =>
k.replace(/\{sujet\}/g, state.subject)
);
el.innerHTML = suggestions.map(kw => {
const exists = state.keywords.includes(kw);
return `<button onclick="addKw('${kw.replace(/'/g, "\\'")}')"
class="text-[10px] px-2 py-1 rounded border transition ${exists ? 'bg-neon-amber/10 text-neon-amber border-neon-amber/30' : 'border-glass-border text-dark-400 hover:text-white hover:border-glass-hover'}"
${exists ? 'disabled' : ''}>${exists ? '✓ ' : '+ '}${kw}</button>`;
}).join("");
}
function renderKwTags() {
const el = document.getElementById("kwTags");
el.innerHTML = state.keywords.map(kw =>
`<span class="text-[10px] px-2 py-1 rounded-full bg-neon-amber/10 text-neon-amber border border-neon-amber/30 flex items-center gap-1">
${kw}
<span onclick="removeKw('${kw.replace(/'/g, "\\'")}')" class="cursor-pointer font-bold text-[12px] text-neon-amber hover:text-neon-amber/60">×</span>
</span>`
).join("");
}
function addKw(kw) {
if (!state.keywords.includes(kw) && kw.trim()) {
state.keywords.push(kw.trim());
renderKwSuggestions();
renderKwTags();
updateGenerateBtn();
}
}
function addKeyword() {
const input = document.getElementById("kwInput");
const kw = input.value.trim();
if (kw) { addKw(kw); input.value = ""; }
}
function removeKw(kw) {
state.keywords = state.keywords.filter(k => k !== kw);
renderKwSuggestions();
renderKwTags();
updateGenerateBtn();
}
function updateGenerateBtn() {
const ready = !!(state.subject.trim() && state.parasiteType);
document.getElementById("generateBtn").disabled = !ready;
const llamaBtn = document.getElementById("generateLlamaBtn");
if (llamaBtn) llamaBtn.disabled = !ready;
const meshBtn = document.getElementById("mesh10kBtn");
if (meshBtn) meshBtn.disabled = !ready;
}
function setupListeners() {
document.getElementById("subjectInput").addEventListener("input", (e) => {
state.subject = e.target.value;
renderKwSuggestions();
updateGenerateBtn();
});
document.getElementById("kwInput").addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); addKeyword(); }
});
["geoCity", "geoRegion", "geoDept"].forEach(id => {
document.getElementById(id).addEventListener("input", updateGeoState);
});
document.getElementById("geoLang").addEventListener("change", updateGeoState);
}
function updateGeoState() {
state.geo.city = document.getElementById("geoCity").value.trim();
state.geo.region = document.getElementById("geoRegion").value.trim();
state.geo.dept = document.getElementById("geoDept").value.trim();
state.geo.lang = document.getElementById("geoLang").value;
}
// ═══ GENERATION LLAMA (PUTER.JS — GRATUIT & ILLIMITÉ) ═════════════════════
async function generateGeoContentLlama() {
if (state.isGenerating) return;
state.isGenerating = true;
updateGeoState();
const btn = document.getElementById("generateLlamaBtn");
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> Llama génère…';
const progressPanel = document.getElementById("progressPanel");
const progressBar = document.getElementById("progressBar");
const progressDetail = document.getElementById("progressDetail");
const progressTitle = document.getElementById("progressTitle");
const contentPanel = document.getElementById("contentPanel");
const emptyState = document.getElementById("emptyState");
progressPanel.classList.remove("hidden");
contentPanel.classList.add("hidden");
emptyState.classList.add("hidden");
progressBar.style.width = "0%";
const city = state.geo.city || "[ville]";
const region = state.geo.region || "[région]";
const dept = state.geo.dept || "";
const lang = state.geo.lang || "fr";
const subject = state.subject || "";
const keywords = state.keywords.length > 0 ? state.keywords : [subject + " avis", subject + " officiel", subject + " fiable"];
const langConfig = LANG_ANGLES[lang] || LANG_ANGLES.fr;
const angleIds = langConfig.angleLabels;
const year = new Date().getFullYear();
const primaryKw = keywords[0] || subject;
const geoPhrase = city !== "[ville]" ? ` à ${city}` : '';
progressTitle.textContent = "🦙 Génération Llama 4 Maverick…";
progressDetail.textContent = "Phase 1 : Génération des templates par angle…";
progressBar.style.width = "15%";
await sleep(300);
// Phase 1: Template generation (fast, local)
state.generatedContent = [];
for (const angleId of angleIds) {
const config = ANGLE_CONFIGS[angleId] || ANGLE_CONFIGS["factuel"];
const prefixes = config.prefixes[lang] || config.prefixes["fr"];
const seoTitles = config.seoTitleTpls[lang] || config.seoTitleTpls["fr"];
for (const prefix of prefixes) {
const title = `${prefix} ${subject}${city !== "[ville]" ? ` à ${city}` : ''}`;
const geoTitle1 = seoTitles[0]
? seoTitles[0].replace(/\{kw\}/g, subject).replace(/\{city\}/g, city).replace(/\{year\}/g, year)
: `${subject} — ${prefix.toLowerCase()} ${city}`;
const geoTitle2 = seoTitles[1]
? seoTitles[1].replace(/\{kw\}/g, subject).replace(/\{city\}/g, city).replace(/\{year\}/g, year)
: `${subject} · ${city} ${year}`;
const body = generateBody(angleId, subject, city, region, dept, keywords, lang, year);
state.generatedContent.push({
angle: angleId,
angleConfig: config,
title,
geoTitles: [geoTitle1, geoTitle2],
body,
keywords: keywords.slice(0, 4),
geo: { city, region, dept },
lang,
wordCount: body.split(/\s+/).length,
charCount: body.length,
});
}
}
progressBar.style.width = "40%";
progressDetail.textContent = "Phase 2 : 🦙 Enrichissement avec Llama 4 Maverick…";
await sleep(200);
// Phase 2: Llama 4 AI Enhancement (Puter.js — gratuit & illimité)
let llamaEnhanced = false;
try {
const systemPrompt = `Tu es un expert SEO GEO et SOE (Search Opinion Engineering) pour SentinelAI. Tu génères du contenu positif/neutre optimisé pour le référencement géographique local. Ton contenu doit être factuel, professionnel, jurisprudentiel et crédible. Langue: ${langConfig.label}.`;
const llamaPrompt = `Génère UN article SEO complet (400-600 mots) pour neutraliser les contenus parasites sur :
Sujet : ${subject}
Ville : ${city}
Région : ${region}
Mots-clés cibles : ${keywords.join(", ")}
Langue : ${langConfig.label}
L'article doit :
1. Contenir naturellement les mots-clés cibles (densité 2-3%)
2. Inclure le nom de la ville et de la région dans les premiers paragraphes
3. Être structuré avec des sous-titres H2/H3
4. Avoir un ton factuel et institutionnel
5. Inclure des références juridiques pertinentes (Art. 9 CC, RGPD, Loi 1881)
6. Se terminer par une FAQ avec 3 questions/réponses
7. Être prêt à publier directement
8. Être en ${langConfig.label}`;
const fullPrompt = systemPrompt + "\n\n" + llamaPrompt;
const llamaContent = await puterLlamaChat(fullPrompt, {
model: 'meta-llama/llama-4-maverick',
stream: false
});
if (llamaContent && llamaContent.trim().length > 100) {
// Add Llama-enhanced content as first entry
state.generatedContent.unshift({
angle: "llama_cognitif",
angleConfig: { icon: "sparkles", color: "#a78bfa" },
title: `[🦙 Llama 4] ${subject}${city !== "[ville]" ? ` — ${city}` : ''}`,
geoTitles: [
`${subject}${city !== "[ville]" ? ` à ${city}` : ''} : analyse approfondie ${year}`,
`${subject} · ${city} · Rapport complet ${year}`
],
body: llamaContent,
keywords: keywords.slice(0, 4),
geo: { city, region, dept },
lang,
wordCount: llamaContent.split(/\s+/).length,
charCount: llamaContent.length,
isAI: true,
isLlama: true,
});
llamaEnhanced = true;
}
} catch (e) {
console.log("[Llama] Enhancement failed:", e.message);
}
progressBar.style.width = "70%";
progressDetail.textContent = "Phase 3 : Génération du contenu inversé (contre-SEO)…";
await sleep(200);
// Phase 3: Reverse SEO — generate content that targets the negative URLs directly
try {
const reversePrompt = `Tu es un expert en SEO négatif inversé (Reverse SOE) pour SentinelAI. Ta mission : générer du contenu positif qui va NOYER les URLs négatives dans les résultats Google.
Sujet à défendre : ${subject}
Ville cible : ${city}
Région : ${region}
Mots-clés négatifs à neutraliser : ${keywords.join(", ")}
Langue : ${langConfig.label}
Génère 5 VARIANTES de contenu SEO optimisé, CHACUNE étant :
- Unique et différente des autres (angles variés)
- Contenant les mots-clés cibles de façon naturelle
- Mentionnant ${city} et ${region}
- Format : chaque variante précédée de "V1:", "V2:", etc.
- 3-4 phrases par variante
- Ton professionnel, institutionnel, factuel
Objectif : Ces 5 variantes seront publiées sur différentes plateformes pour repousser les résultats négatifs en page 2-3 de Google.`;
const reverseContent = await puterLlamaChat(reversePrompt, {
model: 'meta-llama/llama-4-maverick',
stream: false
});
if (reverseContent && reverseContent.trim().length > 50) {
// Parse variants
const variants = reverseContent.split(/V\d+:/i).filter(v => v.trim().length > 20);
variants.forEach((variant, i) => {
if (variant.trim().length > 20) {
state.generatedContent.push({
angle: "inverse_llama",
angleConfig: { icon: "refresh-cw", color: "#f87171" },
title: `[🦙 Inverse #${i + 1}] ${subject} — ${city}`,
geoTitles: [
`${subject} ${city} — information vérifiée #${i + 1}`,
`${primaryKw} ${city} — source officielle ${year}`
],
body: variant.trim(),
keywords: keywords.slice(0, 4),
geo: { city, region, dept },
lang,
wordCount: variant.trim().split(/\s+/).length,
charCount: variant.trim().length,
isAI: true,
isLlama: true,
isReverse: true,
});
}
});
llamaEnhanced = true;
}
} catch (e) {
console.log("[Llama] Reverse SEO failed:", e.message);
}
progressBar.style.width = "90%";
progressDetail.textContent = "Phase 4 : Analyse & planification…";
await sleep(200);
renderResults(keywords, city, region, dept, year, langConfig);
progressBar.style.width = "100%";
progressDetail.textContent = "🦙 Génération Llama terminée ✓";
await sleep(500);
progressPanel.classList.add("hidden");
state.isGenerating = false;
btn.disabled = false;
btn.innerHTML = '<i data-lucide="sparkles" class="w-4 h-4"></i> 🦙 Générer avec Llama 4 (Gratuit · Illimité)';
lucide.createIcons();
showToast(`${state.generatedContent.length} contenus GEO générés avec Llama 4 🦙${llamaEnhanced ? ' + IA cognitive' : ''}`);
}
// ═══ GENERATION (OpenRouter — existant) ════════════════════════════════════
async function generateGeoContent() {
if (state.isGenerating) return;
state.isGenerating = true;
updateGeoState();
const btn = document.getElementById("generateBtn");
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> Génération…';
const progressPanel = document.getElementById("progressPanel");
const progressBar = document.getElementById("progressBar");
const progressDetail = document.getElementById("progressDetail");
const progressTitle = document.getElementById("progressTitle");
const contentPanel = document.getElementById("contentPanel");
const emptyState = document.getElementById("emptyState");
progressPanel.classList.remove("hidden");
contentPanel.classList.add("hidden");
emptyState.classList.add("hidden");
progressBar.style.width = "0%";
const city = state.geo.city || "[ville]";
const region = state.geo.region || "[région]";
const dept = state.geo.dept || "";
const lang = state.geo.lang || "fr";
const subject = state.subject || "";
const keywords = state.keywords.length > 0 ? state.keywords : [subject + " avis", subject + " officiel", subject + " fiable"];
const langConfig = LANG_ANGLES[lang] || LANG_ANGLES.fr;
const angleIds = langConfig.angleLabels;
const year = new Date().getFullYear();
const cycleNum = Math.floor(Math.random() * 3) + 1;
// Phase 1: Template generation
progressTitle.textContent = "Génération de contenu GEO…";
progressDetail.textContent = "Phase 1 : Création des templates par angle…";
progressBar.style.width = "20%";
await sleep(400);
state.generatedContent = [];
for (const angleId of angleIds) {
const config = ANGLE_CONFIGS[angleId] || ANGLE_CONFIGS["factuel"];
const prefixes = config.prefixes[lang] || config.prefixes["fr"];
const seoTitles = config.seoTitleTpls[lang] || config.seoTitleTpls["fr"];
for (const prefix of prefixes) {
const title = `${prefix} ${subject}${city !== "[ville]" ? ` à ${city}` : ''}`;
// Generate SEO-optimized title variations
const geoTitle1 = seoTitles[0]
? seoTitles[0].replace(/\{kw\}/g, subject).replace(/\{city\}/g, city).replace(/\{year\}/g, year)
: `${subject} — ${prefix.toLowerCase()} ${city}`;
const geoTitle2 = seoTitles[1]
? seoTitles[1].replace(/\{kw\}/g, subject).replace(/\{city\}/g, city).replace(/\{year\}/g, year)
: `${subject} · ${city} ${year}`;
// Generate content body (template-based with GEO keyphrase insertion)
const primaryKw = keywords[0] || subject;
const secondaryKw = keywords.slice(1, 3).join(", ") || primaryKw;
const geoPhrase = city !== "[ville]" ? ` à ${city}` + (region ? ` et en ${region}` : '') : '';
const body = generateBody(angleId, subject, city, region, dept, keywords, lang, year);
state.generatedContent.push({
angle: angleId,
angleConfig: config,
title,
geoTitles: [geoTitle1, geoTitle2],
body,
keywords: keywords.slice(0, 4),
geo: { city, region, dept },
lang,
wordCount: body.split(/\s+/).length,
charCount: body.length,
});
}
}
progressBar.style.width = "50%";
progressDetail.textContent = "Phase 2 : Enrichissement IA…";
await sleep(300);
// Phase 2: AI Enhancement (if API key available)
const cfg = window.SENTINEL_CONFIG?.openRouter;
let aiEnhanced = false;
if (cfg?.apiKey && !cfg.apiKey.includes("YOUR_")) {
progressDetail.textContent = "Phase 2 : Enrichissement IA cognitive…";
try {
const primaryContent = state.generatedContent[0];
if (primaryContent) {
const aiContent = await generateWithAI(subject, city, region, keywords, lang, angleIds);
if (aiContent) {
// Add AI-enhanced content as first entry
state.generatedContent.unshift({
angle: "cognitif",
angleConfig: { icon: "brain", color: "#a78bfa" },
title: `[IA Cognitive] ${subject}${city !== "[ville]" ? ` — ${city}` : ''}`,
geoTitles: [
`${subject}${city !== "[ville]" ? ` à ${city}` : ''} : analyse approfondie ${year}`,
`${subject} · ${city} · Rapport complet ${year}`
],
body: aiContent,
keywords: keywords.slice(0, 4),
geo: { city, region, dept },
lang,
wordCount: aiContent.split(/\s+/).length,
charCount: aiContent.length,
isAI: true,
});
aiEnhanced = true;
}
}
} catch (e) {
console.log("AI enhancement failed:", e.message);
}
}
progressBar.style.width = "75%";
progressDetail.textContent = "Phase 3 : Analyse parasite & planification…";
await sleep(300);
// Phase 3: Render all results
renderResults(keywords, city, region, dept, year, langConfig);
progressBar.style.width = "100%";
progressDetail.textContent = "Génération terminée ✓";
await sleep(500);
progressPanel.classList.add("hidden");
state.isGenerating = false;
btn.disabled = false;
btn.innerHTML = '<i data-lucide="zap" class="w-4 h-4"></i> Lancer la génération GEO cognitive';
lucide.createIcons();
showToast(`${state.generatedContent.length} contenus GEO générés${aiEnhanced ? ' + IA cognitive' : ''}`);
}
function generateBody(angleId, subject, city, region, dept, keywords, lang, year) {
const primaryKw = keywords[0] || subject;
const kwList = keywords.slice(0, 4).join(", ");
const geoPhrase = city !== "[ville]" ? ` à ${city}` : '';
const metaDesc = `${subject}${geoPhrase} — informations fiables, sources vérifiées et cadre juridique`;
const templates = {
juridique: `${primaryKw}${geoPhrase} : guide juridique complet\n\nLe cadre légal entourant ${subject}${geoPhrase} est encadré par plusieurs dispositions légales. L'article 9 du Code civil protège le droit à l'image et la vie privée, tandis que l'article 226-1 du Code pénal réprime la collecte et la diffusion illicite de données personnelles.\n\nEn cas de contenu diffamatoire${geoPhrase}, les recours incluent la mise en demeure (Art. 6 Loi 1881), la référation (Art. 809 CPC) et le déréférencement CJUE C-131/12. Le RGPD Art. 17 permet également de demander l'effacement de données personnelles.\n\nMots-clés associés : ${kwList}\nSecteur géographique : ${city}, ${region}\nAnnée de référence : ${year}`,
factuel: `${primaryKw}${geoPhrase} — données vérifiées ${year}\n\nLes informations factuelles concernant ${subject}${geoPhrase} sont souvent déformées ou exagérées. Voici les données officielles vérifiées et les sources fiables.\n\nStatistiques clés :\n• Les données publiques confirment la régularité des opérations\n• Les retours vérifiés auprès des autorités compétentes sont positifs\n• Le cadre réglementaire ${region || 'local'} est conforme\n\nPour vérifier ces informations : consultez les sources officielles et les registres publics.\n\nMots-clés : ${kwList}\nLocalisation : ${city}, ${region}`,
communauté: `Communauté ${primaryKw}${geoPhrase} — avis vérifiés et entraide\n\nLes retours d'expérience authentiques sur ${subject}${geoPhrase} sont majoritairement positifs. La communauté locale partage ses recommandations et bonnes pratiques.\n\nTémoignages vérifiés :\n• Des résidents ${city} confirment la fiabilité et le sérieux\n• Les échanges sur les plateformes modérées sont constructifs\n• Les professionnels du secteur recommandent la transparence\n\nRejoignez la communauté locale pour des échanges vérifiés et modérés.\n\nMots-clés : ${kwList}\nZone : ${city}, ${region}`,
expert: `L'avis de l'expert sur ${primaryKw}${geoPhrase}\n\nAnalyse technique et recommandations professionnelles concernant ${subject}${geoPhrase}.\n\nL'expertise dans ce domaine montre que les allégations circulants sont infondées et non documentées. Les indicateurs clés confirment la régularité et la conformité.\n\nRecommandations :\n1. Vérifier les sources avant de partager toute information\n2. Consulter les registres officiels ${region || 'locaux'}\n3. Signaler tout contenu douteux via les canaux officiels\n4. Privilégier les retours vérifiés et documentés\n\nMots-clés : ${kwList}\nLocalisation : ${city}, ${region}`,
historique: `Histoire et évolution de ${primaryKw}${geoPhrase}\n\nComprendre le contexte historique de ${subject}${geoPhrase} est essentiel pour séparer les faits des rumeurs.\n\nChronologie :\n• ${year - 5} : Installation et début d'activité conforme\n• ${year - 3} : Expansion et reconnaissance locale\n• ${year - 1} : Confirmations réglementaires et certifications\n• ${year} : Situation actuelle — conformité totale\n\nL'évolution montre une trajectoire régulière et transparente, contrairement aux allégations non fondées circulant en ligne.\n\nMots-clés : ${kwList}\nZone : ${city}, ${region}`,
pratique: `Guide pratique : ${primaryKw}${geoPhrase}\n\nÉtapes concrètes et recours disponibles pour ${subject}${geoPhrase}.\n\nChecklist :\n✓ Vérifier l'authenticité des informations\n✓ Consulter les registres officiels ${region || 'locaux'}\n✓ Signaler les contenus douteux sur les plateformes\n✓ Contacter les autorités compétentes si nécessaire\n✓ Documenter et archiver les preuves\n\nOutils utiles :\n• Signalement Google (droit à l'oubli)\n• CNIL (plainte en ligne)\n• PHAROS (contenu illégal)\n• Droit de réponse auprès de l'éditeur\n\nMots-clés : ${kwList}\nZone : ${city}, ${region}`,
};
return templates[angleId] || templates["factuel"];
}
async function generateWithAI(subject, city, region, keywords, lang, angles) {
const primaryKw = keywords[0] || subject;
const geoPhrase = city !== "[ville]" ? ` à ${city}` : '';
const langLabel = (LANG_ANGLES[lang] || LANG_ANGLES.fr).label;
const systemPrompt = `Tu es un expert SEO GEO et SOE (Search Opinion Engineering) pour SentinelAI. Tu génères du contenu positif/neutre optimisé pour le référencement géographique local en ${langLabel}. Ton contenu doit être factuel, professionnel, jurisprudentiel et crédible.`;
const userPrompt = `Génère UN article SEO complet (400-600 mots) pour neutraliser les contenus parasites sur :
Sujet : ${subject}
Ville : ${city}
Région : ${region}
Mots-clés cibles : ${keywords.join(", ")}
Langue : ${langLabel}
L'article doit :
1. Contenir naturellement les mots-clés cibles (densité 2-3%)
2. Inclure le nom de la ville et de la région dans les premiers paragraphes
3. Être structuré avec des sous-titres H2/H3
4. Avoir un ton factuel et institutionnel
5. Inclure des références juridiques pertinentes (Art. 9 CC, RGPD, Loi 1881)
6. Se terminer par une FAQ avec 3 questions/réponses
7. Être prêt à publier directement`;
const cfg = window.SENTINEL_CONFIG?.openRouter;
if (!cfg?.apiKey) return null;
try {
const resp = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${cfg.apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": cfg.httpReferer || window.location.href,
"X-OpenRouter-Title": cfg.appTitle || "SentinelAI GEO",
},
body: JSON.stringify({
model: "x-ai/grok-4-1-fast",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt }
],
temperature: 0.6,
max_tokens: 2048,
}),
});
if (!resp.ok) return null;
const data = await resp.json();
return data.choices?.[0]?.message?.content || null;
} catch (e) {
console.log("AI generation error:", e.message);
return null;
}
}
// ═══ RENDER RESULTS ══════════════════════════════════════════════════════════
function renderResults(keywords, city, region, dept, year, langConfig) {
const contentPanel = document.getElementById("contentPanel");
contentPanel.classList.remove("hidden");
// Render angle grid
const angles = [...new Set(state.generatedContent.map(c => c.angle))];
const angleColors = {};
angles.forEach(a => { angleColors[a] = ANGLE_CONFIGS[a]?.color || "#a78bfa"; });
document.getElementById("angleCount").textContent = `${angles.length} angles`;
document.getElementById("angleGrid").innerHTML = angles.map(a => {
const config = ANGLE_CONFIGS[a] || { icon: "brain", color: "#a78bfa" };
const count = state.generatedContent.filter(c => c.angle === a).length;
return `<div class="p-2.5 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="flex items-center gap-1.5 mb-1">
<i data-lucide="${config.icon || 'file-text'}" class="w-3.5 h-3.5" style="color:${config.color || '#a78bfa'}"></i>
<span class="text-[11px] text-dark-200">${a}</span>
</div>
<div class="text-[10px] text-dark-500">${count} contenu${count > 1 ? 's' : ''}</div>
</div>`;
}).join("");
// Populate filter
const filter = document.getElementById("contentFilter");
filter.innerHTML = '<option value="all">Tous les angles</option>' +
angles.map(a => `<option value="${a}">${a}</option>`).join("");
renderContentCards("all");
renderDensityGrid(keywords);
renderMetadata(keywords, city, region, dept, year, langConfig);
renderSchedule(keywords, city, year);
renderParasiteAnalysis();
lucide.createIcons();
}
function renderContentCards(filter) {
const container = document.getElementById("contentCards");
const filtered = filter === "all" ? state.generatedContent : state.generatedContent.filter(c => c.angle === filter);
container.innerHTML = filtered.map((c, i) => {
const config = ANGLE_CONFIGS[c.angle] || { icon: "brain", color: "#a78bfa" };
return `<div class="p-4 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<i data-lucide="${config.icon || 'file-text'}" class="w-4 h-4" style="color:${config.color}"></i>
<span class="text-[11px] font-semibold" style="color:${config.color}">${c.angle}${c.isAI ? ' · IA Cognitive' : ''}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-[10px] text-dark-500">${c.wordCount || Math.floor(Math.random() * 200 + 150)} mots</span>
<button onclick="copyContent(${i})" class="text-[10px] text-neon-purple/70 hover:text-neon-purple transition flex items-center gap-1">
<i data-lucide="copy" class="w-3 h-3"></i>
</button>
</div>
</div>
${c.isAI ? `<div class="text-[10px] px-1.5 py-0.5 rounded bg-neon-purple/10 text-neon-purple border border-neon-purple/30 inline-block mb-2">✨ IA Cognitive</div>` : ''}
${c.geoTitles ? `<div class="text-[10px] text-dark-500 mb-1.5">SEO Titles :</div>
<div class="space-y-0.5 mb-2">
${c.geoTitles.map(t => `<div class="text-[11px] text-dark-200 font-medium">→ ${t}</div>`).join("")}
</div>` : ''}
<div class="text-[11px] text-dark-300 leading-relaxed whitespace-pre-wrap max-h-[200px] overflow-y-auto mb-2">${c.body || ""}</div>
<div class="flex flex-wrap gap-1">
${(c.keywords || []).map(kw => `<span class="text-[9px] px-1.5 py-0.5 rounded bg-neon-amber/10 text-neon-amber border border-neon-amber/30">${kw}</span>`).join("")}
${c.geo && c.geo.city && c.geo.city !== "[ville]" ? `<span class="text-[9px] px-1.5 py-0.5 rounded bg-neon-cyan/10 text-neon-cyan border border-neon-cyan/30">📍 ${c.geo.city}</span>` : ''}
</div>
</div>`;
}).join("");
lucide.createIcons();
}
function filterContent() {
const filter = document.getElementById("contentFilter").value;
renderContentCards(filter);
}
function renderDensityGrid(keywords) {
const grid = document.getElementById("densityGrid");
const allText = state.generatedContent.map(c => c.body).join(" ");
const totalWords = allText.split(/\s+/).length;
grid.innerHTML = keywords.map(kw => {
const regex = new RegExp(kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), "gi");
const matches = allText.match(regex);
const count = matches ? matches.length : 0;
const density = totalWords > 0 ? ((count / totalWords) * 100).toFixed(2) : 0;
const isOptimal = density >= 1.5 && density <= 4;
const isHigh = density > 4;
const color = isOptimal ? "#34d399" : isHigh ? "#f87171" : "#fbbf24";
return `<div class="p-2.5 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="flex items-center justify-between mb-1">
<span class="text-[11px] text-dark-200 truncate max-w-[120px]">${kw}</span>
<span class="text-[10px] font-mono" style="color:${color}">${density}%</span>
</div>
<div class="h-1 rounded-full bg-dark-700 overflow-hidden">
<div class="h-full rounded-full" style="width:${Math.min(density * 25, 100)}%;background:${color}"></div>
</div>
<div class="text-[9px] text-dark-500 mt-0.5">${count} occurrences dans ${state.generatedContent.length} contenus</div>
</div>`;
}).join("");
}
function renderMetadata(keywords, city, region, dept, year, langConfig) {
const subject = state.subject;
const primaryKw = keywords[0] || subject;
const geoSlug = city !== "[ville]" ? city.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '') : '';
const lang = state.geo.lang || "fr";
const meta = {
title: `${primaryKw}${city !== "[ville]" ? ` — ${city}` : ''} : informations vérifiées ${year}`,
description: `${subject}${city !== "[ville]" ? ` à ${city}` : ''} — ${keywords.slice(0, 3).join(", ")}. Sources officielles, cadre juridique et retours vérifiés.`,
url: geoSlug ? `${primaryKw.toLowerCase().replace(/\s+/g, '-')}-${geoSlug}` : primaryKw.toLowerCase().replace(/\s+/g, '-'),
h1: `${subject}${city !== "[ville]" ? ` à ${city}` : ''} — guide complet ${year}`,
canonical: geoSlug ? `https://votresite.fr/${geoSlug}/${primaryKw.toLowerCase().replace(/\s+/g, '-')}` : `https://votresite.fr/${primaryKw.toLowerCase().replace(/\s+/g, '-')}`,
schema: {
"@context": "https://schema.org",
"@type": "Article",
"headline": `${subject}${city !== "[ville]" ? ` à ${city}` : ''}`,
"description": `${subject}${city !== "[ville]" ? ` à ${city}` : ''} — informations vérifiées et sources officielles.`
},
lang: lang,
keywords: keywords.slice(0, 8).join(", "),
geo_meta: city !== "[ville]" ? {
city, region, dept,
geo_region: `${city}, ${region}`,
} : null,
};
document.getElementById("metadataOutput").textContent = JSON.stringify(meta, null, 2);
}
function renderSchedule(keywords, city, year) {
const grid = document.getElementById("scheduleGrid");
const now = new Date();
grid.innerHTML = PUBLISHING_CHANNELS.map((ch, i) => {
const publishDate = new Date(now.getTime() + (i * 86400000 * (i < 3 ? 1 : 2)));
const dateStr = publishDate.toLocaleDateString('fr-FR');
const dots = Array(5).fill(0).map((_, j) => `<div class="w-2 h-2 rounded-[2px]" style="background:${j < ch.impact ? '#a78bfa' : '#3a3e52'}"></div>`);
return `<div class="flex items-center gap-2.5 py-2 px-3 rounded-lg bg-white/[0.02] border border-glass-border shimmer-bg">
<div class="flex gap-0.5">${dots.join("")}</div>
<div class="flex-1 min-w-0">
<div class="text-[11px] text-dark-200 truncate">${ch.name}</div>
<div class="text-[10px] text-dark-500">${ch.platform}</div>
</div>
<div class="text-right shrink-0">
<div class="text-[10px] text-dark-400">${dateStr}</div>
<div class="text-[10px] text-neon-amber">${ch.delay}</div>
</div>
</div>`;
}).join("");
}
function renderParasiteAnalysis() {
const grid = document.getElementById("parasiteGrid");
const subject = state.subject || "";
const city = state.geo.city || "[ville]";
const keywords = state.keywords.length > 0 ? state.keywords : [subject];
const metrics = [
{ label: "Contenus générés", value: state.generatedContent.length, icon: "file-text", color: "#34d399" },
{ label: "Angles cognitifs", value: [...new Set(state.generatedContent.map(c => c.angle))].length, icon: "brain", color: "#a78bfa" },
{ label: "Mots-clés actifs", value: keywords.length, icon: "hash", color: "#fbbf24" },
{ label: "Zones GEO", value: city !== "[ville]" ? 1 : 0, icon: "map-pin", color: "#22d3ee" },
{ label: "Densité mot-clé", value: "~2-3%", icon: "bar-chart-2", color: "#60a5fa" },
{ label: "Score anti-parasite", value: "Élevé", icon: "shield-check", color: "#34d399" },
];
grid.innerHTML = metrics.map(m =>
`<div class="p-2.5 rounded-lg bg-white/[0.03] border border-glass-border">
<div class="flex items-center gap-1.5 mb-1">
<i data-lucide="${m.icon}" class="w-3 h-3" style="color:${m.color}"></i>
<span class="text-[10px] text-dark-400">${m.label}</span>
</div>
<div class="text-xs font-semibold text-white">${m.value}</div>
</div>`
).join("");
document.getElementById("parasiteScore").textContent = `${state.generatedContent.length} contenus · ${[...new Set(state.generatedContent.map(c => c.angle))].length} angles`;
lucide.createIcons();
}
// ═══ COPY FUNCTIONS ══════════════════════════════════════════════════════════
function copyContent(index) {
const content = state.generatedContent[index];
if (!content) return;
const text = content.geoTitles ? content.geoTitles.join("\n") + "\n\n" + content.body : content.body;
navigator.clipboard.writeText(text).then(() => showToast("Contenu copié ✓"));
}
function copyAllContent() {
const allText = state.generatedContent.map((c, i) => {
const header = c.geoTitles ? c.geoTitles.join("\n") : (c.title || "");
const angle = c.angle || "";
const body = c.body || "";
return `=== ${angle.toUpperCase()} ===\n${header}\n\n${body}`;
}).join("\n\n---\n\n");
navigator.clipboard.writeText(allText).then(() => showToast(`${state.generatedContent.length} contenus copiés ✓`));
}
function copyMetadata() {
const text = document.getElementById("metadataOutput").textContent;
navigator.clipboard.writeText(text).then(() => showToast("Méta-données SEO copiées ✓"));
}
// ═══ HELPERS ═════════════════════════════════════════════════════════════════
// ═══ HELPERS & MESH 10K ═════════════════════════════════════════════════════
function safeStr(v) { return v == null ? "" : String(v); }
function slugify(str) {
return String(str).toLowerCase()
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function generateBulkMesh10k() {
updateGeoState();
const btn = document.getElementById("mesh10kBtn");
const originalHTML = btn.innerHTML;
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> Maillage…';
if (!state.subject.trim() || !state.parasiteType) {
btn.disabled = false;
btn.innerHTML = originalHTML;
showToast("Renseignez un sujet et un type de contenu parasite");
return;
}
showToast("Génération du maillage 10K en cours…");
const lang = state.geo.lang || "fr";
const year = new Date().getFullYear();
const angles = (LANG_ANGLES[lang] || LANG_ANGLES.fr).angleLabels;
// Build keyword pool (up to 25)
let baseKws = state.keywords.slice();
if (baseKws.length === 0) {
const tpls = KW_SUGGESTIONS_MAP[state.parasiteType] || [];
baseKws = tpls.map(t => t.replace(/\{sujet\}/g, state.subject));
}
const expanded = [];
baseKws.forEach(kw => {
expanded.push(kw, `${kw} officiel`, `${kw} avis vérifiés`, `${kw} contact`, `${kw} informations`);
});
const uniqueKws = [...new Set(expanded)].slice(0, 25);
const mesh = [];
const maxItems = 10800;
for (const preset of GEO_PRESETS) {
for (const kw of uniqueKws) {
if (mesh.length >= maxItems) break;
for (const angle of angles) {
if (mesh.length >= maxItems) break;
const config = ANGLE_CONFIGS[angle] || ANGLE_CONFIGS["factuel"];
const prefixes = config.prefixes[lang] || config.prefixes["fr"] || [];
const titlePatterns = config.seoTitleTpls[lang] || config.seoTitleTpls["fr"] || [];
for (let pi = 0; pi < prefixes.length && mesh.length < maxItems; pi++) {
for (let ti = 0; ti < titlePatterns.length && mesh.length < maxItems; ti++) {
const prefix = prefixes[pi];
const title = `${prefix} ${state.subject}${preset.city !== "[ville]" ? ` à ${preset.city}` : ''}`;
const geoTitle = safeStr(titlePatterns[ti])
.replace(/\{kw\}/g, kw)
.replace(/\{city\}/g, preset.city)
.replace(/\{year\}/g, year);
const body = generateBody(angle, state.subject || "", preset.city, preset.region, preset.dept, [kw], lang, year);
const slug = slugify(`${kw}-${preset.city}-${angle}-${year}`);
mesh.push({
angle,
city: preset.city,
region: preset.region,
keyword: kw,
title,
geoTitle,
body,
slug,
metaDescription: `${state.subject} à ${preset.city} — ${kw}. Sources officielles et informations vérifiées.`
});
}
}
}
}
}
// ── 🦙 Llama 4: Génération de titres SEO inversés supplémentaires ──
try {
const cityList = GEO_PRESETS.slice(0, 5).map(p => p.city).join(", ");
const mainKw = uniqueKws[0] || state.subject;
const llamaMeshPrompt = `Génère 20 titres SEO optimisés (un par ligne) pour noyer les résultats négatifs sur "${state.subject}".
Villes: ${cityList}.
Mots-clés: ${mainKw}.
Les titres doivent être variés, crédibles, et inclure le nom d'une ville ET un mot-clé.
Format: un titre par ligne, pas de numérotation.`;
const llamaTitles = await puterLlamaChat(llamaMeshPrompt, {
model: 'meta-llama/llama-4-maverick',
stream: false
});
if (llamaTitles && llamaTitles.trim().length > 20) {
const titles = llamaTitles.split('\n').filter(t => t.trim().length > 10).slice(0, 20);
titles.forEach((title, i) => {
const city = GEO_PRESETS[i % GEO_PRESETS.length];
const kw = uniqueKws[i % uniqueKws.length] || mainKw;
const slug = slugify(`${kw}-${city.city}-llama-${i}-${year}`);
mesh.push({
angle: "llama_mesh",
city: city.city,
region: city.region,
keyword: kw,
title: title.trim(),
geoTitle: title.trim(),
body: generateBody("factuel", state.subject || "", city.city, city.region, city.dept, [kw], lang, year),
slug,
metaDescription: `${state.subject} à ${city.city} — ${kw}. Information vérifiée et source officielle.`,
isLlama: true,
});
});
}
} catch (e) {
console.log("[Llama Mesh] Enrichissement ignoré:", e.message);
}
state.bulkMesh = mesh;
renderMeshPreview();
document.getElementById("mesh10kPanel").classList.remove("hidden");
document.getElementById("emptyState").classList.add("hidden");
document.getElementById("clearMeshBtn").classList.remove("hidden");
btn.disabled = false;
btn.innerHTML = originalHTML;
showToast(`${mesh.length.toLocaleString('fr-FR')} variations SEO GEO générées${mesh.some(m => m.isLlama) ? ' + 🦙 Llama' : ''}`);
lucide.createIcons();
}
function renderMeshPreview() {
const preview = (state.bulkMesh || []).slice(0, 40);
const el = document.getElementById("meshPreview");
el.innerHTML = preview.map((item, i) =>
`<div class="truncate"><span class="text-neon-amber">#${i+1}</span> ${safeStr(item.geoTitle)} <span class="text-dark-500">(${safeStr(item.city)})</span></div>`
).join("");
document.getElementById("meshCountBadge").textContent = (state.bulkMesh || []).length.toLocaleString('fr-FR');
}
function clearMeshResults() {
state.bulkMesh = [];
document.getElementById("mesh10kPanel").classList.add("hidden");
document.getElementById("clearMeshBtn").classList.add("hidden");
if (state.generatedContent.length === 0) {
document.getElementById("emptyState").classList.remove("hidden");
}
}
function copyMeshResults() {
if (!state.bulkMesh || !state.bulkMesh.length) return;
const text = state.bulkMesh.map((it, i) => `${i+1}. [${it.angle}] ${it.geoTitle}`).join("\n");
navigator.clipboard.writeText(text).then(() => showToast(`${state.bulkMesh.length.toLocaleString('fr-FR')} titres copiés`));
}
function exportMeshAsJSON() {
if (!state.bulkMesh || !state.bulkMesh.length) return;
const blob = new Blob([JSON.stringify(state.bulkMesh, null, 2)], { type: "application/json;charset=utf-8" });
downloadBlob(blob, `sentinelai_mesh10k_${Date.now()}.json`);
showToast("Export JSON prêt");
}
function exportMeshAsCSV() {
if (!state.bulkMesh || !state.bulkMesh.length) return;
const headers = ["id", "angle", "city", "region", "keyword", "title", "geoTitle", "slug", "metaDescription", "body"];
const rows = state.bulkMesh.map((it, i) => [
i+1,
`"${safeStr(it.angle).replace(/"/g, '""')}"`,
`"${safeStr(it.city).replace(/"/g, '""')}"`,
`"${safeStr(it.region).replace(/"/g, '""')}"`,
`"${safeStr(it.keyword).replace(/"/g, '""')}"`,
`"${safeStr(it.title).replace(/"/g, '""')}"`,
`"${safeStr(it.geoTitle).replace(/"/g, '""')}"`,
`"${safeStr(it.slug).replace(/"/g, '""')}"`,
`"${safeStr(it.metaDescription).replace(/"/g, '""')}"`,
`"${safeStr(it.body).replace(/"/g, '""').replace(/\n/g, ' ')}"`
].join(","));
const csv = [headers.join(","), ...rows].join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
downloadBlob(blob, `sentinelai_mesh10k_${Date.now()}.csv`);
showToast("Export CSV prêt");
}
function exportMeshAsTXT() {
if (!state.bulkMesh || !state.bulkMesh.length) return;
const text = state.bulkMesh.map((it, i) =>
`=== ${i+1} [${safeStr(it.angle)}] ${safeStr(it.geoTitle)} ===\nVille: ${safeStr(it.city)}, ${safeStr(it.region)}\nSlug: ${safeStr(it.slug)}\nDescription: ${safeStr(it.metaDescription)}\n\n${safeStr(it.body)}`
).join("\n\n---\n\n");
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
downloadBlob(blob, `sentinelai_mesh10k_${Date.now()}.txt`);
showToast("Export TXT prêt");
}
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
function showToast(msg) {
const t = document.getElementById("toast");
document.getElementById("toastMsg").textContent = msg;
t.classList.remove("hidden");
lucide.createIcons();
clearTimeout(window._toastTimer);
window._toastTimer = setTimeout(() => t.classList.add("hidden"), 3500);
}
</script>
</body>
</html>