|
|
| Riga 1: |
Riga 1: |
| /* ===================== MPAI – CHAT APP (no inline script) ===================== */ | | /* ============================================================================ |
| | MPAI – CommonDashboard.js (FULL FILE - PASTE ALL) |
| | Compatibile con /dashboard/api/openai_project_gpt.php (Responses API proxy) |
| | - Nessun doppio load |
| | - Renderer Markdown leggero |
| | - Prompt strutturato (system + template + domanda) |
| | - Risposte corpose in 7 sezioni |
| | ============================================================================ */ |
|
| |
|
| /** Guardia anti-doppio-caricamento (strumentata) */ | | /* ----------------------- Guardia anti-doppio-caricamento ------------------- */ |
| if (window.__MP_DASHBOARD_LOADED__) { | | if (window.__MP_COMMON_DASH_LOADED__) { |
| const src = (document.currentScript && document.currentScript.src) || '(inline)'; | | console.warn('CommonDashboard già caricato – stop'); |
| try { const u = new URL(src, location.origin);
| | } else { |
| console.warn('MP Dashboard già caricata – stop. Source:', src, 'from=', u.searchParams.get('from') || '(n/a)');
| | window.__MP_COMMON_DASH_LOADED__ = true; |
| } 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 */ | | /* ----------------------------- Utilità minime ---------------------------- */ |
| if (!(window.$ && window.$.fn && typeof window.$.Deferred === 'function')) {
| | const $ = (sel) => document.querySelector(sel); |
| window.$ = window.jQuery; | | const $$ = (sel) => Array.from(document.querySelectorAll(sel)); |
| }
| | const nowHHMM = () => { |
| | try { return new Date().toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'}); } |
| | catch { return ''; } |
| | }; |
|
| |
|
| /** Shortcut locale */ | | /* --------------------------- Markdown renderer --------------------------- */ |
| window.$mp = window.$mp || (s => document.querySelector(s));
| | function escapeHtml(s){return String(s).replace(/[&<>"]/g,ch=>({'&':'&','<':'<','>':'>','"':'"'}[ch]));} |
| | function renderMarkdown(src){ |
| | // safe |
| | let s = escapeHtml(String(src||'')); |
|
| |
|
| (function () { | | // code fences |
| console.log('🧭 MPAI: init…');
| | s = s.replace(/```([a-z0-9+\-#]*)\n([\s\S]*?)```/gi, (_m,lang,code)=>{ |
| | const l = (lang||'').trim(); |
| | return ( |
| | '<div class="mp-code">' + |
| | '<div class="mp-code__hdr"><span class="mp-code__lang">'+(l||'code')+'</span>' + |
| | '<button class="mp-copy" onclick="navigator.clipboard.writeText(this.closest(\'.mp-code\').querySelector(\'pre\').innerText).then(()=>{this.textContent=\'Copiato\';setTimeout(()=>this.textContent=\'Copia\',900);})">Copia</button>' + |
| | '</div><pre><code>'+code.replace(/</g,'<')+'</code></pre></div>' |
| | ); |
| | }); |
|
| |
|
| /* ===================== STILI “ChatGPT vibe” + tipografia ===================== */
| | // inline code |
| (function attachMpaiDecorCss(){
| | s = s.replace(/`([^`]+?)`/g,'<code>$1</code>'); |
| 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 */ | | // headings |
| #mpAI .mpai-msg.assistant{
| | s = s |
| padding-left:56px; | | .replace(/^\s*######\s+(.+)$/gm,'<h6>$1</h6>') |
| background:#ffffff !important; | | .replace(/^\s*#####\s+(.+)$/gm,'<h5>$1</h5>') |
| border-color:#a7f3d0 !important;
| | .replace(/^\s*####\s+(.+)$/gm,'<h4>$1</h4>') |
| }
| | .replace(/^\s*###\s+(.+)$/gm,'<h3>$1</h3>') |
| #mpAI .mpai-msg.assistant::before{
| | .replace(/^\s*##\s+(.+)$/gm,'<h2>$1</h2>') |
| content:""; | | .replace(/^\s*#\s+(.+)$/gm,'<h1>$1</h1>'); |
| 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 */ | | // bold / italic |
| #mpAI .mpai-msg.user{ | | s = s.replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>'); |
| background:#eaf2ff;
| | s = s.replace(/(^|[^\*])\*([^*]+?)\*(?!\*)/g, (_m,pre,txt)=> pre+'<em>'+txt+'</em>'); |
| 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 */ | | // links |
| #mpAI .mpai-msg .role{ font-weight:600; margin-bottom:4px; color:#374151 } | | s = s.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g,'<a href="$2" target="_blank" rel="nofollow noopener">$1</a>'); |
| #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 */ | | // bullet list (linee che iniziano con "- ") |
| #mpAI #mpai-input{
| | s = s.replace(/(?:^|\n)(?:-\s+[^\n]+)(?:\n-\s+[^\n]+)*\n?/g, block=>{ |
| font-size:16px;
| | const items = block.trim().split('\n').map(l=>l.replace(/^\-\s+/,'').trim()); |
| line-height:1.5;
| | return '<ul>'+items.map(it=>'<li>'+it+'</li>').join('')+'</ul>'; |
| 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) ===================== */
| | // paragrafi |
| function escapeHtml(s){
| | s = s.split(/\n{2,}/).map(par=>{ |
| 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; | | if (/^<h\d|^<ul>|^<pre>|^<blockquote>|^<table>/.test(par.trim())) return par; |
| return `<p>${par.trim().replace(/\n/g,'<br>')}</p>`; | | return '<p>'+par.trim().replace(/\n/g,'<br>')+'</p>'; |
| }).join('\n'); | | }).join('\n'); |
| | |
| return s; | | return s; |
| } | | } |
|
| |
|
| /* ===================== UI HELPERS ===================== */ | | /* ------------------------------- UI Chat --------------------------------- */ |
| function mpaiTime(ts){
| | function appendMsg(role, content){ |
| try { return new Date(ts||Date.now()).toLocaleTimeString('it-IT',{hour:'2-digit',minute:'2-digit'}); }
| | const chat = $('#mpai-chat'); if(!chat) return; |
| 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 box = document.createElement('div'); |
| const btn = document.createElement('button'); btn.type='button'; btn.title='Rimuovi'; btn.textContent='✕'; | | box.className = 'mpai-msg ' + (role==='user'?'user':role==='assistant'?'assistant':'error'); |
| btn.onclick = ()=>{ mpaiState.attachments.splice(i,1); mpaiRenderFiles(); }; | | box.innerHTML = |
| | '<div class="role">'+(role==='user'?'Tu':role==='assistant'?'GPT':'Errore')+'</div>'+ |
| | '<div class="content"></div>'+ |
| | '<div class="meta">'+nowHHMM()+'</div>'; |
| | chat.appendChild(box); |
| | chat.scrollTop = chat.scrollHeight; |
|
| |
|
| pill.appendChild(span); pill.appendChild(btn);
| | const ctn = box.querySelector('.content'); |
| bar.appendChild(pill);
| | ctn.innerHTML = renderMarkdown(String(content||'')); |
| });
| |
| } | | } |
|
| |
|
| /* ===================== GALLERIA FILE ALLEGATI (persistente nella chat) ===================== */
| | function showTyping(){ |
| function mpaiSanitizeAttForHistory(a){ | | const chat = $('#mpai-chat'); if(!chat) return null; |
| const name = (a?.name || a?.file?.name || 'file').toString(); | | const box = document.createElement('div'); |
| const type = (a?.file?.type || '').toLowerCase(); | | box.className = 'mpai-msg assistant'; |
| const dataUrl = a?.dataUrl || null; | | box.innerHTML = |
| return { name, type, dataUrl }; | | '<div class="role">GPT</div>'+ |
| | '<div class="content"><em><span class="dots">● ● ●</span></em></div>'+ |
| | '<div class="meta">'+nowHHMM()+'</div>'; |
| | chat.appendChild(box); |
| | chat.scrollTop = chat.scrollHeight; |
| | return box; |
| } | | } |
| | function hideTyping(el){ try{ el && el.remove(); }catch{} } |
|
| |
|
| function mpaiAppendAttachmentGallery(role, atts){ | | /* ----------------------- Prompt (system + template) ---------------------- */ |
| if (!atts || !atts.length) return;
| | const MASTICATIONPEDIA_SYSTEM = |
| const chatEl = $mp('#mpai-chat'); if (!chatEl) return;
| | "Sei l'assistente scientifico di Masticationpedia, con competenze in neurognatologia, " + |
| | | "linguaggio medico, fisiopatologia dello smalto e clinica odontoiatrica. " + |
| const div = document.createElement('div');
| | "Scrivi in italiano tecnico, completo ma chiaro, con fonti/termini corretti."; |
| 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); | | const ANSWER_TEMPLATE = [ |
| chatEl.appendChild(div); | | "### 1) Definizione in 3–4 righe", |
| chatEl.scrollTop = chatEl.scrollHeight; | | "- …", |
| }
| | "### 2) Classificazione/varianti", |
| | "- …", |
| | "### 3) Genetica e fisiopatologia essenziale", |
| | "- …", |
| | "### 4) Quadro clinico + imaging", |
| | "- …", |
| | "### 5) Diagnosi differenziale", |
| | "- …", |
| | "### 6) Trattamento (età pediatrica/adulto) e follow-up", |
| | "- …", |
| | "### 7) Spunti di ricerca / limiti", |
| | "- …" |
| | ].join("\n"); |
|
| |
|
| /* ===================== FILE HELPERS ===================== */ | | function buildFullPrompt(userQuestion){ |
| async function fileToDataURL(file){
| | return [ |
| return new Promise((resolve, reject)=>{ | | "ISTRUZIONI DI RUOLO:", |
| const r = new FileReader(); | | MASTICATIONPEDIA_SYSTEM, |
| r.onload = () => resolve(r.result); | | "", |
| r.onerror = reject; | | "STRUTTURA OBBLIGATORIA DELLA RISPOSTA (usa i titoli indicati, niente preamboli):", |
| r.readAsDataURL(file); | | ANSWER_TEMPLATE, |
| }); | | "", |
| | "LUNGHEZZA: completa ma concisa; usa elenchi puntati dove aiuta la leggibilità.", |
| | "STILE: clinico-scientifico; niente chiacchiere; non ripetere la domanda.", |
| | "", |
| | "DOMANDA:", |
| | String(userQuestion||'') |
| | ].join("\n"); |
| } | | } |
|
| |
|
| async function fileToText(file){ | | /* -------------------------- Chiamata al proxy PHP ------------------------ */ |
| return new Promise((resolve, reject)=>{ | | async function callOpenAI({model, temperature, prompt, images=[]}){ |
| const r = new FileReader(); | | const res = await fetch('/dashboard/api/openai_project_gpt.php', { |
| r.onload = () => resolve(String(r.result || '')); | | method: 'POST', |
| r.onerror = reject; | | headers: { 'Content-Type': 'application/json' }, |
| r.readAsText(file, 'utf-8'); | | credentials: 'include', |
| | cache: 'no-store', |
| | body: JSON.stringify({ model, temperature, prompt, images }) |
| }); | | }); |
| }
| |
|
| |
| // 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() {
| | const raw = await res.text(); |
| if (!mpaiState.sessionId) { | | // Normalizzazione robusta |
| mpaiState.sessionId = 'sess-' + Date.now();
| | let j; |
| mpaiState.sessionMeta = { title: 'Nuova conversazione', updated: Date.now() };
| | try { j = JSON.parse(raw); } catch { j = { status: 'ok', result: raw }; } |
| 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) ===================== */
| | if (!res.ok || (j.status && String(j.status).toLowerCase() === 'error')) { |
| async function mpaiLoadProjects() {
| | const msg = j && (j.error || j.message || j.raw) ? (j.error || j.message || j.raw) : ('HTTP '+res.status+' '+res.statusText); |
| const box = $mp('#mpai-project-list');
| | throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg)); |
| 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() {
| | // Possibili chiavi: result / text / output_text |
| const inp = $mp('#mpai-project-name'); | | const out = j.result || j.text || j.output_text || j.reply || j.message; |
| const name = (inp?.value || '').trim();
| | return String(out ?? '').replace(/\\n/g,'\n'); |
| 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) ===== */ | | /* ------------------------------ INVIO PROMPT ----------------------------- */ |
| window.sendPrompt = async function () { | | async function sendPrompt(){ |
| try { | | try { |
| const ta = document.querySelector('#mpai-input'); | | const ta = $('#mpai-input'); if(!ta) return; |
| if (!ta) return; | | const q = (ta.value||'').trim(); |
| | if (!q) { ta.focus(); return; } |
|
| |
|
| const content = (ta.value || '').trim();
| | // Echo user |
| if (!content) { ta.focus(); return; }
| | appendMsg('user', q); |
| | |
| // 1) mostra subito il messaggio utente | |
| mpaiState.history.push({ role: 'user', content });
| |
| mpaiAppendMsg('user', content); | |
| ta.value = ''; | | ta.value = ''; |
|
| |
|
| // Galleria allegati nel thread (persistente) | | // Lettura controlli |
| const sentAtt = (mpaiState.attachments || []).map(mpaiSanitizeAttForHistory); | | const model = ($('#mpai-model') && $('#mpai-model').value) || 'gpt-4o-2024-05-13'; |
| mpaiAppendAttachmentGallery('user', sentAtt);
| | const temperature = parseFloat(($('#mpai-temp') && $('#mpai-temp').value) || '0.3') || 0.3; |
| | |
| 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” | | // Prompt |
| try { | | const fullPrompt = buildFullPrompt(q); |
| 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 typing = showTyping(); |
| const r = await fetch('/dashboard/api/openai_project_gpt.php', { | | let text = await callOpenAI({ model, temperature, prompt: fullPrompt, images: [] }); |
| method: 'POST',
| | hideTyping(typing); |
| headers: { 'Content-Type': 'application/json' },
| |
| credentials: 'include',
| |
| cache: 'no-store',
| |
| body: JSON.stringify(payload)
| |
| }); | |
|
| |
|
| const raw = await r.text();
| | if (!text.trim()) text = '[Nessuna risposta]'; |
| mpaiHideTyping(pending);
| | appendMsg('assistant', text); |
| | |
| 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) { | | } catch (e) { |
| mpaiAppendMsg('error', `Errore di rete/JS: ${e.message}`); | | hideTyping(); |
| }
| | appendMsg('error', 'Errore: '+ (e && e.message ? e.message : e)); |
| };
| |
| | |
| /* ===================== 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(); | | // Esporta global per il bottone "Invia" |
| if (!header) return;
| | window.sendPrompt = sendPrompt; |
| | |
| // 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 | | /* --------------------------- Binding interfaccia -------------------------- */ |
| (function bootstrapContextBadges(){ | | function bindUI(){ |
| const MP_CTX_KEY = 'mp_context_files_v1'; | | // Pulsanti |
| let ctx = {};
| | $('#mpai-send')?.addEventListener('click', sendPrompt); |
| try { ctx = JSON.parse(localStorage.getItem(MP_CTX_KEY) || '{}'); } catch { ctx = {}; } | | // Invio con Cmd/Ctrl+Enter |
| Object.keys(ctx).forEach(p => { | | document.addEventListener('keydown', (ev)=>{ |
| const arr = ctx[p];
| | if ((ev.metaKey||ev.ctrlKey) && ev.key === 'Enter') { |
| if (arr && arr.length) { | | const ta = $('#mpai-input'); |
| const lastName = arr[arr.length - 1].split('/').pop();
| | if (ta && document.activeElement === ta) { ev.preventDefault(); sendPrompt(); } |
| 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);
| | // Messaggio di benvenuto se chat vuota |
| })();
| | const chat = $('#mpai-chat'); |
| | | if (chat && !chat.querySelector('.mpai-msg')) { |
| /* ===== MPAI SHIM v2: normalizza la risposta di /dashboard/api/openai_project_gpt.php in JSON ===== */ | | appendMsg('assistant', |
| (() => {
| | "Pronto. Inquadra l’argomento e ti rispondo con 7 sezioni (definizione → ricerca). " + |
| const TARGET = '/dashboard/api/openai_project_gpt.php';
| | "Se vuoi, indica anche età del paziente e contesto clinico." |
| | | ); |
| 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 | | // Bind quando l’area #mpAI è in pagina |
| function deescapeNewlines(s) { | | function bootstrap(){ |
| return String(s).replace(/\\n/g, '\n').replace(/\r/g, '');
| | const ai = $('#mpAI'); |
| }
| | if (ai) bindUI(); |
| | |
| // 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).');
| |
| })();
| |
| | |
| /* === Reserve doc. — versione minima e robusta === */
| |
| mw.loader.using(['mediawiki.api','mediawiki.util']).then(function () {
| |
| | |
| // 1) funzione click: crea Dashboard:TITOLO e apre in modifica
| |
| function onReserveClick() {
| |
| var titolo = prompt('Titolo del documento (senza "Dashboard:")', 'Wellcome_project');
| |
| if (!titolo) return; | |
| | |
| var slug = titolo.trim().replace(/\s+/g, '_');
| |
| var pageTitle = 'Dashboard:' + slug;
| |
| var testo = '== ' + titolo.trim() + ' ==\nScrivi qui...';
| |
| | |
| new mw.Api().postWithToken('csrf', {
| |
| action: 'edit',
| |
| title: pageTitle,
| |
| text: testo,
| |
| createonly: 1,
| |
| summary: 'Creato da Dashboard (Reserve doc.)'
| |
| }).done(function () {
| |
| location.href = mw.util.getUrl(pageTitle, { action: 'edit' });
| |
| }).fail(function (e) {
| |
| if (e && e.error && (e.error.code === 'articleexists' || e.error.code === 'articleexistsforuser')) {
| |
| location.href = mw.util.getUrl(pageTitle);
| |
| } else {
| |
| alert('Errore: ' + (e && e.error && e.error.info ? e.error.info : 'sconosciuto'));
| |
| }
| |
| });
| |
| } | | } |
|
| |
| // 2) crea il bottone una sola volta
| |
| function injectButton() {
| |
| if (document.getElementById('mp-reserve-doc-btn')) return;
| |
| var container = document.querySelector('#contentSub') || document.body;
| |
|
| |
| var btn = document.createElement('button');
| |
| btn.id = 'mp-reserve-doc-btn';
| |
| btn.textContent = 'Reserve doc.';
| |
| btn.style.cssText = 'padding:.45rem .8rem;border:1px solid #d1d5db;border-radius:.5rem;background:#f9fafb;font-weight:600;cursor:pointer;margin-right:.5rem;';
| |
| btn.addEventListener('click', onReserveClick);
| |
| container.prepend(btn);
| |
|
| |
| console.log('✅ Reserve doc. inserito');
| |
| }
| |
|
| |
| // 3) inserisci SUBITO e anche quando la pagina è pronta
| |
| if (document.readyState === 'loading') { | | if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', injectButton); | | document.addEventListener('DOMContentLoaded', bootstrap); |
| } else { | | } else { |
| injectButton(); | | bootstrap(); |
| } | | } |
| mw.hook('wikipage.content').add(injectButton);
| |
| });
| |
|
| |
|
| // Forza il link "Modifica" a VisualEditor solo nel namespace Dashboard (3000) | | /* ----------------------------- Stili minimi ------------------------------ */ |
| (function () { | | (function injectCss(){ |
| if (mw.config.get('wgNamespaceNumber') !== 3000) return;
| | if (document.getElementById('mp-ai-extra-css')) return; |
| var tab = document.querySelector('#ca-edit a'); // la tab "Modifica sorgente"
| | const css = ` |
| if (tab) {
| | #mpai-chat .mpai-msg{border:1px solid #e5e7eb;border-radius:12px;padding:10px;background:#fff} |
| tab.href = mw.util.getUrl(mw.config.get('wgPageName'), { veaction: 'edit' }); | | #mpai-chat .mpai-msg.user{background:#eaf2ff;border-color:#d7e6ff} |
| tab.textContent = 'Modifica'; | | #mpai-chat .mpai-msg.assistant{background:#ffffff;border-color:#a7f3d0} |
| }
| | #mpai-chat .mpai-msg.error{background:#fff1f2;border-color:#fecaca} |
| })();
| | #mpai-chat .mpai-msg .role{font-weight:600;margin-bottom:4px;color:#374151} |
| | | #mpai-chat .mpai-msg .content p{margin:.45em 0} |
| /* Nascondi "Importa Wikitesto da URL" solo nei documenti Dashboard:, con observer */
| | #mpai-chat .mpai-msg .content ul{margin:.4em 0 .4em 1.2em;list-style:disc} |
| (function () {
| | .mp-code{border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;margin:.6rem 0;background:#fff} |
| var ns = mw.config.get('wgNamespaceNumber'); // 3000 = Dashboard
| | .mp-code__hdr{display:flex;justify-content:space-between;align-items:center;padding:.5rem .75rem;border-bottom:1px solid #e5e7eb;background:#f8fafc} |
| var page = mw.config.get('wgPageName'); // es. "Dashboard_Masticationpedia"
| | .mp-code__lang{font-family:ui-monospace, Menlo, Consolas, monospace;color:#334155} |
| | | .mp-copy{border:1px solid #e5e7eb;background:#fff;border-radius:8px;padding:.25rem .5rem;cursor:pointer} |
| // eccezioni: home e (se vuoi) indice
| | .mp-copy:hover{background:#f3f4f6} |
| var EXCEPTIONS = new Set(['Dashboard_Masticationpedia', 'Dashboard_Indice']);
| | .mp-code pre{margin:0;padding:.75rem 1rem;overflow:auto;font-family:ui-monospace, Menlo, Consolas, monospace;font-size:13px} |
| | | `; |
| if (ns !== 3000 || EXCEPTIONS.has(page)) return;
| | const s = document.createElement('style'); s.id='mp-ai-extra-css'; s.textContent = css; |
| | | document.head.appendChild(s); |
| function hideImportButton(root) {
| |
| (root || document).querySelectorAll('a').forEach(function (a) { | |
| // match su title OPPURE sul testo visibile (nel caso il title mancasse)
| |
| if (
| |
| (a.title && a.title.trim() === 'Importa Wikitesto da URL') ||
| |
| /Importa Wikitesto da URL/i.test(a.textContent || '')
| |
| ) {
| |
| a.style.display = 'none';
| |
| a.classList.add('mp-hide-import');
| |
| }
| |
| }); | |
| }
| |
| | |
| // nascondi subito
| |
| hideImportButton();
| |
| | |
| // …e nascondi anche se viene re-iniettato in seguito
| |
| var mo = new MutationObserver(function (muts) {
| |
| muts.forEach(function (m) { hideImportButton(m.target); });
| |
| });
| |
| mo.observe(document.body, { childList: true, subtree: true });
| |
| })();
| |
| | |
| /* ====== Dashboard fixes (solo ns 3000) ====== */
| |
| (function () {
| |
| var ns = mw.config.get('wgNamespaceNumber'); // 3000 = Dashboard
| |
| var page = mw.config.get('wgPageName'); // es. "Dashboard_Masticationpedia"
| |
| | |
| if (ns !== 3000) return;
| |
| | |
| /* 1) Aggiungi la TAB "Modifica sorgente" (wikitesto) accanto a Modifica (VE) */
| |
| (function addSourceTab () {
| |
| // vector 2022: <div id="p-views"> ... <ul class="vector-menu-content-list"> | |
| var list = document.querySelector('#p-views .vector-menu-content-list') || | |
| document.querySelector('#p-views ul');
| |
| if (!list || document.getElementById('ca-mp-edit-source')) return; | |
| | |
| var li = document.createElement('li'); | |
| li.id = 'ca-mp-edit-source';
| |
| li.className = 'mw-list-item';
| |
| var a = document.createElement('a');
| |
| a.textContent = 'Modifica sorgente';
| |
| a.href = mw.util.getUrl(page, { action: 'edit' }); // wikitesto | |
| li.appendChild(a);
| |
| list.appendChild(li);
| |
| })(); | | })(); |
|
| |
|
| /* 2) Nascondi "Importa Wikitesto da URL" nei documenti, MA non in home/indice */ | | console.log('✅ CommonDashboard.js (full) caricato.'); |
| (function hideImportButton () {
| | } |
| // <<<<<< CAMBIA QUI se il nome della home è diverso
| |
| var EXCEPTIONS = new Set(['Dashboard_Masticationpedia', 'Dashboard_Indice']);
| |
| if (EXCEPTIONS.has(page)) return;
| |
| | |
| function hide(root) {
| |
| (root || document).querySelectorAll('a,button').forEach(function (el) {
| |
| var txt = (el.textContent || '').trim();
| |
| var ttl = (el.title || '').trim();
| |
| if (/^Importa Wikitesto da URL$/i.test(txt) || /^Importa Wikitesto da URL$/i.test(ttl)) {
| |
| el.style.display = 'none';
| |
| el.classList.add('mp-hide-import');
| |
| }
| |
| });
| |
| }
| |
| // nascondi subito
| |
| hide();
| |
| // nascondi se riappare dopo
| |
| var mo = new MutationObserver(function (muts) {
| |
| for (var i=0;i<muts.length;i++) hide(muts[i].target);
| |
| });
| |
| mo.observe(document.body, { childList: true, subtree: true });
| |
| })();
| |
| | |
| /* 3) Pulsante "Documenti riservati" (apre Dashboard:Indice) vicino a Reserve doc. */
| |
| (function addIndexButton () {
| |
| if (document.getElementById('mp-doc-index-btn')) return;
| |
| var container = document.querySelector('#contentSub') || document.body;
| |
| var link = document.createElement('a');
| |
| link.id = 'mp-doc-index-btn';
| |
| link.textContent = 'Documenti riservati';
| |
| link.href = mw.util.getUrl('Dashboard:Indice');
| |
| link.style.cssText =
| |
| 'display:inline-block;margin-left:.5rem;padding:.45rem .8rem;border:1px solid #d1d5db;' +
| |
| 'border-radius:.5rem;background:#fff;font-weight:600;text-decoration:none;color:inherit;';
| |
| container.prepend(link);
| |
| })();
| |
| | |
| })();
| |