Nessun oggetto della modifica
Etichette: Ripristino manuale Annullato
Nessun oggetto della modifica
Etichetta: Ripristino manuale
 
Riga 1 600: Riga 1 600:
     return res;
     return res;
   };
   };
})();
/* === Indice domande · stabile (salva “Titolo: …” in sidebar) === */
(() => {
  if (window.__MPAI_QINDEX_INSTALLED__) return;
  window.__MPAI_QINDEX_INSTALLED__ = true;
  const KEY = 'mpai_question_index_v1';
  function ensurePanel(){
    let box = document.getElementById('mpai-qindex');
    if (!box) {
      const aside = document.createElement('div');
      aside.id = 'mpai-qindex';
      aside.style.cssText = 'position:relative;margin:8px 0;padding:10px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;';
      aside.innerHTML = `
        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
          <b>Indice domande</b>
          <button id="mpai-qindex-clear" class="mw-ui-button">Svuota</button>
        </div>
        <input id="mpai-qindex-filter" placeholder="Filtra..." style="width:100%;padding:6px 8px;border:1px solid #ddd;border-radius:8px;margin-bottom:8px;">
        <div id="mpai-qindex-list" style="display:flex;flex-direction:column;gap:6px;max-height:40vh;overflow:auto;"></div>
      `;
      // prova a metterlo sotto il riquadro Progetti
      const projectsHeader = Array.from(document.querySelectorAll('h3,h4'))
        .find(h => /Progetti/i.test(h.textContent||''));
      if (projectsHeader && projectsHeader.parentElement) projectsHeader.parentElement.appendChild(aside);
      else (document.querySelector('.mw-parser-output') || document.body).prepend(aside);
    }
    return document.getElementById('mpai-qindex');
  }
  const load = () => { try { return JSON.parse(localStorage.getItem(KEY)||'[]'); } catch { return []; } };
  const save = (arr) => localStorage.setItem(KEY, JSON.stringify(arr));
  function render(){
    const box = ensurePanel();
    const list = box.querySelector('#mpai-qindex-list');
    const filter = (box.querySelector('#mpai-qindex-filter').value||'').toLowerCase();
    const items = load();
    list.innerHTML = '';
    items
      .filter(it => !filter || it.title.toLowerCase().includes(filter))
      .forEach(it => {
        const row = document.createElement('div');
        row.style.cssText = 'border:1px solid #eee;border-radius:8px;padding:6px 8px;cursor:pointer;';
        row.innerHTML = `<div style="font-weight:600">${it.title}</div><div style="opacity:.6;font-size:.85em">${new Date(it.ts).toLocaleString()}</div>`;
        row.onclick = () => { if (it.anchor && document.getElementById(it.anchor)) document.getElementById(it.anchor).scrollIntoView({behavior:'smooth', block:'start'}); };
        list.appendChild(row);
      });
  }
  function add(title, anchorId){
    if (!title) return;
    const items = load();
    const last = items[items.length-1];
    if (!last || last.title !== title) {
      items.push({ title, anchor: anchorId||null, ts: Date.now() });
      save(items); render();
    }
  }
  function extractTitleFromNode(node){
    const txt = (node.querySelector?.('.content')?.textContent ?? node.textContent ?? '').replace(/\s+/g,' ').trim();
    const m = txt.match(/Titolo\s*:\s*(.+?)(?:\?|$)/i) || txt.match(/^Titolo\s*:\s*(.+)$/i);
    return m ? m[1].trim() : null;
  }
  function tagBubble(el){ if (!el.id) el.id = 'mpq-' + Date.now() + '-' + Math.floor(Math.random()*1e6); return el.id; }
  function watch(){
    const root = document.getElementById('mpAI') || document.body;
    const mo = new MutationObserver(muts => {
      for (const m of muts) for (const n of m.addedNodes||[]) {
        if (!(n instanceof HTMLElement)) continue;
        const userBubble = n.matches?.('.mpai-msg.user, .mpai-bubble.user, .user') ? n
                          : n.querySelector?.('.mpai-msg.user, .mpai-bubble.user, .user');
        if (userBubble){
          const title = extractTitleFromNode(userBubble);
          if (title) add(title, tagBubble(userBubble));
        }
      }
    });
    mo.observe(root, { childList:true, subtree:true });
  }
  // bootstrap UI + binds
  const panel = ensurePanel();
  panel.querySelector('#mpai-qindex-filter').addEventListener('input', render);
  panel.querySelector('#mpai-qindex-clear').addEventListener('click', () => { localStorage.removeItem(KEY); render(); });
  // inizializza
  watch(); render();
  console.log('✅ Indice domande installato.');
})();
})();

Versione attuale delle 18:43, 3 dic 2025

/* ===================== MPAI – CHAT APP (no inline script) ===================== */

/** Guardia anti-doppio-caricamento (strumentata) */
if (window.__MP_DASHBOARD_LOADED__) {
  const src = (document.currentScript && document.currentScript.src) || '(inline)';
  try { const u = new URL(src, location.origin);
    console.warn('MP Dashboard già caricata – stop. Source:', src, 'from=', u.searchParams.get('from') || '(n/a)');
  } catch { console.warn('MP Dashboard già caricata – stop. Source:', src); }
  console.trace('Stack doppio load');
  throw new Error('MP Dashboard già caricata');
}
window.__MP_DASHBOARD_LOADED__ = true;

/** Se qualche script ha rotto $, lo ri-aggancio a jQuery */
if (!(window.$ && window.$.fn && typeof window.$.Deferred === 'function')) {
  window.$ = window.jQuery;
}

/** Shortcut locale */
window.$mp = window.$mp || (s => document.querySelector(s));

