@smooai/chat-widget 0.10.2 → 0.12.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
@@ -334,6 +334,7 @@ function resolveConfig(config) {
334
334
  startOpen: config.startOpen ?? false,
335
335
  hideBranding: config.hideBranding ?? false,
336
336
  examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
337
+ showSuggestedReplies: config.showSuggestedReplies ?? true,
337
338
  requireName: config.requireName ?? false,
338
339
  requireEmail: config.requireEmail ?? false,
339
340
  requirePhone: config.requirePhone ?? false,
@@ -734,6 +735,13 @@ function httpBaseFromWsEndpoint(endpoint) {
734
735
  return null;
735
736
  }
736
737
  }
738
+ /**
739
+ * The Rich-Interaction render capabilities this widget declares at session
740
+ * create (`supports`). Must stay aligned with the card registry in
741
+ * `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
742
+ * capability; a test asserts the two match.
743
+ */
744
+ const SUPPORTED_INTERACTION_CAPABILITIES = ["identity_form"];
737
745
  /** Pull the final assistant text out of an `eventual_response` data payload. */
738
746
  function extractFinalText(response) {
739
747
  if (!response || typeof response !== "object") return null;
@@ -774,6 +782,19 @@ function extractCitations(inner) {
774
782
  }
775
783
  return out;
776
784
  }
785
+ /**
786
+ * Pull the suggested follow-up replies out of a terminal `eventual_response`'s
787
+ * `response` object (`response.suggestedNextActions`). Optional + back-compatible
788
+ * like citations — read defensively (tolerating absence, non-array shapes, and
789
+ * non-string / blank entries) and capped at 4 so a chatty agent can't overflow
790
+ * the composer chip row.
791
+ */
792
+ function extractSuggestions(response) {
793
+ if (!response || typeof response !== "object") return [];
794
+ const raw = response.suggestedNextActions;
795
+ if (!Array.isArray(raw)) return [];
796
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 4);
797
+ }
777
798
  /** Convert a server message row into a finalized {@link ChatMessage}. */
778
799
  function wireMessageToChat(m, idx) {
779
800
  const text = typeof m.content?.text === "string" ? m.content.text : "";
@@ -798,6 +819,7 @@ var ConversationController = class {
798
819
  _defineProperty(this, "seq", 0);
799
820
  _defineProperty(this, "activeRequestId", null);
800
821
  _defineProperty(this, "interrupt", null);
822
+ _defineProperty(this, "pendingInteractionValues", null);
801
823
  _defineProperty(this, "identityRestore", { phase: "idle" });
802
824
  _defineProperty(this, "resumeAttempted", false);
803
825
  _defineProperty(this, "httpBase", void 0);
@@ -876,6 +898,40 @@ var ConversationController = class {
876
898
  });
877
899
  this.setInterrupt(null);
878
900
  }
901
+ /**
902
+ * Submit a Rich Interaction card to resume the parked turn. The server
903
+ * routes to the kind's validator: invalid values come back as an
904
+ * `interaction_invalid` event (the card re-renders with per-field errors —
905
+ * the turn stays parked); a valid submit is acked and the turn resumes.
906
+ * No-op if not awaiting an interaction.
907
+ */
908
+ submitInteraction(values) {
909
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
910
+ this.pendingInteractionValues = {
911
+ kind: this.interrupt.interactionKind,
912
+ values
913
+ };
914
+ this.client.submitInteraction({
915
+ sessionId: this.sessionId,
916
+ requestId: this.activeRequestId,
917
+ interactionId: this.interrupt.interactionId,
918
+ kind: this.interrupt.interactionKind,
919
+ values
920
+ });
921
+ }
922
+ /** Decline the pending Rich Interaction; the agent continues without it. */
923
+ declineInteraction() {
924
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
925
+ this.client.submitInteraction({
926
+ sessionId: this.sessionId,
927
+ requestId: this.activeRequestId,
928
+ interactionId: this.interrupt.interactionId,
929
+ kind: this.interrupt.interactionKind,
930
+ declined: true
931
+ });
932
+ this.pendingInteractionValues = null;
933
+ this.setInterrupt(null);
934
+ }
879
935
  nextId(prefix) {
880
936
  this.seq += 1;
881
937
  return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
@@ -1040,6 +1096,7 @@ var ConversationController = class {
1040
1096
  userName: state.identity.name,
1041
1097
  userEmail: state.identity.email,
1042
1098
  browserFingerprint: this.fingerprint(),
1099
+ supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
1043
1100
  ...metadata ? { metadata } : {}
1044
1101
  });
