SPB Git

Repository Public

intelligence.html 8.4 KB 170 lines html

1<!DOCTYPE html>2<!--3  QC Élection Forecast — Plateforme de prévision électorale du Québec 20264  Auteur : Simon-Pierre Boucher5  Contact : contact@spboucher.ai6  https://www.qc-election.com7-->8<html lang="fr">9<head>10<script>document.documentElement.dataset.theme=localStorage.getItem("qce-theme")||"dark";</script>11<meta charset="utf-8">12<meta name="viewport" content="width=device-width, initial-scale=1">13<title>Intelligence électorale — QC Élection Forecast</title>14<link rel="stylesheet" href="/assets/style.css">15<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>16</head>17<body>18<header class="site"></header>19<div class="wrap">20  <div class="hero">21    <h1>Intelligence électorale</h1>22    <p class="sub">Sentiment médiatique, momentum et nouvelles regroupées.23      <span class="chip badge-aux">signal auxiliaire — n'entre pas dans le forecast</span></p>24  </div>2526  <h2 class="section">Interroger le modèle</h2>27  <p class="section-sub">Les réponses sont générées à partir des données réelles du modèle (sondages, runs, événements) — chaque affirmation est traçable.</p>28  <div class="card">29    <div class="ask-box">30      <input type="text" id="q" placeholder="Ex. : pourquoi la probabilité du PQ a-t-elle changé cette semaine ?" maxlength="500">31      <button class="primary" id="ask">Demander</button>32    </div>33    <div class="answer" id="answer"></div>34  </div>3536  <h2 class="section">Momentum <span class="chip badge-aux">indicateur maison</span></h2>37  <div class="grid parties" id="momentum"></div>3839  <h2 class="section">Marchés prédictifs <span class="chip badge-aux">signal auxiliaire</span></h2>40  <p class="section-sub">Probabilités implicites des parieurs (Polymarket) comparées au modèle — deux visions indépendantes de la même question : « qui remportera le plus de sièges ? »</p>41  <div class="card" id="markets-card"><div class="loading">Chargement des marchés…</div></div>4243  <h2 class="section">Sentiment médiatique par parti</h2>44  <div class="grid parties" id="sent-cards"><div class="loading">Chargement…</div></div>45  <div class="card">46    <h3>Sentiment quotidien (moyenne mobile des scores)</h3>47    <div class="chart" id="sent-chart"></div>48    <p class="chart-note" id="sent-note"></p>49  </div>5051  <h2 class="section">Nouvelles (regroupées par grappe)</h2>52  <div class="card" style="overflow-x:auto"><table class="data" id="news"></table></div>53</div>54<footer class="site"></footer>5556<script src="/assets/app.js"></script>57<script>58(async () => {59  const meta = await initShell("/intelligence");60  const parties = Object.keys(meta.parties).filter(p => p !== "AUT");6162  document.getElementById("ask").onclick = async () => {63    const q = document.getElementById("q").value.trim();64    if (q.length < 3) return;65    const el = document.getElementById("answer");66    el.style.display = "block";67    el.textContent = "Analyse des données du modèle…";68    try {69      const r = await getJSON(`${API}/ask`, { method: "POST",70        headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question: q }) });71      el.textContent = r.answer;72    } catch (e) { el.textContent = "Erreur : " + e.message; }73  };74  document.getElementById("q").addEventListener("keydown", e => {75    if (e.key === "Enter") document.getElementById("ask").click(); });7677  try {78    const mom = await getJSON(`${API}/momentum`);79    document.getElementById("momentum").innerHTML = parties80      .filter(p => mom.momentum[p])81      .sort((a, b) => mom.momentum[b].score - mom.momentum[a].score)82      .map(p => {83        const m = mom.momentum[p];84        const up = m.score > 3, down = m.score < -3;85        return `<div class="card party-card">86          <div class="bar" style="background:${partyColor(p)}"></div>87          <div class="party-head"><span class="avatars-stack">${avatarHTML(p, 34)}</span>88            <div class="pname">${p}</div></div>89          <div class="bignum" style="color:${up ? "var(--good)" : down ? "var(--plq)" : "var(--ink)"}">90            ${m.score > 0 ? "▲" : m.score < 0 ? "▼" : "—"} ${Math.abs(m.score).toFixed(0)}</div>91          <div class="kv"><span>Δ vote 14 j</span><b>${m.d_vote_14j_pp >= 0 ? "+" : ""}${m.d_vote_14j_pp} pp</b></div>92          <div class="kv"><span>Sentiment 7 j</span><b>${m.sentiment_7j != null ? m.sentiment_7j : "—"}</b></div>93          <div class="kv"><span>Mentions 7 j</span><b>${m.volume_7j}</b></div>94        </div>`;95      }).join("");96  } catch (e) { document.getElementById("momentum").innerHTML = ""; }9798  try {99    const mk = await getJSON(`${API}/markets`);100    const rows = Object.entries(mk.implied_prob_most_seats)101      .sort((a, b) => b[1] - a[1]);102    document.getElementById("markets-card").innerHTML = `103      <table class="data">104        <tr><th>Parti</th><th>Marché (Polymarket)</th><th>Modèle QC Forecast</th><th>Écart</th></tr>105        ${rows.map(([p, v]) => {106          const m = (mk.model_prob_most_seats || {})[p];107          const d = m != null ? m - v : null;108          return `<tr><td><span class="dot" style="background:${partyColor(p)}"></span>${partyName(p)}</td>109            <td>${(v * 100).toFixed(1)} %</td>110            <td>${m != null ? (m * 100).toFixed(1) + " %" : "—"}</td>111            <td style="color:var(--muted)">${d != null ? (d >= 0 ? "+" : "−") + Math.abs(d * 100).toFixed(1) + " pts" : "—"}</td></tr>`;112        }).join("")}113      </table>114      <p class="note">${mk.note} Volume : ${mk.volume_usd.toLocaleString("fr-CA")} $ US ·115        <a href="${mk.url}" target="_blank" rel="noopener">${mk.event}</a> ·116        consulté ${new Date(mk.fetched_at).toLocaleString("fr-CA")}</p>`;117  } catch (e) {118    document.getElementById("markets-card").innerHTML =119      '<p class="note">Marchés prédictifs indisponibles pour le moment.</p>';120  }121122  const sent = await getJSON(`${API}/sentiment?days=60`);123  document.getElementById("sent-note").textContent = sent.note +124    ` (${sent.documents} documents analysés — presse québécoise via RSS)`;125  const sp = sent.parties || {};126  document.getElementById("sent-cards").innerHTML = parties.filter(p => sp[p]).map(p => {127    const s = sp[p];128    return `<div class="card party-card">129      <div class="bar" style="background:${partyColor(p)}"></div>130      <div class="pname">${p}</div>131      <div class="bignum">${s.sentiment_moyen > 0 ? "+" : ""}${s.sentiment_moyen.toFixed(2)}</div>132      <div class="kv"><span>Volume total</span><b>${s.volume}</b></div>133      <div class="kv"><span>24 h</span><b>${s.h24.volume} mentions ${s.h24.moyenne != null ? "(" + s.h24.moyenne + ")" : ""}</b></div>134      <div class="kv"><span>Positions</span><b>${s.stances.pro} pro · ${s.stances.anti} anti · ${s.stances.neutre} neutres</b></div>135    </div>`;136  }).join("") || '<div class="card"><p class="note">Aucun document de sentiment encore ingéré — le pipeline RSS alimentera cette section automatiquement.</p></div>';137138  const withTl = parties.filter(p => sp[p] && sp[p].timeline.length > 1);139  if (withTl.length) {140    const c = baseChart(document.getElementById("sent-chart"));141    c.setOption({142      grid: { left: 44, right: 20, top: 34, bottom: 28 },143      legend: { ...LEGEND, data: withTl.map(partyName) },144      tooltip: { ...TOOLTIP },145      xAxis: { type: "time", ...AXIS_STYLE, splitLine: { show: false } },146      yAxis: { type: "value", min: -1, max: 1, ...AXIS_STYLE },147      series: withTl.map(p => ({ name: partyName(p), type: "line", color: partyColor(p),148        symbol: "circle", symbolSize: 6, lineStyle: { width: 2 },149        data: sp[p].timeline.map(t => [t.date, t.sentiment]) })),150    });151  } else {152    document.getElementById("sent-chart").innerHTML =153      '<p class="loading">Pas encore assez de données quotidiennes.</p>';154  }155156  const news = await getJSON(`${API}/news`);157  document.getElementById("news").innerHTML =158    `<tr><th>Nouvelle</th><th>Source</th><th>Date</th><th>Articles</th><th>Partis (stance)</th></tr>` +159    (news.length ? news.map(n => `<tr>160      <td><a href="${n.url}" target="_blank" rel="noopener">${n.title}</a></td>161      <td>${n.source}</td><td>${fmtDate(n.published.slice(0, 10))}</td>162      <td>${n.n_articles}</td>163      <td>${Object.entries(n.parties).map(([p, s]) =>164        `<span class="chip" style="background:${partyColor(p)}22;color:${partyColor(p)}">${p} ${s.stance}</span>`).join(" ")}</td>165    </tr>`).join("") : `<tr><td colspan="5" style="color:var(--muted)">Aucune nouvelle ingérée pour l'instant.</td></tr>`);166})();167</script>168</body>169</html>170
Files 84a6c5d54c9ef9776da4377fb97a252006fc3fc2
Menu

Theme