(function () {
  console.log('🧭 MPAI: init…');

  /* ===================== STILI “ChatGPT vibe” + tipografia ===================== */
  (function attachMpaiDecorCss(){
    if (document.getElementById('mpai-decor-css')) return;
    const css = `
    /* Scoping forte alla dashboard AI */
    #mpAI{ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; font-size:15px; color:#111827 }
    #mpAI .mpai-msg{
      position:relative;
      border:1px solid #e5e7eb;
      border-radius:12px;
      padding:12px 14px;
      background:#fff; /* default bianco */
      line-height:1.6;
    }
    #mpAI .mpai-msg + .mpai-msg{ margin-top:10px }

    /* GPT (assistant) a sinistra → bianco dentro, bordo verde chiaro */
    #mpAI .mpai-msg.assistant{
      padding-left:56px;
      background:#ffffff !important;
      border-color:#a7f3d0 !important;
    }
    #mpAI .mpai-msg.assistant::before{
      content:"";
      position:absolute; left:-8px; top:8px;
      width:36px; height:36px; border-radius:50%;
      background:#fff url('/resources/assets/change-your-logo.svg') center/70% no-repeat;
      border:1px solid #e5e7eb;
      box-shadow:0 1px 0 rgba(0,0,0,.02);
    }

    /* Utente a destra, azzurrino compatto */
    #mpAI .mpai-msg.user{
      background:#eaf2ff;
      border-color:#d7e6ff;
      max-width:78%;
      margin-left:auto;
    }
    #mpAI .mpai-msg.user .role,
    #mpAI .mpai-msg.user .meta{ text-align:right }

    /* Testo e meta */
    #mpAI .mpai-msg .role{ font-weight:600; margin-bottom:4px; color:#374151 }
    #mpAI .mpai-msg .content{ color:#111827; white-space:normal }
    #mpAI .mpai-msg .content p{ margin: .45em 0; }
    #mpAI .mpai-msg .content ul{ margin:.4em 0 .4em 1.2em; list-style:disc; }
    #mpAI .mpai-msg .content ol{ margin:.4em 0 .4em 1.2em; }
    #mpAI .mpai-msg .content h1,
    #mpAI .mpai-msg .content h2,
    #mpAI .mpai-msg .content h3{ margin:.6em 0 .35em; line-height:1.35 }
    #mpAI .mpai-msg .content h1{ font-size:1.25rem }
    #mpAI .mpai-msg .content h2{ font-size:1.15rem }
    #mpAI .mpai-msg .content h3{ font-size:1.05rem }
    #mpAI .mpai-msg .content code{
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
      font-size:.92em; background:#f5f7fa; padding:.1em .35em; border-radius:6px; border:1px solid #e5e7eb;
    }
    #mpAI .mpai-msg .content pre{
      background:#f8fafc; border:1px solid #e5e7eb; border-radius:10px; padding:10px; overflow:auto;
    }
    #mpAI .mpai-msg .meta{ margin-top:6px; font-size:.72em; color:#9aa4b2 }

    /* Input textarea */
    #mpAI #mpai-input{
      font-size:16px;
      line-height:1.5;
      font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
      color:#111827;
    }

    /* “Typing dots” */
    #mpAI .mpai-typing .content{ color:#6b7280 }
    #mpAI .mpai-typing .dots{ display:inline-flex; gap:6px; vertical-align:middle }
    #mpAI .mpai-typing .dot{
      width:6px; height:6px; border-radius:50%;
      background:#9aa4b2; opacity:.55; animation:mpai-bounce 1.2s infinite
    }
    #MPAI .mpai-typing .dot:nth-child(2){ animation-delay:.2s }
    #MPAI .mpai-typing .dot:nth-child(3){ animation-delay:.4s }
    @keyframes mpai-bounce{ 0%,80%,100%{ transform:translateY(0) } 40%{ transform:translateY(-4px) } }
    `;
    const s = document.createElement('style');
    s.id = 'mpai-decor-css';
    s.textContent = css;
    document.head.appendChild(s);
  })();

  /* ===================== STILI: barra anteprime (ChatGPT-like) ===================== */
  (function attachMpaiAttachCss(){
    if (document.getElementById('mpai-attach-css')) return;
    const css = `
      #mpai-attach-previews{
        margin:8px 0 0; display:flex; flex-wrap:wrap; gap:8px;
      }
      .mpai-pill{
        display:inline-flex; align-items:center; gap:8px;
        border:1px solid #e5e7eb; border-radius:10px; padding:6px 8px; background:#fff;
        box-shadow:0 1px 0 rgba(0,0,0,.02); font-size:13px;
      }
      .mpai-thumb{ width:44px; height:44px; border-radius:8px; object-fit:cover; border:1px solid #e5e7eb; }
      .mpai-icon{ width:28px; height:28px; border-radius:6px; display:inline-flex; align-items:center; justify-content:center; border:1px solid #e5e7eb; font-size:11px; color:#374151 }
      .mpai-icon.pdf{ background:#fff0f0 } .mpai-icon.doc{ background:#f0f6ff }
      .mpai-pill .name{ max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
      .mpai-pill button{ border:0; background:transparent; cursor:pointer; font-size:16px; line-height:1; color:#6b7280 }
      #mpai-input.drop-target{ outline:2px dashed #94a3b8; outline-offset:4px; background:#f8fafc }
    `;
    const s = document.createElement('style');
    s.id = 'mpai-attach-css';
    s.textContent = css;
    document.head.appendChild(s);
  })();

  (function attachMpaiGalleryCss(){
    if (document.getElementById('mpai-gallery-css')) return;
    const css = `
      .mpai-attgal{ display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; }
      .mpai-att{ display:flex; align-items:center; gap:8px; border:1px solid #e5e7eb;
                 background:#fff; border-radius:10px; padding:6px 8px; font-size:13px; }
      .mpai-att img{ width:56px; height:56px; border-radius:8px; object-fit:cover; border:1px solid #e5e7eb; }
      .mpai-att .ico{ width:28px; height:28px; border-radius:6px; display:inline-flex; align-items:center;
                      justify-content:center; border:1px solid #e5e7eb; font-size:11px; color:#374151; }
      .mpai-att .ico.pdf{ background:#fff0f0 } .mpai-att .ico.doc{ background:#f0f6ff }
      .mpai-att .name{ max-width:240px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    `;
    const s = document.createElement('style'); s.id='mpai-gallery-css'; s.textContent = css; document.head.appendChild(s);
  })();

  /* ===================== STATO ===================== */
  const mpaiState = {
    currentProject: null,
    sessionId: null,
    sessionMeta: null,      // {title, updated}
    history: [],
    attachments: []         // [{file, name, (dataUrl?)}]
  };
  /* ===================== MASTICATIONPEDIA – SYSTEM PROMPT ===================== */
/* ===== Masticationpedia – System + Template ===== */
const MASTICATIONPEDIA_SYSTEM = [
  "Sei l'assistente scientifico di Masticationpedia, esperto in neurognatologia e linguaggio medico.",
  "OBIETTIVO: spiegare l'amelogenesi imperfetta collegando genetica, fisiopatologia dello smalto (fasi: secretoria, transizione, maturazione),",
  "proteasi (MMP20/KLK4), controllo trigeminale/propriocettivo e possibili adattamenti neuromotori.",
  "STILE: identico al seguente template, senza aggiunte fuori schema."
].join(" ");

const ANSWER_TEMPLATE = [
"**Sì, la conosco bene — l’amelogenesi imperfetta (AI)** è un gruppo di displasie ereditarie dello smalto dentale.",
"Procediamo passo per passo per chiarire il quadro clinico, genetico e fisiopatologico.",
"",
"---",
"",
"🚀 **1. Definizione generale**",
"(testo)",
"",
"📊 **2. Classificazione tradizionale (clinico–genetica)**",
"(tabella sintetica come in Fig.1: I ipoplastica, II ipomaturativa, III ipocalcificata, IV mista con taurodontismo)",
"",
"🧬 **3. Genetica e biologia molecolare**",
"(AMELX, ENAM, MMP20, KLK4, FAM83H, WDR72, SLC24A4, C4orf26, ecc.; trasmissione AD/AR/X-linked; concetto genotipo-specifico)",
"",
"🦷 **4. Aspetti clinici e radiografici**",
"(segni clinici; radiografia con contrasto smalto/dentina; sensibilità)",
"",
"🧠 **5. Diagnosi differenziale**",
"(fluorosi, ipoplasie post-infettive/traumatiche, sindromi sistemiche)",
"",
"🌿 **6. Terapia e gestione**",
"(bambini: sigillature/corone pedo; adulti: restauri adesivi/corone; supporto funzionale/occlusale)",
"",
"🔭 **7. Prospettive di ricerca**",
"(peptidi tipo amelogenina, nanoparticelle HA, editing mirato per AMELX/ENAM)",
"",
"Se vuoi, nel passo successivo posso mostrarti uno schema visivo (diagramma)."
].join("\n");



  /* ======================================================================
     [NUOVO] RENDERER MARKDOWN + HIGHLIGHT + COPIA (CDN, indipendente)
     ====================================================================== */
  window.MPDash = window.MPDash || {};
  (function bootstrapRenderer(){
    // carica risorsa esterna
    function loadScript(src){return new Promise((ok,no)=>{const s=document.createElement('script');s.src=src;s.onload=ok;s.onerror=no;document.head.appendChild(s);});}
    function loadStyle(href){return new Promise((ok,no)=>{const l=document.createElement('link');l.rel='stylesheet';l.href=href;l.onload=ok;l.onerror=no;document.head.appendChild(l);});}
    async function ensureLibs(){
      if(!window.marked){ await loadScript('https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js'); }
      if(!window.hljs){
        await loadStyle('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/php.min.js');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js');
        await loadScript('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js');
      }
    }
    function decorateCodeBlock(codeEl){
      const pre = codeEl.closest('pre'); if(!pre || pre.dataset.mpdDecorated==='1') return;
      pre.dataset.mpdDecorated='1';
      const box  = document.createElement('div'); box.className='mpd-codebox';
      const bar  = document.createElement('div'); bar.className='mpd-codebox-bar';
      const acts = document.createElement('div'); acts.className='mpd-actions';
      const bW = document.createElement('button'); bW.className='mpd-btn'; bW.textContent='Wrap';
      const bC = document.createElement('button'); bC.className='mpd-btn primary'; bC.textContent='Copia';
      acts.appendChild(bW); acts.appendChild(bC); bar.appendChild(acts);
      const inner = document.createElement('div'); inner.className='mpd-codebox-inner';
      pre.parentNode.insertBefore(box, pre); inner.appendChild(pre); box.appendChild(bar); box.appendChild(inner);
      try{ hljs.highlightElement(codeEl); }catch(e){}
      let wr=false; bW.onclick=()=>{wr=!wr; codeEl.classList.toggle('mpd-wrap',wr);};
      bC.onclick=async()=>{ try{await navigator.clipboard.writeText(codeEl.textContent); const t=bC.textContent; bC.textContent='Copiato!'; setTimeout(()=>bC.textContent=t,900);}catch(e){alert('Copia non riuscita: '+e.message);} };
    }
    MPDash.renderMarkdownInto = async function(container, markdownText){
      await ensureLibs();
      const html = window.marked.parse(String(markdownText||''));
      container.innerHTML = html;
      container.querySelectorAll('pre code').forEach(codeEl=>{
        if(!codeEl.className.includes('language-')) codeEl.classList.add('language-plaintext');
        decorateCodeBlock(codeEl);
      });
    };
  })();
  /* ====================================================================== */

  /* ===================== RENDER “MARKDOWN SAFE” (legacy) ===================== */
  function escapeHtml(s){
    return s.replace(/[&<>"]/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));
  }
  function renderMarkdown(src){
    let s = escapeHtml(String(src || ''));
    s = s.replace(/^\s*####\s+(.+)$/gm, '<h4>$1</h4>');
    s = s.replace(/^\s*#####\s+(.+)$/gm, '<h5>$1</h5>');
    s = s.replace(/```([\s\S]*?)```/g, (_m, code) => `<pre><code>${code.trim()}</code></pre>`);
    s = s.replace(/`([^`]+?)`/g, (_m, code) => `<code>${code}</code>`);
    s = s.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
    s = s.replace(/(^|[^\*])\*([^*]+?)\*(?!\*)/g, (_m, pre, it) => `${pre}<em>${it}</em>`);
    s = s.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, `<a href="$2" target="_blank" rel="nofollow noopener">$1</a>`);
    s = s.replace(/^\s*###\s+(.+)$/gm, '<h3>$1</h3>');
    s = s.replace(/^\s*##\s+(.+)$/gm, '<h2>$1</h2>');
    s = s.replace(/^\s*#\s+(.+)$/gm, '<h1>$1</h1>');
    s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block => {
      const items = block.trim().split('\n').map(line => line.replace(/^\-\s+/, '').trim());
      return `<ul>${items.map(it => `<li>${it}</li>`).join('')}</ul>`;
    });
    s = s.split(/\n{2,}/).map(par => {
      if (/^<h\d|^<ul>|^<pre>|^<blockquote>|^<table>/.test(par.trim())) return par;
      return `<p>${par.trim().replace(/\n/g,'<br>')}</p>`;
    }).join('\n');
    return s;
  }

  /* ===================== UI HELPERS ===================== */
  function mpaiTime(ts){
    try { return new Date(ts||Date.now()).toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'}); }
    catch { return ''; }
  }

  function mpaiAppendMsg(role, content) {
    const chatEl = $mp('#mpai-chat');
    if (!chatEl) return;

    const div = document.createElement('div');
    div.className = 'mpai-msg ' + (role === 'user' ? 'user' : (role === 'assistant' ? 'assistant' : 'error'));
    div.innerHTML = `
      <div class="role">${role==='user'?'Tu':role==='assistant'?'GPT':'Errore'}</div>
      <div class="content"><em>…</em></div>
      <div class="meta">${mpaiTime()}</div>`;
    chatEl.appendChild(div);
    chatEl.scrollTop = chatEl.scrollHeight;

    // ⬇️ NUOVO: usa renderer avanzato (colori + copia). Fallback al legacy se serve
    const container = div.querySelector('.content');
    if (role === 'assistant' || /```/.test(String(content||''))) {
      // evidenziazione richiesta
      if (window.MPDash && typeof window.MPDash.renderMarkdownInto === 'function') {
        window.MPDash.renderMarkdownInto(container, String(content||''));
      } else {
        container.innerHTML = renderMarkdown(content);
      }
    } else {
      // messaggi utente: render leggero (grassetti/link/inline code)
      container.innerHTML = renderMarkdown(content);
    }
  }

  function mpaiShowTyping(role='assistant'){
    const chatEl = $mp('#mpai-chat'); if (!chatEl) return null;
    const div = document.createElement('div');
    div.className = 'mpai-msg mpai-typing ' + (role==='user'?'user':'assistant');
    div.innerHTML = `
      <div class="role">${role==='user'?'Tu':'GPT'}</div>
      <div class="content"><span class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span></div>`;
    chatEl.appendChild(div);
    chatEl.scrollTop = chatEl.scrollHeight;
    return div;
  }
  function mpaiHideTyping(el){ try{ el && el.remove(); }catch{} }

  /* ===================== PREVIEW BAR (nuova) ===================== */
  function mpaiEnsurePreviewBar(){
    const ta = document.getElementById('mpai-input');
    if (!ta) return null;
    let bar = document.getElementById('mpai-attach-previews');
    if (!bar) {
      bar = document.createElement('div');
      bar.id = 'mpai-attach-previews';
      ta.insertAdjacentElement('afterend', bar);
    }
    return bar;
  }

  function mpaiRenderFiles(){
    const bar = mpaiEnsurePreviewBar(); if (!bar) return;
    bar.innerHTML = '';
    (mpaiState.attachments || []).forEach((a,i)=>{
      const f = a?.file; const name = (a?.name || f?.name || 'file').toString();
      const type = (f?.type || '').toLowerCase();

      const pill = document.createElement('div'); pill.className = 'mpai-pill';

      if (type.startsWith('image/')) {
        const img = document.createElement('img'); img.className='mpai-thumb';
        if (a.dataUrl) img.src = a.dataUrl;
        else {
          try { const r = new FileReader(); r.onload = e=> img.src = e.target.result; r.readAsDataURL(f); }
          catch {}
        }
        pill.appendChild(img);
      } else {
        const ic = document.createElement('div'); ic.className='mpai-icon';
        if (type === 'application/pdf' || name.endsWith('.pdf')) { ic.classList.add('pdf'); ic.textContent='PDF'; }
        else if (name.endsWith('.doc') || name.endsWith('.docx')) { ic.classList.add('doc'); ic.textContent='DOC'; }
        else { ic.textContent='FILE'; }
        pill.appendChild(ic);
      }

      const span = document.createElement('span'); span.className='name'; span.title = name; span.textContent = name;
      const btn  = document.createElement('button'); btn.type='button'; btn.title='Rimuovi'; btn.textContent='✕';
      btn.onclick = ()=>{ mpaiState.attachments.splice(i,1); mpaiRenderFiles(); };

      pill.appendChild(span); pill.appendChild(btn);
      bar.appendChild(pill);
    });
  }
  
  
  
  
  // === [ Pulsante "Pulisci URL" universale ] =================================
// === Pulsante "Pulisci URL" ===
(function attachClearUrlButton() {
  const KEY = 'mp_context_files_v1';

  function makeButton() {
    const b = document.createElement('a');
    b.id = 'mpai-clear-context';
    b.textContent = 'Pulisci URL';
    b.href = '#';
    // Copia stile del link vicino (pill verde)
    b.className = 'mw-ui-button mw-ui-progressive'; // stile coerente
    b.style.marginLeft = '8px';
    b.addEventListener('click', (e) => {
      e.preventDefault();
      // 1) svuota stato locale (se esiste)
      try { if (window.mpaiState && Array.isArray(window.mpaiState.attachments)) {
        window.mpaiState.attachments = [];
        if (typeof window.mpaiRenderFiles === 'function') window.mpaiRenderFiles();
      }} catch {}
      // 2) svuota localStorage “persistente”
      try { localStorage.removeItem(KEY); } catch {}
      // 3) conferma
      alert('Pulizia completata — URL e allegati locali rimossi.');
    });
    return b;
  }

  function placeButton() {
    // 1) prova a trovarlo già
    if (document.getElementById('mpai-clear-context')) return;

    // 2) trova il link "Importa Wikitesto da URL"
    const importBtn = Array.from(document.querySelectorAll('a,button'))
      .find(el => (el.textContent || '').trim() === 'Importa Wikitesto da URL');

    const clearBtn = makeButton();

    if (importBtn && importBtn.parentElement) {
      // Inseriscilo subito dopo
      importBtn.parentElement.insertBefore(clearBtn, importBtn.nextSibling);
    } else {
      // Fallback: mettilo nella barra comandi in alto (o body)
      const bar = document.querySelector('.dashboard-controls, .dashboard-buttons') || document.body;
      bar.appendChild(clearBtn);
    }
  }

  // Aggancia quando il nodo compare
  const obs = new MutationObserver(() => placeButton());
  obs.observe(document.documentElement, {childList: true, subtree: true});
  // E prova subito
  placeButton();
})();





  /* ===================== GALLERIA FILE ALLEGATI (persistente nella chat) ===================== */
  function mpaiSanitizeAttForHistory(a){
    const name = (a?.name || a?.file?.name || 'file').toString();
    const type = (a?.file?.type || '').toLowerCase();
    const dataUrl = a?.dataUrl || null;
    return { name, type, dataUrl };
  }

  function mpaiAppendAttachmentGallery(role, atts){
    if (!atts || !atts.length) return;
    const chatEl = $mp('#mpai-chat'); if (!chatEl) return;

    const div = document.createElement('div');
    div.className = 'mpai-msg ' + (role === 'user' ? 'user' : 'assistant');
    div.innerHTML = `<div class="role">${role==='user'?'Tu':'GPT'}</div><div class="content"></div><div class="meta">${mpaiTime()}</div>`;

    const gal = document.createElement('div');
    gal.className = 'mpai-attgal';

    atts.forEach(a=>{
      const item = document.createElement('div'); item.className = 'mpai-att';
      if ((a.type||'').startsWith('image/') && a.dataUrl){
        const img = document.createElement('img'); img.src = a.dataUrl; item.appendChild(img);
      } else {
        const ico = document.createElement('div'); ico.className='ico';
        if (a.name.toLowerCase().endsWith('.pdf')) { ico.classList.add('pdf'); ico.textContent='PDF'; }
        else if (/\.(docx?|odt)$/i.test(a.name)) { ico.classList.add('doc'); ico.textContent='DOC'; }
        else { ico.textContent='FILE'; }
        item.appendChild(ico);
      }
      const nm = document.createElement('span'); nm.className='name'; nm.title=a.name; nm.textContent=a.name;
      item.appendChild(nm);
      gal.appendChild(item);
    });

    div.querySelector('.content').appendChild(gal);
    chatEl.appendChild(div);
    chatEl.scrollTop = chatEl.scrollHeight;
  }

  /* ===================== FILE HELPERS ===================== */
  async function fileToDataURL(file){
    return new Promise((resolve, reject)=>{
      const r = new FileReader();
      r.onload = () => resolve(r.result);
      r.onerror = reject;
      r.readAsDataURL(file);
    });
  }

  async function fileToText(file){
    return new Promise((resolve, reject)=>{
      const r = new FileReader();
      r.onload = () => resolve(String(r.result || ''));
      r.onerror = reject;
      r.readAsText(file, 'utf-8');
    });
  }

  // Raccoglie: img/pdf/doc/docx → files[], txt/md → texts[]
  async function collectAttachedPayload(){
    const files = [];
    const texts = [];
    for (const att of (mpaiState.attachments || [])) {
      const f = att?.file; if (!f) continue;
      const name = (f.name || '').toLowerCase();
      const type = (f.type || '').toLowerCase();

      const MAX_TEXT = 2 * 1024 * 1024;   // 2 MB (txt/md)
      const MAX_BIN  = 12 * 1024 * 1024;  // 12 MB (img/pdf/doc/docx)

      // TXT / MD
      if (type === 'text/plain' || name.endsWith('.txt') || name.endsWith('.md')) {
        if (f.size > MAX_TEXT) { console.warn('Testo troppo grande, salto:', name); continue; }
        const t = await fileToText(f);
        if (t && t.trim()) texts.push(t);
        continue;
      }

      // Immagini
      if (['image/png','image/jpeg','image/webp'].includes(type)) {
        if (f.size > MAX_BIN) { console.warn('Immagine troppo grande, salto:', name); continue; }
        const dataUrl = await fileToDataURL(f);
        files.push({ name: f.name, type, dataUrl });
        continue;
      }

      // PDF
      if (type === 'application/pdf' || name.endsWith('.pdf')) {
        if (f.size > MAX_BIN) { console.warn('PDF troppo grande, salto:', name); continue; }
        const dataUrl = await fileToDataURL(f);
        files.push({ name: f.name, type: 'application/pdf', dataUrl });
        continue;
      }

      // DOC / DOCX
      const isDoc  = type === 'application/msword' || name.endsWith('.doc');
      const isDocx = type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name.endsWith('.docx');
      if (isDoc || isDocx) {
        if (f.size > MAX_BIN) { console.warn('DOC/DOCX troppo grande, salto:', name); continue; }
        const dataUrl = await fileToDataURL(f);
        files.push({
          name: f.name,
          type: isDoc ? 'application/msword'
                      : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
          dataUrl
        });
        continue;
      }

      console.warn('Tipo non supportato:', f.name, f.type);
    }
    return { files, texts };
  }

  // (opzionale) raccolta testi diagnostica
  async function collectAttachedTexts(){
    const out = [];
    for (const att of (mpaiState.attachments || [])) {
      const f = att.file; if (!f) continue;
      const name = (f.name || '').toLowerCase();
      const type = (f.type || '').toLowerCase();

      const MAX_TEXT = 2 * 1024 * 1024;
      const MAX_DOC  = 12 * 1024 * 1024;

      if (type === 'text/plain' || name.endsWith('.txt') || name.endsWith('.md')) {
        if (f.size > MAX_TEXT) { console.warn('Testo troppo grande, salto:', name); continue; }
        const text = await fileToText(f);
        out.push({ kind:'text', name, data:text });
        continue;
      }
      if (type === 'application/pdf' || name.endsWith('.pdf')) {
        if (f.size > MAX_DOC) { console.warn('PDF troppo grande, salto:', name); continue; }
        const dataUrl = await fileToDataURL(f);
        out.push({ kind:'pdf', name, data:dataUrl });
        continue;
      }
      const isDoc  = type === 'application/msword' || name.endsWith('.doc');
      const isDocx = type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name.endsWith('.docx');
      if (isDoc || isDocx) {
        if (f.size > MAX_DOC) { console.warn('DOC/DOCX troppo grande, salto:', name); continue; }
        const dataUrl = await fileToDataURL(f);
        out.push({ kind:'doc', name, data:dataUrl });
        continue;
      }
    }
    return out;
  }

  /* ===================== PERSISTENZA ===================== */
  function mpaiSaveLocal() {
    if (!mpaiState.currentProject) return;
    localStorage.setItem('mpai.hist.' + mpaiState.currentProject, JSON.stringify(mpaiState.history.slice(-200)));
  }
  function mpaiLoadLocal(project) {
    const chatEl = $mp('#mpai-chat'); if (!chatEl) return;
    const raw = localStorage.getItem('mpai.hist.' + project);
    mpaiState.history = raw ? JSON.parse(raw) : [];
    chatEl.innerHTML = '';
    if (!mpaiState.history.length) {
      mpaiAppendMsg('assistant', 'Pronto! Dimmi cosa vuoi fare su "' + project + '". Allegami anche file sanificati se servono.');
    } else {
      mpaiState.history.forEach(m => mpaiAppendMsg(m.role, m.content));
    }
  }

  function mpaiEnsureSession() {
    if (!mpaiState.sessionId) {
      mpaiState.sessionId = 'sess-' + Date.now();
      mpaiState.sessionMeta = { title: 'Nuova conversazione', updated: Date.now() };
      localStorage.setItem('mpai.session.meta.' + mpaiState.sessionId, JSON.stringify(mpaiState.sessionMeta));
    }
  }
  function mpaiSaveSession() {
    if (!mpaiState.sessionId) return;
    const key = 'mpai.session.hist.' + mpaiState.sessionId;
    localStorage.setItem(key, JSON.stringify(mpaiState.history.slice(-200)));
    mpaiState.sessionMeta.updated = Date.now();
    localStorage.setItem('mpai.session.meta.' + mpaiState.sessionId, JSON.stringify(mpaiState.sessionMeta));
  }
  function mpaiLoadSession(id) {
    const chatEl = $mp('#mpai-chat'); if (!chatEl) return;
    mpaiState.sessionId = id;
    const key = 'mpai.session.hist.' + id;
    const raw = localStorage.getItem(key);
    mpaiState.history = raw ? JSON.parse(raw) : [];
    const metaRaw = localStorage.getItem('mpai.session.meta.' + id);
    mpaiState.sessionMeta = metaRaw ? JSON.parse(metaRaw) : { title: 'Nuova conversazione', updated: Date.now() };
    chatEl.innerHTML = '';
    if (!mpaiState.history.length) {
      mpaiAppendMsg('assistant', 'Pronto! Dimmi cosa vuoi fare. Puoi allegare file sanificati.');
    } else {
      mpaiState.history.forEach(m => mpaiAppendMsg(m.role, m.content));
    }
  }

  /* ===================== PROGETTI (sidebar) ===================== */
  async function mpaiLoadProjects() {
    const box = $mp('#mpai-project-list');
    if (!box) return;
    box.innerHTML = '<em class="muted">Carico progetti…</em>';
    try {
      const r = await fetch('/dashboard/api/project_list.php', { cache: 'no-store', credentials: 'include' });
      const j = await r.json();
      const arr = Array.isArray(j) ? j : (Array.isArray(j.projects) ? j.projects : []);
      if (!arr.length) { box.innerHTML = '<em class="muted">Nessun progetto</em>'; return; }
      box.innerHTML = '';
      arr.forEach(item => {
        const name = (typeof item === 'string') ? item : (item.name || item);
        const row = document.createElement('div');
        row.className = 'row';
        row.innerHTML = `<span class="title">${name}</span>
                         <span><button class="mpai-btn" data-open="${name}">Apri</button></span>`;
        row.querySelector('[data-open]').onclick = () => {
          mpaiState.currentProject = name;
          mpaiLoadLocal(name);
        };
        box.appendChild(row);
      });
      if (!mpaiState.currentProject) {
        const first = (typeof arr[0] === 'string') ? arr[0] : (arr[0].name || arr[0]);
        mpaiState.currentProject = first;
        mpaiLoadLocal(first);
      }
    } catch (e) {
      box.innerHTML = '<span class="muted">Errore caricamento: ' + e.message + '</span>';
    }
  }

  async function mpaiCreateProject() {
    const inp = $mp('#mpai-project-name');
    const name = (inp?.value || '').trim();
    if (!name) { alert('Inserisci un nome progetto'); return; }
    try {
      const r = await fetch('/dashboard/api/project_create.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ name })
      });
      const txt = await r.text(); let j; try { j = JSON.parse(txt) } catch { j = { ok: false, raw: txt }; }
      if (j.ok !== false) { await mpaiLoadProjects(); mpaiState.currentProject = name; mpaiState.history = []; mpaiLoadLocal(name); inp.value = ''; }
      else { alert('Errore creazione: ' + (j.error || '')); }
    } catch (e) { alert('Errore rete: ' + e.message); }
  }


/* ===== INVIO PROMPT → PROXY (usa messages + determinismo) ===== */
window.sendPrompt = async function () {
  try {
    const ta = document.querySelector('#mpai-input');
    if (!ta) return;

    const userQuestion = (ta.value || '').trim();
    if (!userQuestion) { ta.focus(); return; }

    // UI immediata
    mpaiState.history.push({ role: 'user', content: userQuestion });
    mpaiAppendMsg('user', userQuestion);
    ta.value = '';
    const sentAtt = (mpaiState.attachments || []).map(mpaiSanitizeAttForHistory);
    mpaiAppendAttachmentGallery('user', sentAtt);

    // Modello (fallback al tuo)
    const sel = document.querySelector('#mpai-model');
    const DEFAULT_MODEL = 'gpt-4o-2024-05-13';
    const model = (sel && sel.value && sel.value.trim()) ? sel.value.trim() : DEFAULT_MODEL;

    const pending = mpaiShowTyping('assistant');

    // Allegati
    let files = [], texts = [];
    try { const p = await collectAttachedPayload(); files = p.files; texts = p.texts; } catch(e){}

    // Contesto importato
    try {
      const ctx = JSON.parse(localStorage.getItem('mp_context_files_v1') || '{}');
      const proj = (typeof currentProjectName !== 'undefined' && currentProjectName) ? currentProjectName : 'AI-Chapters';
      const filesFromCtx = Array.isArray(ctx[proj]) ? ctx[proj] : [];
      files = Array.from(new Set([...(files||[]), ...filesFromCtx]));
    } catch {}

    // === QUI LA DIFFERENZA: inviamo anche "messages" (chat) ===
    const messages = [
      { role: "system", content: MASTICATIONPEDIA_SYSTEM },
      // “few-shot” stile: il template obbliga struttura/emoji/ordine
      { role: "assistant", content: ANSWER_TEMPLATE },
      { role: "user", content: userQuestion }
    ];

    // Payload per proxy (retro-compatibile: mando sia "messages" sia "prompt")
    const payload = {
      model,
      messages,                 // <— NUOVO: preferito dal proxy patchato
      prompt: [
        MASTICATIONPEDIA_SYSTEM,
        "RISPONDI SEGUENDO ESATTAMENTE IL TEMPLATE SOTTO.",
        ANSWER_TEMPLATE,
        "DOMANDA:", userQuestion
      ].join("\n\n"),
      files,
      texts,
      // determinismo/completezza
      temperature: 0.0,
      top_p: 1,
      presence_penalty: 0.0,
      frequency_penalty: 0.0,
      max_tokens: 2200,
      seed: 77                 // richiede supporto lato proxy; altrimenti ignorato
    };

    const r = await fetch('/dashboard/api/openai_project_gpt.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      cache: 'no-store',
      body: JSON.stringify(payload)
    });

    const raw = await r.text();
    mpaiHideTyping(pending);
    if (!r.ok) { mpaiAppendMsg('error', `HTTP ${r.status} ${r.statusText}\n\n${raw || '(nessun body)'}`); return; }

    const ct = (r.headers.get('content-type') || '').toLowerCase();
    let outText = '';
    if (ct.includes('application/json')) {
      try {
        let j = JSON.parse(raw);
        if (typeof j === 'string') { try { j = JSON.parse(j); } catch {} }
        outText = j?.output_text || j?.text || j?.message || j?.reply || j?.result || String(j||'');
      } catch { outText = raw; }
    } else {
      outText = raw;
    }
    outText = String(outText || '').replace(/\\n/g, '\n').replace(/\r/g, '');
    if (!outText.trim()) outText = '[vuoto]';

    mpaiState.history.push({ role: 'assistant', content: outText });
    mpaiAppendMsg('assistant', outText);
    mpaiSaveLocal();
    mpaiState.attachments = []; mpaiRenderFiles();

  } catch (e) {
    mpaiAppendMsg('error', `Errore di rete/JS: ${e.message}`);
  }
};



  /* ===================== SHOW UI + BIND UNA SOLA VOLTA ===================== */
  window.showMpAI = function () {
    ['api-settings', 'project-status', 'chatgpt-plus', 'test-tools', 'activity-log']
      .forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; });
    const ai = document.getElementById('mpAI');
    if (ai) { ai.style.display = 'block'; ai.scrollIntoView({ behavior: 'smooth' }); }
    mpaiBindUI();
  };

  function mpaiBindUI() {
    if (window.__MP_AI_BOUND__) return;
    window.__MP_AI_BOUND__ = true;

    if (!$mp('#mpAI')) return;

    // Nascondo la vecchia dropzone/lista (restano compat per chi già li usa)
    const oldDz = document.getElementById('mpai-dropzone');
    if (oldDz) oldDz.style.display = 'none';
    const oldList = document.getElementById('mpai-file-list');
    if (oldList) oldList.style.display = 'none';

    // Assicuro la barra anteprime sotto la textarea
    mpaiEnsurePreviewBar();

    // Drag & Drop direttamente nella textarea
    const ta = document.getElementById('mpai-input');
    if (ta) {
      ta.addEventListener('dragover', (e)=>{ e.preventDefault(); ta.classList.add('drop-target'); });
      ta.addEventListener('dragleave', ()=> ta.classList.remove('drop-target'));
      ta.addEventListener('drop', (e)=>{
        e.preventDefault(); ta.classList.remove('drop-target');
        const files = Array.from(e.dataTransfer.files || []);
        files.forEach(f=> mpaiState.attachments.push({file:f, name:f.name}));
        mpaiRenderFiles();
      });
    }

    // Input file nascosto per click sulla barra anteprime
    let hiddenInput = document.getElementById('mpai-file-input');
    if (!hiddenInput) {
      hiddenInput = document.createElement('input');
      hiddenInput.type = 'file'; hiddenInput.id = 'mpai-file-input'; hiddenInput.multiple = true; hiddenInput.style.display='none';
      document.body.appendChild(hiddenInput);
    }
    hiddenInput.onchange = ()=>{
      Array.from(hiddenInput.files||[]).forEach(f=> mpaiState.attachments.push({file:f,name:f.name}));
      hiddenInput.value=''; mpaiRenderFiles();
    };

    const bar = document.getElementById('mpai-attach-previews');
    if (bar) { bar.addEventListener('click', ()=> hiddenInput.click()); }

    // Bottoni
    $mp('#mpai-send')?.addEventListener('click', window.sendPrompt);
    $mp('#mpai-project-create')?.addEventListener('click', mpaiCreateProject);
    $mp('#mpai-new-chat')?.addEventListener('click', ()=>{
      mpaiState.history=[]; mpaiState.attachments=[]; mpaiRenderFiles();
      const chatEl = $mp('#mpai-chat'); if (chatEl){ chatEl.innerHTML=''; }
      if (mpaiState.currentProject){
        mpaiAppendMsg('assistant','Nuova chat per "'+mpaiState.currentProject+'".'); mpaiSaveLocal();
      } else {
        mpaiState.sessionId=null; mpaiState.sessionMeta=null; mpaiEnsureSession();
        mpaiAppendMsg('assistant','Nuova conversazione.'); mpaiSaveSession();
      }
    });
    $mp('#mpai-clear')?.addEventListener('click', ()=>{
      if (!mpaiState.currentProject){ alert('Crea o seleziona un progetto'); return; }
      if (!confirm('Svuotare la conversazione locale?')) return;
      mpaiState.history=[]; mpaiState.attachments=[]; mpaiRenderFiles();
      const chatEl = $mp('#mpai-chat'); if (chatEl){ chatEl.innerHTML=''; }
      mpaiAppendMsg('assistant','Conversazione pulita.'); mpaiSaveLocal();
    });

    // Invio con Cmd/Ctrl+Enter nella textarea
    document.addEventListener('keydown', (e)=>{
      if ((e.metaKey||e.ctrlKey) && e.key==='Enter'){
        const ta=$mp('#mpai-input');
        if (ta && ta===document.activeElement){ e.preventDefault(); window.sendPrompt(); }
      }
    });

    // Boot
    mpaiLoadProjects();
    console.log('🔗 MPAI listeners collegati.');
  }

  // Bind on ready (se la UI è già presente)
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => { if ($mp('#mpAI')) mpaiBindUI(); });
  } else {
    if ($mp('#mpAI')) mpaiBindUI();
  }

  console.log('✅ MPAI: pronta.');
})();