1045
1102
  this.sessionId = session.sessionId;
@@ -1124,6 +1181,8 @@ var ConversationController = class {
1124
1181
  if (!assistant.text) assistant.text = "(no response)";
1125
1182
  const citations = extractCitations(inner);
1126
1183
  if (citations.length > 0) assistant.citations = citations;
1184
+ const suggestions = extractSuggestions(inner?.response);
1185
+ if (suggestions.length > 0) assistant.suggestions = suggestions;
1127
1186
  assistant.streaming = false;
1128
1187
  this.emitMessages();
1129
1188
  } catch (err) {
@@ -1180,6 +1239,55 @@ var ConversationController = class {
1180
1239
  actionDescription: str(inner.actionDescription)
1181
1240
  });
1182
1241
  break;
1242
+ case "interaction_required": {
1243
+ const interactionId = str(inner.interactionId);
1244
+ const kind = str(inner.kind);
1245
+ const spec = inner.spec && typeof inner.spec === "object" ? inner.spec : {};
1246
+ if (!interactionId || !kind) break;
1247
+ this.pendingInteractionValues = null;
1248
+ this.setInterrupt({
1249
+ kind: "interaction",
1250
+ interactionId,
1251
+ interactionKind: kind,
1252
+ spec,
1253
+ reason: str(inner.reason)
1254
+ });
1255
+ break;
1256
+ }
1257
+ case "interaction_invalid":
1258
+ if (this.interrupt?.kind === "interaction" && this.interrupt.interactionId === str(inner.interactionId)) {
1259
+ const errors = [];
1260
+ if (Array.isArray(inner.errors)) for (const e of inner.errors) {
1261
+ if (!e || typeof e !== "object") continue;
1262
+ const o = e;
1263
+ const field = str(o.field);
1264
+ if (field) errors.push({
1265
+ field,
1266
+ message: str(o.message) ?? "Invalid value"
1267
+ });
1268
+ }
1269
+ this.pendingInteractionValues = null;
1270
+ this.setInterrupt({
1271
+ ...this.interrupt,
1272
+ errors
1273
+ });
1274
+ }
1275
+ break;
1276
+ case "immediate_response":
1277
+ if (this.interrupt?.kind === "interaction") {
1278
+ const pending = this.pendingInteractionValues;
1279
+ if (pending && pending.kind === "identity_intake") {
1280
+ const v = pending.values;
1281
+ this.store.getState().mergeIdentity({
1282
+ name: v.name,
1283
+ email: v.email,
1284
+ phone: v.phone
1285
+ });
1286
+ }
1287
+ this.pendingInteractionValues = null;
1288
+ this.setInterrupt(null);
1289
+ }
1290
+ break;
1183
1291
  default: break;
1184
1292
  }
1185
1293
  }
@@ -1954,6 +2062,10 @@ ${mode === "fullpage" ? `
1954
2062
  transform: translateY(-1px);
1955
2063
  }
1956
2064
 
2065
+ /* ───────────── Mid-conversation suggested-reply chips ─────────────── */
2066
+ .reply-suggestions { padding: 0 14px 4px; }
2067
+ .reply-suggestions:empty { display: none; }
2068
+
1957
2069
  /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
1958
2070
  .interrupt { padding: 0 14px; }
1959
2071
  .int-card {
@@ -1988,6 +2100,9 @@ ${mode === "fullpage" ? `
1988
2100
  border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
1989
2101
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
1990
2102
  }
