@meetelise/chat 1.58.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/MyPubnub.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { getTrackingParams } from "./trackingParams";
1
2
  import { AxiosError } from "axios";
2
3
  import Pubnub, { ListenerParameters, MessageEvent } from "pubnub";
3
4
 
@@ -792,6 +793,7 @@ class MyPubnub {
792
793
  buildingSlug: this.buildingSlug,
793
794
  userId: this.buildingWebchatView?.userId, // this userid is actually the AI user!
794
795
  leadSource: this.leadSource,
796
+ trackingParams: getTrackingParams(this.orgSlug),
795
797
  widgetType: this.widgetType,
796
798
  isDevState: shouldCreateAsDevState(location.href),
797
799
  // Read by the backend only for orgs in the org-level webchat rollout.
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { getTrackingParams } from "../../trackingParams";
2
3
  import { html, LitElement, PropertyValueMap, TemplateResult } from "lit";
3
4
  import { customElement, property, query, state } from "lit/decorators.js";
4
5
  import "../actions/international-phone-input";
@@ -872,6 +873,7 @@ export class TourScheduler extends LitElement {
872
873
  email_address: this.email,
873
874
  phone_number: phoneNumber,
874
875
  building_id: this.buildingId,
876
+ tracking_params: getTrackingParams(this.orgSlug),
875
877
  first_name: this.firstNameInput?.value ?? this.firstNameInputValue,
876
878
  last_name: this.lastNameInput?.value ?? this.lastNameInputValue,
877
879
  tour_type: tourTypeForSubmission[this.tourType],
@@ -1,3 +1,4 @@
1
+ import { getTrackingParams } from "../../trackingParams";
1
2
  import axios from "axios";
2
3
  import { css, html, LitElement, TemplateResult } from "lit";
3
4
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -512,6 +513,7 @@ const createTextWithUs = async (
512
513
  // @ts-ignore
513
514
  query_params: Object.fromEntries(queryParams.entries()),
514
515
  conversation_tracking_id: chatId,
516
+ tracking_params: getTrackingParams(orgSlug),
515
517
  lead_sources_with_timestamps: (leadSourcesWithTimestamps ?? []).map(
516
518
  (source) => snakify({ ...source })
517
519
  ),
@@ -1,3 +1,4 @@
1
+ import { getTrackingParams } from "../../trackingParams";
1
2
  import { css, html, LitElement, TemplateResult } from "lit";
2
3
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
3
4
  import { customElement, property, query, state } from "lit/decorators.js";
@@ -865,6 +866,7 @@ const createEmail = async (
865
866
  // @ts-ignore
866
867
  query_params: Object.fromEntries(queryParams.entries()),
867
868
  conversation_tracking_id: chatId,
869
+ tracking_params: getTrackingParams(orgSlug),
868
870
  lead_sources_with_timestamps: (leadSourcesWithTimestamps ?? []).map(
869
871
  (source) => snakify({ ...source })
870
872
  ),
@@ -47,8 +47,7 @@ export const leasingChatStyles = css`
47
47
  justify-content: space-between;
48
48
  }
49
49
  .leasing-conversation-body-backdrop {
50
- background: rgba(193, 193, 193, 0.5);
51
- backdrop-filter: blur(4px);
50
+ background: #efefef;
52
51
 
53
52
  border-top: 1px solid rgba(0, 0, 0, 0.1);
54
53
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
@@ -1,3 +1,4 @@
1
+ import { getTrackingParams } from "../trackingParams";
1
2
  import { css, html, LitElement, TemplateResult } from "lit";
2
3
  import { customElement, property, state } from "lit/decorators.js";
3
4
  import { classMap } from "lit/directives/class-map.js";
@@ -1519,6 +1520,7 @@ const overrideContactUsForm = async (
1519
1520
  first_message: formValues.message,
1520
1521
  building_id: buildingWebchatView.id,
1521
1522
  is_external_form: true,
1523
+ tracking_params: getTrackingParams(orgSlug),
1522
1524
  lead_sources: [
1523
1525
  ...new Set(
1524
1526
  leadSource
@@ -253,6 +253,8 @@ export class PubnubChat extends LitElement {
253
253
  return html``; // error should be here
254
254
  }
255
255
 
256
+ const assistantName = this.building?.userFirstName || "Elise";
257
+
256
258
  const buildingId = this.building?.id;
257
259
  let styles = {};
258
260
  if (this._isMobile) {
@@ -357,9 +359,8 @@ export class PubnubChat extends LitElement {
357
359
  </li>
358
360
  <li
359
361
  class="message-container ai-message"
360
- tabindex="0"
361
362
  role="article"
362
- aria-label="Elise: Hi I'm Elise. How can I assist you today?"
363
+ aria-label="${assistantName}: Hi I'm ${assistantName}. How can I assist you today?"
363
364
  >
364
365
  <p
365
366
  class=${classMap({
@@ -368,7 +369,7 @@ export class PubnubChat extends LitElement {
368
369
  ["webchat-font__mobile"]: this._isMobile,
369
370
  })}
370
371
  >
371
- Hi I'm Elise. How can I assist you today?
372
+ Hi I'm ${assistantName}. How can I assist you today?
372
373
  </p>
373
374
  </li>
374
375
  ${this.messages.map((message) => {
@@ -378,11 +379,10 @@ export class PubnubChat extends LitElement {
378
379
  ? "message-container lead-message"
379
380
  : "message-container ai-message"}
380
381
  key=${message.timestamp}
381
- tabindex="0"
382
382
  role="article"
383
383
  aria-label="${message.isLeadMessage
384
384
  ? "You"
385
- : "Elise"}: ${messageAriaLabel(message)}"
385
+ : assistantName}: ${messageAriaLabel(message)}"
386
386
  >
387
387
  ${isSimpleNavigationChatMessage(message)
388
388
  ? html`<pubnub-navigation
@@ -436,6 +436,7 @@ export class PubnubChat extends LitElement {
436
436
  <div id="utilities-inner-footer">
437
437
  <textarea
438
438
  id="message-input"
439
+ aria-label="Message"
439
440
  class=${classMap({
440
441
  ["webchat-font__desktop"]: !this._isMobile,
441
442
  ["webchat-font__mobile"]: this._isMobile,
@@ -535,9 +536,8 @@ export class PubnubChat extends LitElement {
535
536
  </li>
536
537
  <li
537
538
  class="message-container ai-message"
538
- tabindex="0"
539
539
  role="article"
540
- aria-label="Elise: Hi I'm Elise. If this is a medical emergency, please call 911. How can I assist you today?"
540
+ aria-label="${assistantName}: Hi I'm ${assistantName}. If this is a medical emergency, please call 911. How can I assist you today?"
541
541
  >
542
542
  <p
543
543
  class=${classMap({
@@ -546,8 +546,8 @@ export class PubnubChat extends LitElement {
546
546
  ["webchat-font__mobile"]: this._isMobile,
547
547
  })}
548
548
  >
549
- Hi I'm Elise. If this is a medical emergency, please call 911.
550
- How can I assist you today?
549
+ Hi I'm ${assistantName}. If this is a medical emergency,
550
+ please call 911. How can I assist you today?
551
551
  </p>
552
552
  </li>
553
553
  ${this.messages.map((message) => {
@@ -557,11 +557,10 @@ export class PubnubChat extends LitElement {
557
557
  ? "message-container lead-message"
558
558
  : "message-container ai-message"}
559
559
  key=${message.timestamp}
560
- tabindex="0"
561
560
  role="article"
562
561
  aria-label="${message.isLeadMessage
563
562
  ? "You"
564
- : "Elise"}: ${messageAriaLabel(message)}"
563
+ : assistantName}: ${messageAriaLabel(message)}"
565
564
  >
566
565
  ${isSimpleNavigationChatMessage(message)
567
566
  ? html`<pubnub-navigation
@@ -615,6 +614,7 @@ export class PubnubChat extends LitElement {
615
614
  <div id="healthcare-inner-footer">
616
615
  <textarea
617
616
  id="message-input"
617
+ aria-label="Message"
618
618
  class=${classMap({
619
619
  ["webchat-font__desktop"]: !this._isMobile,
620
620
  ["webchat-font__mobile"]: this._isMobile,
@@ -734,9 +734,8 @@ export class PubnubChat extends LitElement {
734
734
  "ai-message": true,
735
735
  "with-box-shadow": orgId === 444,
736
736
  })}
737
- tabindex="0"
738
737
  role="article"
739
- aria-label="Elise: ${this.building.welcomeMessage}"
738
+ aria-label="${assistantName}: ${this.building.welcomeMessage}"
740
739
  >
741
740
  <p
742
741
  class=${classMap({
@@ -760,11 +759,10 @@ export class PubnubChat extends LitElement {
760
759
  "with-box-shadow": orgId === 444,
761
760
  })}
762
761
  key=${message.timestamp}
763
- tabindex="0"
764
762
  role="article"
765
763
  aria-label="${message.isLeadMessage
766
764
  ? "You"
767
- : "Elise"}: ${messageAriaLabel(message)}"
765
+ : assistantName}: ${messageAriaLabel(message)}"
768
766
  >
769
767
  ${isSimpleNavigationChatMessage(message)
770
768
  ? html`<pubnub-navigation
@@ -822,6 +820,7 @@ export class PubnubChat extends LitElement {
822
820
  >
823
821
  <textarea
824
822
  id="message-input"
823
+ aria-label="Message"
825
824
  class=${classMap({
826
825
  ["webchat-font__desktop"]: !this._isMobile,
827
826
  ["webchat-font__mobile"]: this._isMobile,
@@ -855,7 +854,6 @@ export class PubnubChat extends LitElement {
855
854
  @click=${() => this.sendMessage(this.messageInput.value)}
856
855
  ?disabled=${this.myPubnub?.isCurrentlyStreamingMessage}
857
856
  aria-label="Send message"
858
- aria-describedby="Send message"
859
857
  >
860
858
  ${shouldUsePrimaryColorForChatFooter(
861
859
  this.building.orgId,
@@ -0,0 +1,152 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import "../public/dist/index";
3
+ import { BuildingWebchatView } from "./fetchBuildingWebchatView";
4
+ import type { SimpleChatMessage } from "./MyPubnub";
5
+ import type { PubnubChat } from "./WebComponent/pubnub-chat";
6
+
7
+ type WidgetTypeName = "default" | "healthcare" | "utilities";
8
+
9
+ declare const afterEach: (callback: () => void) => void;
10
+ declare const it: (name: string, callback: () => void | Promise<void>) => void;
11
+
12
+ const text = (visible: string): string => visible.replace(/\s+/g, " ").trim();
13
+
14
+ const conversation = (): SimpleChatMessage[] => [
15
+ {
16
+ timestamp: 1,
17
+ message: "Do you have a two bedroom?",
18
+ isLeadMessage: true,
19
+ chunks: [{ text: "Do you have a two bedroom?", order: 0, isDone: true }],
20
+ type: "text",
21
+ },
22
+ {
23
+ timestamp: 2,
24
+ message: "Yes, I can help with that.",
25
+ isLeadMessage: false,
26
+ chunks: [{ text: "Yes, I can help with that.", order: 0, isDone: true }],
27
+ type: "text",
28
+ },
29
+ {
30
+ timestamp: 3,
31
+ media: [{ title: null, description: null, url: null }],
32
+ isLeadMessage: false,
33
+ type: "media",
34
+ },
35
+ {
36
+ timestamp: 4,
37
+ navigation: {
38
+ url: "https://example.com/floor-plans",
39
+ label: "the floor plans",
40
+ },
41
+ isLeadMessage: false,
42
+ type: "navigation",
43
+ },
44
+ ];
45
+
46
+ const mount = async (
47
+ widgetType: WidgetTypeName,
48
+ assistantName: string | null
49
+ ): Promise<PubnubChat> => {
50
+ const chat = document.createElement("pubnub-chat") as PubnubChat;
51
+ chat.buildingSlug = "alta-federal-hill";
52
+ chat.orgSlug = "greystar";
53
+ chat.widgetType = widgetType;
54
+ if (assistantName !== null) {
55
+ chat.building = {
56
+ id: 1,
57
+ orgId: 1,
58
+ name: "Alta Federal Hill",
59
+ userFirstName: assistantName,
60
+ welcomeMessage: `Hi, I'm ${assistantName}. How can I help you today?`,
61
+ state: "MD",
62
+ country: "US",
63
+ customAdditionalDisclaimer: null,
64
+ } as BuildingWebchatView;
65
+ }
66
+ document.body.appendChild(chat);
67
+ await chat.updateComplete;
68
+ // firstUpdated clears messages until PubNub has history.
69
+ chat.messages = conversation();
70
+ await chat.updateComplete;
71
+ return chat;
72
+ };
73
+
74
+ const labels = (chat: PubnubChat): string[] =>
75
+ Array.from(
76
+ chat.shadowRoot?.querySelectorAll<HTMLElement>('[role="article"]') ?? []
77
+ ).map((article) => article.getAttribute("aria-label") ?? "");
78
+
79
+ afterEach(() => {
80
+ document.querySelectorAll("pubnub-chat").forEach((chat) => chat.remove());
81
+ });
82
+
83
+ it("announces leasing messages as the configured assistant", async () => {
84
+ const chat = await mount("default", "Emily");
85
+ const articles = Array.from(
86
+ chat.shadowRoot?.querySelectorAll<HTMLElement>('[role="article"]') ?? []
87
+ );
88
+
89
+ expect(text(articles[0].innerText)).to.equal(
90
+ "Hi, I'm Emily. How can I help you today?"
91
+ );
92
+ expect(labels(chat)).to.deep.equal([
93
+ "Emily: Hi, I'm Emily. How can I help you today?",
94
+ "You: Do you have a two bedroom?",
95
+ "Emily: Yes, I can help with that.",
96
+ "Emily: Media message",
97
+ "Emily: Take me to the floor plans",
98
+ ]);
99
+ });
100
+
101
+ it("announces healthcare messages as the configured assistant", async () => {
102
+ const chat = await mount("healthcare", "Ezra");
103
+ const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
104
+ "#message-thread-list > li.message-container"
105
+ );
106
+
107
+ expect(text(greeting?.innerText ?? "")).to.equal(
108
+ "Hi I'm Ezra. If this is a medical emergency, please call 911. How can I assist you today?"
109
+ );
110
+ expect(labels(chat)[0]).to.equal(
111
+ "Ezra: Hi I'm Ezra. If this is a medical emergency, please call 911. How can I assist you today?"
112
+ );
113
+ expect(labels(chat).slice(1)).to.deep.equal([
114
+ "You: Do you have a two bedroom?",
115
+ "Ezra: Yes, I can help with that.",
116
+ "Ezra: Media message",
117
+ "Ezra: Take me to the floor plans",
118
+ ]);
119
+ });
120
+
121
+ it("announces utilities messages as the configured assistant", async () => {
122
+ const chat = await mount("utilities", "Emily");
123
+ const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
124
+ "#message-thread-list > li.message-container"
125
+ );
126
+
127
+ expect(text(greeting?.innerText ?? "")).to.equal(
128
+ "Hi I'm Emily. How can I assist you today?"
129
+ );
130
+ expect(labels(chat)).to.deep.equal([
131
+ "Emily: Hi I'm Emily. How can I assist you today?",
132
+ "You: Do you have a two bedroom?",
133
+ "Emily: Yes, I can help with that.",
134
+ "Emily: Media message",
135
+ "Emily: Take me to the floor plans",
136
+ ]);
137
+ });
138
+
139
+ it("keeps Elise when a chat has no configured assistant name", async () => {
140
+ const chat = await mount("utilities", null);
141
+ const greeting = chat.shadowRoot?.querySelector<HTMLElement>(
142
+ "#message-thread-list > li.message-container"
143
+ );
144
+
145
+ expect(text(greeting?.innerText ?? "")).to.equal(
146
+ "Hi I'm Elise. How can I assist you today?"
147
+ );
148
+ expect(labels(chat)[0]).to.equal(
149
+ "Elise: Hi I'm Elise. How can I assist you today?"
150
+ );
151
+ expect(labels(chat)[2]).to.equal("Elise: Yes, I can help with that.");
152
+ });
@@ -0,0 +1,32 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import { getTrackingParams } from "./trackingParams";
3
+
4
+ const originalUrl = window.location.href;
5
+
6
+ afterEach(() => {
7
+ localStorage.removeItem("xdtid");
8
+ window.history.replaceState(null, "", originalUrl);
9
+ });
10
+
11
+ it("reads Greystar's xdtid from localStorage", () => {
12
+ localStorage.setItem("xdtid", "from-storage");
13
+ window.history.replaceState(null, "", "?xdtid=from-url");
14
+
15
+ expect(getTrackingParams("greystar")).to.deep.equal({
16
+ xdtid: "from-storage",
17
+ });
18
+ });
19
+
20
+ it("falls back to the xdtid URL param", () => {
21
+ window.history.replaceState(null, "", "?xdtid=from-url");
22
+
23
+ expect(getTrackingParams("greystar")).to.deep.equal({ xdtid: "from-url" });
24
+ });
25
+
26
+ it("returns null when nothing is found or the org has no config", () => {
27
+ expect(getTrackingParams("greystar")).to.equal(null);
28
+
29
+ localStorage.setItem("xdtid", "from-storage");
30
+ expect(getTrackingParams("avb")).to.equal(null);
31
+ expect(getTrackingParams(null)).to.equal(null);
32
+ });
@@ -0,0 +1,42 @@
1
+ interface TrackingParamSource {
2
+ key: string;
3
+ localStorageKey?: string;
4
+ queryParam?: string;
5
+ }
6
+
7
+ // Client-specific values sent with each new lead and passed to the client's PMS. Add a client here.
8
+ const TRACKING_PARAMS_BY_ORG_SLUG: Record<string, TrackingParamSource[]> = {
9
+ // Greystar's cross-domain tracking id, set by their GTM; localStorage outlives the landing URL.
10
+ greystar: [{ key: "xdtid", localStorageKey: "xdtid", queryParam: "xdtid" }],
11
+ };
12
+
13
+ const readLocalStorage = (key: string): string | null => {
14
+ try {
15
+ return localStorage.getItem(key);
16
+ } catch (_) {
17
+ return null;
18
+ }
19
+ };
20
+
21
+ const readQueryParam = (key: string): string | null => {
22
+ try {
23
+ return new URLSearchParams(window.location.search).get(key);
24
+ } catch (_) {
25
+ return null;
26
+ }
27
+ };
28
+
29
+ export const getTrackingParams = (
30
+ orgSlug: string | null | undefined
31
+ ): Record<string, string> | null => {
32
+ const sources = orgSlug ? TRACKING_PARAMS_BY_ORG_SLUG[orgSlug] : undefined;
33
+ if (!sources) return null;
34
+ const params: Record<string, string> = {};
35
+ for (const source of sources) {
36
+ const value =
37
+ (source.localStorageKey && readLocalStorage(source.localStorageKey)) ||
38
+ (source.queryParam && readQueryParam(source.queryParam));
39
+ if (value) params[source.key] = value;
40
+ }
41
+ return Object.keys(params).length ? params : null;
42
+ };