// === [UI] Pulsante alto: "Importa Wikitesto da URL" (salva come file di progetto) ===
(() => {
  // Trova la testata della sezione "Masticationpedia AI"
  function findAiHeaderNode() {
    const candidates = Array.from(document.querySelectorAll('h2,h3,.section-title,.mp-section-title'));
    const n = candidates.find(el => /Masticationpedia\s*AI/i.test(el.textContent || ''));
    if (n) return n;
    // Fallback: colonna centrale
    return document.querySelector('#content .mw-parser-output') || document.body;
  }

  const header = findAiHeaderNode();
  if (!header) return;

  // Pulsante compatto accanto al titolo
  const btn = document.createElement('button');
  btn.textContent = 'Importa Wikitesto da URL';
  btn.title = 'Scarica una pagina MediaWiki e salvala come file di progetto';
  btn.className = 'mp-btn-import-url';
  btn.style.cssText = 'margin-left:.5rem;padding:.35rem .7rem;border:1px solid #444;border-radius:10px;cursor:pointer;font-size:.9rem;';
  header.appendChild(btn);

  // ----------------- Modale + logica import -----------------
  function openModal() {
    const modal = document.createElement('div');
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;display:flex;align-items:center;justify-content:center;';

    // Legge i progetti dalla UI; fallback se non trovati
    const projects = (() => {
      const names = new Set();
      document.querySelectorAll('.mp-project-name,[data-project-name],.mp-proj-item').forEach(n => {
        const t = (n.getAttribute('data-project-name') || n.textContent || '').trim();
        if (t) names.add(t);
      });
      return Array.from(names).length ? Array.from(names) : ['AI-Chapters','SSO_Linkedin'];
    })();

    modal.innerHTML = `
      <div style="background:#fff;max-width:720px;width:92%;padding:1rem 1.1rem;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.15);">
        <h3 style="margin:.2rem 0 1rem;">Importa Wikitesto da URL</h3>
        <label style="display:block;margin:.4rem 0 .2rem;">URL della pagina (MediaWiki / Wikipedia / GitHub raw):</label>
        <input id="mp-url" type="url" placeholder="https://staging.masticationpedia.org/wiki/Introduction" style="width:100%;padding:.6rem;border:1px solid #ccc;border-radius:8px;">
        <div style="display:flex;gap:.8rem;flex-wrap:wrap;margin-top:.8rem;">
          <div style="flex:1;min-width:200px;">
            <label style="display:block;margin:.4rem 0 .2rem;">Progetto:</label>
            <select id="mp-proj" style="width:100%;padding:.55rem;border:1px solid #ccc;border-radius:8px;">
              ${projects.map(p=>`<option value="${p}">${p}</option>`).join('')}
            </select>
          </div>
          <div style="flex:1;min-width:220px;">
            <label style="display:block;margin:.4rem 0 .2rem;">Nome file (opzionale):</label>
            <input id="mp-fn" type="text" placeholder="Introduction.wiki" style="width:100%;padding:.55rem;border:1px solid #ccc;border-radius:8px;">
          </div>
        </div>
        <div id="mp-status" style="margin-top:.6rem;font-size:.9rem;opacity:.85;"></div>
        <div style="margin-top:1rem;display:flex;justify-content:flex-end;gap:.6rem;">
          <button id="mp-cancel" style="padding:.5rem 1rem;border:1px solid #666;border-radius:10px;background:#fff;cursor:pointer;">Annulla</button>
          <button id="mp-go" style="padding:.5rem 1rem;border:1px solid #333;border-radius:10px;background:#111;color:#fff;cursor:pointer;">Importa</button>
        </div>
      </div>
    `;
    document.body.appendChild(modal);

    const elUrl = modal.querySelector('#mp-url');
    const elProj = modal.querySelector('#mp-proj');
    const elFn = modal.querySelector('#mp-fn');
    const elStatus = modal.querySelector('#mp-status');
    modal.querySelector('#mp-cancel').onclick = () => modal.remove();

    // --------- Gestione "contesto" (localStorage + badge) ---------
    const MP_CTX_KEY = 'mp_context_files_v1';
    const ctxGet = () => { try { return JSON.parse(localStorage.getItem(MP_CTX_KEY) || '{}'); } catch { return {}; } };
    const ctxSet = (o) => localStorage.setItem(MP_CTX_KEY, JSON.stringify(o));

    const renderBadge = (project, lastName) => {
      let host = document.querySelector(`#mp-proj-badge-${CSS.escape(project)}`);
      if (!host) {
        let box = document.querySelector('.mp-projects') || document.querySelector('#mp-projects') || document.querySelector('.mw-parser-output') || document.body;
        host = document.createElement('div');
        host.id = `mp-proj-badge-${project}`;
        host.style.cssText = 'margin:.4rem 0;padding:.35rem .6rem;border:1px dashed #7c7;display:inline-flex;gap:.5rem;border-radius:10px;background:#f6fff6;';
        box.appendChild(host);
      }
      host.innerHTML = `📎 <b>${project}</b> — ultimo import: <code>${lastName}</code>`;
    };

    const addToContext = (project, relativePath, filename) => {
      const ctx = ctxGet();
      if (!ctx[project]) ctx[project] = [];
      if (!ctx[project].includes(relativePath)) ctx[project].push(relativePath);
      ctxSet(ctx);
      renderBadge(project, filename || relativePath.split('/').pop());
      if (window.mpRequestContext && typeof window.mpRequestContext.onChange === 'function') {
        window.mpRequestContext.onChange(ctx);
      }
    };

    // --------- Azione IMPORTA ---------
    modal.querySelector('#mp-go').onclick = async () => {
      const url = (elUrl.value || '').trim();
      const project = (elProj.value || '').trim();
      let filename = (elFn.value || '').trim();

      if (!url) { elStatus.textContent = 'Inserisci una URL valida.'; elStatus.style.color = '#b00'; return; }
      elStatus.textContent = 'Import in corso...'; elStatus.style.color = '';

      try {
        const form = new FormData();
        form.append('url', url);
        form.append('save', '1');
        form.append('project', project);
        if (filename) form.append('filename', filename);

        const res = await fetch('/dashboard/api/fetch_wikitext.php', { method: 'POST', body: form });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const j = await res.json();
        if (!j.ok) throw new Error(j.error || 'Errore sconosciuto');

        elStatus.innerHTML = '✅ Import riuscito: <code>' + j.relative + '</code> (' + j.bytes + ' bytes)<br><small>Origine: ' + j.resolved_url + '</small>';
        elStatus.style.color = '#0a0';

        // Aggiungi al contesto e badge
        addToContext(j.project, j.relative, j.filename);

        // Toast
        (function toast(msg){
          const t = document.createElement('div');
          t.textContent = msg;
          t.style.cssText = 'position:fixed;right:14px;bottom:16px;background:#111;color:#fff;padding:.6rem .9rem;border-radius:10px;box-shadow:0 8px 20px rgba(0,0,0,.25);z-index:999999;';
          document.body.appendChild(t);
          setTimeout(()=>t.remove(), 2800);
        })('File aggiunto al contesto: ' + j.filename);

        // Auto-chiudi modale
        setTimeout(() => { modal.remove(); }, 800);
      } catch (e) {
        elStatus.textContent = 'Errore: ' + (e && e.message ? e.message : e);
        elStatus.style.color = '#b00';
      }
    };
  }

  // Ripristina badge all’avvio per progetti già con contesto
  (function bootstrapContextBadges(){
    const MP_CTX_KEY = 'mp_context_files_v1';
    let ctx = {};
    try { ctx = JSON.parse(localStorage.getItem(MP_CTX_KEY) || '{}'); } catch { ctx = {}; }
    Object.keys(ctx).forEach(p => {
      const arr = ctx[p];
      if (arr && arr.length) {
        const lastName = arr[arr.length - 1].split('/').pop();
        let host = document.querySelector('#mp-proj-badge-' + CSS.escape(p));
        if (!host) {
          let box = document.querySelector('.mp-projects') || document.querySelector('#mp-projects') || document.querySelector('.mw-parser-output') || document.body;
          host = document.createElement('div');
          host.id = 'mp-proj-badge-' + p;
          host.style.cssText = 'margin:.4rem 0;padding:.35rem .6rem;border:1px dashed #7c7;display:inline-flex;gap:.5rem;border-radius:10px;background:#f6fff6;';
          box.appendChild(host);
        }
        host.innerHTML = '📎 <b>' + p + '</b> — ultimo import: <code>' + lastName + '</code>';
      }
    });
  })();

  btn.addEventListener('click', openModal);
})();

