MediaWiki:CommonDashboard.js: differenze tra le versioni
Nessun oggetto della modifica |
Nessun oggetto della modifica |
||
| Riga 28: | Riga 28: | ||
const css = ` | const css = ` | ||
/* Scoping forte alla dashboard AI */ | /* Scoping forte alla dashboard AI */ | ||
#mpAI{ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; font-size:15px; color:#111827 } | #mpAI{ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; font-size:15px; color:#111827 } | ||
#mpAI .mpai-msg{ | #mpAI .mpai-msg{ | ||
| Riga 41: | Riga 40: | ||
/* GPT (assistant) a sinistra → bianco dentro, bordo verde chiaro */ | /* GPT (assistant) a sinistra → bianco dentro, bordo verde chiaro */ | ||
#mpAI .mpai-msg.assistant{ | #mpAI .mpai-msg.assistant{ | ||
padding-left:56px; | padding-left:56px; | ||
background:#ffffff !important; | background:#ffffff !important; | ||
border-color:#a7f3d0 !important; | border-color:#a7f3d0 !important; | ||
} | } | ||
#mpAI .mpai-msg.assistant::before{ | #mpAI .mpai-msg.assistant::before{ | ||
| Riga 57: | Riga 55: | ||
/* Utente a destra, azzurrino compatto */ | /* Utente a destra, azzurrino compatto */ | ||
#mpAI .mpai-msg.user{ | #mpAI .mpai-msg.user{ | ||
background:#eaf2ff; | background:#eaf2ff; | ||
| Riga 68: | Riga 65: | ||
/* Testo e meta */ | /* Testo e meta */ | ||
#mpAI .mpai-msg .role{ font-weight:600; margin-bottom:4px; color:#374151 } | #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{ color:#111827; white-space:normal } | ||
| Riga 88: | Riga 84: | ||
} | } | ||
#mpAI .mpai-msg .meta{ margin-top:6px; font-size:.72em; color:#9aa4b2 } | #mpAI .mpai-msg .meta{ margin-top:6px; font-size:.72em; color:#9aa4b2 } | ||
/* Input textarea: dimensioni più leggibili */ | /* Input textarea: dimensioni più leggibili */ | ||
#mpAI #mpai-input{ | #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” */ | /* “Typing dots” */ | ||
#mpAI .mpai-typing .content{ color:#6b7280 } | #mpAI .mpai-typing .content{ color:#6b7280 } | ||
#mpAI .mpai-typing .dots{ display:inline-flex; gap:6px; vertical-align:middle } | #mpAI .mpai-typing .dots{ display:inline-flex; gap:6px; vertical-align:middle } | ||
| Riga 134: | Riga 128: | ||
// 2) code block ```...``` | // 2) code block ```...``` | ||
s = s.replace(/```([\s\S]*?)```/g, (_m, code) => | s = s.replace(/```([\s\S]*?)```/g, (_m, code) => `<pre><code>${code.trim()}</code></pre>`); | ||
// 3) inline code `...` | // 3) inline code `...` | ||
| Riga 155: | Riga 147: | ||
s = s.replace(/^\s*#\s+(.+)$/gm, '<h1>$1</h1>'); | s = s.replace(/^\s*#\s+(.+)$/gm, '<h1>$1</h1>'); | ||
// 8) liste non ordinate | // 8) liste non ordinate (blocchi con righe che iniziano con "- ") | ||
s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block => { | s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block => { | ||
const items = block.trim().split('\n').map(line => line.replace(/^\-\s+/, '').trim()); | const items = block.trim().split('\n').map(line => line.replace(/^\-\s+/, '').trim()); | ||
| Riga 161: | Riga 153: | ||
}); | }); | ||
// 9) paragrafi | // 9) paragrafi | ||
s = s.split(/\n{2,}/).map(par => { | s = s.split(/\n{2,}/).map(par => { | ||
if (/^<h\d|^<ul>|^<pre>|^<blockquote>|^<table>/.test(par.trim())) return par; | if (/^<h\d|^<ul>|^<pre>|^<blockquote>|^<table>/.test(par.trim())) return par; | ||
| Riga 180: | Riga 172: | ||
if (!chatEl) return; | if (!chatEl) return; | ||
const html = renderMarkdown(content); // | const html = renderMarkdown(content); // markdown safe | ||
const div = document.createElement('div'); | const div = document.createElement('div'); | ||
| Riga 217: | Riga 209: | ||
}); | }); | ||
} | } | ||
/* ===================== FILE HELPERS ===================== */ | |||
// File -> dataURL (base64) | |||
async function fileToDataURL(file){ | |||
return new Promise((resolve, reject)=>{ | |||
const r = new FileReader(); | |||
r.onload = () => resolve(r.result); | |||
r.onerror = reject; | |||
r.readAsDataURL(file); | |||
}); | |||
} | |||
// | // File di testo -> stringa UTF-8 | ||
async function fileToText(file){ | |||
async function | return new Promise((resolve, reject)=>{ | ||
const r = new FileReader(); | |||
r.onload = () => resolve(String(r.result || '')); | |||
r.onerror = reject; | |||
r.readAsText(file, 'utf-8'); | |||
const | |||
}); | }); | ||
} | } | ||
// Raccoglie gli allegati in due gruppi: | |||
const | // - files: immagini (png/jpg/webp) e pdf -> { name, type, dataUrl } | ||
const | // - texts: file di testo puro -> [stringhe] | ||
async function collectAttachedPayload(){ | |||
const files = []; | |||
const texts = []; | |||
for (const att of (mpaiState.attachments || [])) { | |||
const f = att.file; | |||
if (!f) continue; | |||
// limite 10 MB | |||
if (f.size > 10 * 1024 * 1024) { | |||
if (f.size > | console.warn('File troppo grande, salto:', f.name); | ||
console.warn(' | |||
continue; | continue; | ||
} | } | ||
const isImg = ['image/png','image/jpeg','image/webp'].includes(f.type); | |||
const isPdf = (f.type === 'application/pdf' || (f.name||'').toLowerCase().endsWith('.pdf')); | |||
if (f. | const isTxt = (f.type === 'text/plain' || (f.name||'').toLowerCase().endsWith('.txt') || (f.name||'').toLowerCase().endsWith('.md')); | ||
console.warn(' | |||
if (isImg || isPdf) { | |||
const dataUrl = await fileToDataURL(f); // data:<mime>;base64,.... | |||
files.push({ name: f.name, type: isImg ? f.type : 'application/pdf', dataUrl }); | |||
} else if (isTxt) { | |||
const t = await fileToText(f); // stringa testo | |||
if (t && t.trim()) texts.push(t); | |||
} else { | |||
console.warn('Tipo non supportato:', f.name, f.type); | |||
} | } | ||
} | } | ||
return { files, texts }; | |||
} | } | ||
/* ===================== PERSISTENZA ===================== */ | /* ===================== PERSISTENZA ===================== */ | ||
| Riga 369: | Riga 319: | ||
box.innerHTML = '<em class="muted">Carico progetti…</em>'; | box.innerHTML = '<em class="muted">Carico progetti…</em>'; | ||
try { | try { | ||
const r = await fetch('/dashboard/api/project_list.php', { cache: 'no-store', credentials: 'include' }); | const r = await fetch('/dashboard/api/project_list.php', { cache: 'no-store', credentials: 'include' }); | ||
const j = await r.json(); | const j = await r.json(); | ||
| Riga 414: | Riga 363: | ||
} | } | ||
/* ===================== INVIO PROMPT → PROXY | /* ===================== INVIO PROMPT → PROXY ===================== */ | ||
window.sendPrompt = async function () { | window.sendPrompt = async function () { | ||
try { | try { | ||
| Riga 424: | Riga 373: | ||
// mostra subito il messaggio utente | // mostra subito il messaggio utente | ||
mpaiState.history.push({ role: 'user', content }); | mpaiState.history.push({ role: 'user', content }); | ||
mpaiAppendMsg('user', content); | mpaiAppendMsg('user', content); | ||
| Riga 432: | Riga 380: | ||
// typing dots stile ChatGPT | // typing dots stile ChatGPT | ||
const pending = mpaiShowTyping('assistant'); | const pending = mpaiShowTyping('assistant'); | ||
// raccogli allegati (img/pdf -> files[], txt -> texts[]) | |||
let files = [], texts = []; | |||
try { | |||
const p = await collectAttachedPayload(); | |||
files = p.files; | |||
texts = p.texts; | |||
} catch(e){ | |||
console.warn('collectAttachedPayload failed', e); | |||
} | |||
console.log('[MPAI] Invio', files.length, 'file (img/pdf) e', texts.length, 'testi'); | |||
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({ model, prompt: content, files, texts }) | |||
}); | |||
const text = await r.text(); | const text = await r.text(); | ||
| Riga 509: | Riga 453: | ||
dz && dz.addEventListener('drop', e => { | dz && dz.addEventListener('drop', e => { | ||
e.preventDefault(); dz.style.opacity = 1; | e.preventDefault(); dz.style.opacity = 1; | ||
Array.from(e.dataTransfer.files || []).forEach(f => mpaiState.attachments.push({ file: f, name: f.name })); | Array.from(e.dataTransfer.files || []).forEach(f=> mpaiState.attachments.push({file:f,name:f.name})); | ||
mpaiRenderFiles(); | mpaiRenderFiles(); | ||
}); | }); | ||
fi && fi.addEventListener('change', () => { | fi && fi.addEventListener('change', () => { | ||
Array.from(fi.files || []).forEach(f => mpaiState.attachments.push({ file: f, name: f.name })); | Array.from(fi.files||[]).forEach(f=> mpaiState.attachments.push({file:f,name:f.name})); | ||
fi.value = ''; mpaiRenderFiles(); | fi.value=''; mpaiRenderFiles(); | ||
}); | }); | ||
| Riga 520: | Riga 464: | ||
$mp('#mpai-send')?.addEventListener('click', window.sendPrompt); | $mp('#mpai-send')?.addEventListener('click', window.sendPrompt); | ||
$mp('#mpai-project-create')?.addEventListener('click', mpaiCreateProject); | $mp('#mpai-project-create')?.addEventListener('click', mpaiCreateProject); | ||
$mp('#mpai-new-chat')?.addEventListener('click', () => { | $mp('#mpai-new-chat')?.addEventListener('click', ()=>{ | ||
mpaiState.history = []; mpaiState.attachments = []; mpaiRenderFiles(); | mpaiState.history=[]; mpaiState.attachments=[]; mpaiRenderFiles(); | ||
const chatEl = $mp('#mpai-chat'); if (chatEl) { chatEl.innerHTML = ''; } | const chatEl = $mp('#mpai-chat'); if (chatEl){ chatEl.innerHTML=''; } | ||
if (mpaiState.currentProject) { mpaiAppendMsg('assistant', 'Nuova chat per "' + mpaiState.currentProject + '".'); mpaiSaveLocal(); } | if (mpaiState.currentProject){ mpaiAppendMsg('assistant','Nuova chat per "'+mpaiState.currentProject+'".'); mpaiSaveLocal(); } | ||
else { mpaiState.sessionId = null; mpaiState.sessionMeta = null; mpaiEnsureSession(); mpaiAppendMsg('assistant', 'Nuova conversazione.'); mpaiSaveSession(); } | else { mpaiState.sessionId=null; mpaiState.sessionMeta=null; mpaiEnsureSession(); mpaiAppendMsg('assistant','Nuova conversazione.'); mpaiSaveSession(); } | ||
}); | }); | ||
$mp('#mpai-clear')?.addEventListener('click', () => { | $mp('#mpai-clear')?.addEventListener('click', ()=>{ | ||
if (!mpaiState.currentProject) { alert('Crea o seleziona un progetto'); return; } | if (!mpaiState.currentProject){ alert('Crea o seleziona un progetto'); return; } | ||
if (!confirm('Svuotare la conversazione locale?')) return; | if (!confirm('Svuotare la conversazione locale?')) return; | ||
mpaiState.history = []; mpaiState.attachments = []; mpaiRenderFiles(); | mpaiState.history=[]; mpaiState.attachments=[]; mpaiRenderFiles(); | ||
const chatEl = $mp('#mpai-chat'); if (chatEl) { chatEl.innerHTML = ''; } | const chatEl = $mp('#mpai-chat'); if (chatEl){ chatEl.innerHTML=''; } | ||
mpaiAppendMsg('assistant', 'Conversazione pulita.'); mpaiSaveLocal(); | mpaiAppendMsg('assistant','Conversazione pulita.'); mpaiSaveLocal(); | ||
}); | }); | ||
// Invio con Cmd/Ctrl+Enter nella textarea | // Invio con Cmd/Ctrl+Enter nella textarea | ||
document.addEventListener('keydown', (e) => { | document.addEventListener('keydown', (e)=>{ | ||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { | if ((e.metaKey||e.ctrlKey) && e.key==='Enter'){ | ||
const ta = $mp('#mpai-input'); | const ta=$mp('#mpai-input'); | ||
if (ta && ta === document.activeElement) { e.preventDefault(); window.sendPrompt(); } | if (ta && ta===document.activeElement){ e.preventDefault(); window.sendPrompt(); } | ||
} | } | ||
}); | }); | ||
// | // Boot | ||
mpaiLoadProjects(); | mpaiLoadProjects(); | ||
console.log('🔗 MPAI listeners collegati.'); | console.log('🔗 MPAI listeners collegati.'); | ||
Versione delle 15:33, 4 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: dimensioni più leggibili */
#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);
})();
/* ===================== STATO ===================== */
const mpaiState = {
currentProject: null, // nome progetto (se selezionato)
sessionId: null, // id bozza locale
sessionMeta: null, // {title, updated}
history: [],
attachments: []
};
/* ===================== RENDER “MARKDOWN SAFE” ===================== */
function escapeHtml(s){
return s.replace(/[&<>"]/g, ch => ({'&':'&','<':'<','>':'>','"':'"'}[ch]));
}
function renderMarkdown(src){
// 1) escape tutto
let s = escapeHtml(String(src || ''));
// 2) code block ```...```
s = s.replace(/```([\s\S]*?)```/g, (_m, code) => `<pre><code>${code.trim()}</code></pre>`);
// 3) inline code `...`
s = s.replace(/`([^`]+?)`/g, (_m, code) => `<code>${code}</code>`);
// 4) grassetto **...**
s = s.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
// 5) corsivo *...*
s = s.replace(/(^|[^\*])\*([^*]+?)\*(?!\*)/g, (_m, pre, it) => `${pre}<em>${it}</em>`);
// 6) link [testo](url)
s = s.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, `<a href="$2" target="_blank" rel="nofollow noopener">$1</a>`);
// 7) intestazioni #, ##, ###
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>');
// 8) liste non ordinate (blocchi con righe che iniziano con "- ")
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>`;
});
// 9) 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 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 html = renderMarkdown(content); // markdown safe
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">${html}</div>
<div class="meta">${mpaiTime()}</div>`;
chatEl.appendChild(div);
chatEl.scrollTop = chatEl.scrollHeight;
}
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{} }
function mpaiRenderFiles() {
const fileListEl = $mp('#mpai-file-list');
if (!fileListEl) return;
fileListEl.innerHTML = '';
mpaiState.attachments.forEach((a, i) => {
const pill = document.createElement('span');
pill.className = 'mpai-filepill';
pill.innerHTML = `<span title="Allegato">${a.name}</span><button title="Rimuovi">✕</button>`;
pill.querySelector('button').onclick = () => { mpaiState.attachments.splice(i, 1); mpaiRenderFiles(); };
fileListEl.appendChild(pill);
});
}
/* ===================== FILE HELPERS ===================== */
// File -> dataURL (base64)
async function fileToDataURL(file){
return new Promise((resolve, reject)=>{
const r = new FileReader();
r.onload = () => resolve(r.result);
r.onerror = reject;
r.readAsDataURL(file);
});
}
// File di testo -> stringa UTF-8
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 gli allegati in due gruppi:
// - files: immagini (png/jpg/webp) e pdf -> { name, type, dataUrl }
// - texts: file di testo puro -> [stringhe]
async function collectAttachedPayload(){
const files = [];
const texts = [];
for (const att of (mpaiState.attachments || [])) {
const f = att.file;
if (!f) continue;
// limite 10 MB
if (f.size > 10 * 1024 * 1024) {
console.warn('File troppo grande, salto:', f.name);
continue;
}
const isImg = ['image/png','image/jpeg','image/webp'].includes(f.type);
const isPdf = (f.type === 'application/pdf' || (f.name||'').toLowerCase().endsWith('.pdf'));
const isTxt = (f.type === 'text/plain' || (f.name||'').toLowerCase().endsWith('.txt') || (f.name||'').toLowerCase().endsWith('.md'));
if (isImg || isPdf) {
const dataUrl = await fileToDataURL(f); // data:<mime>;base64,....
files.push({ name: f.name, type: isImg ? f.type : 'application/pdf', dataUrl });
} else if (isTxt) {
const t = await fileToText(f); // stringa testo
if (t && t.trim()) texts.push(t);
} else {
console.warn('Tipo non supportato:', f.name, f.type);
}
}
return { files, texts };
}
/* ===================== 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 ===================== */
window.sendPrompt = async function () {
try {
const ta = document.querySelector('#mpai-input');
if (!ta) return;
const content = (ta.value || '').trim();
if (!content) { ta.focus(); return; }
// mostra subito il messaggio utente
mpaiState.history.push({ role: 'user', content });
mpaiAppendMsg('user', content);
ta.value = '';
const model = (document.querySelector('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim();
// typing dots stile ChatGPT
const pending = mpaiShowTyping('assistant');
// raccogli allegati (img/pdf -> files[], txt -> texts[])
let files = [], texts = [];
try {
const p = await collectAttachedPayload();
files = p.files;
texts = p.texts;
} catch(e){
console.warn('collectAttachedPayload failed', e);
}
console.log('[MPAI] Invio', files.length, 'file (img/pdf) e', texts.length, 'testi');
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({ model, prompt: content, files, texts })
});
const text = await r.text();
mpaiHideTyping(pending);
if (!r.ok) {
mpaiAppendMsg('error', `HTTP ${r.status} ${r.statusText}\n\n${text || '(nessun body)'}`);
return;
}
let j;
try {
j = JSON.parse(text);
if (typeof j === 'string') { try { j = JSON.parse(j); } catch {} }
} catch (e) {
mpaiAppendMsg('error', `Risposta non in JSON:\n\n${text.slice(0, 1500)}`);
return;
}
if (j && j.status === 'ok' && j.result) {
mpaiState.history.push({ role: 'assistant', content: j.result });
mpaiAppendMsg('assistant', j.result);
mpaiSaveLocal();
} else {
mpaiAppendMsg('error', `API error: ${(j && (j.error || j.message)) || 'sconosciuto'}\n\nRAW:\n${text.slice(0, 1500)}`);
}
} 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(); // assicura i listener una sola volta
};
function mpaiBindUI() {
if (window.__MP_AI_BOUND__) return; // evita doppi binding
window.__MP_AI_BOUND__ = true;
if (!$mp('#mpAI')) return; // UI non presente
// Upload locale (solo elenco)
const dz = $mp('#mpai-dropzone');
const fi = $mp('#mpai-file-input');
dz && dz.addEventListener('dragover', e => { e.preventDefault(); dz.style.opacity = .85; });
dz && dz.addEventListener('dragleave', () => { dz.style.opacity = 1; });
dz && dz.addEventListener('drop', e => {
e.preventDefault(); dz.style.opacity = 1;
Array.from(e.dataTransfer.files || []).forEach(f=> mpaiState.attachments.push({file:f,name:f.name}));
mpaiRenderFiles();
});
fi && fi.addEventListener('change', () => {
Array.from(fi.files||[]).forEach(f=> mpaiState.attachments.push({file:f,name:f.name}));
fi.value=''; mpaiRenderFiles();
});
// 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.');
})();