Dashboard Masticationpedia: differenze tra le versioni
Nessun oggetto della modifica |
Nessun oggetto della modifica |
||
| Riga 131: | Riga 131: | ||
<!-- ============ JS (tutto qui) ============ --> | <!-- ============ JS (tutto qui) ============ --> | ||
<script> | |||
/* =========================================================== | |||
0) ENTRY-POINT: esegui tutto solo quando il DOM è pronto | |||
Perché: evitiamo che il codice cerchi elementi che ancora non esistono. | |||
/ | Quando: subito dopo il caricamento dell’HTML. | ||
=========================================================== */ | |||
document.addEventListener('DOMContentLoaded', function(){ | |||
/* =========================================================== | |||
// | 1) STATO DELL’APP (memoria in RAM) | ||
Perché: teniamo traccia di progetto attivo, cronologia locale e allegati. | |||
Quando: usato ovunque, sempre nello stesso script. | |||
=========================================================== */ | |||
let currentProject = null; // nome progetto scelto (server) o null | |||
let sessionId = null; // id sessione "bozza locale" quando non c’è progetto | |||
let sessionMeta = null; // { title, updated } per la bozza locale | |||
let history = []; // [{role:'user'|'assistant'|'error', content:string}] | |||
let attachments = []; // [{file:File, name:string}] | |||
document. | // comodi short-hand | ||
const $ = sel => document.querySelector(sel); | |||
const $$ = sel => Array.from(document.querySelectorAll(sel)); | |||
/* Riferimenti DOM principali (una sola volta) */ | |||
const chatEl = $('#mpai-chat'); | |||
const inputEl = $('#mpai-input'); | |||
const sendBtnEl = $('#mpai-send'); | |||
const projListEl = $('#mpai-project-list'); | |||
const projNameEl = $('#mpai-project-name'); | |||
const dropzoneEl = $('#mpai-dropzone'); | |||
const fileInput = $('#mpai-file-input'); | |||
const fileListEl = $('#mpai-file-list'); | |||
/* =========================================================== | |||
2) HELPER UI: append / render | |||
Perché: centralizziamo come visualizzare messaggi e file. | |||
Quando: ogni volta che mostriamo qualcosa in chat o cambiano gli allegati. | |||
=========================================================== */ | |||
function appendMsg(role, content){ | |||
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" style="white-space:pre-wrap">${content}</div> | |||
`; | |||
chatEl.appendChild(div); | |||
chatEl.scrollTop = chatEl.scrollHeight; | |||
} | |||
function renderFiles(){ | |||
fileListEl.innerHTML = ''; | |||
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 = ()=>{ attachments.splice(i,1); renderFiles(); }; | |||
fileListEl.appendChild(pill); | |||
}); | }); | ||
} | |||
/* =========================================================== | |||
3) PERSISTENZA LOCALE PER PROGETTI (localStorage) | |||
Perché: ogni progetto mantiene una cronologia “solo browser”. | |||
Quando: apri/cambi progetto o aggiungi messaggi. | |||
=========================================================== */ | |||
function saveLocal(){ | |||
if (!currentProject) return; | |||
localStorage.setItem('mpai.hist.'+currentProject, JSON.stringify(history.slice(-200))); | |||
} | |||
function loadLocal(project){ | |||
const raw = localStorage.getItem('mpai.hist.'+project); | |||
history = raw ? JSON.parse(raw) : []; | |||
chatEl.innerHTML = ''; | |||
if (!history.length){ | |||
appendMsg('assistant', `Pronto! Dimmi cosa vuoi fare su "${project}". Allegami anche file sanificati se servono.`); | |||
} else { | |||
history.forEach(m => appendMsg(m.role, m.content)); | |||
} | |||
} | |||
/* =========================================================== | |||
4) “BOZZA LOCALE” QUANDO NON C’È UN PROGETTO | |||
Perché: vuoi fare domande anche senza scegliere o creare un progetto. | |||
Quando: al primo invio se non esiste currentProject. | |||
=========================================================== */ | |||
function ensureSession(){ | |||
if (!sessionId){ | |||
sessionId = 'sess-' + Date.now(); | |||
sessionMeta = { title: 'Nuova conversazione', updated: Date.now() }; | |||
localStorage.setItem('mpai.session.meta.'+sessionId, JSON.stringify(sessionMeta)); | |||
} | |||
} | |||
function saveSession(){ | |||
if (!sessionId) return; | |||
const key = 'mpai.session.hist.' + sessionId; | |||
localStorage.setItem(key, JSON.stringify(history.slice(-200))); | |||
sessionMeta.updated = Date.now(); | |||
localStorage.setItem('mpai.session.meta.'+sessionId, JSON.stringify(sessionMeta)); | |||
} | |||
function loadSession(id){ | |||
sessionId = id; | |||
const | const key = 'mpai.session.hist.' + id; | ||
if ( | const raw = localStorage.getItem(key); | ||
history = raw ? JSON.parse(raw) : []; | |||
const metaRaw = localStorage.getItem('mpai.session.meta.'+id); | |||
sessionMeta = metaRaw ? JSON.parse(metaRaw) : {title:'Nuova conversazione', updated:Date.now()}; | |||
chatEl.innerHTML = ''; | |||
if (!history.length){ | |||
appendMsg('assistant', 'Pronto! Dimmi cosa vuoi fare. Puoi allegare file sanificati.'); | |||
} else { | |||
history.forEach(m => appendMsg(m.role, m.content)); | |||
} | |||
} | |||
/* =========================================================== | |||
5) LISTA PROGETTI (colonna sinistra) | |||
Perché: carica dall’endpoint PHP e permette di aprire/creare progetti. | |||
Quando: al boot e quando crei un progetto nuovo. | |||
=========================================================== */ | |||
async function loadProjects(){ | |||
projListEl.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){ projListEl.innerHTML='<em class="muted">Nessun progetto</em>'; return; } | |||
projListEl.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 = ()=>{ | |||
currentProject=name; loadLocal(name); | |||
}; | |||
projListEl.appendChild(row); | |||
}); | |||
if (!currentProject){ | |||
const first = (typeof arr[0]==='string')?arr[0]:(arr[0].name||arr[0]); | |||
currentProject = first; | |||
loadLocal(first); | |||
} | |||
}catch(e){ | |||
projListEl.innerHTML = '<span class="muted">Errore caricamento: '+e.message+'</span>'; | |||
} | |||
} | |||
async function createProject(){ | |||
const name = (projNameEl.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 loadProjects(); currentProject=name; history=[]; loadLocal(name); projNameEl.value=''; | |||
} else { | |||
alert('Errore creazione: '+(j.error||'')); | |||
} | } | ||
}); | }catch(e){ alert('Errore rete: '+e.message); } | ||
} | } | ||
/* =========================================================== | |||
6) UPLOAD LOCALE (solo elenco file sul client) | |||
Perché: vedi quali file userai come contesto (per ora elenco locale). | |||
Quando: drag&drop o selezione tramite input file. | |||
=========================================================== */ | |||
dropzoneEl.addEventListener('dragover', e => { e.preventDefault(); dropzoneEl.style.opacity = .85; }); | |||
dropzoneEl.addEventListener('dragleave', () => { dropzoneEl.style.opacity = 1; }); | |||
dropzoneEl.addEventListener('drop', e => { | |||
e.preventDefault(); dropzoneEl.style.opacity = 1; | |||
Array.from(e.dataTransfer.files||[]).forEach(f=> attachments.push({file:f,name:f.name})); | |||
renderFiles(); | |||
}); | |||
fileInput.addEventListener('change', () => { | |||
Array.from(fileInput.files||[]).forEach(f=> attachments.push({file:f,name:f.name})); | |||
fileInput.value=''; renderFiles(); | |||
}); | |||
/* =========================================================== | |||
7) INVIO PROMPT → PROXY PHP → OPENAI | |||
Perché: il browser non deve toccare la API key; parliamo col server. | |||
Quando: clic “Invia” o ⌘/Ctrl+Enter. | |||
=========================================================== */ | |||
async function sendPrompt(){ | async function sendPrompt(){ | ||
const model = ($('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim(); | |||
const temp = parseFloat($('#mpai-temp')?.value || '0.7') || 0.7; | |||
const model = ( | const sanitizedOnly = !!($('#mpai-sanitized-only')?.checked); | ||
const temp = parseFloat( | const content = (inputEl.value || '').trim(); | ||
const sanitizedOnly = !!( | if (!content){ inputEl.focus(); return; } | ||
// se non c’è progetto attivo, attiva la bozza locale | |||
// | |||
if (!currentProject && !sessionId){ | if (!currentProject && !sessionId){ | ||
ensureSession(); | ensureSession(); | ||
| Riga 170: | Riga 338: | ||
} | } | ||
// | // mostra subito il messaggio dell’utente | ||
history.push({role:'user', content}); | history.push({role:'user', content}); | ||
appendMsg('user', content); | appendMsg('user', content); | ||
inputEl.value = ''; | |||
// | // prepara il “preface” da mandare al backend | ||
const contextName = currentProject | const contextName = currentProject | ||
? `Progetto: ${currentProject}` | ? `Progetto: ${currentProject}` | ||
: `Sessione: ${sessionMeta?.title || 'Nuova conversazione'}`; | : `Sessione: ${sessionMeta?.title || 'Nuova conversazione'}`; | ||
const fileNames = attachments.map(a=>a.name).join(', '); | |||
const fileNames = | |||
const preface = `${contextName} | const preface = `${contextName} | ||
File allegati${sanitizedOnly ? ' (sanificati)' : ''}: ${fileNames || 'nessuno'} | File allegati${sanitizedOnly ? ' (sanificati)' : ''}: ${fileNames || 'nessuno'} | ||
| Riga 189: | Riga 356: | ||
${content}`; | ${content}`; | ||
// | // placeholder “Elaboro…” | ||
const pending = document.createElement('div'); | const pending = document.createElement('div'); | ||
pending.className = 'mpai-msg'; | pending.className='mpai-msg'; | ||
pending.innerHTML = '<em class="muted">Elaboro…</em>'; | pending.innerHTML='<em class="muted">Elaboro…</em>'; | ||
chatEl.appendChild(pending); | chatEl.appendChild(pending); chatEl.scrollTop=chatEl.scrollHeight; | ||
try{ | try{ | ||
| Riga 215: | Riga 381: | ||
let j; | let j; | ||
try{ j = JSON.parse(raw); } | try{ j = JSON.parse(raw); } | ||
catch(e){ | catch(e){ pending.remove(); appendMsg('error', `Risposta non in JSON:\n\n${raw.slice(0,1500)}`); return; } | ||
pending.remove(); | pending.remove(); | ||
if (j.status === 'ok' && j.result){ | if (j.status === 'ok' && j.result){ | ||
if (!currentProject && sessionMeta && sessionMeta.title === 'Nuova conversazione'){ | if (!currentProject && sessionMeta && sessionMeta.title === 'Nuova conversazione'){ | ||
sessionMeta.title = (content.slice(0,48) || 'Nuova conversazione'); | sessionMeta.title = (content.slice(0,48) || 'Nuova conversazione'); | ||
saveSession(); | |||
} | } | ||
history.push({role:'assistant', content: j.result}); | history.push({role:'assistant', content:j.result}); | ||
appendMsg('assistant', j.result); | appendMsg('assistant', j.result); saveLocal(); | ||
} else { | } else { | ||
const err = j.error || 'Errore sconosciuto'; | const err = j.error || 'Errore sconosciuto'; | ||
| Riga 242: | Riga 402: | ||
} | } | ||
/ | /* =========================================================== | ||
8) BIND EVENTI (una sola volta) | |||
Perché: colleghiamo UI → funzioni, evitando doppi listener. | |||
Quando: al boot, subito dopo aver creato le funzioni. | |||
=========================================================== */ | |||
(function bindUI(){ | |||
// bottone Invia | |||
sendBtnEl?.addEventListener('click', sendPrompt); | |||
// scorciatoia tastiera | |||
document.addEventListener('keydown', (e)=>{ | |||
if ((e.metaKey||e.ctrlKey) && e.key === 'Enter'){ | |||
if (inputEl && inputEl === document.activeElement){ e.preventDefault(); sendPrompt(); } | |||
} | |||
}); | |||
// crea progetto | |||
$('#mpai-project-create')?.addEventListener('click', createProject); | |||
})(); | |||
</script> | /* =========================================================== | ||
9) BOOT (prima vista) | |||
Perché: mostriamo progetti e, se non c’è progetto, apriamo una bozza locale. | |||
Quando: subito alla fine dell’inizializzazione. | |||
=========================================================== */ | |||
(async function boot(){ | |||
await loadProjects(); | |||
if (!currentProject){ ensureSession(); loadSession(sessionId); } | |||
})(); | |||
}); // fine DOMContentLoaded | |||
</script> | |||
<!-- ========== SEZIONI LEGACY (nascoste) ========== --> | <!-- ========== SEZIONI LEGACY (nascoste) ========== --> | ||
Versione delle 08:13, 28 set 2025
🔧 Dashboard Operativa – Masticationpedia
Centro di comando per progetti, API, file e backup