@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/README.md CHANGED
@@ -186,6 +186,9 @@ mountChatWidget({
186
186
  ```
187
187
 
188
188
  - **`examplePrompts`** render as clickable chips in the empty state (max 5).
189
+ - **`showSuggestedReplies`** (default `true`) shows mid-conversation suggested-reply
190
+ chips under the latest assistant message when the agent returns follow-up
191
+ suggestions; tapping one sends it. Set `false` to hide them.
189
192
  - **`requireName` / `requireEmail` / `requirePhone`** show a styled pre-chat form;
190
193
  the collected name/email are attached to the conversation session (phone rides
191
194
  session metadata). Set **`allowAnonymous: true`** to skip the form.
@@ -11774,6 +11774,7 @@ var SmoothAgentChat = (function(exports) {
11774
11774
  startOpen: config.startOpen ?? false,
11775
11775
  hideBranding: config.hideBranding ?? false,
11776
11776
  examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
11777
+ showSuggestedReplies: config.showSuggestedReplies ?? true,
11777
11778
  requireName: config.requireName ?? false,
11778
11779
  requireEmail: config.requireEmail ?? false,
11779
11780
  requirePhone: config.requirePhone ?? false,
@@ -11841,7 +11842,7 @@ var SmoothAgentChat = (function(exports) {
11841
11842
  }) : e[r] = t, e;
11842
11843
  }
11843
11844
  //#endregion
11844
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/transport.js
11845
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/transport.js
11845
11846
  /**
11846
11847
  * Transport abstraction for the client.
11847
11848
  *
@@ -11966,7 +11967,7 @@ var SmoothAgentChat = (function(exports) {
11966
11967
  }
11967
11968
  };
11968
11969
  //#endregion
11969
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/types.js
11970
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/types.js
11970
11971
  /** Every server→client `type` discriminator value. */
11971
11972
  const EVENT_TYPES = [
11972
11973
  "immediate_response",
@@ -11979,6 +11980,8 @@ var SmoothAgentChat = (function(exports) {
11979
11980
  "otp_sent",
11980
11981
  "otp_verified",
11981
11982
  "otp_invalid",
11983
+ "interaction_required",
11984
+ "interaction_invalid",
11982
11985
  "error",
11983
11986
  "pong"
11984
11987
  ];
@@ -11987,7 +11990,7 @@ var SmoothAgentChat = (function(exports) {
11987
11990
  return typeof frame === "object" && frame !== null && "type" in frame && typeof frame.type === "string" && EVENT_TYPES.includes(frame.type);
11988
11991
  }
11989
11992
  //#endregion
11990
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/client.js
11993
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/client.js
11991
11994
  /**
11992
11995
  * SmoothAgentClient — a minimal, idiomatic, transport-agnostic client for the
11993
11996
  * smooth-operator WebSocket protocol.
@@ -12272,6 +12275,20 @@ var SmoothAgentChat = (function(exports) {
12272
12275
  ...req
12273
12276
  }));
12274
12277
  }
12278
+ /**
12279
+ * Submit (or decline) a Rich Interaction, resuming the turn parked by an
12280
+ * `interaction_required` event. This ONE verb serves every interaction kind
12281
+ * (identity intake, future date pickers, choice chips, …) — adding a kind
12282
+ * needs no new client method. Server-side validation may reply with an
12283
+ * `interaction_invalid` event (the turn stays parked — resubmit); a valid
12284
+ * submit resumes the stream back into the original {@link MessageTurn}.
12285
+ */
12286
+ submitInteraction(req) {
12287
+ this.transport.send(JSON.stringify({
12288
+ action: "submit_interaction",
12289
+ ...req
12290
+ }));
12291
+ }
12275
12292
  /** Send an action that expects a single correlated response event. */
12276
12293
  request(action) {
12277
12294
  const requestId = action.requestId ?? this.generateRequestId();
@@ -12854,6 +12871,13 @@ var SmoothAgentChat = (function(exports) {
12854
12871
  return null;
12855
12872
  }
12856
12873
  }
12874
+ /**
12875
+ * The Rich-Interaction render capabilities this widget declares at session
12876
+ * create (`supports`). Must stay aligned with the card registry in
12877
+ * `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
12878
+ * capability; a test asserts the two match.
12879
+ */
12880
+ const SUPPORTED_INTERACTION_CAPABILITIES = ["identity_form"];
12857
12881
  /** Pull the final assistant text out of an `eventual_response` data payload. */
12858
12882
  function extractFinalText(response) {
12859
12883
  if (!response || typeof response !== "object") return null;
@@ -12894,6 +12918,19 @@ var SmoothAgentChat = (function(exports) {
12894
12918
  }
12895
12919
  return out;
12896
12920
  }
12921
+ /**
12922
+ * Pull the suggested follow-up replies out of a terminal `eventual_response`'s
12923
+ * `response` object (`response.suggestedNextActions`). Optional + back-compatible
12924
+ * like citations — read defensively (tolerating absence, non-array shapes, and
12925
+ * non-string / blank entries) and capped at 4 so a chatty agent can't overflow
12926
+ * the composer chip row.
12927
+ */
12928
+ function extractSuggestions(response) {
12929
+ if (!response || typeof response !== "object") return [];
12930
+ const raw = response.suggestedNextActions;
12931
+ if (!Array.isArray(raw)) return [];
12932
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 4);
12933
+ }
12897
12934
  /** Convert a server message row into a finalized {@link ChatMessage}. */
12898
12935
  function wireMessageToChat(m, idx) {
12899
12936
  const text = typeof m.content?.text === "string" ? m.content.text : "";
@@ -12918,6 +12955,7 @@ var SmoothAgentChat = (function(exports) {
12918
12955
  _defineProperty(this, "seq", 0);
12919
12956
  _defineProperty(this, "activeRequestId", null);
12920
12957
  _defineProperty(this, "interrupt", null);
12958
+ _defineProperty(this, "pendingInteractionValues", null);
12921
12959
  _defineProperty(this, "identityRestore", { phase: "idle" });
12922
12960
  _defineProperty(this, "resumeAttempted", false);
12923
12961
  _defineProperty(this, "httpBase", void 0);
@@ -12996,6 +13034,40 @@ var SmoothAgentChat = (function(exports) {
12996
13034
  });
12997
13035
  this.setInterrupt(null);
12998
13036
  }
13037
+ /**
13038
+ * Submit a Rich Interaction card to resume the parked turn. The server
13039
+ * routes to the kind's validator: invalid values come back as an
13040
+ * `interaction_invalid` event (the card re-renders with per-field errors —
13041
+ * the turn stays parked); a valid submit is acked and the turn resumes.
13042
+ * No-op if not awaiting an interaction.
13043
+ */
13044
+ submitInteraction(values) {
13045
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
13046
+ this.pendingInteractionValues = {
13047
+ kind: this.interrupt.interactionKind,
13048
+ values
13049
+ };
13050
+ this.client.submitInteraction({
13051
+ sessionId: this.sessionId,
13052
+ requestId: this.activeRequestId,
13053
+ interactionId: this.interrupt.interactionId,
13054
+ kind: this.interrupt.interactionKind,
13055
+ values
13056
+ });
13057
+ }
13058
+ /** Decline the pending Rich Interaction; the agent continues without it. */
13059
+ declineInteraction() {
13060
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
13061
+ this.client.submitInteraction({
13062
+ sessionId: this.sessionId,
13063
+ requestId: this.activeRequestId,
13064
+ interactionId: this.interrupt.interactionId,
13065
+ kind: this.interrupt.interactionKind,
13066
+ declined: true
13067
+ });
13068
+ this.pendingInteractionValues = null;
13069
+ this.setInterrupt(null);
13070
+ }
12999
13071
  nextId(prefix) {
13000
13072
  this.seq += 1;
13001
13073
  return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
@@ -13160,6 +13232,7 @@ var SmoothAgentChat = (function(exports) {
13160
13232
  userName: state.identity.name,
13161
13233
  userEmail: state.identity.email,
13162
13234
  browserFingerprint: this.fingerprint(),
13235
+ supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
13163
13236
  ...metadata ? { metadata } : {}
13164
13237
  });
13165
13238
  this.sessionId = session.sessionId;
@@ -13244,6 +13317,8 @@ var SmoothAgentChat = (function(exports) {
13244
13317
  if (!assistant.text) assistant.text = "(no response)";
13245
13318
  const citations = extractCitations(inner);
13246
13319
  if (citations.length > 0) assistant.citations = citations;
13320
+ const suggestions = extractSuggestions(inner?.response);
13321
+ if (suggestions.length > 0) assistant.suggestions = suggestions;
13247
13322
  assistant.streaming = false;
13248
13323
  this.emitMessages();
13249
13324
  } catch (err) {
@@ -13300,6 +13375,55 @@ var SmoothAgentChat = (function(exports) {
13300
13375
  actionDescription: str(inner.actionDescription)
13301
13376
  });
13302
13377
  break;
13378
+ case "interaction_required": {
13379
+ const interactionId = str(inner.interactionId);
13380
+ const kind = str(inner.kind);
13381
+ const spec = inner.spec && typeof inner.spec === "object" ? inner.spec : {};
13382
+ if (!interactionId || !kind) break;
13383
+ this.pendingInteractionValues = null;
13384
+ this.setInterrupt({
13385
+ kind: "interaction",
13386
+ interactionId,
13387
+ interactionKind: kind,
13388
+ spec,
13389
+ reason: str(inner.reason)
13390
+ });
13391
+ break;
13392
+ }
13393
+ case "interaction_invalid":
13394
+ if (this.interrupt?.kind === "interaction" && this.interrupt.interactionId === str(inner.interactionId)) {
13395
+ const errors = [];
13396
+ if (Array.isArray(inner.errors)) for (const e of inner.errors) {
13397
+ if (!e || typeof e !== "object") continue;
13398
+ const o = e;
13399
+ const field = str(o.field);
13400
+ if (field) errors.push({
13401
+ field,
13402
+ message: str(o.message) ?? "Invalid value"
13403
+ });
13404
+ }
13405
+ this.pendingInteractionValues = null;
13406
+ this.setInterrupt({
13407
+ ...this.interrupt,
13408
+ errors
13409
+ });
13410
+ }
13411
+ break;
13412
+ case "immediate_response":
13413
+ if (this.interrupt?.kind === "interaction") {
13414
+ const pending = this.pendingInteractionValues;
13415
+ if (pending && pending.kind === "identity_intake") {
13416
+ const v = pending.values;
13417
+ this.store.getState().mergeIdentity({
13418
+ name: v.name,
13419
+ email: v.email,
13420
+ phone: v.phone
13421
+ });
13422
+ }
13423
+ this.pendingInteractionValues = null;
13424
+ this.setInterrupt(null);
13425
+ }
13426
+ break;
13303
13427
  default: break;
13304
13428
  }
13305
13429
  }
@@ -14074,6 +14198,10 @@ ${mode === "fullpage" ? `
14074
14198
  transform: translateY(-1px);
14075
14199
  }
14076
14200
 
14201
+ /* ───────────── Mid-conversation suggested-reply chips ─────────────── */
14202
+ .reply-suggestions { padding: 0 14px 4px; }
14203
+ .reply-suggestions:empty { display: none; }
14204
+
14077
14205
  /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
14078
14206
  .interrupt { padding: 0 14px; }
14079
14207
  .int-card {
@@ -14108,6 +14236,9 @@ ${mode === "fullpage" ? `
14108
14236
  border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
14109
14237
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
14110
14238
  }
14239
+ .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14240
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14241
+ .int-form .int-error { margin-top: 2px; }
14111
14242
  .int-btn {
14112
14243
  border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
14113
14244
  background: var(--sac-surface-2);
@@ -14127,8 +14258,14 @@ ${mode === "fullpage" ? `
14127
14258
  color: var(--sac-primary-text);
14128
14259
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
14129
14260
  }
14130
- .int-row .int-btn { flex: 1; }
14131
- .int-row .int-input + .int-btn { flex: 0 0 auto; }
14261
+ .int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14262
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14263
+ .int-form .int-error { margin-top: 2px; }
14264
+ .int-btn { flex: 1; }
14265
+ .int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14266
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14267
+ .int-form .int-error { margin-top: 2px; }
14268
+ .int-btn { flex: 0 0 auto; }
14132
14269
  .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
14133
14270
  .int-card { position: relative; }
14134
14271
  .int-close {
@@ -14266,9 +14403,139 @@ ${mode === "fullpage" ? `
14266
14403
  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>`,
14267
14404
  /** OTP interrupt — a padlock. */
14268
14405
  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>`,
14406
+ /** Identity-intake interrupt — a person. */
14407
+ 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>`,
14269
14408
  /** Tool-confirmation interrupt — a shield. */
14270
14409
  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>`
14271
14410
  };
14411
+ /**
14412
+ * The identity_intake card: the fields the agent requested (pre-chat form field
14413
+ * pattern — same classes, same phone formatting), per-field server errors, a
14414
+ * submit and a decline affordance. Server-supplied text (reason, labels, error
14415
+ * messages) is set via `textContent` — never innerHTML.
14416
+ */
14417
+ function buildIdentityIntakeCard(it, ctx) {
14418
+ const form = document.createElement("form");
14419
+ form.className = "int-form";
14420
+ form.noValidate = true;
14421
+ if (it.reason) {
14422
+ const desc = document.createElement("div");
14423
+ desc.className = "int-desc";
14424
+ desc.textContent = it.reason;
14425
+ form.appendChild(desc);
14426
+ }
14427
+ const DEFAULTS = {
14428
+ name: {
14429
+ label: "Name",
14430
+ type: "text",
14431
+ autocomplete: "name"
14432
+ },
14433
+ email: {
14434
+ label: "Email",
14435
+ type: "email",
14436
+ autocomplete: "email"
14437
+ },
14438
+ phone: {
14439
+ label: "Phone",
14440
+ type: "tel",
14441
+ autocomplete: "tel"
14442
+ }
14443
+ };
14444
+ const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
14445
+ const fields = [];
14446
+ for (const f of rawFields) {
14447
+ if (!f || typeof f !== "object") continue;
14448
+ const o = f;
14449
+ const key = typeof o.key === "string" ? o.key : "";
14450
+ if (key !== "name" && key !== "email" && key !== "phone") continue;
14451
+ fields.push({
14452
+ key,
14453
+ required: o.required === true,
14454
+ label: typeof o.label === "string" ? o.label : void 0
14455
+ });
14456
+ }
14457
+ if (fields.length === 0) fields.push({
14458
+ key: "email",
14459
+ required: true
14460
+ });
14461
+ for (const f of fields) {
14462
+ const d = DEFAULTS[f.key];
14463
+ const label = document.createElement("label");
14464
+ label.className = "pc-field";
14465
+ const caption = document.createElement("span");
14466
+ caption.textContent = f.label ?? d.label;
14467
+ const input = document.createElement("input");
14468
+ input.name = f.key;
14469
+ input.type = d.type;
14470
+ input.setAttribute("autocomplete", d.autocomplete);
14471
+ input.required = f.required;
14472
+ const prefill = ctx.prefill[f.key];
14473
+ if (prefill) input.value = prefill;
14474
+ label.append(caption, input);
14475
+ if (f.key === "phone") {
14476
+ const hint = document.createElement("span");
14477
+ hint.className = "pc-hint";
14478
+ hint.setAttribute("aria-live", "polite");
14479
+ label.appendChild(hint);
14480
+ }
14481
+ const serverError = it.errors?.find((e) => e.field === f.key);
14482
+ if (serverError) {
14483
+ label.classList.add("invalid");
14484
+ const err = document.createElement("span");
14485
+ err.className = "int-error";
14486
+ err.textContent = serverError.message;
14487
+ label.appendChild(err);
14488
+ }
14489
+ form.appendChild(label);
14490
+ }
14491
+ const row = document.createElement("div");
14492
+ row.className = "int-row";
14493
+ const decline = document.createElement("button");
14494
+ decline.className = "int-btn";
14495
+ decline.type = "button";
14496
+ decline.textContent = "No thanks";
14497
+ decline.addEventListener("click", () => ctx.decline());
14498
+ const share = document.createElement("button");
14499
+ share.className = "int-btn primary";
14500
+ share.type = "submit";
14501
+ share.textContent = "Share details";
14502
+ row.append(decline, share);
14503
+ form.appendChild(row);
14504
+ form.addEventListener("submit", (ev) => {
14505
+ ev.preventDefault();
14506
+ if (!form.reportValidity()) return;
14507
+ const data = new FormData(form);
14508
+ const val = (k) => data.get(k)?.trim() || void 0;
14509
+ const rawPhone = val("phone");
14510
+ const phoneInput = form.querySelector("input[name=\"phone\"]");
14511
+ if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
14512
+ const field = phoneInput.closest(".pc-field");
14513
+ field?.classList.add("invalid");
14514
+ const hint = field?.querySelector(".pc-hint");
14515
+ if (hint) hint.textContent = "Enter a valid phone number";
14516
+ if (phoneInput.hasAttribute("required")) {
14517
+ phoneInput.focus();
14518
+ return;
14519
+ }
14520
+ }
14521
+ const phone = rawPhone ? phoneToE164(rawPhone) ?? rawPhone : void 0;
14522
+ ctx.submit({
14523
+ name: val("name"),
14524
+ email: val("email"),
14525
+ phone
14526
+ });
14527
+ });
14528
+ ctx.wirePhoneField(form);
14529
+ queueMicrotask(() => form.querySelector("input")?.focus());
14530
+ return form;
14531
+ }
14532
+ /** Kind → card. See the registry doc above. */
14533
+ const INTERACTION_CARDS = { identity_intake: {
14534
+ capability: "identity_form",
14535
+ title: "Share your details",
14536
+ icon: ICON.user,
14537
+ build: buildIdentityIntakeCard
14538
+ } };
14272
14539
  var SmoothAgentChatElement = class extends HTMLElement {
14273
14540
  static get observedAttributes() {
14274
14541
  return OBSERVED;
@@ -14285,6 +14552,7 @@ ${mode === "fullpage" ? `
14285
14552
  _defineProperty(this, "userInfoSatisfied", false);
14286
14553
  _defineProperty(this, "hasSent", false);
14287
14554
  _defineProperty(this, "examplePrompts", []);
14555
+ _defineProperty(this, "showSuggestedReplies", true);
14288
14556
  _defineProperty(this, "greeting", "");
14289
14557
  _defineProperty(this, "interrupt", null);
14290
14558
  _defineProperty(this, "interruptEl", null);
@@ -14298,6 +14566,7 @@ ${mode === "fullpage" ? `
14298
14566
  _defineProperty(this, "dotEl", null);
14299
14567
  _defineProperty(this, "inputEl", null);
14300
14568
  _defineProperty(this, "sendBtn", null);
14569
+ _defineProperty(this, "suggestionsEl", null);
14301
14570
  _defineProperty(this, "streamBubbleEl", null);
14302
14571
  _defineProperty(this, "streamMsgId", null);
14303
14572
  _defineProperty(this, "streamTarget", "");
@@ -14366,6 +14635,7 @@ ${mode === "fullpage" ? `
14366
14635
  startOpen: this.overrides.startOpen ?? this.hasAttribute("start-open"),
14367
14636
  hideBranding: this.overrides.hideBranding ?? this.hasAttribute("hide-branding"),
14368
14637
  examplePrompts: this.overrides.examplePrompts,
14638
+ showSuggestedReplies: this.overrides.showSuggestedReplies,
14369
14639
  requireName: this.overrides.requireName,
14370
14640
  requireEmail: this.overrides.requireEmail,
14371
14641
  requirePhone: this.overrides.requirePhone,
@@ -14397,10 +14667,12 @@ ${mode === "fullpage" ? `
14397
14667
  onInterrupt: (interrupt) => {
14398
14668
  this.interrupt = interrupt;
14399
14669
  this.renderInterrupt();
14670
+ this.renderSuggestions();
14400
14671
  },
14401
14672
  onIdentityRestore: (state) => {
14402
14673
  this.identityRestore = state;
14403
14674
  this.renderInterrupt();
14675
+ this.renderSuggestions();
14404
14676
  }
14405
14677
  });
14406
14678
  if (resolved.startOpen) this.open = true;
@@ -14428,6 +14700,7 @@ ${mode === "fullpage" ? `
14428
14700
  <button class="close" aria-label="Close chat">${ICON.close}</button>
14429
14701
  </div>`;
14430
14702
  this.examplePrompts = resolved.examplePrompts;
14703
+ this.showSuggestedReplies = resolved.showSuggestedReplies;
14431
14704
  this.greeting = resolved.greeting;
14432
14705
  const gating = needsUserInfo(resolved) && !this.userInfoSatisfied;
14433
14706
  this.gating = gating;
@@ -14456,6 +14729,7 @@ ${mode === "fullpage" ? `
14456
14729
  const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : "";
14457
14730
  const chatHtml = `
14458
14731
  <div class="messages"></div>
14732
+ <div class="reply-suggestions"></div>
14459
14733
  <div class="interrupt hidden"></div>
14460
14734
  <div class="composer-wrap">
14461
14735
  <div class="composer">
@@ -14486,6 +14760,7 @@ ${mode === "fullpage" ? `
14486
14760
  this.inputEl = container.querySelector("textarea");
14487
14761
  this.sendBtn = container.querySelector(".send");
14488
14762
  this.interruptEl = container.querySelector(".interrupt");
14763
+ this.suggestionsEl = container.querySelector(".reply-suggestions");
14489
14764
  this.launcherEl?.addEventListener("click", () => this.openChat());
14490
14765
  container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
14491
14766
  this.sendBtn?.addEventListener("click", () => this.submit());
@@ -14543,19 +14818,31 @@ ${mode === "fullpage" ? `
14543
14818
  head.className = "int-head";
14544
14819
  const ico = document.createElement("span");
14545
14820
  ico.className = "int-ico";
14546
- ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
14821
+ const card_meta = it.kind === "interaction" ? INTERACTION_CARDS[it.interactionKind] : void 0;
14822
+ if (it.kind === "interaction" && !card_meta) {
14823
+ this.controller?.declineInteraction();
14824
+ el.classList.add("hidden");
14825
+ return;
14826
+ }
14827
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : it.kind === "interaction" ? card_meta?.icon ?? ICON.user : ICON.shield;
14547
14828
  const title = document.createElement("span");
14548
14829
  title.className = "int-title";
14549
- title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
14830
+ title.textContent = it.kind === "otp" ? "Verification required" : it.kind === "interaction" ? card_meta?.title ?? "One more thing" : "Confirm this action";
14550
14831
  head.append(ico, title);
14551
14832
  card.appendChild(head);
14552
- if (it.actionDescription) {
14833
+ if (it.kind !== "interaction" && it.actionDescription) {
14553
14834
  const desc = document.createElement("div");
14554
14835
  desc.className = "int-desc";
14555
14836
  desc.textContent = it.actionDescription;
14556
14837
  card.appendChild(desc);
14557
14838
  }
14558
- if (it.kind === "otp") {
14839
+ if (it.kind === "interaction") card.appendChild(card_meta.build(it, {
14840
+ submit: (values) => this.controller?.submitInteraction(values),
14841
+ decline: () => this.controller?.declineInteraction(),
14842
+ prefill: this.controller?.getStore().getState().identity ?? {},
14843
+ wirePhoneField: (form) => this.wirePhoneField(form)
14844
+ }));
14845
+ else if (it.kind === "otp") {
14559
14846
  if (it.sent?.maskedDestination) {
14560
14847
  const sent = document.createElement("div");
14561
14848
  sent.className = "int-sent";
@@ -14940,6 +15227,36 @@ ${mode === "fullpage" ? `
14940
15227
  if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
14941
15228
  }
14942
15229
  this.scrollToBottom(true);
15230
+ this.renderSuggestions();
15231
+ }
15232
+ /**
15233
+ * Render (or clear) the mid-conversation suggested-reply chips under the
15234
+ * composer. Shown ONLY when: the feature is enabled, no interrupt / restore
15235
+ * overlay is active (those reuse the slot above the composer), and the LATEST
15236
+ * message is a finalized assistant turn that carried suggestions. A new turn
15237
+ * (agent or the visitor typing) appends messages, so the latest is no longer
15238
+ * that assistant message and the chips clear on the next render. Chips reuse
15239
+ * the empty-state `.prompts`/`.chip` styling and send via the same path.
15240
+ */
15241
+ renderSuggestions() {
15242
+ const el = this.suggestionsEl;
15243
+ if (!el) return;
15244
+ el.replaceChildren();
15245
+ if (!this.showSuggestedReplies) return;
15246
+ if (this.interrupt || this.identityRestore.phase !== "idle") return;
15247
+ const last = this.messages[this.messages.length - 1];
15248
+ if (!last || last.role !== "assistant" || last.streaming || !last.suggestions || last.suggestions.length === 0) return;
15249
+ const chips = document.createElement("div");
15250
+ chips.className = "prompts";
15251
+ for (const suggestion of last.suggestions) {
15252
+ const chip = document.createElement("button");
15253
+ chip.type = "button";
15254
+ chip.className = "chip";
15255
+ chip.textContent = suggestion;
15256
+ chip.addEventListener("click", () => this.submitPrompt(suggestion));
15257
+ chips.appendChild(chip);
15258
+ }
15259
+ el.appendChild(chips);
14943
15260
  }
14944
15261
  /** True when the host/user prefers reduced motion (snap, no typewriter). */
14945
15262
  prefersReducedMotion() {