UNPKG

@iblai/iblai-web-mentor

Version:

IBL AI Mentor

640 lines (563 loc) 17.5 kB
import { cleanElement, getParamsFromComponent, getUrlFromComponent, } from "./utils"; import { fetchUserTenants, fetchUserTokens } from "./api"; import { Theme } from "./models"; export { sendHTMLContentToHost, sendHTMLContentToIframe, proxyContextPostMessage, } from "./context-share"; export default class MentorAI extends HTMLElement { isEmbeddedMentorReady: boolean = false; iblData: string = ""; // Keeps track of the hosts' page URL lastUrl: string = ""; private iframeContexts: { [key: string]: string } = {}; // Object to keep track of iframe contexts constructor() { super(); const _iblData: string | null = new URL( window.location.href ).searchParams.get("ibl-data"); if (_iblData) { this.iblData = _iblData; } this.attachShadow({ mode: "open" }); const template = ` <style> iframe { border: 0px white; height: 100%; width: 100%; border-radius: 0; } #ibl-chat-widget-container { /* border: 1px solid #dfdfdf; */ height: 100%; position: relative; } @media screen and (max-width: 768px) { #ibl-chat-widget-container { } img.ibl-chat-bubble { right: 20px !important; } } .spinner { border: 3px solid #f3f3f3; /* Light grey */ border-top: 3px solid #6cafe1; /* Blue */ border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: block; /* Initially hidden */ } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> <div id="ibl-chat-widget-container"> <div class="spinner" id="loading-spinner"></div> <iframe allow="clipboard-read; clipboard-write" onload="this.parentNode.querySelector('#loading-spinner').style.display='none';" onloadstart="this.parentNode.querySelector('#loading-spinner').style.display='block';" ></iframe> </div> `; if (this.shadowRoot) { this.shadowRoot.innerHTML = template; } } async onPostMessage(event: MessageEvent) { let message: any = event.data; if (typeof message === "string") { try { message = JSON.parse(message); } catch (error) { return; } } // New context handling if (message?.type === "context") { const origin = event.origin; // Get the origin of the iframe if (this.contextOrigins.includes(origin)) { // Check if the origin is whitelisted this.iframeContexts[origin] = message.data; // Store the context data } } if (message?.closeEmbed) { window.parent.postMessage(JSON.stringify(message), "*"); } // New height handling if (message?.height) { const container = this.shadowRoot?.querySelector( "#ibl-chat-widget-container" ) as HTMLElement; if (container) { container.style.height = `${message.height}px`; // Set the height based on the message } } if (!this.isAnonymous) { if ( message?.loaded && (!message.auth.axd_token || !message.auth.dm_token || message.auth.tenant !== this.tenant || this.isTokenExpired(message.auth.dm_token_expires) || this.isTokenExpired(message.auth.axd_token_expires)) ) { try { const userTenants = await fetchUserTenants(this.lmsUrl); const selectedTenant = userTenants.find( (tenant) => tenant.key === this.tenant ); if (selectedTenant) { const userTokens = await fetchUserTokens( this.lmsUrl, selectedTenant.key ); const userObject = { axd_token: userTokens.axd_token.token, axd_token_expires: userTokens.axd_token.expires, userData: JSON.stringify(userTokens.user), dm_token_expires: userTokens.dm_token.expires, tenant: selectedTenant.key, tenants: JSON.stringify(userTenants), dm_token: userTokens.dm_token.token, }; this.sendAuthDataToIframe(userObject); } } catch (error) {} !this.iblData && this.redirectToAuthSPA(); } if (message?.loaded && message.auth.userData) { try { if ( this.edxUserId && this.edxUserId != JSON.parse(message.auth.userData).user_id.toString() ) { if (this.iblData) { this.sendAuthDataToIframe(this.iblData); } else { try { const userTenants = await fetchUserTenants(this.lmsUrl); const selectedTenant = userTenants.find( (tenant) => tenant.key === this.tenant ); if (selectedTenant) { const userTokens = await fetchUserTokens( this.lmsUrl, selectedTenant.key ); const userObject = { axd_token: userTokens.axd_token.token, axd_token_expires: userTokens.axd_token.expires, userData: JSON.stringify(userTokens.user), dm_token_expires: userTokens.dm_token.expires, tenant: selectedTenant.key, tenants: JSON.stringify(userTenants), dm_token: userTokens.dm_token.token, }; this.sendAuthDataToIframe(userObject); } } catch (error) {} } } } catch (error) { console.error("Error parsing userData from auth:", error); } } } if (message?.authExpired) { if (!this.isAnonymous) { if (this.iblData) { this.sendAuthDataToIframe(this.iblData); } else { this.redirectToAuthSPA(true); } } } else if (message?.ready) { this.isEmbeddedMentorReady = true; if (this.iblData) { this.sendAuthDataToIframe(this.iblData); } else if (!this.authRelyOnHost) { if (!this.isAnonymous) { this.redirectToAuthSPA(); } } } if (message?.loaded) { this.isEmbeddedMentorReady = true; if (this.isContextAware) { this.sendHostInfoToIframe(); } if (this.theme) { this.switchTheme(this.theme); } if (this.documentFilter) { this.sendDocumentFilterToIframe(); } if (this.edxUsageId) { this.sendDataToIframe({ type: "MENTOR:EDX_USAGE_ID", data: { edxUsageId: this.edxUsageId }, }); } if (this.edxCourseId) { this.sendDataToIframe({ type: "MENTOR:EDX_COURSE_ID", data: { edxCourseId: this.edxCourseId }, }); } } } connectedCallback() { if (this.iblData) { const url = new URL(window.location.href); url.searchParams.delete("ibl-data"); window.history.replaceState({}, document.title, url); const userData: any = JSON.parse(this.iblData).userData; document.cookie = `userData=${userData}; domain=${document.domain}; path=/;`; } window.addEventListener("message", (event: MessageEvent) => this.onPostMessage(event) ); // Show the spinner when the iframe starts loading const iframe = this.shadowRoot?.querySelector("iframe"); if (iframe) { iframe.onloadstart = () => { const spinner = this.shadowRoot?.querySelector( "#loading-spinner" ) as HTMLElement; if (spinner) { spinner.style.display = "block"; } }; iframe.onload = () => { const spinner = this.shadowRoot?.querySelector( "#loading-spinner" ) as HTMLElement; if (spinner) { spinner.style.display = "none"; } }; } } disconnectedCallback() { window.removeEventListener("message", this.onPostMessage); } get mentorUrl() { return this.getAttribute("mentorurl") || "https://mentor.iblai.app"; } set mentorUrl(value) { this.setAttribute("mentorurl", value); } get authUrl() { return this.getAttribute("authurl") || "https://auth.iblai.app"; } set authUrl(value) { this.setAttribute("authurl", value); } get lmsUrl() { return this.getAttribute("lmsurl") || "https://learn.iblai.app"; } set lmsUrl(value) { this.setAttribute("lmsurl", value); } get theme(): Theme { return (this.getAttribute("theme") as Theme) || "light"; } set theme(value: Theme) { this.setAttribute("theme", value); } get tenant(): string | null { return this.getAttribute("tenant"); } set tenant(value: string) { this.setAttribute("tenant", value); } get extraParams(): string | null { return this.getAttribute("extraparams"); } set extraParams(value: string) { this.setAttribute("extraparams", value); } get contextOrigins(): string[] { return this.getAttribute("contextorigins")?.split(",") || []; } set contextOrigins(value: string) { this.setAttribute("contextorigins", value); } get mentor(): string | null { return this.getAttribute("mentor"); } set mentor(value: string) { this.setAttribute("mentor", value); } get edxUsageId(): string | null { return this.getAttribute("edxusageid"); } set edxUsageId(value: string) { this.setAttribute("edxusageid", value); } get edxCourseId(): string | null { return this.getAttribute("edxcourseid"); } set edxCourseId(value: string) { this.setAttribute("edxcourseid", value); } get edxUserId(): string | null { return this.getAttribute("edxuserid"); } set edxUserId(value: string) { this.setAttribute("edxuserid", value); } get authRelyOnHost() { return this.hasAttribute("authrelyonhost"); } set authRelyOnHost(value) { if (value) { this.setAttribute("authrelyonhost", ""); } else { this.removeAttribute("authrelyonhost"); } } get isAnonymous() { return this.hasAttribute("isanonymous"); } set isAnonymous(value) { if (value) { this.setAttribute("isanonymous", ""); } else { this.removeAttribute("isanonymous"); } } get isAdvanced() { return this.hasAttribute("isadvanced"); } set isAdvanced(value) { if (value) { this.setAttribute("isadvanced", ""); } else { this.removeAttribute("isadvanced"); } } get isContextAware() { return this.hasAttribute("iscontextaware"); } set isContextAware(value) { if (value) { this.setAttribute("iscontextaware", ""); } else { this.removeAttribute("iscontextaware"); } } get redirectToken(): string | null { return this.getAttribute("redirecttoken"); } set redirectToken(value: string) { this.setAttribute("redirecttoken", value); } get component(): "chat" | null { return this.getAttribute("component") as "chat" | null; } set component(value: string) { this.setAttribute("component", value); } get modal(): "dataset" | "settings" | null { return this.getAttribute("modal") as "dataset" | "settings" | null; } set modal(value: string) { this.setAttribute("modal", value); } get documentFilter(): string | null { return this.getAttribute("documentfilter") as string | null; } set documentFilter(value: string) { this.setAttribute("documentfilter", value); } static get observedAttributes() { return [ "mentorUrl", "tenant", "mentor", "isadvanced", "iscontextaware", "contextOrigins", // Add the new attribute to observed attributes "component", "modal", "extraparams", "documentfilter", ]; } attributeChangedCallback(name: string, oldValue: any, newValue: any) { if ( [ "mentorUrl", "tenant", "mentor", "isadvanced", "component", "modal", "extraparams", ].includes(name) ) { const iframe = this.shadowRoot?.querySelector("iframe"); if (this.shadowRoot && iframe) { iframe.src = `${this.mentorUrl}/platform/${ this.tenant }${getUrlFromComponent(this.component, this.mentor)}/${ this.modal ? this.modal : "" }?embed=true&mode=anonymous&extra-body-classes=iframed-externally${ this.isAdvanced ? "&chat=advanced" : "" }${this.modal ? "&modal=" + this.modal : ""}${getParamsFromComponent( this.component )}${this.extraParams ? "&" + this.extraParams : ""}`; } } if (this.isContextAware) { this.lastUrl = window.location.href; setInterval(() => { // const currentUrl = window.location.href; // if (currentUrl !== this.lastUrl) { // this.lastUrl = currentUrl; // this.isContextAware && this.sendHostInfoToIframe(); // } this.isContextAware && this.sendHostInfoToIframe(); }, 1000); } if (this.documentFilter) { this.sendDocumentFilterToIframe(); } if (name === "contextOrigins") { this.contextOrigins = newValue?.split(",") || []; // Update the context origins when the attribute changes } if (name === "theme") { this.switchTheme(newValue); } } getCleanBodyContent(): string { const bodyClone: HTMLElement = document.body.cloneNode(true) as HTMLElement; // Clean the bodyClone cleanElement(bodyClone.outerHTML); const removeComments = (node: Node) => { for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 8) { node.removeChild(child); i--; } else if (child.nodeType === 1) { removeComments(child); } } }; removeComments(bodyClone); // Clean each iframeContext (HTML string) and merge their HTML const iframeHtmls = Object.values(this.iframeContexts).map( (iframeHtml: string) => { return cleanElement(iframeHtml); // Clean unwanted selectors } ); // Merge bodyClone HTML with cleaned iframe HTMLs const mergedContent = bodyClone.innerHTML + iframeHtmls.join(""); return mergedContent; // Return the merged HTML content } sendHostInfoToIframe() { const iframe = this.shadowRoot?.querySelector( "#ibl-chat-widget-container iframe" ) as HTMLIFrameElement; if (iframe && iframe.contentWindow) { const bodyContent = this.getCleanBodyContent(); const payload = { type: "MENTOR:CONTEXT_UPDATE", hostInfo: { title: document.title, href: window.location.href, }, pageContent: bodyContent, }; iframe.contentWindow.postMessage(payload, "*"); } } sendDocumentFilterToIframe() { this.sendDataToIframe({ type: "MENTOR:DOCUMENTFILTER", data: this.documentFilter, }); } sendDataToIframe(data: Record<string, any>) { const iframe = this.shadowRoot?.querySelector( "#ibl-chat-widget-container iframe" ) as HTMLIFrameElement; if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage(data, "*"); } } switchTheme(theme: string) { const iframe = this.shadowRoot?.querySelector( "#ibl-chat-widget-container iframe" ) as HTMLIFrameElement; if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage(JSON.stringify({ theme }), "*"); } } sendAuthDataToIframe(iblData: any) { const iframe = this.shadowRoot?.querySelector( "#ibl-chat-widget-container iframe" ) as HTMLIFrameElement; if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage(iblData, "*"); } } isTokenExpired(token_expires: string) { const expirationDate = new Date(token_expires); const now = new Date(); return now >= expirationDate; } redirectToAuthSPA(forceLogout?: boolean) { if (this.authRelyOnHost) { const iframe = this.shadowRoot?.querySelector( "#ibl-chat-widget-container iframe" ) as HTMLIFrameElement; if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage({ ...localStorage }, "*"); } return; } const redirectPath: string = window.location.pathname + window.location.search; window.location.href = `${ this.authUrl }/login?redirect-path=${redirectPath}&tenant=${this.tenant}${ forceLogout ? "&logout=true" : "" }&redirect-token=${this.redirectToken}`; } toggleWidget() { const widget: HTMLElement | null = document.getElementById( "ibl-chat-widget-container" ); if (widget) { if (widget.style.display === "none") { widget.style.display = ""; } else { widget.style.display = "none"; } } } } function defineMentorAI() { if (typeof window !== "undefined" && !customElements.get("mentor-ai")) { customElements.define("mentor-ai", MentorAI); } } defineMentorAI(); export * from "./mentor-ai";