Nessun oggetto della modifica
Nessun oggetto della modifica
Riga 131: Riga 131:


   <!-- ============ JS (tutto qui) ============ -->
   <!-- ============ JS (tutto qui) ============ -->
  <script>
<script>
  // ======== Invio prompt via proxy sicuro ========
/* ===========================================================
// ======== Invio prompt via proxy sicuro ========
  0) ENTRY-POINT: esegui tutto solo quando il DOM è pronto
// NOTA: questo codice deve stare dentro allo stesso <script> dove
  Perché: evitiamo che il codice cerchi elementi che ancora non esistono.
// sono definiti: let currentProject, let sessionId, let sessionMeta, let history, let attachments
  Quando: subito dopo il caricamento dell’HTML.
=========================================================== */
document.addEventListener('DOMContentLoaded', function(){


function bindSendHandlersOnce(){
/* ===========================================================
   // Evita doppi listener
  1) STATO DELL’APP (memoria in RAM)
  if (window.__mpai_bound) return;
  Perché: teniamo traccia di progetto attivo, cronologia locale e allegati.
  window.__mpai_bound = true;
  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.addEventListener('click', (e)=>{
// comodi short-hand
     if (e.target && e.target.id === 'mpai-send') sendPrompt();
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));
}


   document.addEventListener('keydown', (e)=>{
function loadSession(id){
     if ((e.metaKey||e.ctrlKey) && e.key === 'Enter'){
  sessionId = id;
       const ta = document.querySelector('#mpai-input');
  const key = 'mpai.session.hist.' + id;
       if (ta && ta === document.activeElement){ e.preventDefault(); sendPrompt(); }
  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 chatEl = document.querySelector('#mpai-chat');
   const model  = ($('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim();
  const ta    = document.querySelector('#mpai-input');
   const temp  = parseFloat($('#mpai-temp')?.value || '0.7') || 0.7;
   const model  = (document.querySelector('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim();
   const sanitizedOnly = !!($('#mpai-sanitized-only')?.checked);
   const temp  = parseFloat(document.querySelector('#mpai-temp')?.value || '0.7') || 0.7;
  const content = (inputEl.value || '').trim();
   const sanitizedOnly = !!(document.querySelector('#mpai-sanitized-only')?.checked);
  if (!content){ inputEl.focus(); return; }


  const content = (ta.value || '').trim();
   // se non c’è progetto attivo, attiva la bozza locale
  if (!content){ ta.focus(); return; }
 
   // Se non c’è un progetto attivo, usa una “bozza locale”
   if (!currentProject && !sessionId){
   if (!currentProject && !sessionId){
     ensureSession();
     ensureSession();
Riga 170: Riga 338:
   }
   }


   // Mostra subito il messaggio dell’utente
   // mostra subito il messaggio dell’utente
   history.push({role:'user', content});
   history.push({role:'user', content});
   appendMsg('user', content);
   appendMsg('user', content);
   ta.value = '';
   inputEl.value = '';


   // Contesto
   // 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 = (attachments || []).map(a => a.name).join(', ');
   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…”
   // 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;
  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();
      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){
      // Rinomina la bozza alla prima risposta
       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 && saveSession();
         saveSession();
       }
       }
       history.push({role:'assistant', content: j.result});
       history.push({role:'assistant', content:j.result});
       appendMsg('assistant', j.result);
       appendMsg('assistant', j.result); saveLocal();
      saveLocal && saveLocal();
     } else {
     } else {
       const err = j.error || 'Errore sconosciuto';
       const err = j.error || 'Errore sconosciuto';
Riga 242: Riga 402:
}
}


// esponi per test manuali e fai il binding una sola volta
/* ===========================================================
window.sendPrompt = sendPrompt;
  8) BIND EVENTI (una sola volta)
bindSendHandlersOnce();
  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

🧾 Apri Log Dashboard