@oyehello/chat 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -37,6 +37,15 @@ async function openSession(opts) {
37
37
  if (!res.ok) throw await asError(res);
38
38
  return await res.json();
39
39
  }
40
+ async function getWidgetConfig(opts) {
41
+ const res = await opts.fetchImpl(`${opts.baseUrl}/chat/widget-config?agentId=${encodeURIComponent(opts.agentId)}`, {
42
+ method: "GET",
43
+ credentials: "omit",
44
+ headers: { Authorization: `Bearer ${opts.apiKey}` }
45
+ });
46
+ if (!res.ok) throw await asError(res);
47
+ return await res.json();
48
+ }
40
49
  async function postMessage(opts) {
41
50
  const body = { text: opts.text };
42
51
  if (opts.attachments && opts.attachments.length > 0) body.attachments = opts.attachments;
@@ -214,6 +223,8 @@ function createChatClient(options) {
214
223
  }
215
224
  } else if (ev.type === "agent_typing") {
216
225
  emit({ type: "agentTyping", active: !!ev.active });
226
+ } else if (ev.type === "suggestions") {
227
+ emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
217
228
  }
218
229
  };
219
230
  const openStream = async () => {
@@ -281,6 +292,9 @@ function createChatClient(options) {
281
292
  if (!session) throw new Error("call start() before getAttachmentUrl");
282
293
  return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
283
294
  },
295
+ getWidgetConfig() {
296
+ return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
297
+ },
284
298
  async start() {
285
299
  if (state === "opening" || state === "ready") return;
286
300
  closed = false;
@@ -527,8 +541,11 @@ var STYLES = `
527
541
  .i-expand { opacity: 1; } .i-shrink { opacity: 0; }
528
542
  .oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
529
543
 
530
- .err { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: #fff; background: var(--oc-danger); }
531
- .err .retry { padding: 3px 10px; font-size: 11px; background: rgba(255,255,255,0.2); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
544
+ /* Inline connection error, shown inside the chat window (no banner) */
545
+ .oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
546
+ .oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
547
+ .oc-err-retry { padding: 7px 15px; font-size: 12.5px; font-weight: 500; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-2); color: var(--oc-fg); cursor: pointer; transition: background .18s var(--oc-ease); }
548
+ .oc-err-retry:hover { background: var(--oc-tint-3); }
532
549
 
533
550
  /* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
534
551
  .body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 16px 26px; background: var(--oc-bg); scroll-behavior: smooth; }
@@ -796,6 +813,13 @@ var OyehelloChatElement = class extends HTMLElement {
796
813
  renderedCount = 0;
797
814
  renderedBotFinal = 0;
798
815
  timers = [];
816
+ // Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
817
+ // widget prefers these over the static `suggestions` attribute.
818
+ serverSuggestions = null;
819
+ teaserText = "Need a hand? Ask me anything.";
820
+ teaserShownOnce = false;
821
+ idleTimer = null;
822
+ idleReset = null;
799
823
  // ── shadow refs ──
800
824
  oc;
801
825
  bodyEl;
@@ -803,7 +827,6 @@ var OyehelloChatElement = class extends HTMLElement {
803
827
  fileEl;
804
828
  titleEl;
805
829
  statusEl;
806
- errEl;
807
830
  emojiListEl;
808
831
  emojiSearchEl;
809
832
  connectedCallback() {
@@ -815,6 +838,7 @@ var OyehelloChatElement = class extends HTMLElement {
815
838
  this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
816
839
  return;
817
840
  }
841
+ this.teaserText = this.getAttribute("teaser") || this.teaserText;
818
842
  this.client = createChatClient({ apiKey, agentId, baseUrl });
819
843
  if (Object.keys(this.pendingTools).length > 0) {
820
844
  this.client.registerTools(this.pendingTools);
@@ -826,6 +850,7 @@ var OyehelloChatElement = class extends HTMLElement {
826
850
  this.updateHeader();
827
851
  this.updateStatus();
828
852
  this.updateClasses();
853
+ void this.loadWidgetConfig();
829
854
  let wasOpen = false;
830
855
  try {
831
856
  wasOpen = sessionStorage.getItem(this.key("open")) === "1";
@@ -839,24 +864,81 @@ var OyehelloChatElement = class extends HTMLElement {
839
864
  this.timers.push(setTimeout(() => {
840
865
  this.oc?.classList.remove("ring");
841
866
  }, 6e3));
842
- const teaserText = this.getAttribute("teaser");
867
+ this.armIdleTeaser();
868
+ }
869
+ // Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
870
+ // is already open or the teaser was dismissed this session. Fires at most once.
871
+ armIdleTeaser() {
843
872
  let dismissed = false;
844
873
  try {
845
874
  dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
846
875
  } catch {
847
876
  }
848
- if (teaserText && !dismissed) {
849
- this.timers.push(setTimeout(() => {
850
- if (!this.uiOpen) {
877
+ if (dismissed) return;
878
+ const reset = () => {
879
+ if (this.idleTimer) clearTimeout(this.idleTimer);
880
+ this.idleTimer = setTimeout(() => {
881
+ if (!this.uiOpen && !this.teaserShownOnce) {
851
882
  this.teaserActive = true;
883
+ this.teaserShownOnce = true;
852
884
  this.updateClasses();
885
+ this.teardownIdle();
886
+ }
887
+ }, 1e4);
888
+ };
889
+ this.idleReset = reset;
890
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
891
+ reset();
892
+ }
893
+ teardownIdle() {
894
+ if (this.idleTimer) {
895
+ clearTimeout(this.idleTimer);
896
+ this.idleTimer = null;
897
+ }
898
+ if (this.idleReset) {
899
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
900
+ this.idleReset = null;
901
+ }
902
+ }
903
+ async loadWidgetConfig() {
904
+ try {
905
+ const cfg = await this.client.getWidgetConfig();
906
+ if (!this.getAttribute("teaser") && cfg.teaser) {
907
+ this.teaserText = cfg.teaser;
908
+ this.applyTeaserText();
909
+ }
910
+ if (cfg.brand) {
911
+ if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
912
+ if (cfg.brand.name && !this.getAttribute("title")) {
913
+ this.agentName = cfg.brand.name;
914
+ this.updateHeader();
853
915
  }
854
- }, 1400));
916
+ if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
917
+ }
918
+ } catch {
855
919
  }
856
920
  }
921
+ applyTeaserText() {
922
+ const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
923
+ if (el) el.textContent = this.teaserText;
924
+ }
925
+ applyBrandLogo(url) {
926
+ const set = (host) => {
927
+ if (!host) return;
928
+ host.textContent = "";
929
+ const img = document.createElement("img");
930
+ img.src = url;
931
+ img.alt = "";
932
+ img.style.cssText = "width:100%;height:100%;object-fit:contain";
933
+ host.appendChild(img);
934
+ };
935
+ set(this.shadowRoot?.querySelector(".hd .logo"));
936
+ set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
937
+ }
857
938
  disconnectedCallback() {
858
939
  this.off?.();
859
940
  void this.client?.end();
941
+ this.teardownIdle();
860
942
  for (const t of this.timers) clearTimeout(t);
861
943
  for (const url of this.imagePreviewCache.values()) {
862
944
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
@@ -870,18 +952,22 @@ var OyehelloChatElement = class extends HTMLElement {
870
952
  key(k) {
871
953
  return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
872
954
  }
873
- get suggestions() {
955
+ // Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
956
+ get attrSuggestions() {
874
957
  return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
875
958
  }
959
+ // Effective chips: server (flow) suggestions once received, else the static attribute fallback.
960
+ get chipLabels() {
961
+ return this.serverSuggestions ?? this.attrSuggestions;
962
+ }
876
963
  // ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
877
964
  renderShell() {
878
965
  const root = this.shadowRoot;
879
- const hasTeaser = !!this.getAttribute("teaser");
880
966
  const hasPrivacy = !!this.getAttribute("privacy-url");
881
- const teaserText = this.getAttribute("teaser") ?? "";
967
+ const teaserText = this.teaserText;
882
968
  root.innerHTML = `
883
969
  <style>${STYLES}</style>
884
- <div class="oc${hasTeaser ? " has-teaser" : ""}${hasPrivacy ? " has-privacy" : ""}">
970
+ <div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
885
971
  <div class="panel" role="dialog" aria-label="Chat">
886
972
  <div class="hd">
887
973
  <span class="hd-grid"></span>
@@ -890,7 +976,6 @@ var OyehelloChatElement = class extends HTMLElement {
890
976
  <button type="button" class="hd-btn max" aria-label="Expand"><span class="i i-expand">${ICON.expand}</span><span class="i i-shrink">${ICON.shrink}</span></button>
891
977
  <button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
892
978
  </div>
893
- <div class="err" hidden></div>
894
979
  <div class="body"></div>
895
980
  <span class="emoji-scrim"></span>
896
981
  <div class="emoji-panel">
@@ -934,7 +1019,6 @@ var OyehelloChatElement = class extends HTMLElement {
934
1019
  this.fileEl = root.querySelector('input[type="file"]');
935
1020
  this.titleEl = root.querySelector(".hd-title");
936
1021
  this.statusEl = root.querySelector(".hd-status .t");
937
- this.errEl = root.querySelector(".err");
938
1022
  this.emojiListEl = root.querySelector(".emoji-list");
939
1023
  this.emojiSearchEl = root.querySelector(".emoji-search input");
940
1024
  root.querySelector(".launcher").addEventListener("click", () => this.toggle());
@@ -1015,6 +1099,7 @@ var OyehelloChatElement = class extends HTMLElement {
1015
1099
  this.uiOpen = true;
1016
1100
  this.teaserActive = false;
1017
1101
  this.unseen = 0;
1102
+ this.teardownIdle();
1018
1103
  try {
1019
1104
  sessionStorage.setItem(this.key("open"), "1");
1020
1105
  } catch {
@@ -1057,6 +1142,7 @@ var OyehelloChatElement = class extends HTMLElement {
1057
1142
  this.updateHeader();
1058
1143
  } else if (e.type === "handoff") this.handoffState = e.state;
1059
1144
  else if (e.type === "agentTyping") this.agentTyping = e.active;
1145
+ else if (e.type === "suggestions") this.serverSuggestions = e.items;
1060
1146
  else if (e.type === "error") this.lastError = e.error.message;
1061
1147
  this.updateStatus();
1062
1148
  this.renderMessages();
@@ -1071,16 +1157,6 @@ var OyehelloChatElement = class extends HTMLElement {
1071
1157
  const handoff = HANDOFF_STATUS[this.handoffState];
1072
1158
  const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
1073
1159
  if (this.statusEl) this.statusEl.textContent = text;
1074
- if (this.state === "error" && this.lastError) {
1075
- this.errEl.hidden = false;
1076
- this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
1077
- this.errEl.querySelector(".retry").addEventListener("click", () => {
1078
- void this.client?.start();
1079
- });
1080
- } else {
1081
- this.errEl.hidden = true;
1082
- this.errEl.innerHTML = "";
1083
- }
1084
1160
  }
1085
1161
  // ── Message list + scroll ─────────────────────────────────────────────────────────────────────
1086
1162
  renderMessages() {
@@ -1098,10 +1174,13 @@ var OyehelloChatElement = class extends HTMLElement {
1098
1174
  })();
1099
1175
  let html = this.messages.map((m, i) => this.turnHtml(m, i === lastBot && !this.agentTyping)).join("");
1100
1176
  if (this.agentTyping) html += '<span class="typing"><span></span><span></span><span></span></span>';
1101
- const sugg = this.suggestions;
1177
+ const sugg = this.chipLabels;
1102
1178
  if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
1103
1179
  html += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
1104
1180
  }
1181
+ if (this.state === "error" && this.lastError) {
1182
+ html += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
1183
+ }
1105
1184
  body.innerHTML = html;
1106
1185
  this.hydrateAttachmentImages();
1107
1186
  this.bindChips();
@@ -1179,12 +1258,15 @@ var OyehelloChatElement = class extends HTMLElement {
1179
1258
  this.updateClasses();
1180
1259
  }
1181
1260
  bindChips() {
1182
- const sugg = this.suggestions;
1261
+ const sugg = this.chipLabels;
1183
1262
  this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
1184
1263
  const i = Number(btn.getAttribute("data-chip"));
1185
1264
  const text = sugg[i];
1186
1265
  if (text) btn.addEventListener("click", () => this.sendChip(text));
1187
1266
  });
1267
+ this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
1268
+ void this.client?.start();
1269
+ });
1188
1270
  }
1189
1271
  // ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
1190
1272
  toggleEmoji() {