/** * 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 = '