@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/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.
|
|
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.
|
|
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.
|
|
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-
|
|
14131
|
-
.int-
|
|
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
|
-
|
|
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 === "
|
|
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() {
|