/* ===== MPAI SHIM v2: normalizza la risposta di /dashboard/api/openai_project_gpt.php in JSON ===== */
/*
(() => {
  const TARGET = '/dashboard/api/openai_project_gpt.php';

  function normalizeToOkJson(raw) {
    try {
      const j = JSON.parse(raw);
      // Se è già JSON "ok", lascio così
      if (j && (j.ok === true || (j.status && String(j.status).toLowerCase() === 'ok'))) return JSON.stringify(j);
      // Se è JSON ma non nel formato atteso, provo ad estrarre un testo sensato
      const guess = j && (j.text || j.message || j.reply || (typeof j === 'string' ? j : JSON.stringify(j)));
      return JSON.stringify({ ok: true, status: 'ok', text: String(guess || '') });
    } catch {
      // Non-JSON: impacchetto in JSON compatibile con la UI
      return JSON.stringify({ ok: true, status: 'ok', text: String(raw || '') });
    }
  }
  // Hook fetch
  if (window.fetch) {
    const _fetch = window.fetch;
    window.fetch = async function(url, opts) {
      const res = await _fetch.apply(this, arguments);
      try {
        const u = (typeof url === 'string') ? url : (url && url.url) || '';
        if (u.includes(TARGET)) {
          const clone = res.clone();
          const raw = await clone.text();
          const body = normalizeToOkJson(raw);
          return new Response(body, {
            status: 200,
            headers: { 'Content-Type': 'application/json; charset=utf-8' }
          });
        }
      } catch {}
      return res;
    };
  }

  // Hook XHR
  const XHR = window.XMLHttpRequest;
  if (XHR && XHR.prototype) {
    const open = XHR.prototype.open;
    const send = XHR.prototype.send;
    XHR.prototype.open = function(method, url) { this.__mpai_url = url; return open.apply(this, arguments); };
    XHR.prototype.send = function() {
      const self = this;
      const onload = function() {
        try {
          if ((self.__mpai_url || '').includes(TARGET) && self.status === 200) {
            const raw = self.responseText || '';
            const body = (function(){
              try {
                const j = JSON.parse(raw);
                if (j && (j.ok === true || (j.status && String(j.status).toLowerCase() === 'ok'))) return JSON.stringify(j);
                const guess = j && (j.text || j.message || j.reply || (typeof j === 'string' ? j : JSON.stringify(j)));
                return JSON.stringify({ ok: true, status: 'ok', text: String(guess || '') });
              } catch {
                return JSON.stringify({ ok: true, status: 'ok', text: String(raw || '') });
              }
            })();
            Object.defineProperty(self, 'responseText', { value: body });
            Object.defineProperty(self, 'response',     { value: body });
          }
        } catch {}
      };
      this.addEventListener('load', onload);
      return send.apply(this, arguments);
    };
  }

  console.log('MPAI SHIM v2 attivo (risposte normalizzate a JSON ok).');
})();
*/

