Nessun oggetto della modifica
Etichetta: Annullato
Nessun oggetto della modifica
Etichetta: Annullato
Riga 1: Riga 1:
/* ============================================================================
/* =========================================================================
   MPAI – CommonDashboard.js (FULL FILE - PASTE ALL)
   CommonDashboard.js — STABILE SEMPLICE (ripristino sicuro)
   Compatibile con /dashboard/api/openai_project_gpt.php (Responses API proxy)
   - NON nasconde elementi esistenti
   - Nessun doppio load
   - Ripristina pulsante "Importa Wikitesto da URL"
   - Renderer Markdown leggero
   - Risposte sempre mostrate anche se il server manda text/plain
   - Prompt strutturato (system + template + domanda)
   - Renderer Markdown + Highlight "leggero"
  - Risposte corpose in 7 sezioni
   ========================================================================= */
   ============================================================================ */


/* ----------------------- Guardia anti-doppio-caricamento ------------------- */
/* Guardia anti doppio-caricamento */
if (window.__MP_COMMON_DASH_LOADED__) {
if (window.__MP_DASHBOARD_SAFE__) {
   console.warn('CommonDashboard già caricato – stop');
   console.warn('CommonDashboard.js già caricato.');
} else {
} else {
   window.__MP_COMMON_DASH_LOADED__ = true;
   window.__MP_DASHBOARD_SAFE__ = true;


   /* ----------------------------- Utilità minime ---------------------------- */
   (function () {
  const $ = (sel) => document.querySelector(sel);
     console.log('🧭 CommonDashboard SAFE: init');
  const $$ = (sel) => Array.from(document.querySelectorAll(sel));
  const nowHHMM = () => {
     try { return new Date().toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'}); }
    catch { return ''; }
  };


  /* --------------------------- Markdown renderer --------------------------- */
    /* ========== Loader minimale per marked + highlight ========== */
  function escapeHtml(s){return String(s).replace(/[&<>"]/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));}
    function loadScript(src) {
  function renderMarkdown(src){
      return new Promise((ok, no) => {
    // safe
        const s = document.createElement('script'); s.src = src; s.onload = ok; s.onerror = no;
    let s = escapeHtml(String(src||''));
        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 ensureRenderer() {
      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');
      }
    }
    function renderMarkdownInto(el, md) {
      if (!el) return;
      if (window.marked) {
        el.innerHTML = window.marked.parse(String(md || ''));
        if (window.hljs) {
          el.querySelectorAll('pre code').forEach(c => { try { window.hljs.highlightElement(c); } catch {} });
        }
      } else {
        el.textContent = String(md || '');
      }
    }


     // code fences
     /* ========== Helper UI di chat, non nasconde nulla della tua pagina ========== */
     s = s.replace(/```([a-z0-9+\-#]*)\n([\s\S]*?)```/gi, (_m,lang,code)=>{
     function $(sel) { return document.querySelector(sel); }
       const l = (lang||'').trim();
    function appendMsg(role, content) {
       return (
       const host = $('#mpai-chat'); if (!host) return;
         '<div class="mp-code">' +
      const box = document.createElement('div');
          '<div class="mp-code__hdr"><span class="mp-code__lang">'+(l||'code')+'</span>' +
       box.className = 'mpai-msg ' + (role === 'assistant' ? 'assistant' : role === 'user' ? 'user' : 'error');
          '<button class="mp-copy" onclick="navigator.clipboard.writeText(this.closest(\'.mp-code\').querySelector(\'pre\').innerText).then(()=>{this.textContent=\'Copiato\';setTimeout(()=>this.textContent=\'Copia\',900);})">Copia</button>' +
      box.innerHTML = `
          '</div><pre><code>'+code.replace(/</g,'&lt;')+'</code></pre></div>'
         <div class="role">${role === 'assistant' ? 'GPT' : role === 'user' ? 'Tu' : 'Errore'}</div>
       );
        <div class="content"><em>…</em></div>
     });
        <div class="meta"></div>`;
      host.appendChild(box);
      host.scrollTop = host.scrollHeight;
      const c = box.querySelector('.content');
      renderMarkdownInto(c, String(content || ''));
    }
    function showTyping() {
      const host = $('#mpai-chat'); if (!host) return null;
      const box = document.createElement('div');
      box.className = 'mpai-msg assistant';
      box.innerHTML = `<div class="role">GPT</div><div class="content"></div>`;
       host.appendChild(box);
      host.scrollTop = host.scrollHeight;
      return box;
     }
    function hideTyping(el) { try { el && el.remove(); } catch {} }


     // inline code
     /* ========== INVIO PROMPT — semplice e robusto ========== */
     s = s.replace(/`([^`]+?)`/g,'<code>$1</code>');
     window.sendPrompt = async function () {
      const ta = $('#mpai-input'); if (!ta) return;
      const content = (ta.value || '').trim();
      if (!content) { ta.focus(); return; }


    // headings
       appendMsg('user', content);
    s = s
       ta.value = '';
       .replace(/^\s*######\s+(.+)$/gm,'<h6>$1</h6>')
       const typing = showTyping();
      .replace(/^\s*#####\s+(.+)$/gm,'<h5>$1</h5>')
       .replace(/^\s*####\s+(.+)$/gm,'<h4>$1</h4>')
       .replace(/^\s*###\s+(.+)$/gm,'<h3>$1</h3>')
      .replace(/^\s*##\s+(.+)$/gm,'<h2>$1</h2>')
      .replace(/^\s*#\s+(.+)$/gm,'<h1>$1</h1>');


    // bold / italic
      // Legge modello dall’UI se presente
    s = s.replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>');
      const modelSel = $('#mpai-model');
    s = s.replace(/(^|[^\*])\*([^*]+?)\*(?!\*)/g, (_m,pre,txt)=> pre+'<em>'+txt+'</em>');
      const model = modelSel ? (modelSel.value || 'gpt-4o-2024-05-13') : 'gpt-4o-2024-05-13';


    // links
      // PAYLOAD minimale (non tocco la tua dropzone/filelist esistente)
    s = s.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g,'<a href="$2" target="_blank" rel="nofollow noopener">$1</a>');
      const payload = { model, prompt: content, temperature: 0.7 };


    // bullet list (linee che iniziano con "- ")
      try {
    s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block=>{
        const res = await fetch('/dashboard/api/openai_project_gpt.php', {
      const items = block.trim().split('\n').map(l=>l.replace(/^\-\s+/,'').trim());
          method: 'POST',
      return '<ul>'+items.map(it=>'<li>'+it+'</li>').join('')+'</ul>';
          headers: { 'Content-Type': 'application/json' },
    });
          credentials: 'include',
          cache: 'no-store',
          body: JSON.stringify(payload)
        });


    // paragrafi
        const raw = await res.text();
    s = s.split(/\n{2,}/).map(par=>{
        hideTyping(typing);
      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;
        // Mostra sempre qualcosa anche se è text/plain
  }
        let out = '';
        try {
          const j = JSON.parse(raw);
          out = j.result || j.text || j.reply || j.message || j.output_text || JSON.stringify(j, null, 2);
        } catch {
          out = raw;
        }
        out = String(out || '').replace(/\\n/g, '\n');


  /* ------------------------------- UI Chat --------------------------------- */
        if (!res.ok) {
  function appendMsg(role, content){
          appendMsg('error', `HTTP ${res.status} ${res.statusText}\n\n${out}`);
    const chat = $('#mpai-chat'); if(!chat) return;
        } else {
          appendMsg('assistant', out || '[nessun testo]');
        }
      } catch (e) {
        hideTyping(typing);
        appendMsg('error', 'Errore rete/JS: ' + (e && e.message ? e.message : e));
      }
    };


     const box = document.createElement('div');
     /* ========== BIND UI: non nascondo la tua dropzone né la lista file ========== */
    box.className = 'mpai-msg ' + (role==='user'?'user':role==='assistant'?'assistant':'error');
     function bindUI() {
    box.innerHTML =
      const sendBtn = $('#mpai-send');
      '<div class="role">'+(role==='user'?'Tu':role==='assistant'?'GPT':'Errore')+'</div>'+
      if (sendBtn && !sendBtn.__bound) {
      '<div class="content"></div>'+
        sendBtn.__bound = true;
      '<div class="meta">'+nowHHMM()+'</div>';
        sendBtn.addEventListener('click', window.sendPrompt);
     chat.appendChild(box);
       }
    chat.scrollTop = chat.scrollHeight;
       // Invio con Cmd/Ctrl+Enter
 
       document.addEventListener('keydown', (e) => {
    const ctn = box.querySelector('.content');
        if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
    ctn.innerHTML = renderMarkdown(String(content||''));
          const ta = $('#mpai-input');
  }
          if (ta && document.activeElement === ta) { e.preventDefault(); window.sendPrompt(); }
 
        }
  function showTyping(){
      });
    const chat = $('#mpai-chat'); if(!chat) return null;
      console.log('🔗 SAFE bind ok');
    const box = document.createElement('div');
     }
    box.className = 'mpai-msg assistant';
    box.innerHTML =
       '<div class="role">GPT</div>'+
       '<div class="content"><em><span class="dots">● ● ●</span></em></div>'+
       '<div class="meta">'+nowHHMM()+'</div>';
    chat.appendChild(box);
    chat.scrollTop = chat.scrollHeight;
    return box;
  }
  function hideTyping(el){ try{ el && el.remove(); }catch{} }
 
  /* ----------------------- Prompt (system + template) ---------------------- */
  const MASTICATIONPEDIA_SYSTEM =
     "Sei l'assistente scientifico di Masticationpedia, con competenze in neurognatologia, " +
    "linguaggio medico, fisiopatologia dello smalto e clinica odontoiatrica. " +
    "Scrivi in italiano tecnico, completo ma chiaro, con fonti/termini corretti.";


  const ANSWER_TEMPLATE = [
    /* ========== RIPRISTINO: Pulsante "Importa Wikitesto da URL" ========== */
    "### 1) Definizione in 3–4 righe",
     function ensureImportButton() {
     "- …",
      // NON lo nascondo più in nessun caso
    "### 2) Classificazione/varianti",
      // Lo aggancio vicino al titolo "Masticationpedia AI" se esiste, altrimenti in alto nella pagina
    "- …",
      const target =
    "### 3) Genetica e fisiopatologia essenziale",
        Array.from(document.querySelectorAll('h2,h3,.section-title,.mpai-header,.mp-section-title'))
    "- …",
          .find(n => /Masticationpedia\s*AI/i.test(n.textContent || '')) ||
    "### 4) Quadro clinico + imaging",
        document.querySelector('#contentSub') ||
    "- …",
        document.body;
    "### 5) Diagnosi differenziale",
    "- …",
    "### 6) Trattamento (età pediatrica/adulto) e follow-up",
    "- …",
    "### 7) Spunti di ricerca / limiti",
    "- …"
  ].join("\n");


  function buildFullPrompt(userQuestion){
       if (!target || document.getElementById('mp-btn-import-url')) return;
    return [
      "ISTRUZIONI DI RUOLO:",
      MASTICATIONPEDIA_SYSTEM,
      "",
       "STRUTTURA OBBLIGATORIA DELLA RISPOSTA (usa i titoli indicati, niente preamboli):",
      ANSWER_TEMPLATE,
      "",
      "LUNGHEZZA: completa ma concisa; usa elenchi puntati dove aiuta la leggibilità.",
      "STILE: clinico-scientifico; niente chiacchiere; non ripetere la domanda.",
      "",
      "DOMANDA:",
      String(userQuestion||'')
    ].join("\n");
  }


  /* -------------------------- Chiamata al proxy PHP ------------------------ */
      const btn = document.createElement('button');
  async function callOpenAI({model, temperature, prompt, images=[]}){
       btn.id = 'mp-btn-import-url';
    const res = await fetch('/dashboard/api/openai_project_gpt.php', {
       btn.textContent = 'Importa Wikitesto da URL';
       method: 'POST',
       btn.title = 'Scarica una pagina MediaWiki e salvala tra i file';
       headers: { 'Content-Type': 'application/json' },
       btn.style.cssText = 'margin-left:.5rem;padding:.35rem .7rem;border:1px solid #444;border-radius:10px;cursor:pointer;font-size:.9rem;';
       credentials: 'include',
       target.appendChild(btn);
       cache: 'no-store',
       body: JSON.stringify({ model, temperature, prompt, images })
    });


    const raw = await res.text();
      btn.addEventListener('click', () => openImportModal());
    // Normalizzazione robusta
       console.log('✅ Pulsante "Importa Wikitesto da URL" ripristinato');
    let j;
    try { j = JSON.parse(raw); } catch { j = { status: 'ok', result: raw }; }
 
    if (!res.ok || (j.status && String(j.status).toLowerCase() === 'error')) {
      const msg = j && (j.error || j.message || j.raw) ? (j.error || j.message || j.raw) : ('HTTP '+res.status+' '+res.statusText);
       throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
     }
     }


     // Possibili chiavi: result / text / output_text
     function openImportModal() {
    const out = j.result || j.text || j.output_text || j.reply || j.message;
      const modal = document.createElement('div');
    return String(out ?? '').replace(/\\n/g,'\n');
      modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;display:flex;align-items:center;justify-content:center;';
  }
      modal.innerHTML = `
 
        <div style="background:#fff;max-width:720px;width:92%;padding:1rem;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.15);">
  /* ------------------------------ INVIO PROMPT ----------------------------- */
          <h3 style="margin:.2rem 0 1rem;">Importa Wikitesto da URL</h3>
  async function sendPrompt(){
          <label style="display:block;margin:.4rem 0 .2rem;">URL della pagina (MediaWiki / Wikipedia / GitHub raw):</label>
    try {
          <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;">
      const ta = $('#mpai-input'); if(!ta) return;
          <div id="mp-status" style="margin-top:.6rem;font-size:.9rem;opacity:.85;"></div>
      const q = (ta.value||'').trim();
          <div style="margin-top:1rem;display:flex;justify-content:flex-end;gap:.6rem;">
      if (!q) { ta.focus(); return; }
            <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>
      // Echo user
          </div>
      appendMsg('user', q);
        </div>`;
      ta.value = '';
      document.body.appendChild(modal);
 
      // Lettura controlli
      const model = ($('#mpai-model') && $('#mpai-model').value) || 'gpt-4o-2024-05-13';
      const temperature = parseFloat(($('#mpai-temp') && $('#mpai-temp').value) || '0.3') || 0.3;


       // Prompt
       const elUrl = modal.querySelector('#mp-url');
       const fullPrompt = buildFullPrompt(q);
       const elStatus = modal.querySelector('#mp-status');
      modal.querySelector('#mp-cancel').onclick = () => modal.remove();
      modal.querySelector('#mp-go').onclick = async () => {
        const url = (elUrl.value || '').trim();
        if (!url) { elStatus.textContent = 'Inserisci una URL valida.'; elStatus.style.color = '#b00'; return; }
        elStatus.textContent = 'Import in corso...'; elStatus.style.color = '';


      const typing = showTyping();
        try {
      let text = await callOpenAI({ model, temperature, prompt: fullPrompt, images: [] });
          const form = new FormData();
      hideTyping(typing);
          form.append('url', url);
          form.append('save', '1'); // server salverà nel progetto di default


      if (!text.trim()) text = '[Nessuna risposta]';
          const res = await fetch('/dashboard/api/fetch_wikitext.php', { method: 'POST', body: form, credentials: 'include' });
      appendMsg('assistant', text);
          const raw = await res.text();
          if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + res.statusText + ' — ' + raw);
          let j = {};
          try { j = JSON.parse(raw); } catch { j = { ok:false, raw }; }
          if (!j.ok) throw new Error(j.error || 'Errore server');


    } catch (e) {
          elStatus.innerHTML = '✅ Import riuscito: <code>' + j.relative + '</code> (' + j.bytes + ' bytes)<br><small>Origine: ' + j.resolved_url + '</small>';
      hideTyping();
          elStatus.style.color = '#0a0';
      appendMsg('error', 'Errore: '+ (e && e.message ? e.message : e));
          setTimeout(() => modal.remove(), 1200);
        } catch (e) {
          elStatus.textContent = 'Errore: ' + (e && e.message ? e.message : e);
          elStatus.style.color = '#b00';
        }
      };
     }
     }
  }
  // Esporta global per il bottone "Invia"
  window.sendPrompt = sendPrompt;


  /* --------------------------- Binding interfaccia -------------------------- */
    /* ========== AVVIO ========== */
  function bindUI(){
    function boot() {
    // Pulsanti
      ensureRenderer().then(() => {
    $('#mpai-send')?.addEventListener('click', sendPrompt);
        bindUI();
    // Invio con Cmd/Ctrl+Enter
         ensureImportButton();
    document.addEventListener('keydown', (ev)=>{
         console.log('✅ CommonDashboard SAFE: pronta');
      if ((ev.metaKey||ev.ctrlKey) && ev.key === 'Enter') {
       }).catch((e) => {
         const ta = $('#mpai-input');
        console.warn('Renderer non caricato:', e);
         if (ta && document.activeElement === ta) { ev.preventDefault(); sendPrompt(); }
         bindUI();
       }
         ensureImportButton();
    });
       });
 
    // Messaggio di benvenuto se chat vuota
    const chat = $('#mpai-chat');
    if (chat && !chat.querySelector('.mpai-msg')) {
      appendMsg('assistant',
         "Pronto. Inquadra l’argomento e ti rispondo con 7 sezioni (definizione → ricerca). " +
         "Se vuoi, indica anche età del paziente e contesto clinico."
       );
     }
     }
  }


  // Bind quando l’area #mpAI è in pagina
     if (document.readyState === 'loading') {
  function bootstrap(){
      document.addEventListener('DOMContentLoaded', boot);
    const ai = $('#mpAI');
    } else { boot(); }
     if (ai) bindUI();
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', bootstrap);
  } else {
    bootstrap();
  }
 
  /* ----------------------------- Stili minimi ------------------------------ */
  (function injectCss(){
    if (document.getElementById('mp-ai-extra-css')) return;
    const css = `
    #mpai-chat .mpai-msg{border:1px solid #e5e7eb;border-radius:12px;padding:10px;background:#fff}
    #mpai-chat .mpai-msg.user{background:#eaf2ff;border-color:#d7e6ff}
    #mpai-chat .mpai-msg.assistant{background:#ffffff;border-color:#a7f3d0}
    #mpai-chat .mpai-msg.error{background:#fff1f2;border-color:#fecaca}
    #mpai-chat .mpai-msg .role{font-weight:600;margin-bottom:4px;color:#374151}
    #mpai-chat .mpai-msg .content p{margin:.45em 0}
    #mpai-chat .mpai-msg .content ul{margin:.4em 0 .4em 1.2em;list-style:disc}
    .mp-code{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;margin:.6rem 0;background:#fff}
    .mp-code__hdr{display:flex;justify-content:space-between;align-items:center;padding:.5rem .75rem;border-bottom:1px solid #e5e7eb;background:#f8fafc}
    .mp-code__lang{font-family:ui-monospace, Menlo, Consolas, monospace;color:#334155}
    .mp-copy{border:1px solid #e5e7eb;background:#fff;border-radius:8px;padding:.25rem .5rem;cursor:pointer}
    .mp-copy:hover{background:#f3f4f6}
    .mp-code pre{margin:0;padding:.75rem 1rem;overflow:auto;font-family:ui-monospace, Menlo, Consolas, monospace;font-size:13px}
    `;
    const s = document.createElement('style'); s.id='mp-ai-extra-css'; s.textContent = css;
    document.head.appendChild(s);
   })();
   })();
  console.log('✅ CommonDashboard.js (full) caricato.');
}
}

Versione delle 10:51, 2 nov 2025

/* =========================================================================
   CommonDashboard.js — STABILE SEMPLICE (ripristino sicuro)
   - NON nasconde elementi esistenti
   - Ripristina pulsante "Importa Wikitesto da URL"
   - Risposte sempre mostrate anche se il server manda text/plain
   - Renderer Markdown + Highlight "leggero"
   ========================================================================= */

/* Guardia anti doppio-caricamento */
if (window.__MP_DASHBOARD_SAFE__) {
  console.warn('CommonDashboard.js già caricato.');
} else {
  window.__MP_DASHBOARD_SAFE__ = true;

  (function () {
    console.log('🧭 CommonDashboard SAFE: init');

    /* ========== Loader minimale per marked + highlight ========== */
    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 ensureRenderer() {
      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');
      }
    }
    function renderMarkdownInto(el, md) {
      if (!el) return;
      if (window.marked) {
        el.innerHTML = window.marked.parse(String(md || ''));
        if (window.hljs) {
          el.querySelectorAll('pre code').forEach(c => { try { window.hljs.highlightElement(c); } catch {} });
        }
      } else {
        el.textContent = String(md || '');
      }
    }

    /* ========== Helper UI di chat, non nasconde nulla della tua pagina ========== */
    function $(sel) { return document.querySelector(sel); }
    function appendMsg(role, content) {
      const host = $('#mpai-chat'); if (!host) return;
      const box = document.createElement('div');
      box.className = 'mpai-msg ' + (role === 'assistant' ? 'assistant' : role === 'user' ? 'user' : 'error');
      box.innerHTML = `
        <div class="role">${role === 'assistant' ? 'GPT' : role === 'user' ? 'Tu' : 'Errore'}</div>
        <div class="content"><em>…</em></div>
        <div class="meta"></div>`;
      host.appendChild(box);
      host.scrollTop = host.scrollHeight;
      const c = box.querySelector('.content');
      renderMarkdownInto(c, String(content || ''));
    }
    function showTyping() {
      const host = $('#mpai-chat'); if (!host) return null;
      const box = document.createElement('div');
      box.className = 'mpai-msg assistant';
      box.innerHTML = `<div class="role">GPT</div><div class="content">…</div>`;
      host.appendChild(box);
      host.scrollTop = host.scrollHeight;
      return box;
    }
    function hideTyping(el) { try { el && el.remove(); } catch {} }

    /* ========== INVIO PROMPT — semplice e robusto ========== */
    window.sendPrompt = async function () {
      const ta = $('#mpai-input'); if (!ta) return;
      const content = (ta.value || '').trim();
      if (!content) { ta.focus(); return; }

      appendMsg('user', content);
      ta.value = '';
      const typing = showTyping();

      // Legge modello dall’UI se presente
      const modelSel = $('#mpai-model');
      const model = modelSel ? (modelSel.value || 'gpt-4o-2024-05-13') : 'gpt-4o-2024-05-13';

      // PAYLOAD minimale (non tocco la tua dropzone/filelist esistente)
      const payload = { model, prompt: content, temperature: 0.7 };

      try {
        const res = 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 res.text();
        hideTyping(typing);

        // Mostra sempre qualcosa anche se è text/plain
        let out = '';
        try {
          const j = JSON.parse(raw);
          out = j.result || j.text || j.reply || j.message || j.output_text || JSON.stringify(j, null, 2);
        } catch {
          out = raw;
        }
        out = String(out || '').replace(/\\n/g, '\n');

        if (!res.ok) {
          appendMsg('error', `HTTP ${res.status} ${res.statusText}\n\n${out}`);
        } else {
          appendMsg('assistant', out || '[nessun testo]');
        }
      } catch (e) {
        hideTyping(typing);
        appendMsg('error', 'Errore rete/JS: ' + (e && e.message ? e.message : e));
      }
    };

    /* ========== BIND UI: non nascondo la tua dropzone né la lista file ========== */
    function bindUI() {
      const sendBtn = $('#mpai-send');
      if (sendBtn && !sendBtn.__bound) {
        sendBtn.__bound = true;
        sendBtn.addEventListener('click', window.sendPrompt);
      }
      // Invio con Cmd/Ctrl+Enter
      document.addEventListener('keydown', (e) => {
        if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
          const ta = $('#mpai-input');
          if (ta && document.activeElement === ta) { e.preventDefault(); window.sendPrompt(); }
        }
      });
      console.log('🔗 SAFE bind ok');
    }

    /* ========== RIPRISTINO: Pulsante "Importa Wikitesto da URL" ========== */
    function ensureImportButton() {
      // NON lo nascondo più in nessun caso
      // Lo aggancio vicino al titolo "Masticationpedia AI" se esiste, altrimenti in alto nella pagina
      const target =
        Array.from(document.querySelectorAll('h2,h3,.section-title,.mpai-header,.mp-section-title'))
          .find(n => /Masticationpedia\s*AI/i.test(n.textContent || '')) ||
        document.querySelector('#contentSub') ||
        document.body;

      if (!target || document.getElementById('mp-btn-import-url')) return;

      const btn = document.createElement('button');
      btn.id = 'mp-btn-import-url';
      btn.textContent = 'Importa Wikitesto da URL';
      btn.title = 'Scarica una pagina MediaWiki e salvala tra i file';
      btn.style.cssText = 'margin-left:.5rem;padding:.35rem .7rem;border:1px solid #444;border-radius:10px;cursor:pointer;font-size:.9rem;';
      target.appendChild(btn);

      btn.addEventListener('click', () => openImportModal());
      console.log('✅ Pulsante "Importa Wikitesto da URL" ripristinato');
    }

    function openImportModal() {
      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;';
      modal.innerHTML = `
        <div style="background:#fff;max-width:720px;width:92%;padding: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 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 elStatus = modal.querySelector('#mp-status');
      modal.querySelector('#mp-cancel').onclick = () => modal.remove();
      modal.querySelector('#mp-go').onclick = async () => {
        const url = (elUrl.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'); // server salverà nel progetto di default

          const res = await fetch('/dashboard/api/fetch_wikitext.php', { method: 'POST', body: form, credentials: 'include' });
          const raw = await res.text();
          if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + res.statusText + ' — ' + raw);
          let j = {};
          try { j = JSON.parse(raw); } catch { j = { ok:false, raw }; }
          if (!j.ok) throw new Error(j.error || 'Errore server');

          elStatus.innerHTML = '✅ Import riuscito: <code>' + j.relative + '</code> (' + j.bytes + ' bytes)<br><small>Origine: ' + j.resolved_url + '</small>';
          elStatus.style.color = '#0a0';
          setTimeout(() => modal.remove(), 1200);
        } catch (e) {
          elStatus.textContent = 'Errore: ' + (e && e.message ? e.message : e);
          elStatus.style.color = '#b00';
        }
      };
    }

    /* ========== AVVIO ========== */
    function boot() {
      ensureRenderer().then(() => {
        bindUI();
        ensureImportButton();
        console.log('✅ CommonDashboard SAFE: pronta');
      }).catch((e) => {
        console.warn('Renderer non caricato:', e);
        bindUI();
        ensureImportButton();
      });
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', boot);
    } else { boot(); }
  })();
}