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=>({'&':'&','<':'<','>':'>','"':'"'}[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,'<')+'</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.');
}