/* ===== MPAI PARACHUTE v3 (fix errore, \n, tagli) ===== */
(() => {
  const TARGET = 'openai_project_gpt.php';

  // a) Normalizza risposte dell'endpoint in JSON {ok:true, text:"..."}
  function normalizeOkJson(raw) {
    try {
      const j = JSON.parse(raw);
      if (j && (j.ok === true || (j.status && String(j.status).toLowerCase() === 'ok'))) return j;
      const guess = j && (j.text || j.message || j.reply || (typeof j === 'string' ? j : JSON.stringify(j)));
      return { ok: true, status: 'ok', text: String(guess || '') };
    } catch {
      return { ok: true, status: 'ok', text: String(raw || '') };
    }
  }

  // b) Trasforma sequenze letterali "\n" in newline reali
  function deescapeNewlines(s) {
    return String(s).replace(/\\n/g, '\n').replace(/\r/g, '');
  }

  // c) Hook fetch → garantisce JSON-ok per quell’endpoint
  if (window.fetch) {
    const _fetch = window.fetch;
    window.fetch = async function(url, opts) {
      const res = await _fetch.apply(this, arguments);
      try {
        const u = (typeof url === 'string') ? url : (url && url.url) || '';
        if (u && u.indexOf(TARGET) !== -1) {
          const raw = await res.clone().text();
          const j = normalizeOkJson(raw);
          j.text = deescapeNewlines(j.text);
          j.message = j.message ? deescapeNewlines(j.message) : j.text;
          j.reply = j.reply ? deescapeNewlines(j.reply) : j.text;
          return new Response(JSON.stringify(j), {
            status: 200,
            headers: { 'Content-Type': 'application/json; charset=utf-8' }
          });
        }
      } catch {}
      return res;
    };
  }

  // d) Hook XHR (se la chat usa XMLHttpRequest / jQuery)
  const XHR = window.XMLHttpRequest;
  if (XHR && XHR.prototype) {
    const open = XHR.prototype.open, send = XHR.prototype.send;
    XHR.prototype.open = function(m,u){ this.__mpai_url = u; return open.apply(this, arguments); };
    XHR.prototype.send = function() {
      const self = this;
      const onload = function() {
        try {
          if ((self.__mpai_url || '').indexOf(TARGET) !== -1 && self.status === 200) {
            let raw = self.responseText || '';
            const j = normalizeOkJson(raw);
            j.text = deescapeNewlines(j.text);
            j.message = j.message ? deescapeNewlines(j.message) : j.text;
            j.reply = j.reply ? deescapeNewlines(j.reply) : j.text;
            const body = JSON.stringify(j);
            Object.defineProperty(self, 'responseText', { value: body });
            Object.defineProperty(self, 'response',     { value: body });
          }
        } catch {}
      };
      this.addEventListener('load', onload);
      return send.apply(this, arguments);
    };
  }

  // e) Ultimo “salvagente”: se qualcuno forza ancora mpaiAppendMsg('error', 'API error: ...')
  const g = (typeof window !== 'undefined') ? window : globalThis;
  if (g.mpaiAppendMsg && typeof g.mpaiAppendMsg === 'function') {
    const _append = g.mpaiAppendMsg;
    g.mpaiAppendMsg = function(kind, content) {
      try {
        const s = String(content || '');
        // Se è "API error: <testo normale>" e NON un vero HTTP error → mostra come risposta normale
        if (kind === 'error' && /^API error:/i.test(s) && !/^API error:\s*HTTP\s+\d+/i.test(s)) {
          const clean = s.replace(/^API error:\s*/i,'');
          return _append.call(this, 'assistant', deescapeNewlines(clean));
        }
      } catch {}
      return _append.call(this, kind, content);
    };
  }

  console.log('MPAI PARACHUTE v3 attivo (normalize JSON, fix \\n, reroute error→assistant).');
})();

