Nota: dopo aver pubblicato, potrebbe essere necessario pulire la cache del proprio browser per vedere i cambiamenti.

  • Firefox / Safari: tieni premuto il tasto delle maiuscole Shift e fai clic su Ricarica, oppure premi Ctrl-F5 o Ctrl-R (⌘-R su Mac)
  • Google Chrome: premi Ctrl-Shift-R (⌘-Shift-R su un Mac)
  • Edge: tieni premuto il tasto Ctrl e fai clic su Aggiorna, oppure premi Ctrl-F5.
/* ============================================================================
   MPAI – CommonDashboard.js  (FULL FILE - PASTE ALL)
   Compatibile con /dashboard/api/openai_project_gpt.php (Responses API proxy)
   - Nessun doppio load
   - Renderer Markdown leggero
   - Prompt strutturato (system + template + domanda)
   - Risposte corpose in 7 sezioni
   ============================================================================ */

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

  /* ----------------------------- Utilità minime ---------------------------- */
  const $ = (sel) => document.querySelector(sel);
  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 --------------------------- */
  function escapeHtml(s){return String(s).replace(/[&<>"]/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[ch]));}
  function renderMarkdown(src){
    // safe
    let s = escapeHtml(String(src||''));

    // code fences
    s = s.replace(/```([a-z0-9+\-#]*)\n([\s\S]*?)```/gi, (_m,lang,code)=>{
      const l = (lang||'').trim();
      return (
        '<div class="mp-code">' +
          '<div class="mp-code__hdr"><span class="mp-code__lang">'+(l||'code')+'</span>' +
          '<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>' +
          '</div><pre><code>'+code.replace(/</g,'&lt;')+'</code></pre></div>'
      );
    });

    // inline code
    s = s.replace(/`([^`]+?)`/g,'<code>$1</code>');

    // headings
    s = s
      .replace(/^\s*######\s+(.+)$/gm,'<h6>$1</h6>')
      .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
    s = s.replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>');
    s = s.replace(/(^|[^\*])\*([^*]+?)\*(?!\*)/g, (_m,pre,txt)=> pre+'<em>'+txt+'</em>');

    // links
    s = s.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g,'<a href="$2" target="_blank" rel="nofollow noopener">$1</a>');

    // bullet list (linee che iniziano con "- ")
    s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block=>{
      const items = block.trim().split('\n').map(l=>l.replace(/^\-\s+/,'').trim());
      return '<ul>'+items.map(it=>'<li>'+it+'</li>').join('')+'</ul>';
    });

    // paragrafi
    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 Chat --------------------------------- */
  function appendMsg(role, content){
    const chat = $('#mpai-chat'); if(!chat) return;

    const box = document.createElement('div');
    box.className = 'mpai-msg ' + (role==='user'?'user':role==='assistant'?'assistant':'error');
    box.innerHTML =
      '<div class="role">'+(role==='user'?'Tu':role==='assistant'?'GPT':'Errore')+'</div>'+
      '<div class="content"></div>'+
      '<div class="meta">'+nowHHMM()+'</div>';
    chat.appendChild(box);
    chat.scrollTop = chat.scrollHeight;

    const ctn = box.querySelector('.content');
    ctn.innerHTML = renderMarkdown(String(content||''));
  }

  function showTyping(){
    const chat = $('#mpai-chat'); if(!chat) return null;
    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 = [
    "### 1) Definizione in 3–4 righe",
    "- …",
    "### 2) Classificazione/varianti",
    "- …",
    "### 3) Genetica e fisiopatologia essenziale",
    "- …",
    "### 4) Quadro clinico + imaging",
    "- …",
    "### 5) Diagnosi differenziale",
    "- …",
    "### 6) Trattamento (età pediatrica/adulto) e follow-up",
    "- …",
    "### 7) Spunti di ricerca / limiti",
    "- …"
  ].join("\n");

  function buildFullPrompt(userQuestion){
    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 ------------------------ */
  async function callOpenAI({model, temperature, prompt, images=[]}){
    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({ model, temperature, prompt, images })
    });

    const raw = await res.text();
    // Normalizzazione robusta
    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
    const out = j.result || j.text || j.output_text || j.reply || j.message;
    return String(out ?? '').replace(/\\n/g,'\n');
  }

  /* ------------------------------ INVIO PROMPT ----------------------------- */
  async function sendPrompt(){
    try {
      const ta = $('#mpai-input'); if(!ta) return;
      const q = (ta.value||'').trim();
      if (!q) { ta.focus(); return; }

      // Echo user
      appendMsg('user', q);
      ta.value = '';

      // 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 fullPrompt = buildFullPrompt(q);

      const typing = showTyping();
      let text = await callOpenAI({ model, temperature, prompt: fullPrompt, images: [] });
      hideTyping(typing);

      if (!text.trim()) text = '[Nessuna risposta]';
      appendMsg('assistant', text);

    } catch (e) {
      hideTyping();
      appendMsg('error', 'Errore: '+ (e && e.message ? e.message : e));
    }
  }

  // Esporta global per il bottone "Invia"
  window.sendPrompt = sendPrompt;

  /* --------------------------- Binding interfaccia -------------------------- */
  function bindUI(){
    // Pulsanti
    $('#mpai-send')?.addEventListener('click', sendPrompt);
    // Invio con Cmd/Ctrl+Enter
    document.addEventListener('keydown', (ev)=>{
      if ((ev.metaKey||ev.ctrlKey) && ev.key === 'Enter') {
        const ta = $('#mpai-input');
        if (ta && document.activeElement === ta) { ev.preventDefault(); sendPrompt(); }
      }
    });

    // 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
  function bootstrap(){
    const ai = $('#mpAI');
    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.');
}