@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.d.cts CHANGED
@@ -52,7 +52,19 @@ type ChatEvent = {
52
52
  } | {
53
53
  type: 'agent_typing';
54
54
  active: boolean;
55
+ } | {
56
+ type: 'suggestions';
57
+ turnId: string;
58
+ items: string[];
55
59
  };
60
+ interface WidgetConfig {
61
+ teaser: string;
62
+ brand: {
63
+ name: string | null;
64
+ logoUrl: string | null;
65
+ accentColor: string | null;
66
+ } | null;
67
+ }
56
68
  interface OpenSessionResponse {
57
69
  sessionId: string;
58
70
  sessionToken: string;
@@ -116,6 +128,9 @@ type ChatClientEvent = {
116
128
  } | {
117
129
  type: 'agentTyping';
118
130
  active: boolean;
131
+ } | {
132
+ type: 'suggestions';
133
+ items: string[];
119
134
  };
120
135
  type ChatToolHandler = (args: Record<string, unknown>) => unknown | Promise<unknown>;
121
136
  interface ChatClient {
@@ -127,6 +142,7 @@ interface ChatClient {
127
142
  on(listener: (e: ChatClientEvent) => void): () => void;
128
143
  registerTools(handlers: Record<string, ChatToolHandler>): void;
129
144
  getAttachmentUrl(storageRef: string): Promise<string>;
145
+ getWidgetConfig(): Promise<WidgetConfig>;
130
146
  }
131
147
  declare function createChatClient(options: ChatClientOptions): ChatClient;
132
148
 
@@ -158,20 +174,30 @@ declare class OyehelloChatElement extends HTMLElement {
158
174
  private renderedCount;
159
175
  private renderedBotFinal;
160
176
  private timers;
177
+ private serverSuggestions;
178
+ private teaserText;
179
+ private teaserShownOnce;
180
+ private idleTimer;
181
+ private idleReset;
161
182
  private oc;
162
183
  private bodyEl;
163
184
  private textareaEl;
164
185
  private fileEl;
165
186
  private titleEl;
166
187
  private statusEl;
167
- private errEl;
168
188
  private emojiListEl;
169
189
  private emojiSearchEl;
170
190
  connectedCallback(): void;
191
+ private armIdleTeaser;
192
+ private teardownIdle;
193
+ private loadWidgetConfig;
194
+ private applyTeaserText;
195
+ private applyBrandLogo;
171
196
  disconnectedCallback(): void;
172
197
  registerTools(handlers: Record<string, ChatToolHandler>): void;
173
198
  private key;
174
- private get suggestions();
199
+ private get attrSuggestions();
200
+ private get chipLabels();
175
201
  private renderShell;
176
202
  private renderError;
177
203
  private updateClasses;
@@ -200,4 +226,4 @@ declare class OyehelloChatElement extends HTMLElement {
200
226
  private escapeAttr;
201
227
  }
202
228
 
203
- export { ChatApiError, type ChatAttachmentDescriptor, type ChatClient, type ChatClientEvent, type ChatClientOptions, type ChatClientState, type ChatEvent, type ChatMessage, type ChatToolHandler, type OpenSessionResponse, OyehelloChatElement, createChatClient };
229
+ export { ChatApiError, type ChatAttachmentDescriptor, type ChatClient, type ChatClientEvent, type ChatClientOptions, type ChatClientState, type ChatEvent, type ChatMessage, type ChatToolHandler, type OpenSessionResponse, OyehelloChatElement, type WidgetConfig, createChatClient };
package/dist/index.d.ts CHANGED
@@ -52,7 +52,19 @@ type ChatEvent = {
52
52
  } | {
53
53
  type: 'agent_typing';
54
54
  active: boolean;
55
+ } | {
56
+ type: 'suggestions';
57
+ turnId: string;
58
+ items: string[];
55
59
  };
60
+ interface WidgetConfig {
61
+ teaser: string;
62
+ brand: {
63
+ name: string | null;
64
+ logoUrl: string | null;
65
+ accentColor: string | null;
66
+ } | null;
67
+ }
56
68
  interface OpenSessionResponse {
57
69
  sessionId: string;
58
70
  sessionToken: string;
@@ -116,6 +128,9 @@ type ChatClientEvent = {
116
128
  } | {
117
129
  type: 'agentTyping';
118
130
  active: boolean;
131
+ } | {
132
+ type: 'suggestions';
133
+ items: string[];
119
134
  };
120
135
  type ChatToolHandler = (args: Record<string, unknown>) => unknown | Promise<unknown>;
121
136
  interface ChatClient {
@@ -127,6 +142,7 @@ interface ChatClient {
127
142
  on(listener: (e: ChatClientEvent) => void): () => void;
128
143
  registerTools(handlers: Record<string, ChatToolHandler>): void;
129
144
  getAttachmentUrl(storageRef: string): Promise<string>;
145
+ getWidgetConfig(): Promise<WidgetConfig>;
130
146
  }
131
147
  declare function createChatClient(options: ChatClientOptions): ChatClient;
132
148
 
@@ -158,20 +174,30 @@ declare class OyehelloChatElement extends HTMLElement {
158
174
  private renderedCount;
159
175
  private renderedBotFinal;
160
176
  private timers;
177
+ private serverSuggestions;
178
+ private teaserText;
179
+ private teaserShownOnce;
180
+ private idleTimer;
181
+ private idleReset;
161
182
  private oc;
162
183
  private bodyEl;
163
184
  private textareaEl;
164
185
  private fileEl;
165
186
  private titleEl;
166
187
  private statusEl;
167
- private errEl;
168
188
  private emojiListEl;
169
189
  private emojiSearchEl;
170
190
  connectedCallback(): void;
191
+ private armIdleTeaser;
192
+ private teardownIdle;
193
+ private loadWidgetConfig;
194
+ private applyTeaserText;
195
+ private applyBrandLogo;
171
196
  disconnectedCallback(): void;
172
197
  registerTools(handlers: Record<string, ChatToolHandler>): void;
173
198
  private key;
174
- private get suggestions();
199
+ private get attrSuggestions();
200
+ private get chipLabels();
175
201
  private renderShell;
176
202
  private renderError;
177
203
  private updateClasses;
@@ -200,4 +226,4 @@ declare class OyehelloChatElement extends HTMLElement {
200
226
  private escapeAttr;
201
227
  }
202
228
 
203
- export { ChatApiError, type ChatAttachmentDescriptor, type ChatClient, type ChatClientEvent, type ChatClientOptions, type ChatClientState, type ChatEvent, type ChatMessage, type ChatToolHandler, type OpenSessionResponse, OyehelloChatElement, createChatClient };
229
+ export { ChatApiError, type ChatAttachmentDescriptor, type ChatClient, type ChatClientEvent, type ChatClientOptions, type ChatClientState, type ChatEvent, type ChatMessage, type ChatToolHandler, type OpenSessionResponse, OyehelloChatElement, type WidgetConfig, createChatClient };
@@ -37,6 +37,15 @@ var OyehelloChat = (function (exports) {
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 @@ var OyehelloChat = (function (exports) {
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 @@ var OyehelloChat = (function (exports) {
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;
@@ -1972,8 +1986,8 @@ Please report this to https://github.com/markedjs/marked.`, e) {
1972
1986
  DOMPurify.isSupported = false;
1973
1987
  return DOMPurify;
1974
1988
  }
1975
- let document = window2.document;
1976
- const originalDocument = document;
1989
+ let document2 = window2.document;
1990
+ const originalDocument = document2;
1977
1991
  const currentScript = originalDocument.currentScript;
1978
1992
  window2.DocumentFragment;
1979
1993
  const HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
@@ -1999,9 +2013,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
1999
2013
  return getNodeName ? getNodeName(node) : node.nodeName;
2000
2014
  };
2001
2015
  if (typeof HTMLTemplateElement === "function") {
2002
- const template = document.createElement("template");
2016
+ const template = document2.createElement("template");
2003
2017
  if (template.content && template.content.ownerDocument) {
2004
- document = template.content.ownerDocument;
2018
+ document2 = template.content.ownerDocument;
2005
2019
  }
2006
2020
  }
2007
2021
  let trustedTypesPolicy;
@@ -2039,7 +2053,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2039
2053
  }
2040
2054
  return defaultTrustedTypesPolicy;
2041
2055
  };
2042
- const _document = document, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
2056
+ const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
2043
2057
  const importNode = originalDocument.importNode;
2044
2058
  let hooks = _createHooksMap();
2045
2059
  DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0;
@@ -2164,7 +2178,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2164
2178
  const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
2165
2179
  let transformCaseFunc = null;
2166
2180
  let CONFIG = null;
2167
- const formElement = document.createElement("form");
2181
+ const formElement = document2.createElement("form");
2168
2182
  const isRegexOrFunction = function isRegexOrFunction2(testValue) {
2169
2183
  return testValue instanceof RegExp || testValue instanceof Function;
2170
2184
  };
@@ -2592,7 +2606,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2592
2606
  }
2593
2607
  const body = doc.body || doc.documentElement;
2594
2608
  if (dirty && leadingWhitespace) {
2595
- body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
2609
+ body.insertBefore(document2.createTextNode(leadingWhitespace), body.childNodes[0] || null);
2596
2610
  }
2597
2611
  if (NAMESPACE === HTML_NAMESPACE) {
2598
2612
  return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0];
@@ -2827,7 +2841,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2827
2841
  if (_isPatchLinkageAttribute(lcName, lcTag)) {
2828
2842
  return false;
2829
2843
  }
2830
- if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document || value in formElement)) {
2844
+ if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) {
2831
2845
  return false;
2832
2846
  }
2833
2847
  const nameIsPermitted = ALLOWED_ATTR2[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
@@ -3407,8 +3421,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3407
3421
  .i-expand { opacity: 1; } .i-shrink { opacity: 0; }
3408
3422
  .oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
3409
3423
 
3410
- .err { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: #fff; background: var(--oc-danger); }
3411
- .err .retry { padding: 3px 10px; font-size: 11px; background: rgba(255,255,255,0.2); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
3424
+ /* Inline connection error, shown inside the chat window (no banner) */
3425
+ .oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
3426
+ .oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
3427
+ .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); }
3428
+ .oc-err-retry:hover { background: var(--oc-tint-3); }
3412
3429
 
3413
3430
  /* \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 */
3414
3431
  .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; }
@@ -3676,6 +3693,13 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3676
3693
  renderedCount = 0;
3677
3694
  renderedBotFinal = 0;
3678
3695
  timers = [];
3696
+ // Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
3697
+ // widget prefers these over the static `suggestions` attribute.
3698
+ serverSuggestions = null;
3699
+ teaserText = "Need a hand? Ask me anything.";
3700
+ teaserShownOnce = false;
3701
+ idleTimer = null;
3702
+ idleReset = null;
3679
3703
  // ── shadow refs ──
3680
3704
  oc;
3681
3705
  bodyEl;
@@ -3683,7 +3707,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3683
3707
  fileEl;
3684
3708
  titleEl;
3685
3709
  statusEl;
3686
- errEl;
3687
3710
  emojiListEl;
3688
3711
  emojiSearchEl;
3689
3712
  connectedCallback() {
@@ -3695,6 +3718,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3695
3718
  this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
3696
3719
  return;
3697
3720
  }
3721
+ this.teaserText = this.getAttribute("teaser") || this.teaserText;
3698
3722
  this.client = createChatClient({ apiKey, agentId, baseUrl });
3699
3723
  if (Object.keys(this.pendingTools).length > 0) {
3700
3724
  this.client.registerTools(this.pendingTools);
@@ -3706,6 +3730,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3706
3730
  this.updateHeader();
3707
3731
  this.updateStatus();
3708
3732
  this.updateClasses();
3733
+ void this.loadWidgetConfig();
3709
3734
  let wasOpen = false;
3710
3735
  try {
3711
3736
  wasOpen = sessionStorage.getItem(this.key("open")) === "1";
@@ -3719,24 +3744,81 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3719
3744
  this.timers.push(setTimeout(() => {
3720
3745
  this.oc?.classList.remove("ring");
3721
3746
  }, 6e3));
3722
- const teaserText = this.getAttribute("teaser");
3747
+ this.armIdleTeaser();
3748
+ }
3749
+ // Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
3750
+ // is already open or the teaser was dismissed this session. Fires at most once.
3751
+ armIdleTeaser() {
3723
3752
  let dismissed = false;
3724
3753
  try {
3725
3754
  dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
3726
3755
  } catch {
3727
3756
  }
3728
- if (teaserText && !dismissed) {
3729
- this.timers.push(setTimeout(() => {
3730
- if (!this.uiOpen) {
3757
+ if (dismissed) return;
3758
+ const reset = () => {
3759
+ if (this.idleTimer) clearTimeout(this.idleTimer);
3760
+ this.idleTimer = setTimeout(() => {
3761
+ if (!this.uiOpen && !this.teaserShownOnce) {
3731
3762
  this.teaserActive = true;
3763
+ this.teaserShownOnce = true;
3732
3764
  this.updateClasses();
3765
+ this.teardownIdle();
3733
3766
  }
3734
- }, 1400));
3767
+ }, 1e4);
3768
+ };
3769
+ this.idleReset = reset;
3770
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
3771
+ reset();
3772
+ }
3773
+ teardownIdle() {
3774
+ if (this.idleTimer) {
3775
+ clearTimeout(this.idleTimer);
3776
+ this.idleTimer = null;
3777
+ }
3778
+ if (this.idleReset) {
3779
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
3780
+ this.idleReset = null;
3735
3781
  }
3736
3782
  }
3783
+ async loadWidgetConfig() {
3784
+ try {
3785
+ const cfg = await this.client.getWidgetConfig();
3786
+ if (!this.getAttribute("teaser") && cfg.teaser) {
3787
+ this.teaserText = cfg.teaser;
3788
+ this.applyTeaserText();
3789
+ }
3790
+ if (cfg.brand) {
3791
+ if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
3792
+ if (cfg.brand.name && !this.getAttribute("title")) {
3793
+ this.agentName = cfg.brand.name;
3794
+ this.updateHeader();
3795
+ }
3796
+ if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
3797
+ }
3798
+ } catch {
3799
+ }
3800
+ }
3801
+ applyTeaserText() {
3802
+ const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
3803
+ if (el) el.textContent = this.teaserText;
3804
+ }
3805
+ applyBrandLogo(url) {
3806
+ const set = (host) => {
3807
+ if (!host) return;
3808
+ host.textContent = "";
3809
+ const img = document.createElement("img");
3810
+ img.src = url;
3811
+ img.alt = "";
3812
+ img.style.cssText = "width:100%;height:100%;object-fit:contain";
3813
+ host.appendChild(img);
3814
+ };
3815
+ set(this.shadowRoot?.querySelector(".hd .logo"));
3816
+ set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
3817
+ }
3737
3818
  disconnectedCallback() {
3738
3819
  this.off?.();
3739
3820
  void this.client?.end();
3821
+ this.teardownIdle();
3740
3822
  for (const t of this.timers) clearTimeout(t);
3741
3823
  for (const url of this.imagePreviewCache.values()) {
3742
3824
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
@@ -3750,18 +3832,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3750
3832
  key(k2) {
3751
3833
  return `oyehello-chat:${k2}:${this.getAttribute("agent-id") ?? ""}`;
3752
3834
  }
3753
- get suggestions() {
3835
+ // Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
3836
+ get attrSuggestions() {
3754
3837
  return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3755
3838
  }
3839
+ // Effective chips: server (flow) suggestions once received, else the static attribute fallback.
3840
+ get chipLabels() {
3841
+ return this.serverSuggestions ?? this.attrSuggestions;
3842
+ }
3756
3843
  // ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
3757
3844
  renderShell() {
3758
3845
  const root = this.shadowRoot;
3759
- const hasTeaser = !!this.getAttribute("teaser");
3760
3846
  const hasPrivacy = !!this.getAttribute("privacy-url");
3761
- const teaserText = this.getAttribute("teaser") ?? "";
3847
+ const teaserText = this.teaserText;
3762
3848
  root.innerHTML = `
3763
3849
  <style>${STYLES}</style>
3764
- <div class="oc${hasTeaser ? " has-teaser" : ""}${hasPrivacy ? " has-privacy" : ""}">
3850
+ <div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
3765
3851
  <div class="panel" role="dialog" aria-label="Chat">
3766
3852
  <div class="hd">
3767
3853
  <span class="hd-grid"></span>
@@ -3770,7 +3856,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3770
3856
  <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>
3771
3857
  <button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
3772
3858
  </div>
3773
- <div class="err" hidden></div>
3774
3859
  <div class="body"></div>
3775
3860
  <span class="emoji-scrim"></span>
3776
3861
  <div class="emoji-panel">
@@ -3814,7 +3899,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3814
3899
  this.fileEl = root.querySelector('input[type="file"]');
3815
3900
  this.titleEl = root.querySelector(".hd-title");
3816
3901
  this.statusEl = root.querySelector(".hd-status .t");
3817
- this.errEl = root.querySelector(".err");
3818
3902
  this.emojiListEl = root.querySelector(".emoji-list");
3819
3903
  this.emojiSearchEl = root.querySelector(".emoji-search input");
3820
3904
  root.querySelector(".launcher").addEventListener("click", () => this.toggle());
@@ -3895,6 +3979,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3895
3979
  this.uiOpen = true;
3896
3980
  this.teaserActive = false;
3897
3981
  this.unseen = 0;
3982
+ this.teardownIdle();
3898
3983
  try {
3899
3984
  sessionStorage.setItem(this.key("open"), "1");
3900
3985
  } catch {
@@ -3937,6 +4022,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3937
4022
  this.updateHeader();
3938
4023
  } else if (e.type === "handoff") this.handoffState = e.state;
3939
4024
  else if (e.type === "agentTyping") this.agentTyping = e.active;
4025
+ else if (e.type === "suggestions") this.serverSuggestions = e.items;
3940
4026
  else if (e.type === "error") this.lastError = e.error.message;
3941
4027
  this.updateStatus();
3942
4028
  this.renderMessages();
@@ -3951,16 +4037,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3951
4037
  const handoff = HANDOFF_STATUS[this.handoffState];
3952
4038
  const text2 = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
3953
4039
  if (this.statusEl) this.statusEl.textContent = text2;
3954
- if (this.state === "error" && this.lastError) {
3955
- this.errEl.hidden = false;
3956
- this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
3957
- this.errEl.querySelector(".retry").addEventListener("click", () => {
3958
- void this.client?.start();
3959
- });
3960
- } else {
3961
- this.errEl.hidden = true;
3962
- this.errEl.innerHTML = "";
3963
- }
3964
4040
  }
3965
4041
  // ── Message list + scroll ─────────────────────────────────────────────────────────────────────
3966
4042
  renderMessages() {
@@ -3978,10 +4054,13 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3978
4054
  })();
3979
4055
  let html2 = this.messages.map((m2, i) => this.turnHtml(m2, i === lastBot && !this.agentTyping)).join("");
3980
4056
  if (this.agentTyping) html2 += '<span class="typing"><span></span><span></span><span></span></span>';
3981
- const sugg = this.suggestions;
4057
+ const sugg = this.chipLabels;
3982
4058
  if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
3983
4059
  html2 += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
3984
4060
  }
4061
+ if (this.state === "error" && this.lastError) {
4062
+ html2 += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
4063
+ }
3985
4064
  body.innerHTML = html2;
3986
4065
  this.hydrateAttachmentImages();
3987
4066
  this.bindChips();
@@ -4059,12 +4138,15 @@ Please report this to https://github.com/markedjs/marked.`, e) {
4059
4138
  this.updateClasses();
4060
4139
  }
4061
4140
  bindChips() {
4062
- const sugg = this.suggestions;
4141
+ const sugg = this.chipLabels;
4063
4142
  this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
4064
4143
  const i = Number(btn.getAttribute("data-chip"));
4065
4144
  const text2 = sugg[i];
4066
4145
  if (text2) btn.addEventListener("click", () => this.sendChip(text2));
4067
4146
  });
4147
+ this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
4148
+ void this.client?.start();
4149
+ });
4068
4150
  }
4069
4151
  // ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
4070
4152
  toggleEmoji() {