/* === Reserve doc. — versione minima e robusta === */
mw.loader.using(['mediawiki.api','mediawiki.util']).then(function () {

  // 1) funzione click: crea Dashboard:TITOLO e apre in modifica
  function onReserveClick() {
    var titolo = prompt('Titolo del documento (senza "Dashboard:")', 'Wellcome_project');
    if (!titolo) return;

    var slug = titolo.trim().replace(/\s+/g, '_');
    var pageTitle = 'Dashboard:' + slug;
    var testo = '== ' + titolo.trim() + ' ==\nScrivi qui...';

    new mw.Api().postWithToken('csrf', {
      action: 'edit',
      title: pageTitle,
      text: testo,
      createonly: 1,
      summary: 'Creato da Dashboard (Reserve doc.)'
    }).done(function () {
      location.href = mw.util.getUrl(pageTitle, { action: 'edit' });
    }).fail(function (e) {
      if (e && e.error && (e.error.code === 'articleexists' || e.error.code === 'articleexistsforuser')) {
        location.href = mw.util.getUrl(pageTitle);
      } else {
        alert('Errore: ' + (e && e.error && e.error.info ? e.error.info : 'sconosciuto'));
      }
    });
  }

  // 2) crea il bottone una sola volta
  function injectButton() {
    if (document.getElementById('mp-reserve-doc-btn')) return;
    var container = document.querySelector('#contentSub') || document.body;

    var btn = document.createElement('button');
    btn.id = 'mp-reserve-doc-btn';
    btn.textContent = 'Reserve doc.';
    btn.style.cssText = 'padding:.45rem .8rem;border:1px solid #d1d5db;border-radius:.5rem;background:#f9fafb;font-weight:600;cursor:pointer;margin-right:.5rem;';
    btn.addEventListener('click', onReserveClick);
    container.prepend(btn);

    console.log('✅ Reserve doc. inserito');
  }

  // 3) inserisci SUBITO e anche quando la pagina è pronta
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', injectButton);
  } else {
    injectButton();
  }
  mw.hook('wikipage.content').add(injectButton);
});

