@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 +3 -0
- package/dist/chat-widget.global.js +326 -9
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +275 -223
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +307 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +7 -0
- package/src/conversation.ts +138 -2
- package/src/element.ts +222 -5
- package/src/styles.ts +15 -2
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-
|
|
2011
|
-
.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; }
|
|
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
|
-
|
|
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 === "
|
|
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() {
|