2103
+ .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2104
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2105
+ .int-form .int-error { margin-top: 2px; }
1991
2106
  .int-btn {
1992
2107
  border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
1993
2108
  background: var(--sac-surface-2);
@@ -2007,8 +2122,14 @@ ${mode === "fullpage" ? `
2007
2122
  color: var(--sac-primary-text);
2008
2123
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
2009
2124
  }
2010
- .int-row .int-btn { flex: 1; }
2011
- .int-row .int-input + .int-btn { flex: 0 0 auto; }
2125
+ .int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2126
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2127
+ .int-form .int-error { margin-top: 2px; }
2128
+ .int-btn { flex: 1; }
2129
+ .int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2130
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2131
+ .int-form .int-error { margin-top: 2px; }
2132
+ .int-btn { flex: 0 0 auto; }
2012
2133
  .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
2013
2134
  .int-card { position: relative; }
2014
2135
  .int-close {
@@ -2146,9 +2267,139 @@ const ICON = {
2146
2267
  chev: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
2147
2268
  /** OTP interrupt — a padlock. */
2148
2269
  lock: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="10.5" width="14" height="9.5" rx="2.2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" stroke="currentColor" stroke-width="1.7"/></svg>`,
2270
+ /** Identity-intake interrupt — a person. */
2271
+ user: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8.2" r="3.4" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 19.5c.8-3.1 3.4-4.8 6.5-4.8s5.7 1.7 6.5 4.8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`,
2149
2272
  /** Tool-confirmation interrupt — a shield. */
2150
2273
  shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`
2151
2274
  };
2275
+ /**
2276
+ * The identity_intake card: the fields the agent requested (pre-chat form field
2277
+ * pattern — same classes, same phone formatting), per-field server errors, a
2278
+ * submit and a decline affordance. Server-supplied text (reason, labels, error
2279
+ * messages) is set via `textContent` — never innerHTML.
2280
+ */
2281
+ function buildIdentityIntakeCard(it, ctx) {
2282
+ const form = document.createElement("form");
2283
+ form.className = "int-form";
2284
+ form.noValidate = true;
2285
+ if (it.reason) {
2286
+ const desc = document.createElement("div");
2287
+ desc.className = "int-desc";
2288
+ desc.textContent = it.reason;
2289
+ form.appendChild(desc);
2290
+ }
2291
+ const DEFAULTS = {
2292
+ name: {
2293
+ label: "Name",
2294
+ type: "text",
2295
+ autocomplete: "name"
2296
+ },
2297
+ email: {
2298
+ label: "Email",
2299
+ type: "email",
2300
+ autocomplete: "email"
2301
+ },
2302
+ phone: {
2303
+ label: "Phone",
2304
+ type: "tel",
2305
+ autocomplete: "tel"
2306
+ }
2307
+ };
2308
+ const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
2309
+ const fields = [];
2310
+ for (const f of rawFields) {
2311
+ if (!f || typeof f !== "object") continue;
2312
+ const o = f;
2313
+ const key = typeof o.key === "string" ? o.key : "";
2314
+ if (key !== "name" && key !== "email" && key !== "phone") continue;
2315
+ fields.push({
2316
+ key,
2317
+ required: o.required === true,
2318
+ label: typeof o.label === "string" ? o.label : void 0
2319
+ });
2320
+ }
2321
+ if (fields.length === 0) fields.push({
2322
+ key: "email",
2323
+ required: true
2324
+ });
2325
+ for (const f of fields) {
2326
+ const d = DEFAULTS[f.key];
2327
+ const label = document.createElement("label");
2328
+ label.className = "pc-field";
2329
+ const caption = document.createElement("span");
2330
+ caption.textContent = f.label ?? d.label;
2331
+ const input = document.createElement("input");
2332
+ input.name = f.key;
2333
+ input.type = d.type;
2334
+ input.setAttribute("autocomplete", d.autocomplete);
2335
+ input.required = f.required;
2336
+ const prefill = ctx.prefill[f.key];
2337
+ if (prefill) input.value = prefill;
2338
+ label.append(caption, input);
2339
+ if (f.key === "phone") {
2340
+ const hint = document.createElement("span");
2341
+ hint.className = "pc-hint";
2342
+ hint.setAttribute("aria-live", "polite");
2343
+ label.appendChild(hint);
2344
+ }
2345
+ const serverError = it.errors?.find((e) => e.field === f.key);
2346
+ if (serverError) {
2347
+ label.classList.add("invalid");
2348
+ const err = document.createElement("span");
2349
+ err.className = "int-error";
2350
+ err.textContent = serverError.message;
2351
+ label.appendChild(err);
2352
+ }
2353
+ form.appendChild(label);
2354
+ }
2355
+ const row = document.createElement("div");
2356
+ row.className = "int-row";
2357
+ const decline = document.createElement("button");
2358
+ decline.className = "int-btn";
2359
+ decline.type = "button";
2360
+ decline.textContent = "No thanks";
2361
+ decline.addEventListener("click", () => ctx.decline());
2362
+ const share = document.createElement("button");
2363
+ share.className = "int-btn primary";
2364
+ share.type = "submit";
2365
+ share.textContent = "Share details";
2366
+ row.append(decline, share);
2367
+ form.appendChild(row);
2368
+ form.addEventListener("submit", (ev) => {
2369
+ ev.preventDefault();
2370
+ if (!form.reportValidity()) return;
2371
+ const data = new FormData(form);
2372
+ const val = (k) => data.get(k)?.trim() || void 0;
2373
+ const rawPhone = val("phone");
2374
+ const phoneInput = form.querySelector("input[name=\"phone\"]");
2375
+ if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
2376
+ const field = phoneInput.closest(".pc-field");
2377
+ field?.classList.add("invalid");
2378
+ const hint = field?.querySelector(".pc-hint");
2379
+ if (hint) hint.textContent = "Enter a valid phone number";
2380
+ if (phoneInput.hasAttribute("required")) {
2381
+ phoneInput.focus();
2382
+ return;
2383
+ }
2384
+ }
2385
+ const phone = rawPhone ? phoneToE164(rawPhone) ?? rawPhone : void 0;
2386
+ ctx.submit({
2387
+ name: val("name"),
2388
+ email: val("email"),
2389
+ phone
2390
+ });
2391
+ });
2392
+ ctx.wirePhoneField(form);
2393
+ queueMicrotask(() => form.querySelector("input")?.focus());
2394
+ return form;
2395
+ }
2396
+ /** Kind → card. See the registry doc above. */
2397
+ const INTERACTION_CARDS = { identity_intake: {
2398
+ capability: "identity_form",
2399
+ title: "Share your details",
2400
+ icon: ICON.user,
2401
+ build: buildIdentityIntakeCard
2402
+ } };
2152
2403
  var SmoothAgentChatElement = class extends HTMLElement {
2153
2404
  static get observedAttributes() {
2154
2405
  return OBSERVED;
@@ -2165,6 +2416,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2165
2416
  _defineProperty(this, "userInfoSatisfied", false);
2166
2417
  _defineProperty(this, "hasSent", false);
2167
2418
  _defineProperty(this, "examplePrompts", []);
2419
+ _defineProperty(this, "showSuggestedReplies", true);
2168
2420
  _defineProperty(this, "greeting", "");
2169
2421
  _defineProperty(this, "interrupt", null);
2170
2422
  _defineProperty(this, "interruptEl", null);
@@ -2178,6 +2430,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2178
2430
  _defineProperty(this, "dotEl", null);
2179
2431
  _defineProperty(this, "inputEl", null);
2180
2432
  _defineProperty(this, "sendBtn", null);
2433
+ _defineProperty(this, "suggestionsEl", null);
2181
2434
  _defineProperty(this, "streamBubbleEl", null);
2182
2435
  _defineProperty(this, "streamMsgId", null);
2183
2436
  _defineProperty(this, "streamTarget", "");
@@ -2246,6 +2499,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2246
2499
  startOpen: this.overrides.startOpen ?? this.hasAttribute("start-open"),
2247
2500
  hideBranding: this.overrides.hideBranding ?? this.hasAttribute("hide-branding"),
2248
2501
  examplePrompts: this.overrides.examplePrompts,
2502
+ showSuggestedReplies: this.overrides.showSuggestedReplies,
2249
2503
  requireName: this.overrides.requireName,
2250
2504
  requireEmail: this.overrides.requireEmail,
2251
2505
  requirePhone: this.overrides.requirePhone,
@@ -2277,10 +2531,12 @@ var SmoothAgentChatElement = class extends HTMLElement {
2277
2531
  onInterrupt: (interrupt) => {
2278
2532
  this.interrupt = interrupt;
2279
2533
  this.renderInterrupt();
2534
+ this.renderSuggestions();
2280
2535
  },
2281
2536
  onIdentityRestore: (state) => {
2282
2537
  this.identityRestore = state;
2283
2538
  this.renderInterrupt();
2539
+ this.renderSuggestions();
2284
2540
  }
2285
2541
  });
2286
2542
  if (resolved.startOpen) this.open = true;
@@ -2308,6 +2564,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2308
2564
  <button class="close" aria-label="Close chat">${ICON.close}</button>
2309
2565
  </div>`;
2310
2566
  this.examplePrompts = resolved.examplePrompts;
2567
+ this.showSuggestedReplies = resolved.showSuggestedReplies;
2311
2568
  this.greeting = resolved.greeting;
2312
2569
  const gating = needsUserInfo(resolved) && !this.userInfoSatisfied;
2313
2570
  this.gating = gating;
@@ -2336,6 +2593,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2336
2593
  const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : "";
2337
2594
  const chatHtml = `
2338
2595
  <div class="messages"></div>
2596
+ <div class="reply-suggestions"></div>
2339
2597
  <div class="interrupt hidden"></div>
2340
2598
  <div class="composer-wrap">
2341
2599
  <div class="composer">
@@ -2366,6 +2624,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2366
2624
  this.inputEl = container.querySelector("textarea");
2367
2625
  this.sendBtn = container.querySelector(".send");
2368
2626
  this.interruptEl = container.querySelector(".interrupt");
2627
+ this.suggestionsEl = container.querySelector(".reply-suggestions");
2369
2628
  this.launcherEl?.addEventListener("click", () => this.openChat());
2370
2629
  container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
2371
2630
  this.sendBtn?.addEventListener("click", () => this.submit());
@@ -2423,19 +2682,31 @@ var SmoothAgentChatElement = class extends HTMLElement {
2423
2682
  head.className = "int-head";
2424
2683
  const ico = document.createElement("span");
2425
2684
  ico.className = "int-ico";
2426
- ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
2685
+ const card_meta = it.kind === "interaction" ? INTERACTION_CARDS[it.interactionKind] : void 0;
2686
+ if (it.kind === "interaction" && !card_meta) {
2687
+ this.controller?.declineInteraction();
2688
+ el.classList.add("hidden");
2689
+ return;
2690
+ }
2691
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : it.kind === "interaction" ? card_meta?.icon ?? ICON.user : ICON.shield;
2427
2692
  const title = document.createElement("span");
2428
2693
  title.className = "int-title";
2429
- title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
2694
+ title.textContent = it.kind === "otp" ? "Verification required" : it.kind === "interaction" ? card_meta?.title ?? "One more thing" : "Confirm this action";
2430
2695
  head.append(ico, title);
2431
2696
  card.appendChild(head);
2432
- if (it.actionDescription) {
2697
+ if (it.kind !== "interaction" && it.actionDescription) {
2433
2698
  const desc = document.createElement("div");
2434
2699
  desc.className = "int-desc";
2435
2700
  desc.textContent = it.actionDescription;
2436
2701
  card.appendChild(desc);
2437
2702
  }
2438
- if (it.kind === "otp") {
2703
+ if (it.kind === "interaction") card.appendChild(card_meta.build(it, {
2704
+ submit: (values) => this.controller?.submitInteraction(values),
2705
+ decline: () => this.controller?.declineInteraction(),
2706
+ prefill: this.controller?.getStore().getState().identity ?? {},
2707
+ wirePhoneField: (form) => this.wirePhoneField(form)
2708
+ }));
2709
+ else if (it.kind === "otp") {
2439
2710
  if (it.sent?.maskedDestination) {
2440
2711
  const sent = document.createElement("div");
2441
2712
  sent.className = "int-sent";
@@ -2820,6 +3091,36 @@ var SmoothAgentChatElement = class extends HTMLElement {
2820
3091
  if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
2821
3092
  }
2822
3093
  this.scrollToBottom(true);
3094
+ this.renderSuggestions();
3095
+ }
3096
+ /**
3097
+ * Render (or clear) the mid-conversation suggested-reply chips under the
3098
+ * composer. Shown ONLY when: the feature is enabled, no interrupt / restore
3099
+ * overlay is active (those reuse the slot above the composer), and the LATEST
3100
+ * message is a finalized assistant turn that carried suggestions. A new turn
3101
+ * (agent or the visitor typing) appends messages, so the latest is no longer
3102
+ * that assistant message and the chips clear on the next render. Chips reuse
3103
+ * the empty-state `.prompts`/`.chip` styling and send via the same path.
3104
+ */
3105
+ renderSuggestions() {
3106
+ const el = this.suggestionsEl;
3107
+ if (!el) return;
3108
+ el.replaceChildren();
3109
+ if (!this.showSuggestedReplies) return;
3110
+ if (this.interrupt || this.identityRestore.phase !== "idle") return;
3111
+ const last = this.messages[this.messages.length - 1];
3112
+ if (!last || last.role !== "assistant" || last.streaming || !last.suggestions || last.suggestions.length === 0) return;
3113
+ const chips = document.createElement("div");
3114
+ chips.className = "prompts";
3115
+ for (const suggestion of last.suggestions) {
3116
+ const chip = document.createElement("button");
3117
+ chip.type = "button";
3118
+ chip.className = "chip";
3119
+ chip.textContent = suggestion;
3120
+ chip.addEventListener("click", () => this.submitPrompt(suggestion));
3121
+ chips.appendChild(chip);
3122
+ }
3123
+ el.appendChild(chips);
2823
3124
  }
2824
3125
  /** True when the host/user prefers reduced motion (snap, no typewriter). */
2825
3126
  prefersReducedMotion() {