@smooai/chat-widget 0.11.0 → 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
@@ -735,6 +735,13 @@ function httpBaseFromWsEndpoint(endpoint) {
735
735
  return null;
736
736
  }
737
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"];
738
745
  /** Pull the final assistant text out of an `eventual_response` data payload. */
739
746
  function extractFinalText(response) {
740
747
  if (!response || typeof response !== "object") return null;
@@ -812,6 +819,7 @@ var ConversationController = class {
812
819
  _defineProperty(this, "seq", 0);
813
820
  _defineProperty(this, "activeRequestId", null);
814
821
  _defineProperty(this, "interrupt", null);
822
+ _defineProperty(this, "pendingInteractionValues", null);
815
823
  _defineProperty(this, "identityRestore", { phase: "idle" });
816
824
  _defineProperty(this, "resumeAttempted", false);
817
825
  _defineProperty(this, "httpBase", void 0);
@@ -890,6 +898,40 @@ var ConversationController = class {
890
898
  });
891
899
  this.setInterrupt(null);
892
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
+ }
893
935
  nextId(prefix) {
894
936
  this.seq += 1;
895
937
  return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
@@ -1054,6 +1096,7 @@ var ConversationController = class {
1054
1096
  userName: state.identity.name,
1055
1097
  userEmail: state.identity.email,
1056
1098
  browserFingerprint: this.fingerprint(),
1099
+ supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
1057
1100
  ...metadata ? { metadata } : {}
1058
1101
  });
1059
1102
  this.sessionId = session.sessionId;
@@ -1196,6 +1239,55 @@ var ConversationController = class {
1196
1239
  actionDescription: str(inner.actionDescription)
1197
1240
  });
1198
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;
1199
1291
  default: break;
1200
1292
  }
1201
1293
  }
@@ -2008,6 +2100,9 @@ ${mode === "fullpage" ? `
2008
2100
  border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
2009
2101
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
2010
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; }
2011
2106
  .int-btn {
2012
2107
  border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
2013
2108
  background: var(--sac-surface-2);
@@ -2027,8 +2122,14 @@ ${mode === "fullpage" ? `
2027
2122
  color: var(--sac-primary-text);
2028
2123
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
2029
2124
  }
2030
- .int-row .int-btn { flex: 1; }
2031
- .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; }
2032
2133
  .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
2033
2134
  .int-card { position: relative; }
2034
2135
  .int-close {
@@ -2166,9 +2267,139 @@ const ICON = {
2166
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>`,
2167
2268
  /** OTP interrupt — a padlock. */
2168
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>`,
2169
2272
  /** Tool-confirmation interrupt — a shield. */
2170
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>`
2171
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
+ } };
2172
2403
  var SmoothAgentChatElement = class extends HTMLElement {
2173
2404
  static get observedAttributes() {
2174
2405
  return OBSERVED;
@@ -2451,19 +2682,31 @@ var SmoothAgentChatElement = class extends HTMLElement {
2451
2682
  head.className = "int-head";
2452
2683
  const ico = document.createElement("span");
2453
2684
  ico.className = "int-ico";
2454
- 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;
2455
2692
  const title = document.createElement("span");
2456
2693
  title.className = "int-title";
2457
- 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";
2458
2695
  head.append(ico, title);
2459
2696
  card.appendChild(head);
2460
- if (it.actionDescription) {
2697
+ if (it.kind !== "interaction" && it.actionDescription) {
2461
2698
  const desc = document.createElement("div");
2462
2699
  desc.className = "int-desc";
2463
2700
  desc.textContent = it.actionDescription;
2464
2701
  card.appendChild(desc);
2465
2702
  }
2466
- 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") {
2467
2710
  if (it.sent?.maskedDestination) {
2468
2711
  const sent = document.createElement("div");
2469
2712
  sent.className = "int-sent";