// Forza il link "Modifica" a VisualEditor solo nel namespace Dashboard (3000)
(function () {
  if (mw.config.get('wgNamespaceNumber') !== 3000) return;
  var tab = document.querySelector('#ca-edit a'); // la tab "Modifica sorgente"
  if (tab) {
    tab.href = mw.util.getUrl(mw.config.get('wgPageName'), { veaction: 'edit' });
    tab.textContent = 'Modifica';
  }
})();

/* Nascondi "Importa Wikitesto da URL" solo nei documenti Dashboard:, con observer */
(function () {
  var ns   = mw.config.get('wgNamespaceNumber');     // 3000 = Dashboard
  var page = mw.config.get('wgPageName');            // es. "Dashboard_Masticationpedia"

  // eccezioni: home e (se vuoi) indice
  var EXCEPTIONS = new Set(['Dashboard_Masticationpedia', 'Dashboard_Indice']);

  if (ns !== 3000 || EXCEPTIONS.has(page)) return;

  function hideImportButton(root) {
    (root || document).querySelectorAll('a').forEach(function (a) {
      // match su title OPPURE sul testo visibile (nel caso il title mancasse)
      if (
        (a.title && a.title.trim() === 'Importa Wikitesto da URL') ||
        /Importa Wikitesto da URL/i.test(a.textContent || '')
      ) {
        a.style.display = 'none';
        a.classList.add('mp-hide-import');
      }
    });
  }

  // nascondi subito
  hideImportButton();

  // …e nascondi anche se viene re-iniettato in seguito
  var mo = new MutationObserver(function (muts) {
    muts.forEach(function (m) { hideImportButton(m.target); });
  });
  mo.observe(document.body, { childList: true, subtree: true });
})();

/* ====== Dashboard fixes (solo ns 3000) ====== */
(function () {
  var ns   = mw.config.get('wgNamespaceNumber'); // 3000 = Dashboard
  var page = mw.config.get('wgPageName');        // es. "Dashboard_Masticationpedia"

  if (ns !== 3000) return;

  /* 1) Aggiungi la TAB "Modifica sorgente" (wikitesto) accanto a Modifica (VE) */
  (function addSourceTab () {
    // vector 2022: <div id="p-views"> ... <ul class="vector-menu-content-list">
    var list = document.querySelector('#p-views .vector-menu-content-list') ||
               document.querySelector('#p-views ul');
    if (!list || document.getElementById('ca-mp-edit-source')) return;

    var li = document.createElement('li');
    li.id = 'ca-mp-edit-source';
    li.className = 'mw-list-item';
    var a = document.createElement('a');
    a.textContent = 'Modifica sorgente';
    a.href = mw.util.getUrl(page, { action: 'edit' }); // wikitesto
    li.appendChild(a);
    list.appendChild(li);
  })();

  /* 2) Nascondi "Importa Wikitesto da URL" nei documenti, MA non in home/indice */
  (function hideImportButton () {
    // <<<<<< CAMBIA QUI se il nome della home è diverso
    var EXCEPTIONS = new Set(['Dashboard_Masticationpedia', 'Dashboard_Indice']);
    if (EXCEPTIONS.has(page)) return;

    function hide(root) {
      (root || document).querySelectorAll('a,button').forEach(function (el) {
        var txt = (el.textContent || '').trim();
        var ttl = (el.title || '').trim();
        if (/^Importa Wikitesto da URL$/i.test(txt) || /^Importa Wikitesto da URL$/i.test(ttl)) {
          el.style.display = 'none';
          el.classList.add('mp-hide-import');
        }
      });
    }
    // nascondi subito
    hide();
    // nascondi se riappare dopo
    var mo = new MutationObserver(function (muts) {
      for (var i=0;i<muts.length;i++) hide(muts[i].target);
    });
    mo.observe(document.body, { childList: true, subtree: true });
  })();

  /* 3) Pulsante "Documenti riservati" (apre Dashboard:Indice) vicino a Reserve doc. */
  (function addIndexButton () {
    if (document.getElementById('mp-doc-index-btn')) return;
    var container = document.querySelector('#contentSub') || document.body;
    var link = document.createElement('a');
    link.id = 'mp-doc-index-btn';
    link.textContent = 'Documenti riservati';
    link.href = mw.util.getUrl('Dashboard:Indice');
    link.style.cssText =
      'display:inline-block;margin-left:.5rem;padding:.45rem .8rem;border:1px solid #d1d5db;' +
      'border-radius:.5rem;background:#fff;font-weight:600;text-decoration:none;color:inherit;';
    container.prepend(link);
  })();

})();
/* === Dashboard 11 · OpenAI Payload Logger (diagnostic only) ===
   Mostra nella console ogni messaggio inviato a OpenAI.
   Non modifica nulla del funzionamento della Dashboard. */

