(() => { const ATTRIBUTE_SELECTORS = { DATA_MOUNT_CHAT_AI_DRAWER: '[data-mount="ai-chat-drawer"]', }; const ATTRIBUTES = { IS_AUTHOR: 'data-is-author', IS_ENV_PROD: 'data-is-env-prod', PARAMS: 'data-params', }; const EventName = { KEY_DOWN: 'keydown', WEBCHAT_CONNECT_FULFILLED: 'webchatconnectfulfilled', WEBCHAT_MESSAGE_RECEIVED: 'webchatmessagereceived' }; const Selector = { WEB_CHAT_FEED: '[role="feed"]', FOOTNOTE_LINK: '.ac-horizontal-separator + .ac-container a, .webchat__link-definitions__list-item-box--as-link', ACTION_SET_BUTTON: '.ac-pushButton:not(.action--ai-feedback)', DEFLECTION_LINKS: '[id^="deflection-"] .ac-actionSet button', POSITIVE_FEEDBACK_BUTTON: '[id$="-positive"] .ac-pushButton', NEGATIVE_FEEDBACK_BUTTON: '[id$="-negative"] .ac-pushButton', RELATED_LINKS: '[id$="-related"] .ac-anchor', SUPERSCRIPT_LINKS: '.ac-container p a.webchat__render-markdown__pure-identifier' }; const TELEMETRY_BEHAVIORS = { VOTE: 140, UNDEFINED: 0, PROACTIVE_CHAT_POPUP: "14" }; const TELEMETRY_TYPES = { AUTO: 'A', OTHER: 'O' }; const TELEMETRY_NAMES = { AI_CHAT_START: 'Ai Chat Start', PROACTIVE_CHAT: 'ChatInvite', PROACTIVE_CHAT_POPUP: 'AI Chat Proactive Popup' }; const TELEMETRY_IDS = { AI_CHAT_DRAWER: 'ai-chat-drawer' }; // Constant values for the AI Chat Drawer component. const IS_AUTHOR = document.querySelector(ATTRIBUTE_SELECTORS.DATA_MOUNT_CHAT_AI_DRAWER).getAttribute(ATTRIBUTES.IS_AUTHOR); const IS_ENV_PROD = document.querySelector(ATTRIBUTE_SELECTORS.DATA_MOUNT_CHAT_AI_DRAWER).getAttribute(ATTRIBUTES.IS_ENV_PROD); // Get param for author env. const PARAMS = document.querySelector(ATTRIBUTE_SELECTORS.DATA_MOUNT_CHAT_AI_DRAWER).getAttribute(ATTRIBUTES.PARAMS); const AI_CHAT_DRAWER_API_URL = "/GenerativeAIToken/getSessionToken"; // Ajax instance for the AI Chat Drawer component. let ajaxUtilAiChatDrawer = null; // Class instance for the AI Chat Drawer component (There is only one per page). let aiChatDrawerInstance; // Class instance for the AI Chat Search Form component let aiSearchFormInstance; // analytics object for telemetry let analytics = null; // tuid query param let tuidQueryParam = ''; /** * Callback for Proactive Chat to detect when the 'd-none' class is removed from the element. */ const pcObserverCallback = (mutationsList, observer) => { for (const mutation of mutationsList) { // Check if the class attribute has changed if (mutation.type === 'attributes' && mutation.attributeName === 'class') { // Get the current classList of the element const classList = mutation.target.classList; // Check if the 'd-none' class is removed if (!classList.contains('d-none')) { const proactiveChatContentTags = { cN: TELEMETRY_NAMES.PROACTIVE_CHAT, compnm: TELEMETRY_NAMES.PROACTIVE_CHAT_POPUP }; // Send telemetry for the AI Chat Drawer and banner search form sendTelemetry(TELEMETRY_BEHAVIORS.PROACTIVE_CHAT_POPUP, TELEMETRY_TYPES.OTHER, proactiveChatContentTags); // Optionally, stop observing if no longer needed observer.disconnect(); } } } }; /** * Set up the MutationObserver to detect when the 'd-none' class is removed from the Proactive Chat element * and when that happens sends telemetry for the Proactive Chat. */ const setUpProactiveChatTelemetry = () => { // Proactive Chat Element const proactiveChat = document.querySelector('.proactive-chat'); if (!proactiveChat) return; // Set up the MutationObserver to listen for attribute changes const pcObserver = new MutationObserver(pcObserverCallback); // Configure the observer to watch for attribute changes in the classList pcObserver.observe(proactiveChat, { attributes: true, // Listen for changes to attributes attributeFilter: ['class'], // Only watch the class attribute }); } /** * Wait for an element to be available in the DOM, and return a promise that resolves when the element is available. * @param {string} selector - The selector for the element to wait for. * @param {HTMLElement} containerElement - The container element to search within. Defaults to document. * @returns {Promise} - A promise that resolves when the element is available. */ const waitForElement = (selector, containerElement) => { containerElement = containerElement || document; const elementToObserve = containerElement === document ? document.body : containerElement; return new Promise(resolve => { if (containerElement.querySelector(selector)) { return resolve(containerElement.querySelector(selector)); } const observer = new MutationObserver(() => { if (containerElement.querySelector(selector)) { observer.disconnect(); resolve(containerElement.querySelector(selector)); } }); observer.observe(elementToObserve, { childList: true, subtree: true }); }); }; /** * Send telemetry for the AI Chat Drawer and banner search form. * * @param {number} behaviorId - The behavior ID for the telemetry event. * @param {string} actionType - The action type for the telemetry event. * @param {object} contentTags - The content tags for the telemetry event. */ const sendTelemetry = (behaviorId, actionType, contentTags) => { if (analytics !== null) { const overrides = { behavior: behaviorId, actionType: actionType, contentTags: contentTags }; analytics.capturePageAction(null, overrides); } }; /** * Set the token endpoint for the AI Chat Drawer instance, appending the tuid query parameter. */ const setTokenEndpoint = () => { // Use a temp variable for modifications let tokenEndpointBuilder = aiChatDrawerInstance.tokenEndpoint; let botEnvQueryParam = ""; var url = new URL(window.location.href); var agent = url.searchParams.get('agent') || ''; if (agent === "modern" && tokenEndpointBuilder.includes('/v1/')) { if (tokenEndpointBuilder.includes('webassistant-dev')) { tokenEndpointBuilder = tokenEndpointBuilder.replace('/v1/', '/v2/'); botEnvQueryParam = `&botEnvironment=medev-26e91437-5150-f011-877a-6045bdd935d3`; } else if (tokenEndpointBuilder.includes('webassistant-ppe')) { tokenEndpointBuilder = tokenEndpointBuilder.replace('/v1/', '/v2/'); botEnvQueryParam = `&botEnvironment=medev-26e91437-5150-f011-877a-6045bdd935d3`; } else if (tokenEndpointBuilder.includes('webassistant-prod')) { tokenEndpointBuilder = tokenEndpointBuilder.replace('/v1/', '/v2/'); botEnvQueryParam = `&botEnvironment=meprod-df89d4a3-358e-4db1-b79d-702c8acbce18`; } } // Append tuid query param and any extra parameters tokenEndpointBuilder = `${tokenEndpointBuilder}?${tuidQueryParam}${botEnvQueryParam}`; // Assign the final value to the instance aiChatDrawerInstance.tokenEndpoint = tokenEndpointBuilder; }; /** * Capture telemetry when the user submits the AI chat form using the Enter key. Click should be handled automatically. * * @param {HTMLElement} inputElem - The input/textarea element in the AI chat drawer. * @param {HTMLElement} sendButton - The submit button in the AI chat drawer. */ const handleAiFormSubmitTelemetry = (inputElem, sendButton) => { if (!inputElem || !sendButton) { return; } inputElem.addEventListener(EventName.KEY_DOWN, (event) => { if (event.key === 'Enter' && inputElem.value.trim() !== '' && !(event.shiftKey && inputElem.tagName === 'TEXTAREA')) { const behaviorId = TELEMETRY_BEHAVIORS.UNDEFINED; const actionType = TELEMETRY_TYPES.OTHER; const contentTags = { id: sendButton.dataset.biId, compnm: sendButton.dataset.biCompnm, fbid: sendButton.dataset.biFbid, fbnm: sendButton.dataset.biFbnm }; sendTelemetry(behaviorId, actionType, contentTags); } }); }; /** * Set the telemetry attributes on interactive elements once webchat conversation is started, and when messages from bot * are received. When 'webchatconnectfulfilled' event is fired, wait until the webchat feed is available, then set the * telemetry attributes on the send button. When a new message is added to the feed, set the telemetry attributes on the * footnote links and action buttons in the message from bot. */ const setTelemetryAttributes = () => { window.addEventListener(EventName.WEBCHAT_CONNECT_FULFILLED, () => { const webChatContainer = aiChatDrawerInstance.webChatContainer; waitForElement(Selector.WEB_CHAT_FEED, webChatContainer).then(() => { const conversationId = aiChatDrawerInstance.directLine.conversationId; const behaviorId = TELEMETRY_BEHAVIORS.VOTE; const actionType = TELEMETRY_TYPES.AUTO; const contentTags = { cN: TELEMETRY_NAMES.AI_CHAT_START, fbid: conversationId, fbnm: `${conversationId}` }; sendTelemetry(behaviorId, actionType, contentTags); setSendButtonTelemetryAttributes(); setBotMessageTelemetryAttributes(); setupMessageReceivedListener(); handleAiFormSubmitTelemetry(aiChatDrawerInstance.webChatTextarea, aiChatDrawerInstance.webChatSendButton); }); }, { once: true }); }; /** * Set up a listener for the 'webchatmessagereceived' event, and send telemetry when a new message is received. */ const setupMessageReceivedListener = () => { if (!aiChatDrawerInstance.directLine) { return; } window.addEventListener(EventName.WEBCHAT_MESSAGE_RECEIVED, () => { const conversationId = aiChatDrawerInstance.directLine.conversationId; const behaviorId = TELEMETRY_BEHAVIORS.VOTE; const actionType = TELEMETRY_TYPES.AUTO; const contentTags = { fbid: conversationId, fbnm: `${conversationId}`, }; sendTelemetry(behaviorId, actionType, contentTags); }); }; /** * Set telemetry attributes on the send button. */ const setSendButtonTelemetryAttributes = () => { const sendButton = aiChatDrawerInstance.webChatSendButton; if (!sendButton) { console.error('No send button found in AI chat drawer'); return; } sendButton.dataset.biCompnm = 'AI Chat Drawer'; sendButton.dataset.biId = 'Prompt submit'; if (!aiChatDrawerInstance.directLine) { return; } const conversationId = aiChatDrawerInstance.directLine.conversationId; sendButton.dataset.biFbid = conversationId; sendButton.dataset.biFbnm = `${conversationId}`; }; /** * Set telemetry attributes on footnote links and action buttons in messages from bot. */ const setBotMessageTelemetryAttributes = () => { // Set up MutationObserver to detect when new messages are added to the chat, and set the telemetry attributes on links/buttons // in messages from bot const webChatFeed = aiChatDrawerInstance.webChatContainer?.querySelector(Selector.WEB_CHAT_FEED); if (!webChatFeed) { console.error('No web chat feed found in AI chat drawer'); return; } const config = { subtree: true, childList: true }; const webChatFeedObserver = new MutationObserver((mutationList) => { for (const mutation of mutationList) { if (!mutation.addedNodes.length) { continue; } for (const addedNode of mutation.addedNodes) { if (!addedNode.classList?.contains('ac-adaptiveCard')) { continue; } setTimeout(() => { // Superscript links const superscriptLinks = addedNode.querySelectorAll(Selector.SUPERSCRIPT_LINKS); superscriptLinks.forEach(link => { link.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; link.dataset.biType = 'Footnote link'; }); // Tag footnote links const footnoteLinks = addedNode.querySelectorAll(Selector.FOOTNOTE_LINK); footnoteLinks.forEach(link => { link.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; link.dataset.biType = 'Footnote link'; }); // Tag positive feedback button const positiveFeedbackButton = addedNode.querySelector(Selector.POSITIVE_FEEDBACK_BUTTON); if (positiveFeedbackButton) { positiveFeedbackButton.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; positiveFeedbackButton.dataset.biType = 'Thumbs up'; positiveFeedbackButton.dataset.biFbid = aiChatDrawerInstance.directLine.conversationId; positiveFeedbackButton.dataset.biFbnm = `${aiChatDrawerInstance.directLine.conversationId}`; } // Tag negative feedback button const negativeFeedbackButton = addedNode.querySelector(Selector.NEGATIVE_FEEDBACK_BUTTON); if (negativeFeedbackButton) { negativeFeedbackButton.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; negativeFeedbackButton.dataset.biType = 'Thumbs down'; negativeFeedbackButton.dataset.biFbid = aiChatDrawerInstance.directLine.conversationId; negativeFeedbackButton.dataset.biFbnm = `${aiChatDrawerInstance.directLine.conversationId}`; } // Tag action buttons const actionSetButtons = addedNode.querySelectorAll(Selector.ACTION_SET_BUTTON); actionSetButtons.forEach(button => { button.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; button.dataset.biType = 'Canned prompt button'; }); // Tag deflection buttons const deflectionButtons = addedNode.querySelectorAll(Selector.DEFLECTION_LINKS); deflectionButtons.forEach(button => { // Get the title of the deflection button card const deflectionCardTitle = button.closest('.ac-adaptiveCard')?.querySelector('.ac-textBlock p')?.textContent; const deflectionButtonText = button.textContent?.trim(); button.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; button.dataset.biCn = deflectionButtonText; if (deflectionCardTitle) { button.dataset.biHn = deflectionCardTitle; } }); // Tag Related links const relatedLinks = addedNode.querySelectorAll(Selector.RELATED_LINKS); relatedLinks.forEach(link => { link.dataset.biId = TELEMETRY_IDS.AI_CHAT_DRAWER; link.dataset.biType = 'Related link'; }); }, 500); } } }); webChatFeedObserver.observe(webChatFeed, config); }; document.addEventListener('DOMContentLoaded', () => { // Set up Proactive Chat telemetry setUpProactiveChatTelemetry(); // Get the AI Chat Drawer instance. There should only be one instance on the page. if (window.ocrReimagine !== undefined) { aiChatDrawerInstance = window.ocrReimagine.AIChatDrawer.getInstances()[0]; aiSearchFormInstance = window.ocrReimagine.AISearchForm.getInstances()[0]; } else { aiChatDrawerInstance = window.m365.AIChatDrawer.getInstances()[0]; // Moray Extensions version of the AI Chat Drawer } if (typeof telemetry !== 'undefined') { // AEM analytics = telemetry.webAnalyticsPlugin; } else if (typeof awa !== 'undefined') { // Red tiger analytics = awa.ct; } if (!aiChatDrawerInstance) { console.error('No AI Chat Drawer instance found on page'); return; } // Grab tuid (random uuid) from cookie. If it doesn't exist, save to cookie. const cookieObj = aiChatDrawerInstance.getAIChatDrawerCookieObject(); let tuid = cookieObj?.tuid; if (!tuid) { tuid = self.crypto.randomUUID(); } aiChatDrawerInstance.tuid = tuid; tuidQueryParam = `tuid=${tuid}`; setTokenEndpoint(); setTelemetryAttributes(); if (!aiSearchFormInstance) { return; } handleAiFormSubmitTelemetry(aiSearchFormInstance.searchInput, aiSearchFormInstance.submitButton); }); })();