/** * AgileHub — AI Assistant Widget (L1 Self-Service) * Embeddabile in qualsiasi prodotto della suite. * * Flusso: L1 AI (KB search) -> feedback -> L2 Formatore -> L3 Ticket * * Uso: * * * 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 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; }); } function renderMessages() { return messages.map(function (m, idx) { var isUser = m.role === 'user'; var isSystem = m.role === 'system'; var bubble = '
' + '
' + esc(m.content); // Sources if (m.sources && m.sources.length > 0) { bubble += '
'; m.sources.forEach(function (s, si) { bubble += '
' + t('source') + ' [' + (si + 1) + '] ' + esc(s.question || s.title || '') + '
'; }); bubble += '
'; } bubble += '
'; // Feedback buttons if (m.feedbackShown && m.role === 'assistant' && idx === messages.length - 1) { bubble += '
' + '' + '' + '
'; } return bubble; }).join(''); } function renderEscalation() { return '
' + '
' + esc(t('escalation_title')) + '
' + '' + '' + '' + '
'; } 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 '
' + '
' + ' ' + esc(t('title')) + '
' + '' + '
' // Context bar + (page ? '
' + '📍 ' + esc(t('viewing')) + ': ' + esc(page) + '
' : '') // Messages area + '
' + (messages.length === 0 && suggestions.length > 0 ? '
' + esc(t('suggestions_intro')) + '
' + suggestions.map(function (s) { return ''; }).join('') : '') + renderMessages() + (showEscalation ? renderEscalation() : '') + '
' // Input bar + '
' + '' + '' + '' + '
'; // 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 = ''; 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(); } })();