(function () {
  // Evita di attivarsi due volte
  if (window.__mpaiFetchLogged) return;
  window.__mpaiFetchLogged = true;

  const ORIG_FETCH = window.fetch;

  // Riconosce quando la Dashboard parla con OpenAI
  const isOpenAIEndpoint = (url) => {
    try {
      const u = String(url);
      return u.includes('openai_project_gpt.php') || u.includes('/oauth/openai_project_gpt.php');
    } catch { return false; }
  };

  // Intercetta la chiamata e mostra il contenuto nella console
  window.fetch = async function (input, init = {}) {
    // Impedisce che il browser usi la cache
    init.headers = Object.assign({}, init.headers, { 'Cache-Control': 'no-store' });

    if (isOpenAIEndpoint(input)) {
      try {
        let bodyText = init && typeof init.body === 'string' ? init.body : null;
        if (!bodyText && init && init.body && typeof init.body.text === 'function') {
          bodyText = await init.body.text();
        }
        const payload = bodyText ? JSON.parse(bodyText) : null;

        console.group('%cMPAI → OpenAI payload', 'color:#0b70ff;font-weight:700;');
        console.log('URL:', input);
        console.log('Modello:', payload?.model);
        console.log('Temperatura:', payload?.temperature);
        console.log('Messaggi:', payload?.messages);
        const last = payload?.messages?.[payload.messages.length - 1];
        console.log('Ultimo messaggio:', last);
        console.groupEnd();

      } catch (e) {
        console.warn('Errore nel logger del payload:', e);
      }
    }

    // Continua normalmente
    return ORIG_FETCH(input, init);
  };
})();
/* === Dashboard 11 · Fix stato chat e memoria per progetto ============ */

window.MPAI = window.MPAI || {};
MPAI.chat = MPAI.chat || { byProject: {} };

// Crea o restituisce lo stato della chat per un progetto
function mpaiGetChatState(project) {
  if (!MPAI.chat.byProject[project]) {
    MPAI.chat.byProject[project] = {
      messages: [
        { role: "system", content: "Sei l'assistente della Dashboard Masticationpedia. Rispondi in modo chiaro e conciso." }
      ]
    };
  }
  return MPAI.chat.byProject[project];
}

// Pulisce la conversazione di un progetto
function mpaiClearChat(project) {
  const state = mpaiGetChatState(project);
  state.messages = [
    { role: "system", content: "Sei l'assistente della Dashboard Masticationpedia. Rispondi in modo chiaro e conciso." }
  ];
}

// Funzione principale che gestisce l’invio a OpenAI
async function mpaiHandleSend(projectName, textarea, sendBtn, chatBox) {
  const userText = textarea.value.trim();
  if (!userText) return;

  // Aggiorna la memoria locale
  const state = mpaiGetChatState(projectName);
  state.messages.push({ role: "user", content: userText });

  // Mostra subito la domanda dell’utente
  const userBubble = document.createElement("div");
  userBubble.className = "mpai-bubble user";
  userBubble.textContent = userText;
  chatBox.appendChild(userBubble);
  chatBox.scrollTop = chatBox.scrollHeight;

  textarea.value = "";
  sendBtn.disabled = true;

  // Prepara il payload per OpenAI
  const payload = {
    model: "gpt-4o-2024-05-13",
    temperature: 0.7,
    messages: state.messages
  };

  console.log("PAYLOAD inviato a OpenAI:", payload);

  try {
    const res = await fetch("/oauth/openai_project_gpt.php", {
      method: "POST",
      headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
      body: JSON.stringify(payload)
    });

    if (!res.ok) throw new Error(`Errore HTTP ${res.status}`);
    const data = await res.json();

    const assistantText = (data && data.content) ? data.content : "[Nessuna risposta]";
    state.messages.push({ role: "assistant", content: assistantText });

    // Mostra la risposta nella chat
    const botBubble = document.createElement("div");
    botBubble.className = "mpai-bubble assistant";
    botBubble.textContent = assistantText;
    chatBox.appendChild(botBubble);
    chatBox.scrollTop = chatBox.scrollHeight;

  } catch (err) {
    console.error("Errore nella chiamata:", err);
    const errorBubble = document.createElement("div");
    errorBubble.className = "mpai-bubble error";
    errorBubble.textContent = "⚠️ Errore nella comunicazione con OpenAI.";
    chatBox.appendChild(errorBubble);
  } finally {
    sendBtn.disabled = false;
  }
}
// Collegamento ai pulsanti
document.addEventListener("DOMContentLoaded", () => {
  const textarea = document.querySelector("#mpai-chat-input");
  const sendBtn  = document.querySelector("#mpai-send-btn");
  const chatBox  = document.querySelector("#mpai-chat-box");
  const clearBtn = document.querySelector("#mpai-clear-btn");
  const projectName = (window.currentProjectName && window.currentProjectName()) || "AI-Chapters";

  if (sendBtn && textarea && chatBox) {
    sendBtn.addEventListener("click", () => mpaiHandleSend(projectName, textarea, sendBtn, chatBox));
  }

  if (clearBtn) {
    clearBtn.addEventListener("click", () => {
      mpaiClearChat(projectName);
      chatBox.innerHTML = "";
    });
  }
});

/* === Dashboard 11 · Step A+ : Solo Novità + Chat pulita + anti-cache === */
(function () {
  if (window.__mpaiDeltaPruneInstalled) return;
  window.__mpaiDeltaPruneInstalled = true;

  const KEEP_VISIBLE_BUBBLES = 2; // quante “bolle” tenere a schermo (ultima domanda + risposta)

  const OrigFetch = window.fetch;
  function isOpenAI(url) {
    try {
      const u = String(url);
      return u.includes('openai_project_gpt.php') || u.includes('/oauth/openai_project_gpt.php');
    } catch { return false; }
  }

  // Forza il SYSTEM "solo novità" dentro al payload prima di inviare
  function enforceDeltaSystem(init){
    if (!init || !init.body || typeof init.body !== 'string') return;
    try {
      const payload = JSON.parse(init.body);
      const msgs = Array.isArray(payload.messages) ? payload.messages : [];

      const deltaSystem =
        "Sei l'assistente della Dashboard Masticationpedia. Rispondi in italiano SOLO con ciò che aggiunge rispetto a prima ('Novità'). " +
        "Niente riassunti e niente frasi già dette. Risposte brevi e puntuali, elenco puntato quando utile.";

      if (!msgs.length || msgs[0].role !== 'system') {
        msgs.unshift({ role: 'system', content: deltaSystem });
      } else {
        msgs[0].content = deltaSystem;
      }

      payload.messages = msgs;
      init.body = JSON.stringify(payload);
    } catch {/* ignore */}
  }

  // Dopo ogni risposta: mantieni visibili solo gli ultimi N messaggi
  function pruneUI() {
    const bubbles = Array.from(document.querySelectorAll('.mpai-bubble, .chat-bubble, .gpt-bubble'));
    if (!bubbles.length) return;
    const toRemove = bubbles.slice(0, Math.max(0, bubbles.length - KEEP_VISIBLE_BUBBLES));
    toRemove.forEach(el => { try { el.remove(); } catch {} });
  }

  window.fetch = async function(input, init = {}) {
    if (isOpenAI(input)) {
      // anti-cache
      const ts = Date.now();
      let url = String(input);
      url += (url.includes('?') ? '&' : '?') + 'ts=' + ts;
      input = url;

      // header no-store
      init.headers = Object.assign({}, init.headers, { 'Cache-Control': 'no-store' });

      // forza “solo novità”
      enforceDeltaSystem(init);

      // log sintetico (puoi togliere se ti dà fastidio)
      try {
        const bodyText = (init && typeof init.body === 'string') ? init.body : null;
        const payload = bodyText ? JSON.parse(bodyText) : null;
        console.group('%cMPAI → OpenAI (Step A+)', 'color:#0b70ff;font-weight:700;');
        console.log('URL:', input);
        console.log('Modello:', payload?.model);
        console.log('Temperatura:', payload?.temperature);
        const last = payload?.messages?.[payload.messages?.length - 1];
        console.log('Ultimo messaggio:', last);
        console.groupEnd();
      } catch {}
    }

    const res = await OrigFetch(input, init);

    // dopo la risposta dal bridge, ripulisci la UI
    if (isOpenAI(input)) setTimeout(pruneUI, 200);

    return res;
  };
})();