@smooai/chat-widget 0.10.1 → 0.11.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
@@ -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.
@@ -228,6 +231,7 @@ Declarative attributes mirror the programmatic [`ChatWidgetConfig`](./src/config
228
231
  | `greeting` | `greeting` | Shown before the first message. |
229
232
  | `mode` | `mode` | `popover` (default) or `fullpage`. |
230
233
  | `start-open` | `startOpen` | Open the panel immediately (no launcher click). |
234
+ | `hide-branding` | `hideBranding` | Hide the "powered by smooth-operator" tag + footer link. Default shown. |
231
235
  | — | `theme` | Brand colors (see [Theming](#theming)). |
232
236
  | — | `userName` / `userEmail` | Optional participant identity. |
233
237
 
@@ -11772,7 +11772,9 @@ var SmoothAgentChat = (function(exports) {
11772
11772
  greeting: config.greeting ?? "Hi! How can I help you today?",
11773
11773
  connectionErrorMessage: config.connectionErrorMessage ?? "We couldn't reach the chat. Please try again in a moment.",
11774
11774
  startOpen: config.startOpen ?? false,
11775
+ hideBranding: config.hideBranding ?? false,
11775
11776
  examplePrompts: (config.examplePrompts ?? []).filter((p) => p.trim().length > 0).slice(0, 5),
11777
+ showSuggestedReplies: config.showSuggestedReplies ?? true,
11776
11778
  requireName: config.requireName ?? false,
11777
11779
  requireEmail: config.requireEmail ?? false,
11778
11780
  requirePhone: config.requirePhone ?? false,
@@ -12893,6 +12895,19 @@ var SmoothAgentChat = (function(exports) {
12893
12895
  }
12894
12896
  return out;
12895
12897
  }
12898
+ /**
12899
+ * Pull the suggested follow-up replies out of a terminal `eventual_response`'s
12900
+ * `response` object (`response.suggestedNextActions`). Optional + back-compatible
12901
+ * like citations — read defensively (tolerating absence, non-array shapes, and
12902
+ * non-string / blank entries) and capped at 4 so a chatty agent can't overflow
12903
+ * the composer chip row.
12904
+ */
12905
+ function extractSuggestions(response) {
12906
+ if (!response || typeof response !== "object") return [];
12907
+ const raw = response.suggestedNextActions;
12908
+ if (!Array.isArray(raw)) return [];
12909
+ return raw.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 4);
12910
+ }
12896
12911
  /** Convert a server message row into a finalized {@link ChatMessage}. */
12897
12912
  function wireMessageToChat(m, idx) {
12898
12913
  const text = typeof m.content?.text === "string" ? m.content.text : "";
@@ -13243,6 +13258,8 @@ var SmoothAgentChat = (function(exports) {
13243
13258
  if (!assistant.text) assistant.text = "(no response)";
13244
13259
  const citations = extractCitations(inner);
13245
13260
  if (citations.length > 0) assistant.citations = citations;
13261
+ const suggestions = extractSuggestions(inner?.response);
13262
+ if (suggestions.length > 0) assistant.suggestions = suggestions;
13246
13263
  assistant.streaming = false;
13247
13264
  this.emitMessages();
13248
13265
  } catch (err) {
@@ -13707,7 +13724,8 @@ ${mode === "fullpage" ? `
13707
13724
  }
13708
13725
  .close:hover { background: color-mix(in srgb, var(--sac-text) 12%, transparent); transform: translateY(1px); }
13709
13726
  .close svg { width: 16px; height: 16px; opacity: .8; }
13710
- .powered { margin-left: auto; font-size: 10.5px; letter-spacing: .02em; opacity: .6; }
13727
+ .powered { margin-left: auto; font-size: 10.5px; letter-spacing: .02em; opacity: .6; color: inherit; text-decoration: none; }
13728
+ .powered:hover { opacity: .85; text-decoration: underline; }
13711
13729
  .header-sep { height: 1px; margin: 0 16px; background: linear-gradient(90deg, transparent, var(--sac-border), transparent); }
13712
13730
 
13713
13731
  /* Full-page header: taller, logo-led, no close. */
@@ -13979,6 +13997,8 @@ ${mode === "fullpage" ? `
13979
13997
  color: color-mix(in srgb, var(--sac-text) 38%, transparent);
13980
13998
  }
13981
13999
  .footer b { font-weight: 600; color: color-mix(in srgb, var(--sac-text) 55%, transparent); }
14000
+ .footer a { color: inherit; text-decoration: none; }
14001
+ .footer a:hover { text-decoration: underline; }
13982
14002
 
13983
14003
  /* ─────────────────── Pre-chat identity form ───────────────────────── */
13984
14004
  .prechat { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 18px; padding: 22px 20px; }
@@ -14070,6 +14090,10 @@ ${mode === "fullpage" ? `
14070
14090
  transform: translateY(-1px);
14071
14091
  }
14072
14092
 
14093
+ /* ───────────── Mid-conversation suggested-reply chips ─────────────── */
14094
+ .reply-suggestions { padding: 0 14px 4px; }
14095
+ .reply-suggestions:empty { display: none; }
14096
+
14073
14097
  /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
14074
14098
  .interrupt { padding: 0 14px; }
14075
14099
  .int-card {
@@ -14232,6 +14256,8 @@ ${mode === "fullpage" ? `
14232
14256
  return null;
14233
14257
  }
14234
14258
  }
14259
+ /** Public smooth-operator repo — the "powered by" header tag + footer link here. */
14260
+ const SMOOTH_OPERATOR_URL = "https://github.com/SmooAI/smooth-operator";
14235
14261
  const OBSERVED = [
14236
14262
  "endpoint",
14237
14263
  "agent-id",
@@ -14240,7 +14266,8 @@ ${mode === "fullpage" ? `
14240
14266
  "placeholder",
14241
14267
  "greeting",
14242
14268
  "start-open",
14243
- "mode"
14269
+ "mode",
14270
+ "hide-branding"
14244
14271
  ];
14245
14272
  /**
14246
14273
  * Inline SVG icons (static, trusted strings — never interpolated with user data).
@@ -14278,6 +14305,7 @@ ${mode === "fullpage" ? `
14278
14305
  _defineProperty(this, "userInfoSatisfied", false);
14279
14306
  _defineProperty(this, "hasSent", false);
14280
14307
  _defineProperty(this, "examplePrompts", []);
14308
+ _defineProperty(this, "showSuggestedReplies", true);
14281
14309
  _defineProperty(this, "greeting", "");
14282
14310
  _defineProperty(this, "interrupt", null);
14283
14311
  _defineProperty(this, "interruptEl", null);
@@ -14291,6 +14319,7 @@ ${mode === "fullpage" ? `
14291
14319
  _defineProperty(this, "dotEl", null);
14292
14320
  _defineProperty(this, "inputEl", null);
14293
14321
  _defineProperty(this, "sendBtn", null);
14322
+ _defineProperty(this, "suggestionsEl", null);
14294
14323
  _defineProperty(this, "streamBubbleEl", null);
14295
14324
  _defineProperty(this, "streamMsgId", null);
14296
14325
  _defineProperty(this, "streamTarget", "");
@@ -14357,7 +14386,9 @@ ${mode === "fullpage" ? `
14357
14386
  greeting: this.overrides.greeting ?? this.getAttribute("greeting") ?? void 0,
14358
14387
  connectionErrorMessage: this.overrides.connectionErrorMessage,
14359
14388
  startOpen: this.overrides.startOpen ?? this.hasAttribute("start-open"),
14389
+ hideBranding: this.overrides.hideBranding ?? this.hasAttribute("hide-branding"),
14360
14390
  examplePrompts: this.overrides.examplePrompts,
14391
+ showSuggestedReplies: this.overrides.showSuggestedReplies,
14361
14392
  requireName: this.overrides.requireName,
14362
14393
  requireEmail: this.overrides.requireEmail,
14363
14394
  requirePhone: this.overrides.requirePhone,
@@ -14389,10 +14420,12 @@ ${mode === "fullpage" ? `
14389
14420
  onInterrupt: (interrupt) => {
14390
14421
  this.interrupt = interrupt;
14391
14422
  this.renderInterrupt();
14423
+ this.renderSuggestions();
14392
14424
  },
14393
14425
  onIdentityRestore: (state) => {
14394
14426
  this.identityRestore = state;
14395
14427
  this.renderInterrupt();
14428
+ this.renderSuggestions();
14396
14429
  }
14397
14430
  });
14398
14431
  if (resolved.startOpen) this.open = true;
@@ -14410,7 +14443,7 @@ ${mode === "fullpage" ? `
14410
14443
  <span class="title">${escapeHtml(resolved.agentName)}</span>
14411
14444
  <span class="status"><span class="dot off"></span><span class="status-text"></span></span>
14412
14445
  </div>
14413
- <span class="powered">powered by smooth-operator</span>
14446
+ ${resolved.hideBranding ? "" : `<a class="powered" href="${SMOOTH_OPERATOR_URL}" target="_blank" rel="noopener noreferrer">powered by smooth-operator</a>`}
14414
14447
  </div>` : `<div class="header">
14415
14448
  <div class="avatar">${monogram}</div>
14416
14449
  <div class="meta">
@@ -14420,6 +14453,7 @@ ${mode === "fullpage" ? `
14420
14453
  <button class="close" aria-label="Close chat">${ICON.close}</button>
14421
14454
  </div>`;
14422
14455
  this.examplePrompts = resolved.examplePrompts;
14456
+ this.showSuggestedReplies = resolved.showSuggestedReplies;
14423
14457
  this.greeting = resolved.greeting;
14424
14458
  const gating = needsUserInfo(resolved) && !this.userInfoSatisfied;
14425
14459
  this.gating = gating;
@@ -14444,16 +14478,18 @@ ${mode === "fullpage" ? `
14444
14478
  <button type="submit" class="pc-submit">Start chat</button>
14445
14479
  </form>
14446
14480
  </div>`;
14447
- const restoreLink = this.allowChatRestore ? ` · <button type="button" class="restore-link">Restore my chats</button>` : "";
14481
+ const footerInner = [resolved.hideBranding ? "" : `<a href="${SMOOTH_OPERATOR_URL}" target="_blank" rel="noopener noreferrer">powered by <b>smooth&#8209;operator</b></a>`, this.allowChatRestore ? `<button type="button" class="restore-link">Restore my chats</button>` : ""].filter(Boolean).join(" · ");
14482
+ const footerHtml = footerInner ? `<div class="footer">${footerInner}</div>` : "";
14448
14483
  const chatHtml = `
14449
14484
  <div class="messages"></div>
14485
+ <div class="reply-suggestions"></div>
14450
14486
  <div class="interrupt hidden"></div>
14451
14487
  <div class="composer-wrap">
14452
14488
  <div class="composer">
14453
14489
  <textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
14454
14490
  <button class="send" type="button" aria-label="Send message">${ICON.send}</button>
14455
14491
  </div>
14456
- <div class="footer">powered by <b>smooth&#8209;operator</b>${restoreLink}</div>
14492
+ ${footerHtml}
14457
14493
  </div>`;
14458
14494
  const container = document.createElement("div");
14459
14495
  container.className = "wrap";
@@ -14477,6 +14513,7 @@ ${mode === "fullpage" ? `
14477
14513
  this.inputEl = container.querySelector("textarea");
14478
14514
  this.sendBtn = container.querySelector(".send");
14479
14515
  this.interruptEl = container.querySelector(".interrupt");
14516
+ this.suggestionsEl = container.querySelector(".reply-suggestions");
14480
14517
  this.launcherEl?.addEventListener("click", () => this.openChat());
14481
14518
  container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
14482
14519
  this.sendBtn?.addEventListener("click", () => this.submit());
@@ -14931,6 +14968,36 @@ ${mode === "fullpage" ? `
14931
14968
  if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
14932
14969
  }
14933
14970
  this.scrollToBottom(true);
14971
+ this.renderSuggestions();
14972
+ }
14973
+ /**
14974
+ * Render (or clear) the mid-conversation suggested-reply chips under the
14975
+ * composer. Shown ONLY when: the feature is enabled, no interrupt / restore
14976
+ * overlay is active (those reuse the slot above the composer), and the LATEST
14977
+ * message is a finalized assistant turn that carried suggestions. A new turn
14978
+ * (agent or the visitor typing) appends messages, so the latest is no longer
14979
+ * that assistant message and the chips clear on the next render. Chips reuse
14980
+ * the empty-state `.prompts`/`.chip` styling and send via the same path.
14981
+ */
14982
+ renderSuggestions() {
14983
+ const el = this.suggestionsEl;
14984
+ if (!el) return;
14985
+ el.replaceChildren();
14986
+ if (!this.showSuggestedReplies) return;
14987
+ if (this.interrupt || this.identityRestore.phase !== "idle") return;
14988
+ const last = this.messages[this.messages.length - 1];
14989
+ if (!last || last.role !== "assistant" || last.streaming || !last.suggestions || last.suggestions.length === 0) return;
14990
+ const chips = document.createElement("div");
14991
+ chips.className = "prompts";
14992
+ for (const suggestion of last.suggestions) {
14993
+ const chip = document.createElement("button");
14994
+ chip.type = "button";
14995
+ chip.className = "chip";
14996
+ chip.textContent = suggestion;
14997
+ chip.addEventListener("click", () => this.submitPrompt(suggestion));
14998
+ chips.appendChild(chip);
14999
+ }
15000
+ el.appendChild(chips);
14934
15001
  }
14935
15002
  /** True when the host/user prefers reduced motion (snap, no typewriter). */
14936
15003
  prefersReducedMotion() {