@smooai/chat-widget 0.11.0 → 0.13.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 +1 -0
- package/dist/chat-widget.global.js +473 -21
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +333 -236
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +454 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +11 -0
- package/src/conversation.ts +205 -3
- package/src/element.ts +315 -18
- package/src/index.ts +2 -0
- package/src/styles.ts +53 -2
package/dist/index.js
CHANGED
|
@@ -342,6 +342,7 @@ function resolveConfig(config) {
|
|
|
342
342
|
collectConsent: config.collectConsent ?? true,
|
|
343
343
|
allowChatRestore: config.allowChatRestore ?? true,
|
|
344
344
|
allowAnonymous: config.allowAnonymous ?? false,
|
|
345
|
+
showToolActivity: config.showToolActivity ?? false,
|
|
345
346
|
theme: {
|
|
346
347
|
text: theme.text ?? "#f8fafc",
|
|
347
348
|
background: theme.background ?? "#040d30",
|
|
@@ -735,6 +736,13 @@ function httpBaseFromWsEndpoint(endpoint) {
|
|
|
735
736
|
return null;
|
|
736
737
|
}
|
|
737
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* The Rich-Interaction render capabilities this widget declares at session
|
|
741
|
+
* create (`supports`). Must stay aligned with the card registry in
|
|
742
|
+
* `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
|
|
743
|
+
* capability; a test asserts the two match.
|
|
744
|
+
*/
|
|
745
|
+
const SUPPORTED_INTERACTION_CAPABILITIES = ["identity_form"];
|
|
738
746
|
/** Pull the final assistant text out of an `eventual_response` data payload. */
|
|
739
747
|
function extractFinalText(response) {
|
|
740
748
|
if (!response || typeof response !== "object") return null;
|
|
@@ -800,6 +808,59 @@ function wireMessageToChat(m, idx) {
|
|
|
800
808
|
streaming: false
|
|
801
809
|
};
|
|
802
810
|
}
|
|
811
|
+
let toolSeq = 0;
|
|
812
|
+
const nextToolId = () => `tool-${++toolSeq}`;
|
|
813
|
+
/** Grow the trailing text block, or open a new one if the last block was a tool. */
|
|
814
|
+
function growTextBlock(blocks, text) {
|
|
815
|
+
if (!text) return;
|
|
816
|
+
const last = blocks[blocks.length - 1];
|
|
817
|
+
if (last && last.kind === "text") last.text += text;
|
|
818
|
+
else blocks.push({
|
|
819
|
+
kind: "text",
|
|
820
|
+
text
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Fold a `stream_chunk` node-state into the ordered block list, returning `true`
|
|
825
|
+
* when the chunk carried tool activity.
|
|
826
|
+
*
|
|
827
|
+
* Tool activity rides `state.rawResponse.toolCall` / `state.rawResponse.toolResult`
|
|
828
|
+
* — **NOT** `state.toolResult`. Reading the wrong path leaves every chip stuck on
|
|
829
|
+
* "running…" forever (the exact bug the daemon SPA hit and this mirror avoids).
|
|
830
|
+
*/
|
|
831
|
+
function applyToolChunk(blocks, state) {
|
|
832
|
+
const raw = state?.rawResponse;
|
|
833
|
+
if (!raw || typeof raw !== "object") return false;
|
|
834
|
+
const call = raw.toolCall;
|
|
835
|
+
const res = raw.toolResult;
|
|
836
|
+
if (call) {
|
|
837
|
+
const args = typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {});
|
|
838
|
+
blocks.push({
|
|
839
|
+
kind: "tool",
|
|
840
|
+
tool: {
|
|
841
|
+
id: nextToolId(),
|
|
842
|
+
name: call.name ?? "",
|
|
843
|
+
args,
|
|
844
|
+
done: false
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
if (res) {
|
|
850
|
+
const result = typeof res.result === "string" ? res.result : JSON.stringify(res.result ?? "");
|
|
851
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
852
|
+
const b = blocks[i];
|
|
853
|
+
if (b && b.kind === "tool" && b.tool.name === (res.name ?? "") && !b.tool.done) {
|
|
854
|
+
b.tool.done = true;
|
|
855
|
+
b.tool.isError = !!res.isError;
|
|
856
|
+
b.tool.result = result;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
803
864
|
var ConversationController = class {
|
|
804
865
|
constructor(config, events, store) {
|
|
805
866
|
_defineProperty(this, "config", void 0);
|
|
@@ -812,6 +873,7 @@ var ConversationController = class {
|
|
|
812
873
|
_defineProperty(this, "seq", 0);
|
|
813
874
|
_defineProperty(this, "activeRequestId", null);
|
|
814
875
|
_defineProperty(this, "interrupt", null);
|
|
876
|
+
_defineProperty(this, "pendingInteractionValues", null);
|
|
815
877
|
_defineProperty(this, "identityRestore", { phase: "idle" });
|
|
816
878
|
_defineProperty(this, "resumeAttempted", false);
|
|
817
879
|
_defineProperty(this, "httpBase", void 0);
|
|
@@ -890,6 +952,40 @@ var ConversationController = class {
|
|
|
890
952
|
});
|
|
891
953
|
this.setInterrupt(null);
|
|
892
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Submit a Rich Interaction card to resume the parked turn. The server
|
|
957
|
+
* routes to the kind's validator: invalid values come back as an
|
|
958
|
+
* `interaction_invalid` event (the card re-renders with per-field errors —
|
|
959
|
+
* the turn stays parked); a valid submit is acked and the turn resumes.
|
|
960
|
+
* No-op if not awaiting an interaction.
|
|
961
|
+
*/
|
|
962
|
+
submitInteraction(values) {
|
|
963
|
+
if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
|
|
964
|
+
this.pendingInteractionValues = {
|
|
965
|
+
kind: this.interrupt.interactionKind,
|
|
966
|
+
values
|
|
967
|
+
};
|
|
968
|
+
this.client.submitInteraction({
|
|
969
|
+
sessionId: this.sessionId,
|
|
970
|
+
requestId: this.activeRequestId,
|
|
971
|
+
interactionId: this.interrupt.interactionId,
|
|
972
|
+
kind: this.interrupt.interactionKind,
|
|
973
|
+
values
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/** Decline the pending Rich Interaction; the agent continues without it. */
|
|
977
|
+
declineInteraction() {
|
|
978
|
+
if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
|
|
979
|
+
this.client.submitInteraction({
|
|
980
|
+
sessionId: this.sessionId,
|
|
981
|
+
requestId: this.activeRequestId,
|
|
982
|
+
interactionId: this.interrupt.interactionId,
|
|
983
|
+
kind: this.interrupt.interactionKind,
|
|
984
|
+
declined: true
|
|
985
|
+
});
|
|
986
|
+
this.pendingInteractionValues = null;
|
|
987
|
+
this.setInterrupt(null);
|
|
988
|
+
}
|
|
893
989
|
nextId(prefix) {
|
|
894
990
|
this.seq += 1;
|
|
895
991
|
return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
|
|
@@ -1054,6 +1150,7 @@ var ConversationController = class {
|
|
|
1054
1150
|
userName: state.identity.name,
|
|
1055
1151
|
userEmail: state.identity.email,
|
|
1056
1152
|
browserFingerprint: this.fingerprint(),
|
|
1153
|
+
supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
|
|
1057
1154
|
...metadata ? { metadata } : {}
|
|
1058
1155
|
});
|
|
1059
1156
|
this.sessionId = session.sessionId;
|
|
@@ -1110,11 +1207,13 @@ var ConversationController = class {
|
|
|
1110
1207
|
text: trimmed,
|
|
1111
1208
|
streaming: false
|
|
1112
1209
|
});
|
|
1210
|
+
const showTools = this.config.showToolActivity === true;
|
|
1113
1211
|
const assistant = {
|
|
1114
1212
|
id: this.nextId("a"),
|
|
1115
1213
|
role: "assistant",
|
|
1116
1214
|
text: "",
|
|
1117
|
-
streaming: true
|
|
1215
|
+
streaming: true,
|
|
1216
|
+
blocks: showTools ? [] : void 0
|
|
1118
1217
|
};
|
|
1119
1218
|
this.messages.push(assistant);
|
|
1120
1219
|
this.emitMessages();
|
|
@@ -1129,8 +1228,11 @@ var ConversationController = class {
|
|
|
1129
1228
|
const token = event.token ?? event.data?.token ?? "";
|
|
1130
1229
|
if (token) {
|
|
1131
1230
|
assistant.text += token;
|
|
1231
|
+
if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
|
|
1132
1232
|
this.emitMessages();
|
|
1133
1233
|
}
|
|
1234
|
+
} else if (showTools && event.type === "stream_chunk") {
|
|
1235
|
+
if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) this.emitMessages();
|
|
1134
1236
|
} else this.handleTurnEvent(event);
|
|
1135
1237
|
const inner = (await turn).data?.data;
|
|
1136
1238
|
const finalText = extractFinalText(inner?.response);
|
|
@@ -1140,6 +1242,7 @@ var ConversationController = class {
|
|
|
1140
1242
|
if (citations.length > 0) assistant.citations = citations;
|
|
1141
1243
|
const suggestions = extractSuggestions(inner?.response);
|
|
1142
1244
|
if (suggestions.length > 0) assistant.suggestions = suggestions;
|
|
1245
|
+
if (assistant.blocks && !assistant.blocks.some((b) => b.kind === "tool")) assistant.blocks = void 0;
|
|
1143
1246
|
assistant.streaming = false;
|
|
1144
1247
|
this.emitMessages();
|
|
1145
1248
|
} catch (err) {
|
|
@@ -1196,6 +1299,55 @@ var ConversationController = class {
|
|
|
1196
1299
|
actionDescription: str(inner.actionDescription)
|
|
1197
1300
|
});
|
|
1198
1301
|
break;
|
|
1302
|
+
case "interaction_required": {
|
|
1303
|
+
const interactionId = str(inner.interactionId);
|
|
1304
|
+
const kind = str(inner.kind);
|
|
1305
|
+
const spec = inner.spec && typeof inner.spec === "object" ? inner.spec : {};
|
|
1306
|
+
if (!interactionId || !kind) break;
|
|
1307
|
+
this.pendingInteractionValues = null;
|
|
1308
|
+
this.setInterrupt({
|
|
1309
|
+
kind: "interaction",
|
|
1310
|
+
interactionId,
|
|
1311
|
+
interactionKind: kind,
|
|
1312
|
+
spec,
|
|
1313
|
+
reason: str(inner.reason)
|
|
1314
|
+
});
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
case "interaction_invalid":
|
|
1318
|
+
if (this.interrupt?.kind === "interaction" && this.interrupt.interactionId === str(inner.interactionId)) {
|
|
1319
|
+
const errors = [];
|
|
1320
|
+
if (Array.isArray(inner.errors)) for (const e of inner.errors) {
|
|
1321
|
+
if (!e || typeof e !== "object") continue;
|
|
1322
|
+
const o = e;
|
|
1323
|
+
const field = str(o.field);
|
|
1324
|
+
if (field) errors.push({
|
|
1325
|
+
field,
|
|
1326
|
+
message: str(o.message) ?? "Invalid value"
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
this.pendingInteractionValues = null;
|
|
1330
|
+
this.setInterrupt({
|
|
1331
|
+
...this.interrupt,
|
|
1332
|
+
errors
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
break;
|
|
1336
|
+
case "immediate_response":
|
|
1337
|
+
if (this.interrupt?.kind === "interaction") {
|
|
1338
|
+
const pending = this.pendingInteractionValues;
|
|
1339
|
+
if (pending && pending.kind === "identity_intake") {
|
|
1340
|
+
const v = pending.values;
|
|
1341
|
+
this.store.getState().mergeIdentity({
|
|
1342
|
+
name: v.name,
|
|
1343
|
+
email: v.email,
|
|
1344
|
+
phone: v.phone
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
this.pendingInteractionValues = null;
|
|
1348
|
+
this.setInterrupt(null);
|
|
1349
|
+
}
|
|
1350
|
+
break;
|
|
1199
1351
|
default: break;
|
|
1200
1352
|
}
|
|
1201
1353
|
}
|
|
@@ -1402,6 +1554,7 @@ function buildStyles(theme, mode = "popover") {
|
|
|
1402
1554
|
--sac-bg: ${theme.background};
|
|
1403
1555
|
--sac-primary: ${theme.primary};
|
|
1404
1556
|
--sac-primary-text: ${theme.primaryText};
|
|
1557
|
+
--sac-secondary: ${theme.secondary};
|
|
1405
1558
|
--sac-assistant-bubble: ${theme.assistantBubble};
|
|
1406
1559
|
--sac-assistant-bubble-text: ${theme.assistantBubbleText};
|
|
1407
1560
|
--sac-user-bubble: ${theme.userBubble};
|
|
@@ -1714,6 +1867,47 @@ ${mode === "fullpage" ? `
|
|
|
1714
1867
|
}
|
|
1715
1868
|
@keyframes sac-blink { to { opacity: 0 } }
|
|
1716
1869
|
|
|
1870
|
+
/* Interleaved tool-activity strip (gated by showToolActivity): prose bubbles
|
|
1871
|
+
and inline tool chips stacked in the order the model produced them. */
|
|
1872
|
+
.blocks { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
|
|
1873
|
+
.blocks .bubble { align-self: flex-start; }
|
|
1874
|
+
.toolchip {
|
|
1875
|
+
display: inline-flex;
|
|
1876
|
+
align-items: center;
|
|
1877
|
+
gap: 6px;
|
|
1878
|
+
max-width: 100%;
|
|
1879
|
+
padding: 5px 10px;
|
|
1880
|
+
border-radius: 99px;
|
|
1881
|
+
font-size: 12px;
|
|
1882
|
+
line-height: 1.3;
|
|
1883
|
+
color: color-mix(in srgb, var(--sac-text) 78%, transparent);
|
|
1884
|
+
background: color-mix(in srgb, var(--sac-text) 6%, transparent);
|
|
1885
|
+
border: 1px solid color-mix(in srgb, var(--sac-text) 12%, transparent);
|
|
1886
|
+
animation: sac-msg-in .3s var(--sac-ease) both;
|
|
1887
|
+
}
|
|
1888
|
+
.toolchip .ti { display: inline-flex; flex: none; opacity: .8; }
|
|
1889
|
+
.toolchip .ti svg { width: 13px; height: 13px; }
|
|
1890
|
+
.toolchip .tn { font-weight: 600; letter-spacing: .01em; }
|
|
1891
|
+
.toolchip .ts { opacity: .7; }
|
|
1892
|
+
.toolchip .ta {
|
|
1893
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1894
|
+
font-size: 11px;
|
|
1895
|
+
opacity: .6;
|
|
1896
|
+
overflow: hidden;
|
|
1897
|
+
text-overflow: ellipsis;
|
|
1898
|
+
white-space: nowrap;
|
|
1899
|
+
min-width: 0;
|
|
1900
|
+
}
|
|
1901
|
+
.toolchip.running { border-color: color-mix(in srgb, var(--sac-primary) 45%, transparent); }
|
|
1902
|
+
.toolchip.running .ts { color: var(--sac-primary); opacity: 1; animation: sac-typing 1.3s ease-in-out infinite; }
|
|
1903
|
+
.toolchip.done .ts::before { content: '✓ '; }
|
|
1904
|
+
.toolchip.error {
|
|
1905
|
+
color: var(--sac-secondary);
|
|
1906
|
+
border-color: color-mix(in srgb, var(--sac-secondary) 50%, transparent);
|
|
1907
|
+
background: color-mix(in srgb, var(--sac-secondary) 10%, transparent);
|
|
1908
|
+
}
|
|
1909
|
+
.toolchip.error .ts::before { content: '! '; }
|
|
1910
|
+
|
|
1717
1911
|
/* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
|
|
1718
1912
|
/* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
|
|
1719
1913
|
keep them legible inside the tight Aurora-Glass bubble + citation card. */
|
|
@@ -2008,6 +2202,9 @@ ${mode === "fullpage" ? `
|
|
|
2008
2202
|
border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
|
|
2009
2203
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
|
|
2010
2204
|
}
|
|
2205
|
+
.int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
2206
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
2207
|
+
.int-form .int-error { margin-top: 2px; }
|
|
2011
2208
|
.int-btn {
|
|
2012
2209
|
border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
|
|
2013
2210
|
background: var(--sac-surface-2);
|
|
@@ -2027,8 +2224,14 @@ ${mode === "fullpage" ? `
|
|
|
2027
2224
|
color: var(--sac-primary-text);
|
|
2028
2225
|
box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
|
|
2029
2226
|
}
|
|
2030
|
-
.int-row .int-
|
|
2031
|
-
.int-
|
|
2227
|
+
.int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
2228
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
2229
|
+
.int-form .int-error { margin-top: 2px; }
|
|
2230
|
+
.int-btn { flex: 1; }
|
|
2231
|
+
.int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
|
|
2232
|
+
.int-form .pc-field input { width: 100%; box-sizing: border-box; }
|
|
2233
|
+
.int-form .int-error { margin-top: 2px; }
|
|
2234
|
+
.int-btn { flex: 0 0 auto; }
|
|
2032
2235
|
.int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
|
|
2033
2236
|
.int-card { position: relative; }
|
|
2034
2237
|
.int-close {
|
|
@@ -2147,7 +2350,8 @@ const OBSERVED = [
|
|
|
2147
2350
|
"greeting",
|
|
2148
2351
|
"start-open",
|
|
2149
2352
|
"mode",
|
|
2150
|
-
"hide-branding"
|
|
2353
|
+
"hide-branding",
|
|
2354
|
+
"show-tool-activity"
|
|
2151
2355
|
];
|
|
2152
2356
|
/**
|
|
2153
2357
|
* Inline SVG icons (static, trusted strings — never interpolated with user data).
|
|
@@ -2166,9 +2370,141 @@ const ICON = {
|
|
|
2166
2370
|
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
2371
|
/** OTP interrupt — a padlock. */
|
|
2168
2372
|
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>`,
|
|
2373
|
+
/** Identity-intake interrupt — a person. */
|
|
2374
|
+
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
2375
|
/** Tool-confirmation interrupt — a shield. */
|
|
2170
|
-
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
|
|
2376
|
+
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>`,
|
|
2377
|
+
/** Tool-activity chip — a wrench. */
|
|
2378
|
+
tool: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.7 6.3a3.5 3.5 0 0 0-4.6 4.3l-5 5a1.6 1.6 0 0 0 2.3 2.3l5-5a3.5 3.5 0 0 0 4.3-4.6l-2 2-1.7-.3-.3-1.7 2-2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`
|
|
2171
2379
|
};
|
|
2380
|
+
/**
|
|
2381
|
+
* The identity_intake card: the fields the agent requested (pre-chat form field
|
|
2382
|
+
* pattern — same classes, same phone formatting), per-field server errors, a
|
|
2383
|
+
* submit and a decline affordance. Server-supplied text (reason, labels, error
|
|
2384
|
+
* messages) is set via `textContent` — never innerHTML.
|
|
2385
|
+
*/
|
|
2386
|
+
function buildIdentityIntakeCard(it, ctx) {
|
|
2387
|
+
const form = document.createElement("form");
|
|
2388
|
+
form.className = "int-form";
|
|
2389
|
+
form.noValidate = true;
|
|
2390
|
+
if (it.reason) {
|
|
2391
|
+
const desc = document.createElement("div");
|
|
2392
|
+
desc.className = "int-desc";
|
|
2393
|
+
desc.textContent = it.reason;
|
|
2394
|
+
form.appendChild(desc);
|
|
2395
|
+
}
|
|
2396
|
+
const DEFAULTS = {
|
|
2397
|
+
name: {
|
|
2398
|
+
label: "Name",
|
|
2399
|
+
type: "text",
|
|
2400
|
+
autocomplete: "name"
|
|
2401
|
+
},
|
|
2402
|
+
email: {
|
|
2403
|
+
label: "Email",
|
|
2404
|
+
type: "email",
|
|
2405
|
+
autocomplete: "email"
|
|
2406
|
+
},
|
|
2407
|
+
phone: {
|
|
2408
|
+
label: "Phone",
|
|
2409
|
+
type: "tel",
|
|
2410
|
+
autocomplete: "tel"
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
|
|
2414
|
+
const fields = [];
|
|
2415
|
+
for (const f of rawFields) {
|
|
2416
|
+
if (!f || typeof f !== "object") continue;
|
|
2417
|
+
const o = f;
|
|
2418
|
+
const key = typeof o.key === "string" ? o.key : "";
|
|
2419
|
+
if (key !== "name" && key !== "email" && key !== "phone") continue;
|
|
2420
|
+
fields.push({
|
|
2421
|
+
key,
|
|
2422
|
+
required: o.required === true,
|
|
2423
|
+
label: typeof o.label === "string" ? o.label : void 0
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
if (fields.length === 0) fields.push({
|
|
2427
|
+
key: "email",
|
|
2428
|
+
required: true
|
|
2429
|
+
});
|
|
2430
|
+
for (const f of fields) {
|
|
2431
|
+
const d = DEFAULTS[f.key];
|
|
2432
|
+
const label = document.createElement("label");
|
|
2433
|
+
label.className = "pc-field";
|
|
2434
|
+
const caption = document.createElement("span");
|
|
2435
|
+
caption.textContent = f.label ?? d.label;
|
|
2436
|
+
const input = document.createElement("input");
|
|
2437
|
+
input.name = f.key;
|
|
2438
|
+
input.type = d.type;
|
|
2439
|
+
input.setAttribute("autocomplete", d.autocomplete);
|
|
2440
|
+
input.required = f.required;
|
|
2441
|
+
const prefill = ctx.prefill[f.key];
|
|
2442
|
+
if (prefill) input.value = prefill;
|
|
2443
|
+
label.append(caption, input);
|
|
2444
|
+
if (f.key === "phone") {
|
|
2445
|
+
const hint = document.createElement("span");
|
|
2446
|
+
hint.className = "pc-hint";
|
|
2447
|
+
hint.setAttribute("aria-live", "polite");
|
|
2448
|
+
label.appendChild(hint);
|
|
2449
|
+
}
|
|
2450
|
+
const serverError = it.errors?.find((e) => e.field === f.key);
|
|
2451
|
+
if (serverError) {
|
|
2452
|
+
label.classList.add("invalid");
|
|
2453
|
+
const err = document.createElement("span");
|
|
2454
|
+
err.className = "int-error";
|
|
2455
|
+
err.textContent = serverError.message;
|
|
2456
|
+
label.appendChild(err);
|
|
2457
|
+
}
|
|
2458
|
+
form.appendChild(label);
|
|
2459
|
+
}
|
|
2460
|
+
const row = document.createElement("div");
|
|
2461
|
+
row.className = "int-row";
|
|
2462
|
+
const decline = document.createElement("button");
|
|
2463
|
+
decline.className = "int-btn";
|
|
2464
|
+
decline.type = "button";
|
|
2465
|
+
decline.textContent = "No thanks";
|
|
2466
|
+
decline.addEventListener("click", () => ctx.decline());
|
|
2467
|
+
const share = document.createElement("button");
|
|
2468
|
+
share.className = "int-btn primary";
|
|
2469
|
+
share.type = "submit";
|
|
2470
|
+
share.textContent = "Share details";
|
|
2471
|
+
row.append(decline, share);
|
|
2472
|
+
form.appendChild(row);
|
|
2473
|
+
form.addEventListener("submit", (ev) => {
|
|
2474
|
+
ev.preventDefault();
|
|
2475
|
+
if (!form.reportValidity()) return;
|
|
2476
|
+
const data = new FormData(form);
|
|
2477
|
+
const val = (k) => data.get(k)?.trim() || void 0;
|
|
2478
|
+
const rawPhone = val("phone");
|
|
2479
|
+
const phoneInput = form.querySelector("input[name=\"phone\"]");
|
|
2480
|
+
if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
|
|
2481
|
+
const field = phoneInput.closest(".pc-field");
|
|
2482
|
+
field?.classList.add("invalid");
|
|
2483
|
+
const hint = field?.querySelector(".pc-hint");
|
|
2484
|
+
if (hint) hint.textContent = "Enter a valid phone number";
|
|
2485
|
+
if (phoneInput.hasAttribute("required")) {
|
|
2486
|
+
phoneInput.focus();
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const phone = rawPhone ? phoneToE164(rawPhone) ?? rawPhone : void 0;
|
|
2491
|
+
ctx.submit({
|
|
2492
|
+
name: val("name"),
|
|
2493
|
+
email: val("email"),
|
|
2494
|
+
phone
|
|
2495
|
+
});
|
|
2496
|
+
});
|
|
2497
|
+
ctx.wirePhoneField(form);
|
|
2498
|
+
queueMicrotask(() => form.querySelector("input")?.focus());
|
|
2499
|
+
return form;
|
|
2500
|
+
}
|
|
2501
|
+
/** Kind → card. See the registry doc above. */
|
|
2502
|
+
const INTERACTION_CARDS = { identity_intake: {
|
|
2503
|
+
capability: "identity_form",
|
|
2504
|
+
title: "Share your details",
|
|
2505
|
+
icon: ICON.user,
|
|
2506
|
+
build: buildIdentityIntakeCard
|
|
2507
|
+
} };
|
|
2172
2508
|
var SmoothAgentChatElement = class extends HTMLElement {
|
|
2173
2509
|
static get observedAttributes() {
|
|
2174
2510
|
return OBSERVED;
|
|
@@ -2205,6 +2541,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2205
2541
|
_defineProperty(this, "streamTarget", "");
|
|
2206
2542
|
_defineProperty(this, "displayedLength", 0);
|
|
2207
2543
|
_defineProperty(this, "rafId", 0);
|
|
2544
|
+
_defineProperty(this, "prevBlockSig", "");
|
|
2208
2545
|
this.root = this.attachShadow({ mode: "open" });
|
|
2209
2546
|
}
|
|
2210
2547
|
connectedCallback() {
|
|
@@ -2276,6 +2613,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2276
2613
|
collectConsent: this.overrides.collectConsent,
|
|
2277
2614
|
allowChatRestore: this.overrides.allowChatRestore,
|
|
2278
2615
|
allowAnonymous: this.overrides.allowAnonymous,
|
|
2616
|
+
showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
|
|
2279
2617
|
theme
|
|
2280
2618
|
};
|
|
2281
2619
|
}
|
|
@@ -2451,19 +2789,31 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2451
2789
|
head.className = "int-head";
|
|
2452
2790
|
const ico = document.createElement("span");
|
|
2453
2791
|
ico.className = "int-ico";
|
|
2454
|
-
|
|
2792
|
+
const card_meta = it.kind === "interaction" ? INTERACTION_CARDS[it.interactionKind] : void 0;
|
|
2793
|
+
if (it.kind === "interaction" && !card_meta) {
|
|
2794
|
+
this.controller?.declineInteraction();
|
|
2795
|
+
el.classList.add("hidden");
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
ico.innerHTML = it.kind === "otp" ? ICON.lock : it.kind === "interaction" ? card_meta?.icon ?? ICON.user : ICON.shield;
|
|
2455
2799
|
const title = document.createElement("span");
|
|
2456
2800
|
title.className = "int-title";
|
|
2457
|
-
title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
|
|
2801
|
+
title.textContent = it.kind === "otp" ? "Verification required" : it.kind === "interaction" ? card_meta?.title ?? "One more thing" : "Confirm this action";
|
|
2458
2802
|
head.append(ico, title);
|
|
2459
2803
|
card.appendChild(head);
|
|
2460
|
-
if (it.actionDescription) {
|
|
2804
|
+
if (it.kind !== "interaction" && it.actionDescription) {
|
|
2461
2805
|
const desc = document.createElement("div");
|
|
2462
2806
|
desc.className = "int-desc";
|
|
2463
2807
|
desc.textContent = it.actionDescription;
|
|
2464
2808
|
card.appendChild(desc);
|
|
2465
2809
|
}
|
|
2466
|
-
if (it.kind === "
|
|
2810
|
+
if (it.kind === "interaction") card.appendChild(card_meta.build(it, {
|
|
2811
|
+
submit: (values) => this.controller?.submitInteraction(values),
|
|
2812
|
+
decline: () => this.controller?.declineInteraction(),
|
|
2813
|
+
prefill: this.controller?.getStore().getState().identity ?? {},
|
|
2814
|
+
wirePhoneField: (form) => this.wirePhoneField(form)
|
|
2815
|
+
}));
|
|
2816
|
+
else if (it.kind === "otp") {
|
|
2467
2817
|
if (it.sent?.maskedDestination) {
|
|
2468
2818
|
const sent = document.createElement("div");
|
|
2469
2819
|
sent.className = "int-sent";
|
|
@@ -2803,15 +3153,38 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2803
3153
|
const prev = this.messages;
|
|
2804
3154
|
const last = messages[messages.length - 1];
|
|
2805
3155
|
const prevLast = prev[prev.length - 1];
|
|
2806
|
-
const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text;
|
|
3156
|
+
const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text || this.blockSig(last) !== this.prevBlockSig;
|
|
2807
3157
|
this.messages = messages;
|
|
2808
|
-
|
|
2809
|
-
|
|
3158
|
+
this.prevBlockSig = this.blockSig(last);
|
|
3159
|
+
if (!structural && last && last.streaming && this.tailKey(last) === this.streamMsgId) {
|
|
3160
|
+
this.streamTarget = this.tailText(last);
|
|
2810
3161
|
this.ensureRevealLoop();
|
|
2811
3162
|
return;
|
|
2812
3163
|
}
|
|
2813
3164
|
this.renderMessages();
|
|
2814
3165
|
}
|
|
3166
|
+
/** True when an assistant message's turn invoked at least one tool. */
|
|
3167
|
+
hasToolBlocks(m) {
|
|
3168
|
+
return !!m?.blocks?.some((b) => b.kind === "tool");
|
|
3169
|
+
}
|
|
3170
|
+
/** Signature that changes only when a tool chip is added or resolved (text growth is 'x'). */
|
|
3171
|
+
blockSig(m) {
|
|
3172
|
+
if (!m?.blocks) return "";
|
|
3173
|
+
return m.blocks.map((b) => b.kind === "tool" ? `t:${b.tool.id}:${b.tool.done ? 1 : 0}` : "x").join("|");
|
|
3174
|
+
}
|
|
3175
|
+
/** The live (last) text block for a tool turn, else the whole message text. */
|
|
3176
|
+
tailText(m) {
|
|
3177
|
+
if (this.hasToolBlocks(m) && m.blocks) {
|
|
3178
|
+
const last = m.blocks[m.blocks.length - 1];
|
|
3179
|
+
return last?.kind === "text" ? last.text : "";
|
|
3180
|
+
}
|
|
3181
|
+
return m.text;
|
|
3182
|
+
}
|
|
3183
|
+
/** Reveal-binding key — composite for a tool-turn tail (so a new trailing block rebinds), else msg id. */
|
|
3184
|
+
tailKey(m) {
|
|
3185
|
+
if (this.hasToolBlocks(m) && m.blocks) return `${m.id}#${m.blocks.length - 1}`;
|
|
3186
|
+
return m.id;
|
|
3187
|
+
}
|
|
2815
3188
|
renderMessages() {
|
|
2816
3189
|
if (!this.messagesEl) return;
|
|
2817
3190
|
this.resetReveal();
|
|
@@ -2832,6 +3205,11 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2832
3205
|
this.messagesEl.appendChild(chips);
|
|
2833
3206
|
}
|
|
2834
3207
|
for (const msg of this.messages) {
|
|
3208
|
+
if (msg.role === "assistant" && this.hasToolBlocks(msg)) {
|
|
3209
|
+
this.messagesEl.appendChild(this.buildRow("assistant", this.renderAssistantBlocks(msg)));
|
|
3210
|
+
if (!msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
2835
3213
|
const bubble = document.createElement("div");
|
|
2836
3214
|
bubble.className = `bubble ${msg.role}`;
|
|
2837
3215
|
if (msg.role === "assistant" && msg.streaming && !msg.text) {
|
|
@@ -2889,19 +3267,77 @@ var SmoothAgentChatElement = class extends HTMLElement {
|
|
|
2889
3267
|
* doesn't restart the reveal from zero), then resumes the loop.
|
|
2890
3268
|
*/
|
|
2891
3269
|
bindReveal(msg, bubble) {
|
|
2892
|
-
const
|
|
3270
|
+
const key = this.tailKey(msg);
|
|
3271
|
+
const target = this.tailText(msg);
|
|
3272
|
+
const carryOver = key === this.streamMsgId ? Math.min(this.displayedLength, target.length) : 0;
|
|
2893
3273
|
this.streamBubbleEl = bubble;
|
|
2894
|
-
this.streamMsgId =
|
|
2895
|
-
this.streamTarget =
|
|
3274
|
+
this.streamMsgId = key;
|
|
3275
|
+
this.streamTarget = target;
|
|
2896
3276
|
this.displayedLength = carryOver;
|
|
2897
3277
|
if (this.prefersReducedMotion()) {
|
|
2898
|
-
this.displayedLength =
|
|
2899
|
-
bubble.textContent =
|
|
3278
|
+
this.displayedLength = target.length;
|
|
3279
|
+
bubble.textContent = target;
|
|
2900
3280
|
return;
|
|
2901
3281
|
}
|
|
2902
|
-
bubble.textContent =
|
|
3282
|
+
bubble.textContent = target.slice(0, this.displayedLength);
|
|
2903
3283
|
this.ensureRevealLoop();
|
|
2904
3284
|
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Render a tool-activity assistant turn as an ordered strip: prose bubbles and
|
|
3287
|
+
* inline tool chips in the order the model produced them (mirrors the daemon
|
|
3288
|
+
* SPA's `blocks`). The live trailing text block (while streaming) binds to the
|
|
3289
|
+
* rAF reveal; earlier/finalized text blocks render as sanitized markdown.
|
|
3290
|
+
*/
|
|
3291
|
+
renderAssistantBlocks(msg) {
|
|
3292
|
+
const wrap = document.createElement("div");
|
|
3293
|
+
wrap.className = "blocks";
|
|
3294
|
+
const blocks = msg.blocks ?? [];
|
|
3295
|
+
const lastIdx = blocks.length - 1;
|
|
3296
|
+
blocks.forEach((block, i) => {
|
|
3297
|
+
if (block.kind === "tool") {
|
|
3298
|
+
wrap.appendChild(this.buildToolChip(block.tool));
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const bubble = document.createElement("div");
|
|
3302
|
+
bubble.className = "bubble assistant";
|
|
3303
|
+
if (msg.streaming && i === lastIdx) {
|
|
3304
|
+
bubble.classList.add("cursor");
|
|
3305
|
+
this.bindReveal(msg, bubble);
|
|
3306
|
+
} else {
|
|
3307
|
+
bubble.classList.add("md");
|
|
3308
|
+
bubble.innerHTML = renderMarkdown(block.text);
|
|
3309
|
+
}
|
|
3310
|
+
wrap.appendChild(bubble);
|
|
3311
|
+
});
|
|
3312
|
+
return wrap;
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* A single tool-activity chip: icon + tool name + status (running… / done / error),
|
|
3316
|
+
* with a truncated args preview. Tool name/args are set via `textContent` so a
|
|
3317
|
+
* tool payload can never inject markup.
|
|
3318
|
+
*/
|
|
3319
|
+
buildToolChip(tool) {
|
|
3320
|
+
const chip = document.createElement("div");
|
|
3321
|
+
chip.className = `toolchip ${tool.done ? tool.isError ? "error" : "done" : "running"}`;
|
|
3322
|
+
chip.setAttribute("part", "tool-chip");
|
|
3323
|
+
const icon = document.createElement("span");
|
|
3324
|
+
icon.className = "ti";
|
|
3325
|
+
icon.innerHTML = ICON.tool;
|
|
3326
|
+
const name = document.createElement("span");
|
|
3327
|
+
name.className = "tn";
|
|
3328
|
+
name.textContent = tool.name || "tool";
|
|
3329
|
+
const status = document.createElement("span");
|
|
3330
|
+
status.className = "ts";
|
|
3331
|
+
status.textContent = tool.done ? tool.isError ? "error" : "done" : "running…";
|
|
3332
|
+
chip.append(icon, name, status);
|
|
3333
|
+
if (tool.args && tool.args !== "{}" && tool.args !== "\"\"") {
|
|
3334
|
+
const args = document.createElement("span");
|
|
3335
|
+
args.className = "ta";
|
|
3336
|
+
args.textContent = tool.args.length > 80 ? `${tool.args.slice(0, 80)}…` : tool.args;
|
|
3337
|
+
chip.appendChild(args);
|
|
3338
|
+
}
|
|
3339
|
+
return chip;
|
|
3340
|
+
}
|
|
2905
3341
|
/** Start the rAF loop if it isn't already running. */
|
|
2906
3342
|
ensureRevealLoop() {
|
|
2907
3343
|
if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|