|
|
| Riga 130: |
Riga 130: |
| </style> | | </style> |
|
| |
|
| <!-- ============ 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}]
| |
|
| |
| // 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 key = 'mpai.session.hist.' + id;
| |
| 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(){
| |
| const model = ($('#mpai-model')?.value || 'gpt-4o-2024-05-13').trim();
| |
| const temp = parseFloat($('#mpai-temp')?.value || '0.7') || 0.7;
| |
| const sanitizedOnly = !!($('#mpai-sanitized-only')?.checked);
| |
| const content = (inputEl.value || '').trim();
| |
| if (!content){ inputEl.focus(); return; }
| |
|
| |
| // se non c’è progetto attivo, attiva la bozza locale
| |
| if (!currentProject && !sessionId){
| |
| ensureSession();
| |
| loadSession(sessionId);
| |
| }
| |
|
| |
| // mostra subito il messaggio dell’utente
| |
| history.push({role:'user', content});
| |
| appendMsg('user', content);
| |
| inputEl.value = '';
| |
|
| |
| // prepara il “preface” da mandare al backend
| |
| const contextName = currentProject
| |
| ? `Progetto: ${currentProject}`
| |
| : `Sessione: ${sessionMeta?.title || 'Nuova conversazione'}`;
| |
| const fileNames = attachments.map(a=>a.name).join(', ');
| |
| const preface = `${contextName}
| |
| File allegati${sanitizedOnly ? ' (sanificati)' : ''}: ${fileNames || 'nessuno'}
| |
|
| |
| Istruzioni: rispondi in ITALIANO, struttura chiara (titoli, punti elenco), sii operativo. Temperatura: ${temp}.
| |
| ---
| |
| Utente:
| |
| ${content}`;
| |
|
| |
| // placeholder “Elaboro…”
| |
| const pending = document.createElement('div');
| |
| pending.className='mpai-msg';
| |
| pending.innerHTML='<em class="muted">Elaboro…</em>';
| |
| chatEl.appendChild(pending); chatEl.scrollTop=chatEl.scrollHeight;
| |
|
| |
| try{
| |
| 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: preface })
| |
| });
| |
|
| |
| const raw = await r.text();
| |
|
| |
| if (!r.ok){
| |
| pending.remove();
| |
| appendMsg('error', `HTTP ${r.status} ${r.statusText}\n\n${raw || '(nessun body)'}`);
| |
| return;
| |
| }
| |
|
| |
| let j;
| |
| try{ j = JSON.parse(raw); }
| |
| catch(e){ pending.remove(); appendMsg('error', `Risposta non in JSON:\n\n${raw.slice(0,1500)}`); return; }
| |
|
| |
| pending.remove();
| |
|
| |
| if (j.status === 'ok' && j.result){
| |
| if (!currentProject && sessionMeta && sessionMeta.title === 'Nuova conversazione'){
| |
| sessionMeta.title = (content.slice(0,48) || 'Nuova conversazione');
| |
| saveSession();
| |
| }
| |
| history.push({role:'assistant', content:j.result});
| |
| appendMsg('assistant', j.result); saveLocal();
| |
| } else {
| |
| const err = j.error || 'Errore sconosciuto';
| |
| appendMsg('error', `API error: ${err}\n\nRAW:\n${(j.raw||'').slice(0,1500)}`);
| |
| }
| |
| }catch(e){
| |
| pending.remove();
| |
| appendMsg('error', `Errore di rete/JS: ${e.message}`);
| |
| }
| |
| }
| |
|
| |
| /* ===========================================================
| |
| 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);
| |
| })();
| |
|
| |
| /* ===========================================================
| |
| 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) ========== --> |