MediaWiki:CommonDashboard.js: differenze tra le versioni
Nessun oggetto della modifica |
Nessun oggetto della modifica |
||
| Riga 134: | Riga 134: | ||
document.head.appendChild(s); | document.head.appendChild(s); | ||
})(); | })(); | ||
(function attachMpaiGalleryCss(){ | (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 ===================== */ | /* ===================== STATO ===================== */ | ||
| Riga 161: | Riga 159: | ||
}; | }; | ||
/* ===================== RENDER “MARKDOWN SAFE” ===================== */ | /* ====================================================================== | ||
[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){ | function escapeHtml(s){ | ||
return s.replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch])); | return s.replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch])); | ||
| Riga 197: | Riga 242: | ||
const chatEl = $mp('#mpai-chat'); | const chatEl = $mp('#mpai-chat'); | ||
if (!chatEl) return; | if (!chatEl) return; | ||
const div = document.createElement('div'); | const div = document.createElement('div'); | ||
div.className = 'mpai-msg ' + (role === 'user' ? 'user' : (role === 'assistant' ? 'assistant' : 'error')); | div.className = 'mpai-msg ' + (role === 'user' ? 'user' : (role === 'assistant' ? 'assistant' : 'error')); | ||
div.innerHTML = ` | div.innerHTML = ` | ||
<div class="role">${role==='user'?'Tu':role==='assistant'?'GPT':'Errore'}</div> | <div class="role">${role==='user'?'Tu':role==='assistant'?'GPT':'Errore'}</div> | ||
<div class="content"> | <div class="content"><em>…</em></div> | ||
<div class="meta">${mpaiTime()}</div>`; | <div class="meta">${mpaiTime()}</div>`; | ||
chatEl.appendChild(div); | chatEl.appendChild(div); | ||
chatEl.scrollTop = chatEl.scrollHeight; | 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); | |||
} | |||
} | } | ||
| Riga 267: | Riga 326: | ||
}); | }); | ||
} | } | ||
function mpaiSanitizeAttForHistory(a){ | /* ===================== 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){ | 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 ===================== */ | /* ===================== FILE HELPERS ===================== */ | ||
| Riga 516: | Riga 575: | ||
/* ===== INVIO PROMPT → PROXY (robusto a text/plain o JSON) ===== */ | /* ===== INVIO PROMPT → PROXY (robusto a text/plain o JSON) ===== */ | ||
window.sendPrompt = async function () { | window.sendPrompt = async function () { | ||
try { | |||
const ta = document.querySelector('#mpai-input'); | |||
if (!ta) return; | |||
const content = (ta.value || '').trim(); | |||
if (!content) { ta.focus(); return; } | |||
// 1) mostra subito il messaggio utente | |||
mpaiState.history.push({ role: 'user', content }); | |||
mpaiAppendMsg('user', content); | |||
ta.value = ''; | |||
// Galleria allegati nel thread (persistente) | |||
const sentAtt = (mpaiState.attachments || []).map(mpaiSanitizeAttForHistory); | |||
mpaiAppendAttachmentGallery('user', sentAtt); | |||
const model = (document.querySelector('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim(); | |||
const pending = mpaiShowTyping('assistant'); | |||
// 2) raccogli allegati | |||
let files = [], texts = []; | |||
try { | |||
const p = await collectAttachedPayload(); | |||
files = p.files; | |||
texts = p.texts; | |||
} catch(e){ | |||
console.warn('collectAttachedPayload failed', e); | |||
} | |||
// 3) payload base | |||
const payload = { model, prompt: content, files, texts, temperature: 0.7 }; | |||
// 4) merge del CONTEX da “Importa Wikitesto da URL” | |||
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] : []; | |||
payload.files = Array.from(new Set([...(payload.files || []), ...filesFromCtx])); | |||
console.log('📎 Contesto aggiunto al payload:', filesFromCtx); | |||
} catch (e) { | |||
console.warn('Context merge failed', e); | |||
} | |||
// 5) chiamata al proxy | |||
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) { | |||
// vero errore HTTP | |||
mpaiAppendMsg('error', `HTTP ${r.status} ${r.statusText}\n\n${raw || '(nessun body)'}`); | |||
return; | |||
} | |||
// 6) accetta SIA text/plain SIA JSON | |||
const ct = (r.headers.get('content-type') || '').toLowerCase(); | |||
let outText = ''; | |||
if (ct.includes('application/json')) { | |||
// il server ha risposto JSON → prendo i campi utili | |||
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 || ( | |||
typeof j === 'string' ? j : JSON.stringify(j) | |||
); | |||
} catch { | |||
// JSON rotto → mostro il grezzo | |||
outText = raw; | |||
} | |||
} else { | |||
// text/plain (nuovo handler lato server) | |||
outText = raw; | outText = raw; | ||
} | } | ||
// normalizza newline (se arrivano come "\n") | |||
outText = String(outText || '').replace(/\\n/g, '\n').replace(/\r/g, ''); | |||
// 7) mostra come risposta “normale”, niente riquadro Errore | |||
if (outText.trim() === '') outText = '[vuoto]'; | |||
mpaiState.history.push({ role: 'assistant', content: outText }); | |||
mpaiAppendMsg('assistant', outText); | |||
mpaiSaveLocal(); | |||
// 8) pulizia allegati/barrina | |||
mpaiState.attachments = []; | |||
mpaiRenderFiles(); | |||
} catch (e) { | |||
mpaiAppendMsg('error', `Errore di rete/JS: ${e.message}`); | |||
} | |||
}; | }; | ||
/* ===================== SHOW UI + BIND UNA SOLA VOLTA ===================== */ | /* ===================== SHOW UI + BIND UNA SOLA VOLTA ===================== */ | ||
| Riga 879: | Riga 938: | ||
btn.addEventListener('click', openModal); | btn.addEventListener('click', openModal); | ||
})(); | })(); | ||
/* ===== MPAI SHIM v2: normalizza la risposta di /dashboard/api/openai_project_gpt.php in JSON ===== */ | /* ===== MPAI SHIM v2: normalizza la risposta di /dashboard/api/openai_project_gpt.php in JSON ===== */ | ||
| Riga 954: | Riga 1 012: | ||
console.log('MPAI SHIM v2 attivo (risposte normalizzate a JSON ok).'); | console.log('MPAI SHIM v2 attivo (risposte normalizzate a JSON ok).'); | ||
})(); | })(); | ||
/* ===== MPAI PARACHUTE v3 (fix errore, \n, tagli) ===== */ | /* ===== MPAI PARACHUTE v3 (fix errore, \n, tagli) ===== */ | ||
Versione delle 22:57, 17 ott 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?)}]
};
/* ======================================================================
[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 => ({'&':'&','<':'<','>':'>','"':'"'}[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);
});
}
/* ===================== 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 (robusto a text/plain o JSON) ===== */
window.sendPrompt = async function () {
try {
const ta = document.querySelector('#mpai-input');
if (!ta) return;
const content = (ta.value || '').trim();
if (!content) { ta.focus(); return; }
// 1) mostra subito il messaggio utente
mpaiState.history.push({ role: 'user', content });
mpaiAppendMsg('user', content);
ta.value = '';
// Galleria allegati nel thread (persistente)
const sentAtt = (mpaiState.attachments || []).map(mpaiSanitizeAttForHistory);
mpaiAppendAttachmentGallery('user', sentAtt);
const model = (document.querySelector('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim();
const pending = mpaiShowTyping('assistant');
// 2) raccogli allegati
let files = [], texts = [];
try {
const p = await collectAttachedPayload();
files = p.files;
texts = p.texts;
} catch(e){
console.warn('collectAttachedPayload failed', e);
}
// 3) payload base
const payload = { model, prompt: content, files, texts, temperature: 0.7 };
// 4) merge del CONTEX da “Importa Wikitesto da URL”
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] : [];
payload.files = Array.from(new Set([...(payload.files || []), ...filesFromCtx]));
console.log('📎 Contesto aggiunto al payload:', filesFromCtx);
} catch (e) {
console.warn('Context merge failed', e);
}
// 5) chiamata al proxy
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) {
// vero errore HTTP
mpaiAppendMsg('error', `HTTP ${r.status} ${r.statusText}\n\n${raw || '(nessun body)'}`);
return;
}
// 6) accetta SIA text/plain SIA JSON
const ct = (r.headers.get('content-type') || '').toLowerCase();
let outText = '';
if (ct.includes('application/json')) {
// il server ha risposto JSON → prendo i campi utili
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 || (
typeof j === 'string' ? j : JSON.stringify(j)
);
} catch {
// JSON rotto → mostro il grezzo
outText = raw;
}
} else {
// text/plain (nuovo handler lato server)
outText = raw;
}
// normalizza newline (se arrivano come "\n")
outText = String(outText || '').replace(/\\n/g, '\n').replace(/\r/g, '');
// 7) mostra come risposta “normale”, niente riquadro Errore
if (outText.trim() === '') outText = '[vuoto]';
mpaiState.history.push({ role: 'assistant', content: outText });
mpaiAppendMsg('assistant', outText);
mpaiSaveLocal();
// 8) pulizia allegati/barrina
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).');
})();