nis2-agile/public/js/ai-assistant.js
DevEnv nis2-agile 1d934e4e63 [FEAT] UI: guida online, landing EN, mobile-conversion, ai-assistant, bug-reporter + help/i18n
- public/guida.html, index-en.html, service-continuity.html
- public/js/ai-assistant.js, bug-reporter.js (FAB supporto)
- public/mobile-conversion.css/js
- index.html, common.js, help.js, risks.html: aggiornamenti UI/help

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 15:42:00 +02:00

549 lines
21 KiB
JavaScript

/**
* AgileHub — AI Assistant Widget (L1 Self-Service)
* Embeddabile in qualsiasi prodotto della suite.
*
* Flusso: L1 AI (KB search) -> feedback -> L2 Formatore -> L3 Ticket
*
* Uso:
* <script src="js/ai-assistant.js"
* data-product="TRPG"
* data-tenant-id="3"
* data-api-url="https://agilehub.agile.software"
* data-user-name="Mario Rossi"
* data-user-email="mario@studio.it"
* data-user-role="consultant"
* data-lang="it">
* </script>
*
* Prerequisiti backend:
* - POST /api/support-sessions (crea sessione)
* - POST /api/support-sessions/:id/message (AI risponde con KB)
* - POST /api/support-sessions/:id/resolve (feedback positivo)
* - POST /api/support-sessions/:id/forward-expert (escala L2)
* - POST /api/support-sessions/:id/escalate (escala L3 ticket)
*/
(function () {
'use strict';
// ==================== CONFIG ====================
var script = document.currentScript || document.querySelector('script[data-product][data-api-url]');
var CFG = {
apiUrl: (script && script.dataset.apiUrl) || '',
product: (script && script.dataset.product) || 'GENERIC',
tenantId: (script && script.dataset.tenantId) || '1',
userName: (script && script.dataset.userName) || '',
userEmail: (script && script.dataset.userEmail) || '',
userRole: (script && script.dataset.userRole) || '',
lang: (script && script.dataset.lang) || 'it'
};
var LABELS = {
it: {
title: 'Assistente AgileHub',
placeholder: 'Scrivi una domanda...',
viewing: 'Stai guardando',
suggestions_intro: 'Posso aiutarti con:',
feedback_yes: 'Si, grazie',
feedback_no: 'No, aiuto',
escalation_title: 'Come posso aiutarti meglio?',
btn_rephrase: 'Riformula la domanda',
btn_expert: 'Chiedi al formatore',
btn_bug: 'Segnala un bug',
expert_sent: 'Domanda inoltrata al formatore. Riceverai una notifica quando la risposta sara pronta.',
expert_time: 'Tempo stimato: entro 4 ore lavorative.',
resolved_thanks: 'Grazie per il feedback! La tua valutazione migliora l\'assistente.',
preparing: 'Mi sto preparando...',
thinking: 'Sto pensando...',
source: 'Fonte',
resume: 'Riprendiamo la conversazione.',
mic_unsupported: 'Microfono non supportato dal browser.',
error_generic: 'Errore di connessione. Riprova tra poco.'
},
en: {
title: 'AgileHub Assistant',
placeholder: 'Ask a question...',
viewing: 'You are viewing',
suggestions_intro: 'I can help you with:',
feedback_yes: 'Yes, thanks',
feedback_no: 'No, help me',
escalation_title: 'How can I help you better?',
btn_rephrase: 'Rephrase question',
btn_expert: 'Ask an expert',
btn_bug: 'Report a bug',
expert_sent: 'Your question has been forwarded to an expert. You\'ll be notified when the answer is ready.',
expert_time: 'Estimated time: within 4 business hours.',
resolved_thanks: 'Thanks for the feedback! Your rating improves the assistant.',
preparing: 'Preparing...',
thinking: 'Thinking...',
source: 'Source',
resume: 'Let\'s pick up where we left off.',
mic_unsupported: 'Microphone not supported by this browser.',
error_generic: 'Connection error. Please try again shortly.'
}
};
function t(key) {
var lang = CFG.lang;
return (LABELS[lang] && LABELS[lang][key]) || (LABELS.it[key]) || key;
}
// ==================== STATE ====================
var sessionId = null;
var messages = []; // { role: 'user'|'assistant'|'system', content, sources?, feedbackShown? }
var panelOpen = false;
var recognition = null;
var isRecording = false;
// ==================== API HELPERS ====================
function apiHeaders() {
var h = { 'Content-Type': 'application/json', 'x-tenant-id': CFG.tenantId };
// Try to get JWT from the host app
if (typeof Api !== 'undefined' && Api.auth && Api.auth.getToken) {
var tok = Api.auth.getToken();
if (tok) h['Authorization'] = 'Bearer ' + tok;
}
var stored = typeof localStorage !== 'undefined' ? localStorage.getItem('nexus_token') : null;
if (!h['Authorization'] && stored) h['Authorization'] = 'Bearer ' + stored;
return h;
}
function apiPost(path, body) {
return fetch(CFG.apiUrl + path, {
method: 'POST', headers: apiHeaders(), body: JSON.stringify(body)
}).then(function (r) { return r.json(); });
}
function apiGet(path) {
return fetch(CFG.apiUrl + path, { headers: apiHeaders() }).then(function (r) { return r.json(); });
}
// ==================== CONTEXT DETECTION ====================
function detectPage() {
// Try HelpSystem if available
if (typeof HelpSystem !== 'undefined' && HelpSystem._detectPage) {
return HelpSystem._detectPage();
}
// Fallback: extract from URL hash or path
var hash = location.hash.replace('#', '').replace('/', '') || '';
return hash || document.title || '';
}
function getContextualSuggestions() {
if (typeof HelpSystem === 'undefined') return [];
var page = typeof HelpSystem._detectPage === 'function' ? HelpSystem._detectPage() : null;
var helpData = null;
if (page && HelpSystem._helpContent) helpData = HelpSystem._helpContent[page];
if (page && HelpSystem._help) {
var raw = HelpSystem._help[page];
if (raw) helpData = raw[CFG.lang] || raw.it || raw;
}
if (!helpData) return [];
var suggestions = [];
if (helpData.faq) {
suggestions = helpData.faq.slice(0, 3).map(function (f) {
return (typeof f === 'string') ? f : (f.question || f.q || '');
});
} else if (helpData.sections) {
suggestions = helpData.sections.slice(0, 3).map(function (s) {
if (!s.heading) return '';
if (typeof s.heading === 'object') return s.heading[CFG.lang] || s.heading.it || '';
return s.heading;
});
}
return suggestions.filter(Boolean);
}
// ==================== SESSION MANAGEMENT ====================
function createSession(cb) {
apiPost('/api/support-sessions', {
product: CFG.product,
callerEmail: CFG.userEmail,
callerName: CFG.userName
}).then(function (res) {
if (res.success && res.data) {
sessionId = res.data.sessionId || res.data.id;
if (res.data.welcomeMessage) {
messages.push({ role: 'assistant', content: res.data.welcomeMessage });
}
// Store session for resume
try { sessionStorage.setItem('agilehub_ai_session_' + CFG.product, sessionId); } catch (e) {}
}
if (cb) cb();
}).catch(function () {
messages.push({ role: 'system', content: t('error_generic') });
if (cb) cb();
});
}
function tryResumeSession(cb) {
try {
var stored = sessionStorage.getItem('agilehub_ai_session_' + CFG.product);
if (stored) {
sessionId = stored;
messages.push({ role: 'system', content: t('resume') });
cb(true);
return;
}
} catch (e) {}
cb(false);
}
function sendMessage(text, cb) {
if (!sessionId) {
createSession(function () { if (sessionId) sendMessage(text, cb); else cb(); });
return;
}
messages.push({ role: 'user', content: text });
render();
apiPost('/api/support-sessions/' + sessionId + '/message', {
content: text
}).then(function (res) {
if (res.success && res.data) {
var reply = res.data.reply || res.data.message || res.data.answer || '';
var sources = res.data.knowledgeArticles || res.data.sources || [];
messages.push({ role: 'assistant', content: reply, sources: sources, feedbackShown: true });
} else {
messages.push({ role: 'assistant', content: (res.error && res.error.message) || t('error_generic') });
}
cb();
}).catch(function () {
messages.push({ role: 'assistant', content: t('error_generic') });
cb();
});
}
function resolveSession() {
if (!sessionId) return;
apiPost('/api/support-sessions/' + sessionId + '/resolve', {
rating: 5, resolutionSummary: 'auto'
});
}
function forwardToExpert(question, context) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/forward-expert', {
question: question, context: context
});
}
function escalateToTicket(reason) {
if (!sessionId) return;
return apiPost('/api/support-sessions/' + sessionId + '/escalate', {
reason: reason
});
}
// ==================== VOICE INPUT ====================
function initVoice() {
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return null;
recognition = new SR();
recognition.lang = CFG.lang === 'en' ? 'en-US' : 'it-IT';
recognition.interimResults = true;
recognition.continuous = false;
return recognition;
}
function toggleMic() {
if (!recognition) {
if (!initVoice()) {
messages.push({ role: 'system', content: t('mic_unsupported') });
render();
return;
}
}
if (isRecording) {
recognition.stop();
isRecording = false;
render();
return;
}
var input = document.getElementById('aia-input');
recognition.onresult = function (e) {
var transcript = '';
for (var i = e.resultIndex; i < e.results.length; i++) {
transcript += e.results[i][0].transcript;
}
if (input) input.value = transcript;
if (e.results[e.results.length - 1].isFinal) {
isRecording = false;
render();
if (transcript.trim()) submitInput(transcript.trim());
}
};
recognition.onerror = function () { isRecording = false; render(); };
recognition.onend = function () { isRecording = false; render(); };
recognition.start();
isRecording = true;
render();
}
// ==================== RENDERING ====================
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c];
});
}
function renderMessages() {
return messages.map(function (m, idx) {
var isUser = m.role === 'user';
var isSystem = m.role === 'system';
var bubble = '<div style="margin-bottom:10px;display:flex;justify-content:'
+ (isUser ? 'flex-end' : 'flex-start') + '">'
+ '<div style="max-width:82%;padding:10px 14px;border-radius:14px;font-size:13px;line-height:1.5;'
+ 'background:' + (isUser ? 'linear-gradient(135deg,#7C3AED,#3B82F6)' : isSystem ? '#FEF3C7' : '#f3f4f6')
+ ';color:' + (isUser ? '#fff' : '#111827')
+ ';white-space:pre-wrap">'
+ esc(m.content);
// Sources
if (m.sources && m.sources.length > 0) {
bubble += '<div style="margin-top:8px;padding-top:6px;border-top:1px solid rgba(0,0,0,.1);font-size:11px;color:#6B7280">';
m.sources.forEach(function (s, si) {
bubble += '<div>' + t('source') + ' [' + (si + 1) + '] ' + esc(s.question || s.title || '') + '</div>';
});
bubble += '</div>';
}
bubble += '</div></div>';
// Feedback buttons
if (m.feedbackShown && m.role === 'assistant' && idx === messages.length - 1) {
bubble += '<div id="aia-feedback" style="display:flex;gap:8px;justify-content:flex-start;margin-bottom:10px">'
+ '<button onclick="window.__aiaFeedback(true)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#059669">&#x1F44D; ' + esc(t('feedback_yes')) + '</button>'
+ '<button onclick="window.__aiaFeedback(false)" style="padding:6px 14px;border-radius:8px;border:1px solid #D1D5DB;'
+ 'background:#fff;cursor:pointer;font-size:12px;color:#DC2626">&#x1F44E; ' + esc(t('feedback_no')) + '</button>'
+ '</div>';
}
return bubble;
}).join('');
}
function renderEscalation() {
return '<div id="aia-escalation" style="background:#F9FAFB;border-radius:12px;padding:14px;margin-bottom:10px">'
+ '<div style="font-weight:600;margin-bottom:10px;font-size:13px">' + esc(t('escalation_title')) + '</div>'
+ '<button onclick="window.__aiaEscalate(\'rephrase\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F504; ' + esc(t('btn_rephrase')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'expert\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;margin-bottom:6px;font-size:12px">'
+ '&#x1F468;&#x200D;&#x1F3EB; ' + esc(t('btn_expert')) + '</button>'
+ '<button onclick="window.__aiaEscalate(\'bug\')" style="display:block;width:100%;text-align:left;padding:10px 12px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px">'
+ '&#x1F41B; ' + esc(t('btn_bug')) + '</button>'
+ '</div>';
}
function render() {
var panel = document.getElementById('aia-panel');
if (!panel) return;
var page = detectPage();
var suggestions = (messages.length === 0) ? getContextualSuggestions() : [];
var showEscalation = panel.dataset.showEscalation === '1';
panel.innerHTML =
// Header
'<div style="padding:14px 16px;background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'display:flex;justify-content:space-between;align-items:center;border-radius:16px 16px 0 0">'
+ '<div style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px">'
+ '<i class="fa-solid fa-wand-magic-sparkles"></i> ' + esc(t('title'))
+ '</div>'
+ '<button onclick="window.__aiaToggle()" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer">&times;</button>'
+ '</div>'
// Context bar
+ (page ? '<div style="padding:8px 16px;background:#EDE9FE;font-size:11px;color:#6D28D9">'
+ '&#x1F4CD; ' + esc(t('viewing')) + ': <strong>' + esc(page) + '</strong></div>' : '')
// Messages area
+ '<div id="aia-msgs" style="flex:1;overflow-y:auto;padding:14px 16px;background:#fafafa;min-height:200px">'
+ (messages.length === 0 && suggestions.length > 0
? '<div style="color:#6B7280;font-size:13px;margin-bottom:12px">' + esc(t('suggestions_intro')) + '</div>'
+ suggestions.map(function (s) {
return '<button onclick="window.__aiaSuggest(\'' + esc(s).replace(/'/g, "\\'") + '\')" '
+ 'style="display:block;width:100%;text-align:left;padding:8px 12px;margin-bottom:6px;'
+ 'border:1px solid #E5E7EB;border-radius:8px;background:#fff;cursor:pointer;font-size:12px;color:#4B5563">'
+ '&#x1F4A1; ' + esc(s) + '</button>';
}).join('')
: '')
+ renderMessages()
+ (showEscalation ? renderEscalation() : '')
+ '</div>'
// Input bar
+ '<form id="aia-form" onsubmit="return window.__aiaSubmit(event)" '
+ 'style="display:flex;gap:6px;padding:10px;border-top:1px solid #E5E7EB;background:#fff;border-radius:0 0 16px 16px">'
+ '<input id="aia-input" type="text" placeholder="' + esc(t('placeholder')) + '" autocomplete="off" '
+ 'style="flex:1;padding:10px 12px;border:1px solid #D1D5DB;border-radius:10px;font-size:13px;font-family:inherit">'
+ '<button type="button" onclick="window.__aiaMic()" style="padding:8px;border:none;border-radius:10px;'
+ 'background:' + (isRecording ? '#EF4444' : '#F3F4F6') + ';cursor:pointer;font-size:16px" title="Voce">'
+ (isRecording ? '&#x23F9;' : '&#x1F3A4;') + '</button>'
+ '<button type="submit" style="padding:8px 14px;border:none;border-radius:10px;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;font-weight:600;cursor:pointer">'
+ '<i class="fa-solid fa-paper-plane"></i></button>'
+ '</form>';
// Scroll to bottom
var msgsEl = document.getElementById('aia-msgs');
if (msgsEl) msgsEl.scrollTop = msgsEl.scrollHeight;
}
// ==================== HANDLERS ====================
function submitInput(text) {
if (!text) return;
var input = document.getElementById('aia-input');
if (input) input.value = '';
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
// Show thinking
messages.push({ role: 'assistant', content: t('thinking') });
render();
messages.pop(); // remove thinking
sendMessage(text, function () { render(); });
}
window.__aiaToggle = function () {
panelOpen = !panelOpen;
var panel = document.getElementById('aia-panel');
var fab = document.getElementById('ai-chat-fab');
if (panel) panel.style.display = panelOpen ? 'flex' : 'none';
if (fab) fab.style.display = panelOpen ? 'none' : 'flex';
if (panelOpen && messages.length === 0) {
tryResumeSession(function (resumed) {
if (!resumed) {
createSession(function () { render(); });
} else {
render();
}
});
}
if (panelOpen) render();
};
window.__aiaSubmit = function (e) {
e.preventDefault();
var input = document.getElementById('aia-input');
var text = (input && input.value || '').trim();
if (text) submitInput(text);
return false;
};
window.__aiaSuggest = function (text) { submitInput(text); };
window.__aiaMic = function () { toggleMic(); };
window.__aiaFeedback = function (positive) {
var fbEl = document.getElementById('aia-feedback');
if (fbEl) fbEl.remove();
if (positive) {
resolveSession();
messages.push({ role: 'system', content: t('resolved_thanks') });
render();
} else {
// Show escalation options
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '1';
render();
}
};
window.__aiaEscalate = function (action) {
var panel = document.getElementById('aia-panel');
if (panel) panel.dataset.showEscalation = '0';
if (action === 'rephrase') {
render();
var input = document.getElementById('aia-input');
if (input) input.focus();
return;
}
if (action === 'expert') {
var lastUserMsg = '';
var lastAiMsg = '';
for (var i = messages.length - 1; i >= 0; i--) {
if (!lastAiMsg && messages[i].role === 'assistant') lastAiMsg = messages[i].content;
if (!lastUserMsg && messages[i].role === 'user') lastUserMsg = messages[i].content;
if (lastUserMsg && lastAiMsg) break;
}
forwardToExpert(lastUserMsg, lastAiMsg).then(function () {
messages.push({ role: 'system', content: t('expert_sent') + '\n' + t('expert_time') });
render();
});
return;
}
if (action === 'bug') {
var reason = '';
for (var j = messages.length - 1; j >= 0; j--) {
if (messages[j].role === 'user') { reason = messages[j].content; break; }
}
escalateToTicket(reason).then(function () {
messages.push({ role: 'system', content: 'Ticket tecnico creato. Il team di sviluppo lo prendera in carico.' });
render();
});
return;
}
};
// ==================== INIT ====================
function init() {
// Ensure FAB exists (may have been created by product's app.js)
var fab = document.getElementById('ai-chat-fab');
if (!fab) {
fab = document.createElement('button');
fab.id = 'ai-chat-fab';
fab.setAttribute('aria-label', 'Chiedi ad ARIA');
fab.style.cssText = 'position:fixed;bottom:24px;right:24px;width:56px;height:56px;'
+ 'border-radius:50%;border:none;cursor:pointer;z-index:9998;'
+ 'background:linear-gradient(135deg,#7C3AED,#3B82F6);color:#fff;'
+ 'box-shadow:0 6px 20px rgba(124,58,237,.4);font-size:22px;'
+ 'display:flex;align-items:center;justify-content:center';
fab.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i>';
document.body.appendChild(fab);
} else {
// Rimuovi il vecchio pannello AI del prodotto (evita doppia finestra)
var oldPanel = document.getElementById('ai-chat-panel');
if (oldPanel) oldPanel.remove();
// Clona il FAB per eliminare TUTTI gli addEventListener precedenti
var cleanFab = fab.cloneNode(true);
fab.parentNode.replaceChild(cleanFab, fab);
fab = cleanFab;
}
fab.style.display = 'flex';
fab.style.alignItems = 'center';
fab.style.justifyContent = 'center';
fab.onclick = window.__aiaToggle;
// Ensure panel exists
var panel = document.getElementById('aia-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'aia-panel';
panel.style.cssText = 'position:fixed;bottom:90px;right:24px;width:380px;max-height:560px;'
+ 'background:#fff;border-radius:16px;box-shadow:0 12px 32px rgba(0,0,0,.2);'
+ 'display:none;z-index:9999;overflow:hidden;flex-direction:column;'
+ 'font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif';
document.body.appendChild(panel);
}
panel.dataset.showEscalation = '0';
}
// Auto-init when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();