@smooai/chat-widget 0.3.0 → 0.4.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 CHANGED
@@ -155,12 +155,15 @@ mountChatWidget({
155
155
  the collected name/email are attached to the conversation session (phone rides
156
156
  session metadata). Set **`allowAnonymous: true`** to skip the form.
157
157
 
158
- ### OTP & tool confirmation (programmatic)
159
-
160
- The `ConversationController` surfaces mid-turn pauses via an `onInterrupt`
161
- callback `otp_verification_required` and `write_confirmation_required`and
162
- resumes them with `verifyOtp(code)` / `confirmTool(approved)`. (A built-in dialog
163
- UI for these is on the roadmap; the protocol plumbing ships today.)
158
+ ### OTP & tool confirmation (HITL)
159
+
160
+ When a turn pauses for **OTP verification** or a **tool-write approval**, the
161
+ widget shows an inline overlay above the composer an OTP code prompt (with
162
+ masked destination + retry state) or an Approve/Decline confirmation and
163
+ resumes the turn automatically. Under the hood the `ConversationController`
164
+ surfaces `otp_verification_required` / `write_confirmation_required` via an
165
+ `onInterrupt` callback and resumes with `verifyOtp(code)` / `confirmTool(approved)`,
166
+ so you can also drive it programmatically.
164
167
 
165
168
  ## Full-page mode
166
169
 
@@ -1305,6 +1305,63 @@ var SmoothAgentChat = (function(exports) {
1305
1305
  transform: translateY(-1px);
1306
1306
  }
1307
1307
 
1308
+ /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
1309
+ .interrupt { padding: 0 14px; }
1310
+ .int-card {
1311
+ border: 1px solid color-mix(in srgb, var(--sac-primary) 35%, var(--sac-border));
1312
+ background: color-mix(in srgb, var(--sac-primary) 8%, var(--sac-surface-2));
1313
+ border-radius: 14px;
1314
+ padding: 12px 13px;
1315
+ animation: sac-msg-in .3s var(--sac-ease) both;
1316
+ }
1317
+ .int-head { display: flex; align-items: center; gap: 8px; }
1318
+ .int-ico { display: flex; color: var(--sac-primary); }
1319
+ .int-ico svg { width: 17px; height: 17px; }
1320
+ .int-title { font-size: 13.5px; font-weight: 650; }
1321
+ .int-desc { margin-top: 5px; font-size: 12.5px; line-height: 1.45; color: color-mix(in srgb, var(--sac-text) 80%, transparent); }
1322
+ .int-sent { margin-top: 6px; font-size: 11.5px; color: color-mix(in srgb, var(--sac-text) 60%, transparent); }
1323
+ .int-row { display: flex; gap: 8px; margin-top: 10px; }
1324
+ .int-input {
1325
+ flex: 1;
1326
+ min-width: 0;
1327
+ border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
1328
+ background: var(--sac-bg);
1329
+ color: var(--sac-text);
1330
+ border-radius: 10px;
1331
+ padding: 9px 11px;
1332
+ font-family: inherit;
1333
+ font-size: 14px;
1334
+ letter-spacing: .14em;
1335
+ outline: none;
1336
+ transition: border-color .2s ease, box-shadow .2s ease;
1337
+ }
1338
+ .int-input:focus {
1339
+ border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
1340
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
1341
+ }
1342
+ .int-btn {
1343
+ border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
1344
+ background: var(--sac-surface-2);
1345
+ color: var(--sac-text);
1346
+ border-radius: 10px;
1347
+ padding: 9px 14px;
1348
+ font-family: inherit;
1349
+ font-size: 13px;
1350
+ font-weight: 600;
1351
+ cursor: pointer;
1352
+ transition: transform .2s var(--sac-ease), background .2s ease, border-color .2s ease;
1353
+ }
1354
+ .int-btn:hover { transform: translateY(-1px); }
1355
+ .int-btn.primary {
1356
+ border: none;
1357
+ background: linear-gradient(150deg, var(--sac-primary), var(--sac-primary-2));
1358
+ color: var(--sac-primary-text);
1359
+ box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
1360
+ }
1361
+ .int-row .int-btn { flex: 1; }
1362
+ .int-row .int-input + .int-btn { flex: 0 0 auto; }
1363
+ .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
1364
+
1308
1365
  .hidden { display: none !important; }
1309
1366
 
1310
1367
  @media (prefers-reduced-motion: reduce) {
@@ -1339,7 +1396,11 @@ var SmoothAgentChat = (function(exports) {
1339
1396
  /** Send — an upward arrow. */
1340
1397
  send: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V6M12 6l-5.5 5.5M12 6l5.5 5.5" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
1341
1398
  /** Sources disclosure caret. */
1342
- 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>`
1399
+ 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>`,
1400
+ /** OTP interrupt — a padlock. */
1401
+ 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>`,
1402
+ /** Tool-confirmation interrupt — a shield. */
1403
+ 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>`
1343
1404
  };
1344
1405
  /**
1345
1406
  * Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
@@ -1376,6 +1437,9 @@ var SmoothAgentChat = (function(exports) {
1376
1437
  hasSent = false;
1377
1438
  /** Starter prompts shown as chips in the empty state. */
1378
1439
  examplePrompts = [];
1440
+ /** Current mid-turn interrupt (OTP / tool-confirmation), or null. */
1441
+ interrupt = null;
1442
+ interruptEl = null;
1379
1443
  panelEl = null;
1380
1444
  launcherEl = null;
1381
1445
  messagesEl = null;
@@ -1468,6 +1532,10 @@ var SmoothAgentChat = (function(exports) {
1468
1532
  this.status = status;
1469
1533
  this.renderStatus();
1470
1534
  this.renderComposerState();
1535
+ },
1536
+ onInterrupt: (interrupt) => {
1537
+ this.interrupt = interrupt;
1538
+ this.renderInterrupt();
1471
1539
  }
1472
1540
  });
1473
1541
  if (resolved.startOpen) this.open = true;
@@ -1510,6 +1578,7 @@ var SmoothAgentChat = (function(exports) {
1510
1578
  </div>`;
1511
1579
  const chatHtml = `
1512
1580
  <div class="messages"></div>
1581
+ <div class="interrupt hidden"></div>
1513
1582
  <div class="composer-wrap">
1514
1583
  <div class="composer">
1515
1584
  <textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
@@ -1536,6 +1605,7 @@ var SmoothAgentChat = (function(exports) {
1536
1605
  this.dotEl = container.querySelector(".dot");
1537
1606
  this.inputEl = container.querySelector("textarea");
1538
1607
  this.sendBtn = container.querySelector(".send");
1608
+ this.interruptEl = container.querySelector(".interrupt");
1539
1609
  this.launcherEl?.addEventListener("click", () => this.openChat());
1540
1610
  container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
1541
1611
  this.sendBtn?.addEventListener("click", () => this.submit());
@@ -1556,6 +1626,97 @@ var SmoothAgentChat = (function(exports) {
1556
1626
  if (!gating) this.renderMessages(resolved.greeting);
1557
1627
  this.renderStatus();
1558
1628
  this.renderComposerState();
1629
+ this.renderInterrupt();
1630
+ }
1631
+ /**
1632
+ * Render (or clear) the mid-turn interrupt overlay above the composer:
1633
+ * an OTP code prompt or a tool-write confirmation. Server-supplied text is
1634
+ * set via `textContent` (never innerHTML); only static icons use innerHTML.
1635
+ */
1636
+ renderInterrupt() {
1637
+ const el = this.interruptEl;
1638
+ if (!el) return;
1639
+ el.replaceChildren();
1640
+ const it = this.interrupt;
1641
+ if (!it) {
1642
+ el.classList.add("hidden");
1643
+ return;
1644
+ }
1645
+ el.classList.remove("hidden");
1646
+ const card = document.createElement("div");
1647
+ card.className = "int-card";
1648
+ const head = document.createElement("div");
1649
+ head.className = "int-head";
1650
+ const ico = document.createElement("span");
1651
+ ico.className = "int-ico";
1652
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
1653
+ const title = document.createElement("span");
1654
+ title.className = "int-title";
1655
+ title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
1656
+ head.append(ico, title);
1657
+ card.appendChild(head);
1658
+ if (it.actionDescription) {
1659
+ const desc = document.createElement("div");
1660
+ desc.className = "int-desc";
1661
+ desc.textContent = it.actionDescription;
1662
+ card.appendChild(desc);
1663
+ }
1664
+ if (it.kind === "otp") {
1665
+ if (it.sent?.maskedDestination) {
1666
+ const sent = document.createElement("div");
1667
+ sent.className = "int-sent";
1668
+ sent.textContent = `Code sent to ${it.sent.maskedDestination}${it.sent.channel ? ` via ${it.sent.channel}` : ""}.`;
1669
+ card.appendChild(sent);
1670
+ }
1671
+ const row = document.createElement("div");
1672
+ row.className = "int-row";
1673
+ const input = document.createElement("input");
1674
+ input.className = "int-input";
1675
+ input.type = "text";
1676
+ input.inputMode = "numeric";
1677
+ input.autocomplete = "one-time-code";
1678
+ input.placeholder = "Enter code";
1679
+ const submit = () => {
1680
+ const code = input.value.trim();
1681
+ if (code) this.controller?.verifyOtp(code);
1682
+ };
1683
+ input.addEventListener("keydown", (ev) => {
1684
+ if (ev.key === "Enter") {
1685
+ ev.preventDefault();
1686
+ submit();
1687
+ }
1688
+ });
1689
+ const verify = document.createElement("button");
1690
+ verify.className = "int-btn primary";
1691
+ verify.type = "button";
1692
+ verify.textContent = "Verify";
1693
+ verify.addEventListener("click", submit);
1694
+ row.append(input, verify);
1695
+ card.appendChild(row);
1696
+ if (it.error) {
1697
+ const err = document.createElement("div");
1698
+ err.className = "int-error";
1699
+ err.textContent = it.attemptsRemaining != null ? `${it.error} (${it.attemptsRemaining} left)` : it.error;
1700
+ card.appendChild(err);
1701
+ }
1702
+ queueMicrotask(() => input.focus());
1703
+ } else {
1704
+ const row = document.createElement("div");
1705
+ row.className = "int-row";
1706
+ const decline = document.createElement("button");
1707
+ decline.className = "int-btn";
1708
+ decline.type = "button";
1709
+ decline.textContent = "Decline";
1710
+ decline.addEventListener("click", () => this.controller?.confirmTool(false));
1711
+ const approve = document.createElement("button");
1712
+ approve.className = "int-btn primary";
1713
+ approve.type = "button";
1714
+ approve.textContent = "Approve";
1715
+ approve.addEventListener("click", () => this.controller?.confirmTool(true));
1716
+ row.append(decline, approve);
1717
+ card.appendChild(row);
1718
+ }
1719
+ el.appendChild(card);
1559
1720
  }
1560
1721
  /** Collect identity from the pre-chat form, then drop into the chat view. */
1561
1722
  handlePrechatSubmit(form) {