@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/chat-widget.global.js +268 -9
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +262 -236
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +249 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/conversation.ts +112 -2
- package/src/element.ts +178 -5
- package/src/styles.ts +11 -2
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-
|
|
2031
|
-
.int-
|
|
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
|
-
|
|
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 === "
|
|
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";
|